|
| 1 | +package scalus.examples.upgradeableproxy |
| 2 | + |
| 3 | +import scalus.* |
| 4 | +import scalus.cardano.onchain.plutus.prelude.* |
| 5 | +import scalus.cardano.onchain.plutus.v1.{Credential, PubKeyHash} |
| 6 | +import scalus.cardano.onchain.plutus.v2.OutputDatum |
| 7 | +import scalus.cardano.onchain.plutus.v3.* |
| 8 | +import scalus.uplc.builtin.{Data, FromData, ToData} |
| 9 | +import scalus.cardano.onchain.plutus.v3.Validator |
| 10 | + |
| 11 | +/** Upgradeable proxy pattern for Cardano smart contracts. |
| 12 | + * |
| 13 | + * The pattern works by having a spend validator ensure that a stake validator has been called by |
| 14 | + * checking that |
| 15 | + * - a withdrawal has been made; |
| 16 | + * - the withdrawal has been made from a known trusted script address. |
| 17 | + * |
| 18 | + * A combination of these conditions ensure that the validator has been called, thus allowing a |
| 19 | + * forced script composition. |
| 20 | + * |
| 21 | + * This also ensures that the logic can be changed (upgraded), by updating the known trusted script |
| 22 | + * address, that is stored in the datum. In this illustration, a proxy has an owner, which can |
| 23 | + * update the datum. In a real application, a more sophisticated approach should be preferred, to |
| 24 | + * ensure trustlessness. |
| 25 | + */ |
| 26 | + |
| 27 | +case class ProxyDatum(logicHash: ValidatorHash, owner: PubKeyHash) derives FromData, ToData |
| 28 | + |
| 29 | +enum ProxyRedeemer derives FromData, ToData: |
| 30 | + case Call |
| 31 | + |
| 32 | + case Upgrade(newLogicHash: ValidatorHash) |
| 33 | + |
| 34 | +/** Spending validator for the upgradeable proxy. |
| 35 | + * |
| 36 | + * Stores the active logic script hash in the datum. Two actions: |
| 37 | + * |
| 38 | + * - `Call` - verifies the logic stake script was withdrawn and the datum is unchanged. |
| 39 | + * - `Upgrade` - owner replaces the logic script hash; value must be preserved. |
| 40 | + */ |
| 41 | +@Compile |
| 42 | +object ProxyValidator extends Validator { |
| 43 | + |
| 44 | + inline override def spend( |
| 45 | + datum: Option[Data], |
| 46 | + redeemer: Data, |
| 47 | + tx: TxInfo, |
| 48 | + ownRef: TxOutRef |
| 49 | + ): Unit = { |
| 50 | + val d = datum.getOrFail(MissingDatum).to[ProxyDatum] |
| 51 | + val r = redeemer.to[ProxyRedeemer] |
| 52 | + val ownInput = tx.findOwnInputOrFail(ownRef) |
| 53 | + |
| 54 | + val continuationOutput = |
| 55 | + tx.outputs |
| 56 | + .filter(out => out.address === ownInput.resolved.address) |
| 57 | + .headOption |
| 58 | + .getOrFail(MissingContinuation) |
| 59 | + |
| 60 | + val continuationDatum = continuationOutput.datum match |
| 61 | + case OutputDatum.OutputDatum(d) => d.to[ProxyDatum] |
| 62 | + case _ => fail(ContinuationMustHaveInlineDatum) |
| 63 | + |
| 64 | + require( |
| 65 | + continuationOutput.value === ownInput.resolved.value, |
| 66 | + ValueMustBePreserved |
| 67 | + ) |
| 68 | + |
| 69 | + r match |
| 70 | + case ProxyRedeemer.Call => |
| 71 | + // Ensure the logic stake validator was called |
| 72 | + val logicCredential = Credential.ScriptCredential(d.logicHash) |
| 73 | + tx.withdrawals.getOrFail(logicCredential, LogicNotInvoked) |
| 74 | + |
| 75 | + // Ensure the proxy UTxO continues with the same datum (state preserved) |
| 76 | + require(continuationDatum.logicHash === d.logicHash, LogicHashChanged) |
| 77 | + require(continuationDatum.owner === d.owner, OwnerChanged) |
| 78 | + |
| 79 | + case ProxyRedeemer.Upgrade(newLogicHash) => |
| 80 | + // Only the owner can upgrade the logic |
| 81 | + require(tx.isSignedBy(d.owner), NotSignedByOwner) |
| 82 | + |
| 83 | + // Continuation output must carry the updated datum |
| 84 | + require(continuationDatum.logicHash === newLogicHash, LogicHashMismatch) |
| 85 | + require(continuationDatum.owner === d.owner, OwnerChanged) |
| 86 | + } |
| 87 | + |
| 88 | + inline val MissingDatum = "Proxy datum must be present" |
| 89 | + inline val LogicNotInvoked = "Logic stake validator must be invoked in this transaction" |
| 90 | + inline val MissingContinuation = "Proxy continuation output not found" |
| 91 | + inline val ContinuationMustHaveInlineDatum = "Continuation output must have an inline datum" |
| 92 | + inline val LogicHashChanged = "Logic hash must not change on Call" |
| 93 | + inline val OwnerChanged = "Owner must not change" |
| 94 | + inline val NotSignedByOwner = "Transaction must be signed by the proxy owner" |
| 95 | + inline val LogicHashMismatch = "Continuation datum logic hash does not match upgrade target" |
| 96 | + inline val ValueMustBePreserved = "Proxy value must be preserved" |
| 97 | +} |
0 commit comments