From ef898437f7c3d9db61761126501b10b965699077 Mon Sep 17 00:00:00 2001 From: Pi Lanningham Date: Wed, 12 Feb 2025 20:37:15 -0500 Subject: [PATCH] Initial Commit These scripts will represent a set of scripts that can manage treasury budget funds. The treasury withdrawal should target the reward account for the `treasury` validator. The `treasury` validator should only allow those funds to be withdrawn if they are paid out to UTxOs locked by the `treasury` validator payment credential. The script must also enforce that these funds are not used in the following ways: - Must not delegate funds to a stake pool - i.e. must not have a staking credential attached on the outputs - and must not delegate itself to a stake pool - Must not participate in governance - i.e. must not delegate to a DRep - must not register as a DRep itself - must not cast votes - must not submit proposals - Must not register a stake pool This initial commit covers the withdrawal half of this equation (the simpler half). Next, the spend code path will need to be built. --- .github/workflows/continuous-integration.yml | 18 ++++++ .gitignore | 6 ++ README.md | 65 ++++++++++++++++++++ aiken.lock | 15 +++++ aiken.toml | 18 ++++++ validators/treasury.ak | 65 ++++++++++++++++++++ 6 files changed, 187 insertions(+) create mode 100644 .github/workflows/continuous-integration.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 aiken.lock create mode 100644 aiken.toml create mode 100644 validators/treasury.ak diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..2ab4b36 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,18 @@ +name: Continuous Integration + +on: + push: + branches: ["main"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: aiken-lang/setup-aiken@v1 + with: + version: v1.1.4 + - run: aiken fmt --check + - run: aiken check -D + - run: aiken build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff7811b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..53f59af --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# treasury-funds + +Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. + +```aiken +validator my_first_validator { + spend(_datum: Option, _redeemer: Data, _output_reference: Data, _context: Data) { + True + } +} +``` + +## Building + +```sh +aiken build +``` + +## Configuring + +**aiken.toml** +```toml +[config.default] +network_id = 41 +``` + +Or, alternatively, write conditional environment modules under `env`. + +## Testing + +You can write tests in any module using the `test` keyword. For example: + +```aiken +use config + +test foo() { + config.network_id + 1 == 42 +} +``` + +To run all tests, simply do: + +```sh +aiken check +``` + +To run only tests matching the string `foo`, do: + +```sh +aiken check -m foo +``` + +## Documentation + +If you're writing a library, you might want to generate an HTML documentation for it. + +Use: + +```sh +aiken docs +``` + +## Resources + +Find more on the [Aiken's user manual](https://aiken-lang.org). diff --git a/aiken.lock b/aiken.lock new file mode 100644 index 0000000..3195130 --- /dev/null +++ b/aiken.lock @@ -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 = "v2.2.0" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v2.2.0" +requirements = [] +source = "github" + +[etags] diff --git a/aiken.toml b/aiken.toml new file mode 100644 index 0000000..a072efd --- /dev/null +++ b/aiken.toml @@ -0,0 +1,18 @@ +name = "sundae/treasury-funds" +version = "0.0.0" +compiler = "v1.1.4" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'sundae/treasury-funds'" + +[repository] +user = "sundae" +project = "treasury-funds" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v2.2.0" +source = "github" + +[config] diff --git a/validators/treasury.ak b/validators/treasury.ak new file mode 100644 index 0000000..397ef94 --- /dev/null +++ b/validators/treasury.ak @@ -0,0 +1,65 @@ +use cardano/address.{Credential} +use cardano/assets.{lovelace_of} +use cardano/transaction.{Transaction} +use aiken/collection/pairs +use aiken/collection/list + +validator treasury { + spend(_d, _r, _i, _t) { + False + } + + // Funds can be withdrawn only if they are paid to the spending portion of this script + // without a staking address; this is how the funds are moved out of the reward account + // after a treasury withdrawal + withdraw(_, account: Credential, self: Transaction) { + // There must be only one withdrawal from this credential + expect [amount] = self.withdrawals |> pairs.get_all(account) + + // To prevent shenanigans where a disbursement equal to the amount is used to trick + // the withdrawal, we assert that `account` never occurs in the inputs + // This is slightly less flexible than ensuring the totals carry through, but + // also simpler to reason about + expect None = self.inputs + |> list.find(fn(input) { input.output.address.payment_credential == account }) + + // Sum up the amounts of the withdrawal paid to `account` + // This lets us split the funds across multiple UTxOs for accounting purposes + // but still ensures that the total amount is correct + let output_sum = self.outputs + |> list.filter(fn(output) { output.address.payment_credential == account }) + |> list.map(fn(output) { + // Treasury funds must not be staked + expect None = output.address.stake_credential + lovelace_of(output.value) + }) + |> list.reduce(0, +) + + // Ensure that the output sum is *at least* the amount + // This allows some flexibility in case of minUTxO issues, which are unlikely + // We also let the transaction fee be deducted from this withdrawal, + // Which is likely to be small, but still shouldn't be imposed on the administrator + expect output_sum >= amount - self.fee + + // If all the above is true, allow the withdrawal + True + } + + // (n.b. We could just use `else` for these, but it's clearer to make things explicit) + + // The funds received by the treasury are not allowed to vote in governance + vote(_r, _v, _t) { + False + } + + // The funds received by the treasury are not allowed to publish any certificates + // In particular, this prevents the stake script from delegating to a stake pool, + // registering a stake pool, registering as a DRep, or delegating to a DRep. + publish(_r, _c, _t) { + False + } + + else(_) { + False + } +}