Skip to content

Commit

Permalink
feat: crowdfunding contract (#4917)
Browse files Browse the repository at this point in the history
Crowdfunding contract implemented during offsite.
  • Loading branch information
benesjan authored Mar 7, 2024
1 parent f6f34b7 commit ba3aff2
Show file tree
Hide file tree
Showing 15 changed files with 617 additions and 2 deletions.
14 changes: 14 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,18 @@ jobs:
aztec_manifest_key: end-to-end
<<: *defaults_e2e_test

e2e-crowdfunding-and-claim:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_crowdfunding_and_claim.test.ts
aztec_manifest_key: end-to-end

e2e-public-cross-chain-messaging:
steps:
- *checkout
Expand Down Expand Up @@ -1377,6 +1389,7 @@ workflows:
- e2e-multiple-accounts-1-enc-key: *e2e_test
- e2e-cli: *e2e_test
- e2e-cross-chain-messaging: *e2e_test
- e2e-crowdfunding-and-claim: *e2e_test
- e2e-public-cross-chain-messaging: *e2e_test
- e2e-public-to-private-messaging: *e2e_test
- e2e-account-contracts: *e2e_test
Expand Down Expand Up @@ -1440,6 +1453,7 @@ workflows:
- e2e-multiple-accounts-1-enc-key
- e2e-cli
- e2e-cross-chain-messaging
- e2e-crowdfunding-and-claim
- e2e-public-cross-chain-messaging
- e2e-public-to-private-messaging
- e2e-account-contracts
Expand Down
8 changes: 7 additions & 1 deletion noir-projects/aztec-nr/aztec/src/note/note_header.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use dep::protocol_types::address::AztecAddress;
use dep::protocol_types::traits::Empty;
use dep::protocol_types::traits::{Empty, Serialize};

struct NoteHeader {
contract_address: AztecAddress,
Expand All @@ -21,3 +21,9 @@ impl NoteHeader {
NoteHeader { contract_address, nonce, storage_slot, is_transient: false }
}
}

impl Serialize<4> for NoteHeader {
fn serialize(self) -> [Field; 4] {
[self.contract_address.to_field(), self.nonce, self.storage_slot, self.is_transient as Field]
}
}
8 changes: 8 additions & 0 deletions noir-projects/aztec-nr/value-note/src/value_note.nr
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,11 @@ impl ValueNote {
ValueNote { value, owner, randomness, header }
}
}

impl Serialize<7> for ValueNote {
fn serialize(self) -> [Field; 7] {
let header = self.header.serialize();

[self.value, self.owner.to_field(), self.randomness, header[0], header[1], header[2], header[3]]
}
}
2 changes: 2 additions & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ members = [
"contracts/benchmarking_contract",
"contracts/card_game_contract",
"contracts/child_contract",
"contracts/claim_contract",
"contracts/contract_class_registerer_contract",
"contracts/contract_instance_deployer_contract",
"contracts/counter_contract",
"contracts/crowdfunding_contract",
"contracts/delegator_contract",
"contracts/delegated_on_contract",
"contracts/docs_example_contract",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "claim_contract"
authors = [""]
compiler_version = ">=0.18.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
value_note = { path = "../../../aztec-nr/value-note" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use dep::aztec::{
protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress},
context::PrivateContext,
};

struct Token {
address: AztecAddress,
}

impl Token {
pub fn at(address: AztecAddress) -> Self {
Self { address }
}

fn mint_public(self: Self, context: &mut PrivateContext, to: AztecAddress, amount: Field) {
let _ret = context.call_public_function(
self.address,
FunctionSelector::from_signature("mint_public((Field),Field)"),
[to.to_field(), amount]
);
}

pub fn transfer(
self: Self,
context: &mut PrivateContext,
from: AztecAddress,
to: AztecAddress,
amount: Field,
nonce: Field
) {
let _ret = context.call_private_function(
self.address,
FunctionSelector::from_signature("transfer((Field),(Field),Field,Field)"),
[from.to_field(), to.to_field(), amount, nonce]
);
}
}
59 changes: 59 additions & 0 deletions noir-projects/noir-contracts/contracts/claim_contract/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
contract Claim {
mod interfaces;

use dep::aztec::{
history::note_inclusion::prove_note_inclusion,
protocol_types::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
},
state_vars::SharedImmutable,
};
use dep::value_note::value_note::ValueNote;
use interfaces::Token;

struct Storage {
// Address of a contract based on whose notes we distribute the rewards
target_contract: SharedImmutable<AztecAddress>,
// Token to be distributed as a reward when claiming
reward_token: SharedImmutable<AztecAddress>,
}

#[aztec(private)]
fn constructor(target_contract: AztecAddress, reward_token: AztecAddress) {
let selector = FunctionSelector::from_signature("_initialize((Field),(Field))");
context.call_public_function(
context.this_address(),
selector,
[target_contract.to_field(), reward_token.to_field()]
);
}

#[aztec(public)]
#[aztec(internal)]
#[aztec(noinitcheck)]
fn _initialize(target_contract: AztecAddress, reward_token: AztecAddress) {
storage.target_contract.initialize(target_contract);
storage.reward_token.initialize(reward_token);
}

#[aztec(private)]
fn claim(proof_note: ValueNote) {
// 1) Check that the note corresponds to the target contract
let target_address = storage.target_contract.read_private();
assert(target_address == proof_note.header.contract_address, "Note does not correspond to the target contract");

// 2) Prove that the note hash exists in the note hash tree
prove_note_inclusion(proof_note, context);

// 3) Compute and emit a nullifier which is unique to the note and this contract to ensure the reward can be
// claimed only once with the given note.
// Note: The nullifier is unique to the note and THIS contract because the protocol siloes all nullifiers with
// the address of a contract it was emitted from.
context.push_new_nullifier(proof_note.compute_nullifier(&mut context), 0);

// 4) Finally we mint the reward token to the sender of the transaction
let reward_token = Token::at(storage.reward_token.read_private());
reward_token.mint_public(&mut context, context.msg_sender(), proof_note.value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "crowdfunding_contract"
authors = [""]
compiler_version = ">=0.18.0"
type = "contract"

[dependencies]
aztec = { path = "../../../aztec-nr/aztec" }
value_note = { path = "../../../aztec-nr/value-note" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use dep::aztec::protocol_types::{abis::function_selector::FunctionSelector, address::{AztecAddress, EthAddress}};
use dep::aztec::{context::{PrivateContext, PublicContext}};

struct Token {
address: AztecAddress,
}

impl Token {
pub fn at(address: AztecAddress) -> Self {
Self { address }
}

pub fn transfer(
self: Self,
context: &mut PrivateContext,
from: AztecAddress,
to: AztecAddress,
amount: Field,
nonce: Field
) {
let _ret = context.call_private_function(
self.address,
FunctionSelector::from_signature("transfer((Field),(Field),Field,Field)"),
[from.to_field(), to.to_field(), amount, nonce]
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
contract Crowdfunding {
mod interfaces;

use dep::aztec::{
log::emit_unencrypted_log_from_private,
protocol_types::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
traits::Serialize
},
state_vars::{PrivateSet, PublicImmutable, SharedImmutable},
};
use dep::value_note::value_note::ValueNote;
use interfaces::Token;

#[event]
struct WithdrawalProcessed {
who: AztecAddress,
amount: u64,
}

impl Serialize<2> for WithdrawalProcessed {
fn serialize(self: Self) -> [Field; 2] {
[self.who.to_field(), self.amount as Field]
}
}

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
// TODO(#4990): Make deadline a u64 once the neccessary traits are implemented
deadline: PublicImmutable<Field>,
// Notes emitted to donors when they donate (later on used to claim rewards in the Claim contract)
claim_notes: PrivateSet<ValueNote>,
}

#[aztec(private)]
fn constructor(donation_token: AztecAddress, operator: AztecAddress, deadline: u64) {
let selector = FunctionSelector::from_signature("_initialize((Field),(Field),Field)");
context.call_public_function(
context.this_address(),
selector,
[donation_token.to_field(), operator.to_field(), deadline as Field]
);
}

#[aztec(public)]
#[aztec(internal)]
#[aztec(noinitcheck)]
// TODO(#4990): Make deadline a u64 once the neccessary traits are implemented
fn _initialize(donation_token: AztecAddress, operator: AztecAddress, deadline: Field) {
storage.donation_token.initialize(donation_token);
storage.operator.initialize(operator);
storage.deadline.initialize(deadline);
}

#[aztec(public)]
#[aztec(internal)]
fn _check_deadline() {
// TODO(#4990): Remove the cast here once u64 is used directly
let deadline = storage.deadline.read() as u64;
assert(context.timestamp() as u64 < deadline, "Deadline has passed");
}

#[aztec(private)]
fn donate(amount: u64) {
// 1) Check that the deadline has not passed
context.call_public_function(
context.this_address(),
FunctionSelector::from_signature("_check_deadline()"),
[]
);

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

// 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);
}

// 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
let donation_token = Token::at(storage.donation_token.read_private());
donation_token.transfer(&mut context, context.this_address(), operator_address, amount as Field, 0);

// 3) Emit an unencrypted event so that anyone can audit how much the operator has withdrawn
let event = WithdrawalProcessed { amount, who: operator_address };
emit_unencrypted_log_from_private(&mut context, event.serialize());
}
}
4 changes: 4 additions & 0 deletions yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export abstract class BaseWallet implements Wallet {
getNotes(filter: NoteFilter): Promise<ExtendedNote[]> {
return this.pxe.getNotes(filter);
}
// TODO(#4956): Un-expose this
getNoteNonces(note: ExtendedNote): Promise<Fr[]> {
return this.pxe.getNoteNonces(note);
}
getPublicStorageAt(contract: AztecAddress, storageSlot: Fr): Promise<any> {
return this.pxe.getPublicStorageAt(contract, storageSlot);
}
Expand Down
9 changes: 9 additions & 0 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ export interface PXE {
*/
getNotes(filter: NoteFilter): Promise<ExtendedNote[]>;

/**
* Finds the nonce(s) for a given note.
* @param note - The note to find the nonces for.
* @returns The nonces of the note.
* @remarks More than a single nonce may be returned since there might be more than one nonce for a given note.
* TODO(#4956): Un-expose this
*/
getNoteNonces(note: ExtendedNote): Promise<Fr[]>;

/**
* Adds a note to the database.
* @throws If the note hash of the note doesn't exist in the tree.
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/end-to-end/src/cli_docs_sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,11 @@ AppSubscriptionContractArtifact
BenchmarkingContractArtifact
CardGameContractArtifact
ChildContractArtifact
ClaimContractArtifact
ContractClassRegistererContractArtifact
ContractInstanceDeployerContractArtifact
CounterContractArtifact
CrowdfundingContractArtifact
DelegatedOnContractArtifact
DelegatorContractArtifact
DocsExampleContractArtifact
Expand Down
Loading

0 comments on commit ba3aff2

Please sign in to comment.