Write 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.
And if using VSCode, see here 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":
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
- An "Operator" begins a Crowdfunding campaign (contract), specifying:
- an existing token address
- their account address
- a deadline timestamp
- Any address can donate (in private context)
- private transfer token from sender to contract
- transaction receipts allow private claims via another contract
- 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
.
contract Crowdfunding {
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L1-L3
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.
#[aztec(public)]
#[aztec(initializer)]
fn init(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
//...
}
More about initializers here.
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)
#[aztec(public)]
#[aztec(initializer)]
fn init(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
//...
}
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:
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.44.0", directory="noir-projects/aztec-nr/aztec" }
A word about versions:
- Choose the aztec packages version to match your aztec sandbox version
- Check that your
compiler_version
in Nargo.toml is satisified by your aztec compiler -aztec-nargo -V
More about versions here.
Inside the Crowdfunding contract definition, use the dependency that defines the address type AztecAddress
(same syntax as Rust)
use dep::aztec::protocol_types::address::AztecAddress;
The aztec::protocol_types
can be browsed here. 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:
#[aztec(storage)]
struct Storage {
// Token used for donations (e.g. DAI)
donation_token: SharedImmutable<AztecAddress>,
// Crowdfunding campaign operator
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 (can be used as proof to obtain rewards, eg in Claim contracts)
donation_receipts: PrivateSet<ValueNote>,
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L24-L36
The ValueNote
type is in the top-level of the Aztec.nr framework, namely noir-projects/aztec-nr. Like before, you'll need to add the crate to Nargo.toml
(See here for common dependencies).
Back in main.nr, reference use
of the type
use dep::value_note::value_note::ValueNote;
Now complete the initializer by setting the storage variables with the parameters:
#[aztec(public)]
#[aztec(initializer)]
fn init(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
storage.donation_token.initialize(donation_token);
storage.operator.initialize(operator);
storage.deadline.initialize(deadline);
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L38-L51
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.
Declare an Aztec function that is public and internal
#[aztec(public)]
#[aztec(internal)]
#[aztec(view)]
fn _check_deadline() {
//...
}
Read the deadline from storage and assert that the timestamp
from this context is before the deadline
#[aztec(public)]
#[aztec(internal)]
#[aztec(view)]
fn _check_deadline() {
let deadline = storage.deadline.read();
assert(context.timestamp() < deadline, "Deadline has passed");
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L53-L63
Since donations are to be private, the donate function will have the user's private context which has these Private Global Variables. So from the private context there is a little extra to call the (public internal) _check_deadline
function.
#[aztec(private)]
fn donate(amount: u64) {
// 1) Check that the deadline has not passed
Crowdfunding::at(context.this_address())._check_deadline().enqueue_view(&mut context);
//...
}
Namely calling enqueue
and passing the (mutable) context.
Now conclude adding all dependencies to the Crowdfunding
contract:
use dep::aztec::{
protocol_types::{
abis::function_selector::FunctionSelector, address::AztecAddress, traits::Serialize,
grumpkin_point::GrumpkinPoint
},
encrypted_logs::encrypted_note_emission::encode_and_encrypt_note,
state_vars::{PrivateSet, PublicImmutable, SharedImmutable}
};
use dep::value_note::value_note::ValueNote;
use dep::token::Token;
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L5-L16
Like before, you can find these and other aztec::protocol_types
here.
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="aztec-packages-v0.44.0", 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.
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
#[aztec(private)]
fn donate(amount: u64) {
// 1) Check that the deadline has not passed
Crowdfunding::at(context.this_address())._check_deadline().enqueue_view(&mut context);
// 2) Transfer the donation tokens from donor to this contract
let donor = context.msg_sender();
Token::at(storage.donation_token.read_private()).transfer_from(donor, context.this_address(), amount as Field, 0).call(&mut context);
let header = context.get_header();
// 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 donor_npk_m_hash = header.get_npk_m_hash(&mut context, donor);
let mut note = ValueNote::new(amount as Field, donor_npk_m_hash);
storage.donation_receipts.insert(&mut note).emit(encode_and_encrypt_note(&mut context, donor, donor));
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L65-L86
3. Operator withdrawals
The remaining function to implement, withdraw
, is reasonably straight-forward:
- make sure the address calling is the operator address
- transfer tokens from the contract to the operator
- reveal that an amount has been withdrawn to the operator
The last point is achieved by emitting an unencrypted event log, more here.
Copy the last function into your Crowdfunding contract:
// Withdraws balance to the operator. Requires that msg_sender() is the operator.
#[aztec(private)]
fn withdraw(amount: u64) {
// 1) Check that msg_sender() is the operator
let operator_address = storage.operator.read_private();
assert(context.msg_sender() == operator_address, "Not an operator");
// 2) Transfer the donation tokens from this contract to the operator
Token::at(storage.donation_token.read_private()).transfer(operator_address, amount as Field).call(&mut context);
// 3) Emit an unencrypted event so that anyone can audit how much the operator has withdrawn
let event = WithdrawalProcessed { amount: amount as Field, who: operator_address.to_field() };
context.emit_unencrypted_log(event.serialize());
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L88-L103
You should be able to compile successfully with aztec-nargo compile
.
Conclusion
For comparison, the full Crowdfunding contract can be found here.
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.