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
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) {
//...
}
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.51.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
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 (GitHub link). 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#L25-L37
The ValueNote
type is in the top-level of the Aztec.nr framework, namely noir-projects/aztec-nr (GitHub link). Like before, you'll need to add the crate to Nargo.toml
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#L39-L52
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#L54-L64
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::address::AztecAddress,
encrypted_logs::encrypted_note_emission::encode_and_encrypt_note_with_keys,
keys::getters::get_current_public_keys,
state_vars::{PrivateSet, PublicImmutable, SharedImmutable}
};
use dep::aztec::unencrypted_logs::unencrypted_event_emission::encode_event;
use dep::value_note::value_note::ValueNote;
use dep::token::Token;
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L5-L17
Like before, you can find these and other aztec::protocol_types
here (GitHub link).
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.51.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);
// 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_keys = get_current_public_keys(&mut context, donor);
let mut note = ValueNote::new(amount as Field, donor_keys.npk_m.hash());
storage.donation_receipts.insert(&mut note).emit(encode_and_encrypt_note_with_keys(&mut context, donor_keys.ovpk_m, donor_keys.ivpk_m, donor));
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L66-L88
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.
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
Crowdfunding::at(context.this_address())._publish_donation_receipts(amount, operator_address).enqueue(&mut context);
}
Source code: noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr#L90-L103
You should be able to compile successfully with aztec-nargo compile
.
Congratulations, you have just built a multi-contract project on Aztec!
Conclusion
For comparison, the full Crowdfunding contract can be found here (GitHub link).
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 (GitHub link).
Next steps
Build an accounts contract
Follow the account contract tutorial on the next page and learn more about account abstraction.