Assuming Temporary Staking Control of Funds Sent from Cardano Validator Scripts
Introduction
To be clear: this post is concerned with the “staking rights” (or “delegation rights”) of funds, which specifies who controls where funds are staked and receives the rewards accrued by staking those funds. What is described in this post does not allow someone else to spend your wallet balance, and control of the staking rights can be recovered by making a transaction including the funds concerned. This only affects funds which are being sent from validator scripts (or “smart contracts”) which have been implemented in a particular way.
Cardano validator scripts are the on-chain programs which only allow funds to be spent if certain conditions execute successfully and are what most Cardano decentralised apps are built upon. We may use “script” or “contract” to refer to validator scripts.
By sharing this we hope to educate on how Cardano addresses work and to avoid this validator script design flaw from affecting new projects.
Shelley Addresses
First, we need to understand a bit more about how the current address format works (Shelley-era onwards). The Address Specification CIP-19 is a useful document which describes what information is contained in an address and how that information is encoded, in a relatively non-technical way.
Essentially a Shelley address, which is the address type most will be familiar with, is made up of 3 parts:
- Header byte
- Payment part
- Delegation part (optional)
The header byte contains information about which network the address is for (testnet or mainnet) along with what type of address it is (for example, is this the address of a script or of someone’s wallet).
The payment part is the most important part of the address as this describes who has ownership of the funds at the address and can send the funds as they please. For addresses that we are familiar with this part comes in the form of a hash of the public-key associated with our wallet, a PubKeyHash
. We can describe this as having control of the payment key for an address.
Finally, the delegation part of the address describes who has staking rights of the funds at the address. Who ever owns the staking key specified in the delegation part has the ability to choose where the funds at the address are staked and has control over the rewards gained from staking those funds. It may come as a surprise to some that the payment part and delegation part are two separate things. Usually the owner of the payment part and delegation part is the same person, but this does not have to be the case.
Mangled (Hybrid) Addresses
When you create a wallet all the addresses generated by the wallet will use the same staking key, which you control, meaning you have staking rights over the funds at all of those addresses. The slightly strange part, because it’s uncommon in the ecosystem currently, is that someone else can have staking rights of funds even if you have ownership of the funds in the sense that you can spend them. This happens when the owner of the payment key specified in the address is different to the owner of the staking key specified in the delegation part; such an address is called a mangled address (or a hybrid address). It’s trivial to take someone’s address and create a new address from it such that is has their payment key but a staking key of your choosing.
Ok, so what? Some readers may already have realised the issue from the title of this post. Let’s take a look at an example of a popular validator script live on mainnet.
MuesliSwap DEX
MuesliSwap is a decentralised exchange implemented as a validator script where by traders lock assets at the script along with data describing the asset they wish to receive in exchange for their assets. At a high level, an example swap could look as follows:
- Alice sends 100 ADA to the MuesliSwap script along with some data which specifies her address along with the asset and quantity of that asset she wishes to receive in exchange for her ADA. Let’s say she is requesting 25 of a token named ADMNT.
- At some later point, Bob sends 25 ADMNT to the MuesliSwap script along with data specifying that he wishes to receive 100 ADA in exchange for those 25 ADMNT tokens.
- We can see that a swapped can be performed between Alice and Bob because their orders match up. At this point, a third party matchmaker Carol can perform the exchange by taking the assets Alice sent to the script and sending them to Bob, and taking the assets Bob sent to the script and sending them to Alice.
The code in the MuesliSwap script only allows the matchmaker to perform the swap if Alice receives the asset she requested and Bob receives the asset he requested. That is the entire premise of the MuesliSwap contract, and is what protects users funds and ensures trades are executed correctly. The code in the contract that implements this check is as follows:
A transaction performing the swap can only be performed if correctFull
returns True
for both Alice’s order and Bob’s order. Note that we will look at the following in the context of Alice’s order but the same logic applies to Bob’s order.
Lines 5–6 aren’t too important in the context of this post, but can be simplified to: “The party on the other side of this swap must be providing at least the requested assets”.
You can read lines 3–4 as: “The creator (oCreator
) of this order must receive at least their requested amount of the requested asset, along with their deposit of 1.5 ADA”.
The contract checks the outputs of the swap transaction and checks that the order creator, Alice in this case, will receive her requested assets.
The Flaw
The creator of the order is stored in the oCreator
field of the order data structure, which contains all of the information Alice supplies when she creates her order by sending her assets to the contract. We can see the oCreator
field is of type PubKeyHash
:
Here’s the problem: recall earlier in this post we said that the payment part of an address comes in the form of a PubKeyHash
, but we know from earlier in this post that an address is made up of a header byte, a payment part and a delegation part — so what about the other parts?
This contract does not check that the address Alice used to make the order receives the requested assets, it checks that an address with the same payment part as Alice’s address receives the assets. Contracts should be checking the full address.
This function, used in correctFull
via getAtLeastValue
, iterates through the transaction outputs and calculates the total value that is sent to addresses with the same payment part as the payment part passed as the parameter h
. That means funds sent to addresses which have the same payment part as Alice’s address but with a different delegation part will be included in the sum.
In the context of MuesliSwap, this means a third party matchmaker Darth can execute the swap but send the swapped assets to addresses which have payment parts owned by Alice and Bob but with a delegation part of his choosing, which allows him to send the swapped funds to an address for which he owns the staking rights and have control over where they are staked and the rewards gained from staking those funds. That is, Darth can send the swapped funds to a mangled address of his choosing so long as the payment part matches that of the victim’s address.
If this happens Alice and Bob can still send those swapped funds and the funds will appear in most wallets, but they will not be staking the swapped funds or receive the staking rewards for them until they send those funds to an address for which they own the delegation part. User’s don’t lose control of the spending ability of the swapped funds.
Potential Impact
Though users don’t lose control of their funds this is clearly not desirable. It’s like receiving a payment to your bank but someone else is receiving the interest on the funds received in that payment. A malicious actor can stake the swapped funds until the victim realises the swapped funds are not contributing to their staking balance or until their wallet happens to use those funds in a transaction. This might cause issues for single-address wallets such as Nami, which may not detect the swapped funds or allow users to spend those funds.
A nice feature of the Yoroi wallet is that it warns users if they have funds in their wallet for which they do not control the staking rights and prompts them to automatically build a transaction to move those funds back into their control. It detects if any funds are at mangled addresses.
Some Affected Projects
We see that many popular projects are implemented in such a way that some malicious actor can control the staking rights of funds that are sent from the project’s contract. It looks like this programming pattern for checking an address receives funds is used throughout the ecosystem, possibly because this specific pattern was taught during some earlier iterations of the Plutus Pioneers Program, so many contracts may suffer from this flaw. We looked at the following projects:
MuesliSwap (DEX; contract): A malicious matchmaker on MuesliSwap could, if they were efficient enough to be the party performing all swaps, assume temporary control of the staking rights over the entire volume of ADA traded on the exchange. The matchmaker can assume temporary control of the staking rights of funds transferred in swaps they execute and anyone can perform the role of a matchmaker. To put this in perspective: there is currently about 1,000,000 ADA locked in orders, so the amount of ADA that has passed through the exchange is probably in the tens of millions.
It appears that the MuesliSwap team are aware that the address funds are sent to does not have to be the address that created the order, but it is not clear if they see how it can be used maliciously.
SpaceBudz Market (NFT Market; contract): A malicious bidder can assume temporary staking rights over funds locked in a bid by placing a higher bid and thus refunding the previous bid (and then could simply cancel their own bid). A malicious buyer can keep temporary control over the staking rights of the funds they use to purchase an NFT.
The developer of the SpaceBudz Market is aware of this flaw and states it will be fixed in the next version of the market.
Genesis Market (NFT Market; contract): A malicious buyer can keep temporary control over the staking rights of the funds they use to purchase an NFT.
Genesis Market has been made aware of this flaw and is looking into ways to improve their systems.
jpg.store (NFT Market; contract): A malicious buyer can keep temporary control over the staking rights of the funds they use to purchase an NFT.
jpg.store have addressed this issue in the newest version of their smart contracts.
In the cases of the NFT markets the addresses collecting royalties may be unlikely to move funds often, meaning the staking rights will remain with the malicious actor for a longer period of time.
An attacker can probably also split the transferred funds into as many different addresses as they like, bounded by the minimum UTXO amount.
Unclear if Affected
SundaeSwap (CEX; closed-source): SundaeSwap is a closed-source project and there is no technical documentation describing how the protocol works so this is speculation. It appears that SundaeSwap does include information specifying both the payment and delegation part of an order creator’s address in the data sent to the contract when creating an order, which suggests that a malicious “scooper” cannot redirect funds from cancelled and successful trades to addresses for which they control the delegation part, depending on the contract code. Also depending on the implementation, it may be necessary for the code to ensure that a malicious “scooper” cannot redirect liquidity from orders to a different address than the expected liquidity pool address — that is, an address with a payment part which matches that of the liquidity pool but with an arbitrary delegation part.
Minswap (Exchange on testnet; closed-source): Similar to SundaeSwap, Minswap also seems to include both the payment part and delegation part of the creator’s address when they submit an order which may suggest that the creator must receive their requested funds to the complete address they used to create their order. Like SundaeSwap, they must be careful to avoid potential issues with moving funds to liquidity pools.
The Minswap team have confirmed that their smart contract does in-fact check the customer’s full address.
Header Byte?
We mentioned that an address contains a header byte along with the payment and delegation part — if a contract only checks that the payment part is correct can we change the header byte?
The first four bits of the header byte specify the network, mainnet or testnet, but Cardano’s ledger rules on the protocol-level prevent funds from being sent to addresses with the incorrect network ID so there does not seem to be a way to abuse this.
The other four bits specify the type of address, for example is it the address of a script or of a wallet, and does it include a delegation part — the only option a malicious actor has here is to try change the address type so that it the payment part was a ScriptHash
instead of a PubKeyHash
, but then the contract would not recognise the address format because it will be looking for a PubKeyHash
and thus conclude that the owner was not receiving any funds and thus execution of the contract would fail.
Example Exploit on MuesliSwap Testnet
Consider a swap on the testnet MuesliSwap DEX, including three parties:
- Alice, who wants to buy 1337 of the token
adamant
for 50 ADA:
full address:
addr_test1qpxpg8v7x9yte6nxgrnr3e53kkadalyqlt5cdxwzcm2ekptzkpy2ynmwzktmtwjm9d76j87qq4gezkdddfggjv4mnzeslpvl5pdelegation part of address:
stake_test1up3tqj9zfahpt9a4hfdjkldfrlqq25v3txkk55yfx2ae3vc6ct5sp
- Bob, who wants to sell 1337
adamant
for 50 ADA:
full address:
addr_test1qrgepre7aaxa3n466yscmjqjgxaqvhknequ5rcchkkgvtphm27my6w48ze27yg8qcyd92grwk9xulefyxyeqhhclckusll6crddelegation part of address:
stake_test1ura40djd82n3v40zyrsvzxj4yphtznw0u5jrzvstmu0utwgxx2dtx
- Darth, a malicious MuesliSwap match maker:
full address:
addr_test1qz4zspnhun6nxgs4cdtmx9p2nyjnkd3l9wgj8kg75hs94a8g7q509xmv8qtc3su6ym2sy6cxr4fk2xen0s6nvr0vm5lsh5yuc0delegation part of address:
stake_test1ur50q28jndkrs9ugcwdzd4gzdvrp65m9rvehcdfkphkd60cs50lkt
The following transaction show a swap in which Darth assumes control of the staking rights for the funds transferred in the swap:
Update: The previous testnet was stopped, breaking these links.
- Transaction 1: Alice places order requesting 1337
adamant
for 50 ADA - Transaction 2: Bob places order requesting 50 ADA for 1337
adamant
- Transaction 3: Darth executes the swap of Alice and Bob’s assets, but sends the funds to new addresses which have the same delegation part as his own address
In transaction 3 we see that the swapped funds were sent to two brand new addresses, not the addresses Alice and Bob used when placing their orders:
full address:
addr_test1qpxpg8v7x9yte6nxgrnr3e53kkadalyqlt5cdxwzcm2ekp0g7q509xmv8qtc3su6ym2sy6cxr4fk2xen0s6nvr0vm5lsum27nrdelegation part of address:
stake_test1ur50q28jndkrs9ugcwdzd4gzdvrp65m9rvehcdfkphkd60cs50lktfull address:
addr_test1qrgepre7aaxa3n466yscmjqjgxaqvhknequ5rcchkkgvtphg7q509xmv8qtc3su6ym2sy6cxr4fk2xen0s6nvr0vm5ls5uk3andelegation part of address:
stake_test1ur50q28jndkrs9ugcwdzd4gzdvrp65m9rvehcdfkphkd60cs50lkt
As you can see, the addresses which received the swapped funds both have the same delegation part as Darth’s address, for which he controls the staking key for. Alice has the control of the payment part of the first address, which is why the first half of the address is the same as the one she used to create her order. The second address relates to Bob’s address. We can see that Darth’s balance is 300, but his controlled stake is 353.266545
because he also has control over the staking rights of the 50 ADA Bob received as well as the deposits both parties were refunded.
How to know if you are affected
The easiest way to discover if you have been affected by this flaw is probably to use the Yoroi wallet, even just temporarily, as it will warn you if do not have staking rights over some of your funds and prompt you to automatically transfer them back into your control. You can import your wallet into Yoroi by using your secret word phrase.
Eternl Users: in the account details on the summary tab of your wallet you can compare the Total Ada
amount with the Controlled Stake
amount. If the Controlled Stake
amount is less than the Total Ada
you control then you have some funds in a mangled address and you are not receiving rewards for those funds. To recover the staking rights the simplest way is probably to temporarily use Yoroi, as described above.
Nami Users: Nami does not detect funds at mangled addresses due to its single address nature. Update: Adamant worked with Nami to support mangled addresses, now Nami will detect and allow you to spend funds which are held at mangled addresses.