Skip to content

Commit

Permalink
write contract to validate CIP-68 Reference NFT management
Browse files Browse the repository at this point in the history
  • Loading branch information
h2physics committed Aug 3, 2024
0 parents commit 4a280bf
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Tests

on:
push:
branches: ["main"]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: aiken-lang/setup-aiken@v0.1.0
with:
version: v1.0.24-alpha

- run: aiken fmt --check
- run: aiken check -D
- run: aiken build
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Aiken compilation artifacts
artifacts/
# Aiken's project working directory
build/
# Aiken's default documentation export
docs/
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Minswap CIP-68 Reference NFT Management contract
15 changes: 15 additions & 0 deletions aiken.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This file was generated by Aiken
# You typically do not need to edit this file

[[requirements]]
name = "aiken-lang/stdlib"
version = "1.9.0"
source = "github"

[[packages]]
name = "aiken-lang/stdlib"
version = "1.9.0"
requirements = []
source = "github"

[etags]
14 changes: 14 additions & 0 deletions aiken.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name = "minswap/cip_68_nft_management"
version = "0.0.0"
license = "Apache-2.0"
description = "Aiken contracts for project 'minswap/cip_68_nft_management'"

[repository]
user = "minswap"
project = "cip_68_nft_management"
platform = "github"

[[dependencies]]
name = "aiken-lang/stdlib"
version = "1.9.0"
source = "github"
222 changes: 222 additions & 0 deletions plutus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
{
"preamble": {
"title": "minswap/cip_68_nft_management",
"description": "Aiken contracts for project 'minswap/cip_68_nft_management'",
"version": "0.0.0",
"plutusVersion": "v2",
"compiler": {
"name": "Aiken",
"version": "v1.0.29-alpha+16fb02e"
},
"license": "Apache-2.0"
},
"validators": [
{
"title": "nft_management_validator.spend",
"datum": {
"title": "datum",
"schema": {
"$ref": "#/definitions/nft_management_validator~1ValidatorDatum"
}
},
"redeemer": {
"title": "_redeemer",
"schema": {
"$ref": "#/definitions/Data"
}
},
"compiledCode": "5904f70100003232323232323223232323232225333009323232533300c3009300d37540022646464646464646464a66602a6026602c6ea80044c8c8c8c8c8c8c94ccc070c068c074dd500089919299980f180e180f9baa001132533301f3370e900218101baa0011323253330210071533302100513375e601260466ea8c028c08cdd5010180498119baa300a302337540042940528180d800981218109baa0011630233024302430203754604660406ea800458cc02803894ccc078cdd7980698101baa300d30203754002601a60406ea80204c06cccc014dd5980318101baa001375c601a60406ea8c018c080dd5180398101baa01d375c600c60406ea8c018c080dd5180398101baa01d14a064646600200201c44a66604400229404c94ccc080cdc79bae302500200414a2266006006002604a0026eb8c084c078dd50008b1810180e9baa300a301d37546008603a6ea8068c05cccc004dd5980f98100029bae3009301c3754600460386ea8c00cc070dd500c9bae3002301c3754600460386ea8c00cc070dd500c91119299980e980d180f1baa0011480004dd69811180f9baa00132533301d301a301e37540022980103d87a8000132330010013756604660406ea8008894ccc088004530103d87a80001323232325333023337220100042a66604666e3c0200084c044cc09cdd4000a5eb80530103d87a8000133006006003375a60480066eb8c088008c098008c090004c8cc004004010894ccc0840045300103d87a80001323232325333022337220100042a66604466e3c0200084c040cc098dd3000a5eb80530103d87a8000133006006003375660460066eb8c084008c094008c08c0048c078c07c0048c074c078c078004c06c004c05cdd5180d180d980b9baa301a301737540022c660026eb0c06401c8cdd79802180b9baa00100922323300100100322533301a00114c0103d87a80001323253330193005002130073301d0024bd70099802002000980f001180e0009ba5480008c05c004dd6180a980b180b180b180b180b180b0011bac301400130143014001300f37540066022601c6ea800458c040c044008c03c004c02cdd50008a4c26cac600200a4a66600c6008600e6ea80044c8c8c8c8c8c94ccc03cc0480084c8c926533300d300b300e3754004264646464a666028602e00426464932999809180818099baa0021323232325333019301c002149858dd7180d000980d0011bae3018001301437540042ca666022601e60246ea800c4c8c8c8c94ccc060c06c0084c8c926325333017301500113232533301c301f002132498c94ccc068c0600044c8c94ccc07cc0880084c9263018001163020001301c37540042a666034602e0022646464646464a666046604c0042930b1bad30240013024002375a604400260440046eb4c080004c070dd50010b180d1baa00116301d001301937540062a66602e60280022a66603460326ea800c52616163017375400460220062c60320026032004602e00260266ea800c5858c054004c054008c04c004c03cdd50010b19198008008031129998088008a4c26466006006602a00464646eb8c048008dd7180800098098008b180800098080011bad300e001300e0023756601800260106ea8004588c94ccc018c0100044c8c94ccc02cc03800852616375c601800260106ea800854ccc018c00c0044c8c94ccc02cc03800852616375c601800260106ea800858c018dd50009b8748008dc3a4000ae6955ceaab9e5573eae815d0aba21",
"hash": "020f66cee3ccdf8f141adc432ad85fb4458d1f466193c12cb54f4e5c"
}
],
"definitions": {
"ByteArray": {
"dataType": "bytes"
},
"Data": {
"title": "Data",
"description": "Any Plutus data."
},
"Int": {
"dataType": "integer"
},
"List$Pair$ByteArray_ByteArray": {
"dataType": "map",
"keys": {
"$ref": "#/definitions/ByteArray"
},
"values": {
"$ref": "#/definitions/ByteArray"
}
},
"Option$aiken/transaction/credential/Referenced$aiken/transaction/credential/Credential": {
"title": "Optional",
"anyOf": [
{
"title": "Some",
"description": "An optional value.",
"dataType": "constructor",
"index": 0,
"fields": [
{
"$ref": "#/definitions/aiken~1transaction~1credential~1Referenced$aiken~1transaction~1credential~1Credential"
}
]
},
{
"title": "None",
"description": "Nothing.",
"dataType": "constructor",
"index": 1,
"fields": []
}
]
},
"aiken/transaction/credential/Address": {
"title": "Address",
"description": "A Cardano `Address` typically holding one or two credential references.\n\n Note that legacy bootstrap addresses (a.k.a. 'Byron addresses') are\n completely excluded from Plutus contexts. Thus, from an on-chain\n perspective only exists addresses of type 00, 01, ..., 07 as detailed\n in [CIP-0019 :: Shelley Addresses](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0019/#shelley-addresses).",
"anyOf": [
{
"title": "Address",
"dataType": "constructor",
"index": 0,
"fields": [
{
"title": "payment_credential",
"$ref": "#/definitions/aiken~1transaction~1credential~1Credential"
},
{
"title": "stake_credential",
"$ref": "#/definitions/Option$aiken~1transaction~1credential~1Referenced$aiken~1transaction~1credential~1Credential"
}
]
}
]
},
"aiken/transaction/credential/Credential": {
"title": "Credential",
"description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).",
"anyOf": [
{
"title": "VerificationKeyCredential",
"dataType": "constructor",
"index": 0,
"fields": [
{
"$ref": "#/definitions/ByteArray"
}
]
},
{
"title": "ScriptCredential",
"dataType": "constructor",
"index": 1,
"fields": [
{
"$ref": "#/definitions/ByteArray"
}
]
}
]
},
"aiken/transaction/credential/Referenced$aiken/transaction/credential/Credential": {
"title": "Referenced",
"description": "Represent a type of object that can be represented either inline (by hash)\n or via a reference (i.e. a pointer to an on-chain location).\n\n This is mainly use for capturing pointers to a stake credential\n registration certificate in the case of so-called pointer addresses.",
"anyOf": [
{
"title": "Inline",
"dataType": "constructor",
"index": 0,
"fields": [
{
"$ref": "#/definitions/aiken~1transaction~1credential~1Credential"
}
]
},
{
"title": "Pointer",
"dataType": "constructor",
"index": 1,
"fields": [
{
"title": "slot_number",
"$ref": "#/definitions/Int"
},
{
"title": "transaction_index",
"$ref": "#/definitions/Int"
},
{
"title": "certificate_index",
"$ref": "#/definitions/Int"
}
]
}
]
},
"nft_management_validator/Asset": {
"title": "Asset",
"anyOf": [
{
"title": "Asset",
"dataType": "constructor",
"index": 0,
"fields": [
{
"title": "policy_id",
"$ref": "#/definitions/ByteArray"
},
{
"title": "asset_name",
"$ref": "#/definitions/ByteArray"
}
]
}
]
},
"nft_management_validator/Extra": {
"title": "Extra",
"anyOf": [
{
"title": "Extra",
"dataType": "constructor",
"index": 0,
"fields": [
{
"title": "address",
"$ref": "#/definitions/aiken~1transaction~1credential~1Address"
},
{
"title": "nft",
"$ref": "#/definitions/nft_management_validator~1Asset"
}
]
}
]
},
"nft_management_validator/ValidatorDatum": {
"title": "ValidatorDatum",
"anyOf": [
{
"title": "ValidatorDatum",
"dataType": "constructor",
"index": 0,
"fields": [
{
"title": "metadata",
"$ref": "#/definitions/List$Pair$ByteArray_ByteArray"
},
{
"title": "version",
"$ref": "#/definitions/Int"
},
{
"title": "extra",
"$ref": "#/definitions/nft_management_validator~1Extra"
}
]
}
]
}
}
}
89 changes: 89 additions & 0 deletions validators/nft_management_validator.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use aiken/list
use aiken/pairs
use aiken/transaction.{
InlineDatum, Input, Output, ScriptContext, Spend, Transaction,
}
use aiken/transaction/credential.{Address, VerificationKeyCredential}
use aiken/transaction/value

type Asset {
policy_id: ByteArray,
asset_name: ByteArray,
}

type Extra {
// Who has able to spend the UTxO that is holding CIP-68 Reference NFT
address: Address,
// CIP-68 Reference NFT
nft: Asset,
}

// Followed by CIP-68 Metadata standard: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0068
type ValidatorDatum {
metadata: Pairs<ByteArray, Data>,
version: Int,
extra: Extra,
}

validator {
// Validate spend the UTxO that is holding CIP-68 Reference NFT
// This function is almost used for updating Token Metadata
fn spend(datum: ValidatorDatum, _redeemer: Data, ctx: ScriptContext) -> Bool {
let ScriptContext { transaction, purpose } = ctx
expect Spend(out_ref) = purpose
let Transaction { extra_signatories, inputs, outputs, .. } = transaction
expect Some(own_input) =
inputs |> list.find(fn(input) { input.output_reference == out_ref })

let Input {
output: Output { address: own_address, value: own_val, .. },
..
} = own_input

// Validate UTxO has to hold the Reference NFT
let has_nft_in_input =
value.quantity_of(
own_val,
datum.extra.nft.policy_id,
datum.extra.nft.asset_name,
) == 1
// Extract the Public Key Hash of UTxO's owner
// TODO: Consider to support other types of Address such as Native Script and Plutus Script Address
expect Address { payment_credential: VerificationKeyCredential(pkh), .. } =
datum.extra.address
// Transaction has to be signed by the UTxO's owner
let has_signed = list.has(extra_signatories, pkh)

// In order to prevent burning or transfering the Reference NFT, the NFT has to be paid back to new Contract's UTxO
expect Some(own_output_hold_nft) =
outputs
|> list.find(fn(out) { and {
out.address.payment_credential == own_address.payment_credential,
value.quantity_of(
out.value,
datum.extra.nft.policy_id,
datum.extra.nft.asset_name,
) == 1,
} })
expect Output { datum: InlineDatum(out_datum_raw), .. } =
own_output_hold_nft
expect out_datum: ValidatorDatum = out_datum_raw
and {
// Validate UTxO has to hold the Reference NFT
has_nft_in_input,
// Transaction has to be signed by the UTxO's owner
has_signed,
// Reference NFT has to be the same in both input & output datum
datum.extra.nft == out_datum.extra.nft,
validate_token_decimal(out_datum.metadata),
}
}
}

// Zero decimals make the token become a non-division token
// This is not best practise in DeFi world, then contract do not allow updating decimals to zero
fn validate_token_decimal(metadata: Pairs<ByteArray, Data>) -> Bool {
expect Some(decimals_data) = metadata |> pairs.get_first(#"646563696d616c73")
expect decimals: Int = decimals_data
decimals > 0
}

0 comments on commit 4a280bf

Please sign in to comment.