Breaking the Cycle Part 1
When designing Plutus smart contract systems, cyclic dependencies are a major headache.
To understand how these dependencies arise, we'll look at an example staking smart contract system.
An Example Staking System
Staking is common on-chain activity. A user will lock tokens in a contract and based on how long they have locked their tokens, they will receive additional funds.
We need to record the time that the tokens were locked on the staking contract. It is straightforward to include a datum when locking which includes the current time. However, because validators are only run during unlocking, a user could lie and record their start time as something long ago in the past.
We need a way to verify the start time is accurate. Whenever one needs to verify behavior during locking, we can do this by using an additional minting contract.
For a staking system, a user will lock their funds and mint a special token, which is also locked at the contract in the same UTxO as the staked funds. The minting smart contract will verify that the associated datum has a valid time.
Now, when we unlock the funds to disburse additional tokens, we can check for the additional token that was minted during locking. If the token is present, we know the locking is valid. Well almost. We also need to ensure this "witness" token can never leave the staking validator. Otherwise, it could be reused by a new locking operation with an invalid datum. This means when locking, the minting contract must ensure the witness token is locked at the validator address, and the validator must ensure the token is burnt.
We are glossing over some details, but this system, and its checks, are sufficient to ensure the staking is valid.
However, there is a big problem. The staking validator must check for the existence of the token, and the minting contract must ensure the token is outputted to the validator address. Therefore, we have a cyclic dependency between the minter and the validator.
We can break this cycle with a trick.
Instead of having the minting contract depend on the validator, we remove this dependency. The minting contract can no longer verify the token is outputted to the validator address.
Luckily, we can verify a more general condition, which when combined with a new check in the spend validator, will be sufficient.
The minting contract will now verify that the token name and the output address public key hash are identical. Thus, we can deduce the first address a token was sent to, by reading the token name.
Here is some example snippet of the token name check the minting script will use:
The validator will still depend on the minting contract policy id. When verifying the witness token, the validator will ensure the token name matches its own script hash.
Here is an example snippet for that check:
Using these new checks in mint and spend validator, we ensure that valid tokens were outputted to the validator address during mint without a cyclic dependency.
That's Not All
The trick presented here is an alternative to, and could also augment, the solution presented in "Parameterized NFTs" section of the recent Well-Typed blog post "Verifying initial conditions in Plutus."
Recently, in a Canonical Twitter space, @zygomeb pointed out the cyclic dependency issues between minters and validators would not be a problem if a single smart contract could function as both a spend validator and a minter. This would be a much better solution, so let's hope we can move Plutus in that direction in the future.
This solution works great when you have to mint a witness token that must stay locked at a particular validator address. It is a common a situation, but it is not the only situation where cyclic dependencies show up.
In our next blog post, we will discuss a more general case, e.g. how to resolve cyclic dependencies between a group of spend validators.
As always, don't hesitate to contact us if you would like help developing smart contracts on Cardano.