Skip to content

Commit

Permalink
docs: Aztec smart contract tutorial - crowdfunding (#5786)
Browse files Browse the repository at this point in the history
A tutorial to teach some of the fundamentals of building smart contracts
on Aztec.

---------

Co-authored-by: josh crites <critesjosh@gmail.com>
  • Loading branch information
jzaki and critesjosh authored Apr 25, 2024
1 parent 6894fc7 commit 91cc0a4
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ Initializers are regular functions that set an "initialized" flag (a nullifier)

## Annotate with `#[aztec(private)]` and `#[aztec(initializer)]`


Define your initiaizer like so:
Define your initializer like so:

```rust
#[aztec(private)]
Expand Down
216 changes: 216 additions & 0 deletions docs/docs/developers/tutorials/crowdfunding/donations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
title: Build a donations contract
tags: [developers, tutorial, example]
---

# Build a donations contract

In this tutorial we'll create two contracts related to crowdfunding:

- A crowdfunding contract with two core components
- Fully private donations
- Verifiable withdrawals to the operator
- A reward contract for anyone else to anonymously reward donors

Along the way you will:

- Install Aztec developer tools
- Setup a new Noir contract project
- Add base Aztec dependencies
- Call between private and public contexts
- Wrap an address with its interface (token)
- Create custom private value notes

## Setup

### Install tools

Please ensure that the you already have [Installed the Sandbox](https://docs.aztec.network/developers/getting_started/quickstart#install-the-sandbox).

And if using VSCode, see [here](https://docs.aztec.network/developers/contracts/main#install-noir-lsp-recommended) to install Noir LSP, where you'll benefit from syntax highlighting, profiling, and more.

### Create an Aztec project

Use `aztec-nargo` in a terminal to create a new Aztec contract project named "crowdfunding":

```sh
aztec-nargo new --contract crowdfunding
```

Inside the new `crowdfunding` directory you will have a base to implement the Aztec smart contract.

Use `aztec-nargo --help` to see other commands.

## Private donations

1. An "Operator" begins a Crowdfunding campaign (contract), specifying:

- an existing token address
- their account address
- a deadline timestamp

2. Any address can donate (in private context)

- private transfer token from sender to contract
- transaction receipts allow private claims via another contract

3. Only the operator can withdraw from the fund

### 1. Create a campaign

#### Initialize

Open the project in your preferred editor. If using VSCode and the LSP, you'll be able to select the `aztec-nargo` binary to use (instead of `nargo`).

In `main.nr`, rename the contract from `Main`, to `Crowdfunding`.

#include_code empty-contract /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

Replace the example functions with an initializer that takes the required campaign info as parameters. Notice use of `#[aztec(...)]` macros inform the compiler that the function is a public initializer.

```rust
#include_code init-header /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr raw
//...
}
```

More about initializers [here](../../contracts/writing_contracts/functions/initializers.md).

#### Dependencies

When you compile the contracts by running `aztec-nargo compile` in your project directory, you'll notice it cannot resolve `AztecAddress`. (Or hovering over in VSCode)

```rust
#include_code init-header-error /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr raw
//...
}
```

Add the required dependency by going to your project's `Nargo.toml` file, and adding `aztec` from the `aztec-nr` framework. It resides in the `aztec-packages` mono-repo:

```rust
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/aztec-nr/aztec" }
```

A word about versions:
- Choose the aztec packages version to match your aztec tools as seen here - `aztec-cli -V`
- Check that your `compiler_version` in Nargo.toml is satisified by your aztec compiler - `aztec-nargo -V`

More about versions [here](https://docs.aztec.network/developers/versions-updating).

Inside the Crowdfunding contract definition, use the dependency that defines the address type `AztecAddress` (same syntax as Rust)

```rust
use dep::aztec::protocol_types::address::AztecAddress;
```

The `aztec::protocol_types` can be browsed [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-protocol-circuits/crates/types/src). And like rust dependencies, the relative path inside the dependency corresponds to `address::AztecAddress`.


#### Storage

To retain the initializer parameters in the contract's Storage, we'll need to declare them in a preceding `Storage` struct:

#include_code storage /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

The `ValueNote` type is in the top-level of the Aztec.nr framework, namely [noir-projects/aztec-nr](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/value-note/src/value_note.nr). Like before, you'll need to add the crate to Nargo.toml

(See [here](https://docs.aztec.network/developers/contracts/resources/dependencies) for common dependencies).

---

Back in main.nr, reference `use` of the type

```rust
use dep::value_note::value_note::ValueNote;
```

Now complete the initializer by setting the storage variables with the parameters:

#include_code init /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

You can compile the code so far with `aztec-nargo compile`.

### 2. Taking private donations

#### Checking campaign duration against the timestamp

To check that the donation occurs before the campaign deadline, we must access the public `timestamp`. It is one of several [Public Global Variables](https://docs.aztec.network/developers/contracts/references/globals#public-global-variables).

Declare an Aztec function that is public and internal

```rust
#include_code deadline-header /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr raw
//...
}
```

Read the deadline from storage and assert that the `timestamp` from this context is before the deadline

#include_code deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

---

Since donations are to be private, the donate function will have the user's private context which has these [Private Global Variables](https://docs.aztec.network/developers/contracts/references/globals#private-global-variables). So from the private context there is a little extra to call the (public internal) `_check_deadline` function.

```rust
#include_code call-check-deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr raw
//...
}
```

Namely calling `enqueue` and passing the (mutable) context.

Now conclude adding all dependencies to the `Crowdfunding` contract:

#include_code all-deps /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

Like before, you can find these and other `aztec::protocol_types` [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-protocol-circuits/crates/types/src).


#### Interfacing with another contract

The token being used for donations is stored simply as an `AztecAddress` (named `donation_token`). so to easily use it as a token, we let the compiler know that we want the address to have a Token interface. Here we will use a maintained example Token contract.

Add this `Token` contract to Nargo.toml:

```
token = { git="https://github.com/AztecProtocol/aztec-packages/", tag="#include_aztec_version", directory="noir-projects/noir-contracts/contracts/token_contract" }
```

With the dependency already `use`d at the start of the contract, the token contract can be called to make the transfer from msg sender to this contract.

:::note
The user must have authorised this action (concept [here](../../../learn/concepts/accounts/main#authorizing-actions)), example use of `createAuthWit` in 'full donor flow' test [here](../../../../../yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts).
:::

#### Creating and storing a private receipt note

The last thing to do is create a new value note and add it to the `donation_receipts`. So the full donation function is now

#include_code donate /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

### 3. Operator withdrawals

The remaining function to implement, `withdraw`, is reasonably straight-forward:
1. make sure the address calling is the operator address
2. transfer tokens from the contract to the operator
3. reveal that an amount has been withdrawn to the operator

The last point is achieved by emitting an unencrypted event log, more [here](https://docs.aztec.network/developers/contracts/writing_contracts/events/emit_event#unencrypted-events).

Copy the last function into your Crowdfunding contract:

#include_code operator-withdrawals /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

You should be able to compile successfully with `aztec-nargo compile`.

## Conclusion

For comparison, the full Crowdfunding contract can be found [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/crowdfunding_contract).

### Next steps?

If a new token wishes to honour donors with free tokens based on donation amounts, this is possible via the donation_receipts (a `PrivateSet`).
See [claim_contract](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/claim_contract).
2 changes: 1 addition & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const sidebars = {
items: [
"developers/tutorials/writing_token_contract",
"developers/tutorials/writing_private_voting_contract",

"developers/tutorials/crowdfunding/donations",
{
label: "Writing a DApp",
type: "category",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// docs:start:empty-contract
contract Crowdfunding {
// docs:end:empty-contract

// docs:start:all-deps
use dep::aztec::{
protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::Serialize},
state_vars::{PrivateSet, PublicImmutable, SharedImmutable}
};
use dep::value_note::value_note::ValueNote;
use dep::token::Token;
// docs:end:all-deps

#[aztec(event)]
struct WithdrawalProcessed {
Expand All @@ -19,6 +23,7 @@ contract Crowdfunding {
}
}

// docs:start:storage
#[aztec(storage)]
struct Storage {
// Token used for donations (e.g. DAI)
Expand All @@ -27,44 +32,63 @@ contract Crowdfunding {
operator: SharedImmutable<AztecAddress>,
// End of the crowdfunding campaign after which no more donations are accepted
deadline: PublicImmutable<u64>,
// Notes emitted to donors when they donate (later on used to claim rewards in the Claim contract)
claim_notes: PrivateSet<ValueNote>,
// Notes emitted to donors when they donate (can be used as proof to obtain rewards, eg in Claim contracts)
donation_receipts: PrivateSet<ValueNote>,
}
// docs:end:storage

// docs:start:init
// docs:start:init-header
// docs:start:init-header-error
#[aztec(public)]
#[aztec(initializer)]
fn constructor(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
// this-will-error:init-header-error
fn init(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
// docs:end:init-header
// docs:end:init-header-error
storage.donation_token.initialize(donation_token);
storage.operator.initialize(operator);
storage.deadline.initialize(deadline);
}
// docs:end:init

// docs:start:deadline
// docs:start:deadline-header
#[aztec(public)]
#[aztec(internal)]
fn _check_deadline() {
// docs:end:deadline-header
let deadline = storage.deadline.read();
assert(context.timestamp() < deadline, "Deadline has passed");
}
// docs:end:deadline

// docs:start:donate
// docs:start:call-check-deadline
#[aztec(private)]
fn donate(amount: u64) {
// 1) Check that the deadline has not passed
Crowdfunding::at(context.this_address())._check_deadline().enqueue(&mut context);
// docs:end:call-check-deadline

// docs:start:do-transfer
// 2) Transfer the donation tokens from donor to this contract
Token::at(storage.donation_token.read_private()).transfer(
context.msg_sender(),
context.this_address(),
amount as Field,
0
).call(&mut context);
// docs:end:do-transfer

// 3) Create a value note for the donor so that he can later on claim a rewards token in the Claim
// contract by proving that the hash of this note exists in the note hash tree.
let mut note = ValueNote::new(amount as Field, context.msg_sender());
storage.claim_notes.insert(&mut note, true);
storage.donation_receipts.insert(&mut note, true);
}
// docs:end:donate

// docs:start:operator-withdrawals
// Withdraws balance to the operator. Requires that msg_sender() is the operator.
#[aztec(private)]
fn withdraw(amount: u64) {
Expand All @@ -79,4 +103,5 @@ contract Crowdfunding {
let event = WithdrawalProcessed { amount, who: operator_address };
context.emit_unencrypted_log(event.serialize());
}
// docs:end:operator-withdrawals
}

0 comments on commit 91cc0a4

Please sign in to comment.