diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 6bbe50afe66d..c0d18d96b91c 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -33,7 +33,6 @@ members = [ "contracts/pending_note_hashes_contract", "contracts/price_feed_contract", "contracts/private_fpc_contract", - "contracts/token_with_refunds_contract", "contracts/schnorr_account_contract", "contracts/schnorr_hardcoded_account_contract", "contracts/schnorr_single_key_account_contract", diff --git a/noir-projects/noir-contracts/contracts/private_fpc_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/private_fpc_contract/Nargo.toml index 4f07243c139d..d4bcacb4799f 100644 --- a/noir-projects/noir-contracts/contracts/private_fpc_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/private_fpc_contract/Nargo.toml @@ -7,4 +7,4 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } authwit = { path = "../../../aztec-nr/authwit" } -token_with_refunds = { path = "../token_with_refunds_contract" } +token = { path = "../token_contract" } diff --git a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr index 7753e5cd49d3..56e589c9a452 100644 --- a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr @@ -2,7 +2,7 @@ mod settings; contract PrivateFPC { use dep::aztec::{protocol_types::{address::AztecAddress, hash::compute_siloed_nullifier}, state_vars::SharedImmutable}; - use dep::token_with_refunds::TokenWithRefunds; + use dep::token::Token; use crate::settings::Settings; #[aztec(storage)] @@ -25,14 +25,14 @@ contract PrivateFPC { assert(asset == settings.other_asset); // We use different randomness for fee payer to prevent a potential privacy leak (see description - // of `setup_refund(...)` function in TokenWithRefunds for details. + // of `setup_refund(...)` function in Token for details. let fee_payer_randomness = compute_siloed_nullifier(context.this_address(), user_randomness); // We emit fee payer randomness as nullifier to ensure FPC admin can reconstruct their fee note - note that // protocol circuits will perform the siloing as was done above and hence the final nullifier will be correct // fee payer randomness. context.push_nullifier(user_randomness); - TokenWithRefunds::at(asset).setup_refund( + Token::at(asset).setup_refund( settings.admin, context.msg_sender(), amount, diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index ae0e2de42699..253a39a4b4ed 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -17,7 +17,10 @@ contract Token { use dep::aztec::{ context::{PrivateContext, PrivateCallInterface}, hash::compute_secret_hash, - prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, + prelude::{ + NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress, + FunctionSelector, NoteHeader, Point + }, encrypted_logs::{ encrypted_note_emission::{ encode_and_encrypt_note, encode_and_encrypt_note_with_keys, @@ -32,7 +35,10 @@ contract Token { use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; // docs:end:import_authwit - use crate::types::{transparent_note::TransparentNote, token_note::{TokenNote, TOKEN_NOTE_LEN}, balances_map::BalancesMap}; + use crate::types::{ + transparent_note::TransparentNote, + token_note::{TokenNote, TOKEN_NOTE_LEN, TokenNoteHidingPoint}, balances_map::BalancesMap + }; // docs:end::imports // In the first transfer iteration we are computing a lot of additional information (validating inputs, retrieving @@ -478,6 +484,135 @@ contract Token { } // docs:end:burn + /// We need to use different randomness for the user and for the fee payer notes because if the randomness values + /// were the same we could fingerprint the user by doing the following: + /// 1) randomness_influence = fee_payer_point - G_npk * fee_payer_npk = + /// = (G_npk * fee_payer_npk + G_rnd * randomness) - G_npk * fee_payer_npk = + /// = G_rnd * randomness + /// 2) user_fingerprint = user_point - randomness_influence = + /// = (G_npk * user_npk + G_rnd * randomness) - G_rnd * randomness = + /// = G_npk * user_npk + /// 3) Then the second time the user would use this fee paying contract we would recover the same fingerprint + /// and link that the 2 transactions were made by the same user. Given that it's expected that only + /// a limited set of fee paying contracts will be used and they will be known, searching for fingerprints + /// by trying different fee payer npk values of these known contracts is a feasible attack. + /// + /// `fee_payer_point` and `user_point` above are public information because they are passed as args to the public + /// `complete_refund(...)` function. + #[aztec(private)] + fn setup_refund( + fee_payer: AztecAddress, // Address of the entity which will receive the fee note. + user: AztecAddress, // A user for which we are setting up the fee refund. + funded_amount: Field, // The amount the user funded the fee payer with (represents fee limit). + user_randomness: Field, // A randomness to mix in with the generated refund note for the sponsored user. + fee_payer_randomness: Field // A randomness to mix in with the generated fee note for the fee payer. + ) { + // 1. This function is called by fee paying contract (fee_payer) when setting up a refund so we need to support + // the authwit flow here and check that the user really permitted fee_payer to set up a refund on their behalf. + assert_current_call_valid_authwit(&mut context, user); + + // 2. Get all the relevant keys + let header = context.get_header(); + + let fee_payer_npk_m_hash = get_current_public_keys(&mut context, fee_payer).npk_m.hash(); + let user_keys = get_current_public_keys(&mut context, user); + let user_npk_m_hash = user_keys.npk_m.hash(); + + // 3. Deduct the funded amount from the user's balance - this is a maximum fee a user is willing to pay + // (called fee limit in aztec spec). The difference between fee limit and the actual tx fee will be refunded + // to the user in the `complete_refund(...)` function. + let change = subtract_balance( + &mut context, + storage, + user, + U128::from_integer(funded_amount), + INITIAL_TRANSFER_CALL_MAX_NOTES + ); + storage.balances.add(user, change).emit( + encode_and_encrypt_note_with_keys_unconstrained(&mut context, user_keys.ovpk_m, user_keys.ivpk_m, user) + ); + + // 4. We create the partial notes for the fee payer and the user. + // --> Called "partial" because they don't have the amount set yet (that will be done in `complete_refund(...)`). + let fee_payer_partial_note = TokenNote { + header: NoteHeader { + contract_address: AztecAddress::zero(), + nonce: 0, + storage_slot: storage.balances.map.at(fee_payer).storage_slot, + note_hash_counter: 0 + }, + amount: U128::zero(), + npk_m_hash: fee_payer_npk_m_hash, + randomness: fee_payer_randomness + }; + let user_partial_note = TokenNote { + header: NoteHeader { + contract_address: AztecAddress::zero(), + nonce: 0, + storage_slot: storage.balances.map.at(user).storage_slot, + note_hash_counter: 0 + }, + amount: U128::zero(), + npk_m_hash: user_npk_m_hash, + randomness: user_randomness + }; + + // 5. Now we get the note hiding points. + let mut fee_payer_point = fee_payer_partial_note.to_note_hiding_point(); + let mut user_point = user_partial_note.to_note_hiding_point(); + + // 6. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public + // function has access to the final transaction fee, which is needed to compute the actual refund amount. + context.set_public_teardown_function( + context.this_address(), + FunctionSelector::from_signature("complete_refund(((Field,Field,bool)),((Field,Field,bool)),Field)"), + [ + fee_payer_point.inner.x, fee_payer_point.inner.y, fee_payer_point.inner.is_infinite as Field, user_point.inner.x, user_point.inner.y, user_point.inner.is_infinite as Field, funded_amount + ] + ); + } + + // TODO(#7728): even though the funded_amount should be a U128, we can't have that type in a contract interface due + // to serialization issues. + #[aztec(public)] + #[aztec(internal)] + fn complete_refund( + // TODO(#7771): the following makes macros crash --> try getting it work once we migrate to metaprogramming + // mut fee_payer_point: TokenNoteHidingPoint, + // mut user_point: TokenNoteHidingPoint, + fee_payer_point_immutable: TokenNoteHidingPoint, + user_point_immutable: TokenNoteHidingPoint, + funded_amount: Field + ) { + // TODO(#7771): nuke the following 2 lines once we have mutable args + let mut fee_payer_point = fee_payer_point_immutable; + let mut user_point = user_point_immutable; + + // TODO(#7728): Remove the next line + let funded_amount = U128::from_integer(funded_amount); + let tx_fee = U128::from_integer(context.transaction_fee()); + + // 1. We check that user funded the fee payer contract with at least the transaction fee. + // TODO(#7796): we should try to prevent reverts here + assert(funded_amount >= tx_fee, "funded amount not enough to cover tx fee"); + + // 2. We compute the refund amount as the difference between funded amount and tx fee. + let refund_amount = funded_amount - tx_fee; + + // 3. We add fee to the fee payer point and refund amount to the user point. + fee_payer_point.add_amount(tx_fee); + user_point.add_amount(refund_amount); + + // 4. We finalize the hiding points to get the note hashes. + let fee_payer_note_hash = fee_payer_point.finalize(); + let user_note_hash = user_point.finalize(); + + // 5. At last we emit the note hashes. + context.push_note_hash(fee_payer_note_hash); + context.push_note_hash(user_note_hash); + // --> Once the tx is settled user and fee recipient can add the notes to their pixies. + } + /// Internal /// // docs:start:increase_public_balance diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr index cf797ce3bcce..7d23b6d81c01 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr @@ -3,6 +3,7 @@ mod burn; mod utils; mod transfer_public; mod transfer_private; +mod refunds; mod unshielding; mod minting; mod reading_constants; diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/refunds.nr similarity index 86% rename from noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr rename to noir-projects/noir-contracts/contracts/token_contract/src/test/refunds.nr index e12998f965d8..8bd9b5763b81 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/basic.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/refunds.nr @@ -1,4 +1,4 @@ -use crate::{test::utils, TokenWithRefunds, types::token_note::TokenNote}; +use crate::{test::utils, Token, types::token_note::TokenNote}; use dep::aztec::{ test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, hash::compute_secret_hash, @@ -11,7 +11,7 @@ use dep::authwit::cheatcodes as authwit_cheatcodes; unconstrained fn setup_refund_success() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(true); - // Renaming owner and recipient to match naming in TokenWithRefunds + // Renaming owner and recipient to match naming in Token let user = owner; let fee_payer = recipient; @@ -20,7 +20,7 @@ unconstrained fn setup_refund_success() { let fee_payer_randomness = 123; let mut context = env.private(); - let setup_refund_from_call_interface = TokenWithRefunds::at(token_contract_address).setup_refund( + let setup_refund_from_call_interface = Token::at(token_contract_address).setup_refund( fee_payer, user, funded_amount, @@ -37,8 +37,8 @@ unconstrained fn setup_refund_success() { let user_npk_m_hash = get_current_public_keys(&mut context, user).npk_m.hash(); let fee_payer_npk_m_hash = get_current_public_keys(&mut context, fee_payer).npk_m.hash(); - let fee_payer_balances_slot = derive_storage_slot_in_map(TokenWithRefunds::storage().balances.slot, fee_payer); - let user_balances_slot = derive_storage_slot_in_map(TokenWithRefunds::storage().balances.slot, user); + let fee_payer_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, fee_payer); + let user_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, user); // When the refund was set up, we would've spent the note worth mint_amount, and inserted a note worth //`mint_amount - funded_amount`. When completing the refund, we would've constructed a hash corresponding to a note @@ -76,7 +76,7 @@ unconstrained fn setup_refund_success() { unconstrained fn setup_refund_insufficient_funded_amount() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(true); - // Renaming owner and recipient to match naming in TokenWithRefunds + // Renaming owner and recipient to match naming in Token let user = owner; let fee_payer = recipient; @@ -86,7 +86,7 @@ unconstrained fn setup_refund_insufficient_funded_amount() { let fee_payer_randomness = 123; let mut context = env.private(); - let setup_refund_from_call_interface = TokenWithRefunds::at(token_contract_address).setup_refund( + let setup_refund_from_call_interface = Token::at(token_contract_address).setup_refund( fee_payer, user, funded_amount, diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr index a3a42c36a437..4be78977dbfa 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr @@ -1,15 +1,13 @@ use dep::aztec::{ - prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext}, - protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash_with_separator}, + generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd, G_slot}, + prelude::{NoteHeader, NoteInterface, PrivateContext}, + protocol_types::{ + constants::GENERATOR_INDEX__NOTE_NULLIFIER, point::{Point, POINT_LENGTH}, scalar::Scalar, + hash::poseidon2_hash_with_separator, traits::Serialize +}, note::utils::compute_note_hash_for_nullify, oracle::unsafe_rand::unsafe_rand, keys::getters::get_nsk_app }; - -// TODO(#7738): Nuke the following imports -use dep::aztec::{ - generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd, G_slot}, - protocol_types::{point::Point, scalar::Scalar} -}; use dep::std::{embedded_curve_ops::multi_scalar_mul, hash::from_field_unsafe}; trait OwnedNote { @@ -18,7 +16,6 @@ trait OwnedNote { } global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. -// TOKEN_NOTE_LEN * 32 + 32(storage_slot as bytes) + 32(note_type_id as bytes) global TOKEN_NOTE_BYTES_LEN: Field = 3 * 32 + 64; #[aztec(note)] @@ -47,15 +44,9 @@ impl NoteInterface for TokenNote { fn compute_nullifier_without_context(self) -> Field { let note_hash_for_nullify = compute_note_hash_for_nullify(self); let secret = get_nsk_app(self.npk_m_hash); - poseidon2_hash_with_separator([ - note_hash_for_nullify, - secret, - ], - GENERATOR_INDEX__NOTE_NULLIFIER as Field, - ) + poseidon2_hash_with_separator([note_hash_for_nullify, secret],GENERATOR_INDEX__NOTE_NULLIFIER) } - // TODO(#7738): Nuke this function and have it auto-generated by macros fn compute_note_hiding_point(self) -> Point { assert(self.header.storage_slot != 0, "Storage slot must be set before computing note hiding point"); @@ -68,6 +59,10 @@ impl NoteInterface for TokenNote { let npk_m_hash_scalar = from_field_unsafe(self.npk_m_hash); let randomness_scalar = from_field_unsafe(self.randomness); let slot_scalar = from_field_unsafe(self.header.storage_slot); + // We compute the note hiding point as: + // `G_amt * amount + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot` + // instead of using pedersen or poseidon2 because it allows us to privately add and subtract from amount + // in public by leveraging homomorphism. multi_scalar_mul( [G_amt, G_npk, G_rnd, G_slot], [amount_scalar, npk_m_hash_scalar, randomness_scalar, slot_scalar] @@ -75,6 +70,60 @@ impl NoteInterface for TokenNote { } } +impl TokenNote { + // TODO: Merge this func with `compute_note_hiding_point`. I (benesjan) didn't do it in the initial PR to not have + // to modify macros and all the related funcs in it. + fn to_note_hiding_point(self) -> TokenNoteHidingPoint { + TokenNoteHidingPoint::new(self.compute_note_hiding_point()) + } +} + +struct TokenNoteHidingPoint { + inner: Point +} + +impl TokenNoteHidingPoint { + fn new(point: Point) -> Self { + Self { inner: point } + } + + fn add_amount(&mut self, amount: U128) { + // TODO(#7772): decompose amount with from_field_unsafe or constrain it fits into 1 limb + let amount_scalar = Scalar { lo: amount.to_integer(), hi: 0 }; + self.inner = multi_scalar_mul([G_amt], [amount_scalar]) + self.inner; + } + + fn add_npk_m_hash(&mut self, npk_m_hash: Field) { + self.inner = multi_scalar_mul([G_npk], [from_field_unsafe(npk_m_hash)]) + self.inner; + } + + fn add_randomness(&mut self, randomness: Field) { + self.inner = multi_scalar_mul([G_rnd], [from_field_unsafe(randomness)]) + self.inner; + } + + fn add_slot(&mut self, slot: Field) { + self.inner = multi_scalar_mul([G_slot], [from_field_unsafe(slot)]) + self.inner; + } + + fn finalize(self) -> Field { + self.inner.x + } +} + +impl Serialize for TokenNoteHidingPoint { + fn serialize(self) -> [Field; POINT_LENGTH] { + self.inner.serialize() + } +} + +impl Eq for TokenNote { + fn eq(self, other: Self) -> bool { + (self.amount == other.amount) & + (self.npk_m_hash == other.npk_m_hash) & + (self.randomness == other.randomness) + } +} + impl OwnedNote for TokenNote { fn new(amount: U128, owner_npk_m_hash: Field) -> Self { Self { @@ -89,11 +138,3 @@ impl OwnedNote for TokenNote { self.amount } } - -impl Eq for TokenNote { - fn eq(self, other: Self) -> bool { - (self.amount == other.amount) & - (self.npk_m_hash == other.npk_m_hash) & - (self.randomness == other.randomness) - } -} diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/Nargo.toml deleted file mode 100644 index a7f7c4b35868..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/Nargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "token_with_refunds_contract" -authors = [""] -compiler_version = ">=0.25.0" -type = "contract" - -[dependencies] -aztec = { path = "../../../aztec-nr/aztec" } -compressed_string = { path = "../../../aztec-nr/compressed-string" } -authwit = { path = "../../../aztec-nr/authwit" } diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr deleted file mode 100644 index e56d4f73623a..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr +++ /dev/null @@ -1,640 +0,0 @@ -// docs:start:token_all -// docs:start:imports -mod types; -mod test; - -// Copy of standard token contract enhanced with refund functionality. - -contract TokenWithRefunds { - // Libs - - use dep::compressed_string::FieldCompressedString; - - use dep::aztec::{ - context::{PrivateContext, PrivateCallInterface}, hash::compute_secret_hash, - prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, - encrypted_logs::{ - encrypted_note_emission::{ - encode_and_encrypt_note, encode_and_encrypt_note_with_keys, - encode_and_encrypt_note_with_keys_unconstrained - }, - encrypted_event_emission::{encode_and_encrypt_event, encode_and_encrypt_event_with_keys_unconstrained} - }, - keys::getters::get_current_public_keys - }; - - // docs:start:import_authwit - use dep::authwit::auth::{assert_current_call_valid_authwit, assert_current_call_valid_authwit_public, compute_authwit_nullifier}; - // docs:end:import_authwit - - use crate::types::{transparent_note::TransparentNote, token_note::{TokenNote, TOKEN_NOTE_LEN}, balances_map::BalancesMap}; - // docs:end::imports - - // In the first transfer iteration we are computing a lot of additional information (validating inputs, retrieving - // keys, etc.), so the gate count is already relatively high. We therefore only read a few notes to keep the happy - // case with few constraints. - global INITIAL_TRANSFER_CALL_MAX_NOTES = 2; - // All the recursive call does is nullify notes, meaning the gate count is low, but it is all constant overhead. We - // therefore read more notes than in the base case to increase the efficiency of the overhead, since this results in - // an overall small circuit regardless. - global RECURSIVE_TRANSFER_CALL_MAX_NOTES = 8; - - // TODO(#7425): Rename back to `Transfer2` - #[aztec(event)] - struct Transfer2 { - from: AztecAddress, - to: AztecAddress, - amount: Field, - } - - // docs:start:storage_struct - #[aztec(storage)] - struct Storage { - // docs:start:storage_admin - admin: PublicMutable, - // docs:end:storage_admin - // docs:start:storage_minters - minters: Map>, - // docs:end:storage_minters - // docs:start:storage_balances - balances: BalancesMap, - // docs:end:storage_balances - total_supply: PublicMutable, - // docs:start:storage_pending_shields - pending_shields: PrivateSet, - // docs:end:storage_pending_shields - public_balances: Map>, - symbol: SharedImmutable, - name: SharedImmutable, - // docs:start:storage_decimals - decimals: SharedImmutable, - // docs:end:storage_decimals - } - // docs:end:storage_struct - - // docs:start:constructor - #[aztec(public)] - #[aztec(initializer)] - fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) { - assert(!admin.is_zero(), "invalid admin"); - storage.admin.write(admin); - storage.minters.at(admin).write(true); - storage.name.initialize(FieldCompressedString::from_string(name)); - storage.symbol.initialize(FieldCompressedString::from_string(symbol)); - // docs:start:initialize_decimals - storage.decimals.initialize(decimals); - // docs:end:initialize_decimals - } - // docs:end:constructor - - // docs:start:set_admin - #[aztec(public)] - fn set_admin(new_admin: AztecAddress) { - assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin"); - // docs:start:write_admin - storage.admin.write(new_admin); - // docs:end:write_admin - } - // docs:end:set_admin - - #[aztec(public)] - #[aztec(view)] - fn public_get_name() -> pub FieldCompressedString { - storage.name.read_public() - } - - #[aztec(private)] - #[aztec(view)] - fn private_get_name() -> pub FieldCompressedString { - storage.name.read_private() - } - - #[aztec(public)] - #[aztec(view)] - fn public_get_symbol() -> pub FieldCompressedString { - storage.symbol.read_public() - } - - #[aztec(private)] - #[aztec(view)] - fn private_get_symbol() -> pub FieldCompressedString { - storage.symbol.read_private() - } - - #[aztec(public)] - #[aztec(view)] - fn public_get_decimals() -> pub u8 { - // docs:start:read_decimals_public - storage.decimals.read_public() - // docs:end:read_decimals_public - } - - #[aztec(private)] - #[aztec(view)] - fn private_get_decimals() -> pub u8 { - // docs:start:read_decimals_private - storage.decimals.read_private() - // docs:end:read_decimals_private - } - - // docs:start:admin - #[aztec(public)] - #[aztec(view)] - fn admin() -> Field { - storage.admin.read().to_field() - } - // docs:end:admin - - // docs:start:is_minter - #[aztec(public)] - #[aztec(view)] - fn is_minter(minter: AztecAddress) -> bool { - storage.minters.at(minter).read() - } - // docs:end:is_minter - - // docs:start:total_supply - #[aztec(public)] - #[aztec(view)] - fn total_supply() -> Field { - storage.total_supply.read().to_integer() - } - // docs:end:total_supply - - // docs:start:balance_of_public - #[aztec(public)] - #[aztec(view)] - fn balance_of_public(owner: AztecAddress) -> Field { - storage.public_balances.at(owner).read().to_integer() - } - // docs:end:balance_of_public - - // docs:start:set_minter - #[aztec(public)] - fn set_minter(minter: AztecAddress, approve: bool) { - // docs:start:read_admin - assert(storage.admin.read().eq(context.msg_sender()), "caller is not admin"); - // docs:end:read_admin - // docs:start:write_minter - storage.minters.at(minter).write(approve); - // docs:end:write_minter - } - // docs:end:set_minter - - // docs:start:mint_public - #[aztec(public)] - fn mint_public(to: AztecAddress, amount: Field) { - // docs:start:read_minter - assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter"); - // docs:end:read_minter - let amount = U128::from_integer(amount); - let new_balance = storage.public_balances.at(to).read().add(amount); - let supply = storage.total_supply.read().add(amount); - - storage.public_balances.at(to).write(new_balance); - storage.total_supply.write(supply); - } - // docs:end:mint_public - - // docs:start:mint_private - #[aztec(public)] - fn mint_private(amount: Field, secret_hash: Field) { - assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter"); - let pending_shields = storage.pending_shields; - let mut note = TransparentNote::new(amount, secret_hash); - let supply = storage.total_supply.read().add(U128::from_integer(amount)); - - storage.total_supply.write(supply); - // docs:start:insert_from_public - pending_shields.insert_from_public(&mut note); - // docs:end:insert_from_public - } - // docs:end:mint_private - - // TODO: Nuke this - test functions do not belong to token contract! - #[aztec(private)] - fn privately_mint_private_note(amount: Field) { - let caller = context.msg_sender(); - storage.balances.add(caller, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, caller, caller)); - - TokenWithRefunds::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context); - } - - #[aztec(public)] - #[aztec(internal)] - fn assert_minter_and_mint(minter: AztecAddress, amount: Field) { - assert(storage.minters.at(minter).read(), "caller is not minter"); - let supply = storage.total_supply.read() + U128::from_integer(amount); - storage.total_supply.write(supply); - } - - // docs:start:shield - #[aztec(public)] - fn shield(from: AztecAddress, amount: Field, secret_hash: Field, nonce: Field) { - if (!from.eq(context.msg_sender())) { - // The redeem is only spendable once, so we need to ensure that you cannot insert multiple shields from the same message. - assert_current_call_valid_authwit_public(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } - - let amount = U128::from_integer(amount); - let from_balance = storage.public_balances.at(from).read().sub(amount); - - let pending_shields = storage.pending_shields; - let mut note = TransparentNote::new(amount.to_field(), secret_hash); - - storage.public_balances.at(from).write(from_balance); - pending_shields.insert_from_public(&mut note); - } - // docs:end:shield - - // docs:start:transfer_public - #[aztec(public)] - fn transfer_public(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit_public(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } - - let amount = U128::from_integer(amount); - let from_balance = storage.public_balances.at(from).read().sub(amount); - storage.public_balances.at(from).write(from_balance); - - let to_balance = storage.public_balances.at(to).read().add(amount); - storage.public_balances.at(to).write(to_balance); - } - // docs:end:transfer_public - - // docs:start:burn_public - #[aztec(public)] - fn burn_public(from: AztecAddress, amount: Field, nonce: Field) { - // docs:start:assert_current_call_valid_authwit_public - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit_public(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } - // docs:end:assert_current_call_valid_authwit_public - - let amount = U128::from_integer(amount); - let from_balance = storage.public_balances.at(from).read().sub(amount); - storage.public_balances.at(from).write(from_balance); - - let new_supply = storage.total_supply.read().sub(amount); - storage.total_supply.write(new_supply); - } - // docs:end:burn_public - - // docs:start:redeem_shield - #[aztec(private)] - fn redeem_shield(to: AztecAddress, amount: Field, secret: Field) { - let secret_hash = compute_secret_hash(secret); - - // Pop 1 note (set_limit(1)) which has an amount stored in a field with index 0 (select(0, amount)) and - // a secret_hash stored in a field with index 1 (select(1, secret_hash)). - let mut options = NoteGetterOptions::new(); - options = options.select(TransparentNote::properties().amount, amount, Option::none()).select( - TransparentNote::properties().secret_hash, - secret_hash, - Option::none() - ).set_limit(1); - - let notes = storage.pending_shields.pop_notes(options); - assert(notes.len() == 1, "note not popped"); - - // Add the token note to user's balances set - // Note: Using context.msg_sender() as a sender below makes this incompatible with escrows because we send - // outgoing logs to that address and to send outgoing logs you need to get a hold of ovsk_m. - let from = context.msg_sender(); - storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, to)); - } - // docs:end:redeem_shield - - // docs:start:unshield - #[aztec(private)] - fn unshield(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } - - storage.balances.sub(from, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, from)); - - TokenWithRefunds::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context); - } - // docs:end:unshield - - // docs:start:transfer - #[aztec(private)] - fn transfer(to: AztecAddress, amount: Field) { - let from = context.msg_sender(); - - let from_keys = get_current_public_keys(&mut context, from); - let to_keys = get_current_public_keys(&mut context, to); - - let amount = U128::from_integer(amount); - - // We reduce `from`'s balance by amount by recursively removing notes over potentially multiple calls. This - // method keeps the gate count for each individual call low - reading too many notes at once could result in - // circuits in which proving is not feasible. - // Since the sum of the amounts in the notes we nullified was potentially larger than amount, we create a new - // note for `from` with the change amount, e.g. if `amount` is 10 and two notes are nullified with amounts 8 and - // 5, then the change will be 3 (since 8 + 5 - 10 = 3). - let change = subtract_balance( - &mut context, - storage, - from, - amount, - INITIAL_TRANSFER_CALL_MAX_NOTES - ); - - storage.balances.add(from, change).emit( - encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_keys.ovpk_m, from_keys.ivpk_m, from) - ); - - storage.balances.add(to, amount).emit( - encode_and_encrypt_note_with_keys_unconstrained(&mut context, from_keys.ovpk_m, to_keys.ivpk_m, to) - ); - - // We don't constrain encryption of the note log in `transfer` (unlike in `transfer_from`) because the transfer - // function is only designed to be used in situations where the event is not strictly necessary (e.g. payment to - // another person where the payment is considered to be successful when the other party successfully decrypts a - // note). - Transfer2 { from, to, amount: amount.to_field() }.emit( - encode_and_encrypt_event_with_keys_unconstrained(&mut context, from_keys.ovpk_m, to_keys.ivpk_m, to) - ); - } - // docs:end:transfer - - #[contract_library_method] - fn subtract_balance( - context: &mut PrivateContext, - storage: Storage<&mut PrivateContext>, - account: AztecAddress, - amount: U128, - max_notes: u32 - ) -> U128 { - let subtracted = storage.balances.try_sub(account, amount, max_notes); - - // Failing to subtract any amount means that the owner was unable to produce more notes that could be nullified. - // We could in some cases fail early inside try_sub if we detected that fewer notes than the maximum were - // returned and we were still unable to reach the target amount, but that'd make the code more complicated, and - // optimizing for the failure scenario is not as important. - assert(subtracted > U128::from_integer(0), "Balance too low"); - - if subtracted >= amount { - // We have achieved our goal of nullifying notes that add up to more than amount, so we return the change - subtracted - amount - } else { - // try_sub failed to nullify enough notes to reach the target amount, so we compute the amount remaining - // and try again. - let remaining = amount - subtracted; - compute_recurse_subtract_balance_call(*context, account, remaining).call(context) - } - } - - // TODO(#7729): apply no_predicates to the contract interface method directly instead of having to use a wrapper - // like we do here. - #[no_predicates] - #[contract_library_method] - fn compute_recurse_subtract_balance_call( - context: PrivateContext, - account: AztecAddress, - remaining: U128 - ) -> PrivateCallInterface<25, U128, (AztecAddress, Field)> { - TokenWithRefunds::at(context.this_address())._recurse_subtract_balance(account, remaining.to_field()) - } - - // TODO(#7728): even though the amount should be a U128, we can't have that type in a contract interface due to - // serialization issues. - #[aztec(internal)] - #[aztec(private)] - fn _recurse_subtract_balance(account: AztecAddress, amount: Field) -> U128 { - subtract_balance( - &mut context, - storage, - account, - U128::from_integer(amount), - RECURSIVE_TRANSFER_CALL_MAX_NOTES - ) - } - - /** - * Cancel a private authentication witness. - * @param inner_hash The inner hash of the authwit to cancel. - */ - // docs:start:cancel_authwit - #[aztec(private)] - fn cancel_authwit(inner_hash: Field) { - let on_behalf_of = context.msg_sender(); - let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); - context.push_nullifier(nullifier); - } - // docs:end:cancel_authwit - - // docs:start:transfer_from - #[aztec(private)] - fn transfer_from(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) { - // docs:start:assert_current_call_valid_authwit - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } - // docs:end:assert_current_call_valid_authwit - - let from_keys = get_current_public_keys(&mut context, from); - let to_keys = get_current_public_keys(&mut context, to); - - let amount = U128::from_integer(amount); - // docs:start:increase_private_balance - // docs:start:encrypted - storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_keys.ovpk_m, from_keys.ivpk_m, from)); - // docs:end:encrypted - // docs:end:increase_private_balance - storage.balances.add(to, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_keys.ovpk_m, to_keys.ivpk_m, to)); - } - // docs:end:transfer_from - - // docs:start:burn - #[aztec(private)] - fn burn(from: AztecAddress, amount: Field, nonce: Field) { - if (!from.eq(context.msg_sender())) { - assert_current_call_valid_authwit(&mut context, from); - } else { - assert(nonce == 0, "invalid nonce"); - } - - storage.balances.sub(from, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, from, from)); - - TokenWithRefunds::at(context.this_address())._reduce_total_supply(amount).enqueue(&mut context); - } - // docs:end:burn - - /// Internal /// - - // docs:start:increase_public_balance - #[aztec(public)] - #[aztec(internal)] - fn _increase_public_balance(to: AztecAddress, amount: Field) { - let new_balance = storage.public_balances.at(to).read().add(U128::from_integer(amount)); - storage.public_balances.at(to).write(new_balance); - } - // docs:end:increase_public_balance - - // docs:start:reduce_total_supply - #[aztec(public)] - #[aztec(internal)] - fn _reduce_total_supply(amount: Field) { - // Only to be called from burn. - let new_supply = storage.total_supply.read().sub(U128::from_integer(amount)); - storage.total_supply.write(new_supply); - } - // docs:end:reduce_total_supply - - /// Unconstrained /// - - // docs:start:balance_of_private - unconstrained fn balance_of_private(owner: AztecAddress) -> pub Field { - storage.balances.balance_of(owner).to_field() - } - // docs:end:balance_of_private - - // REFUNDS SPECIFIC FUNCTIONALITY FOLLOWS - use dep::aztec::prelude::{FunctionSelector, NoteHeader, Point}; - use crate::types::token_note::TokenNoteHidingPoint; - - /// We need to use different randomness for the user and for the fee payer notes because if the randomness values - /// were the same we could fingerprint the user by doing the following: - /// 1) randomness_influence = fee_payer_point - G_npk * fee_payer_npk = - /// = (G_npk * fee_payer_npk + G_rnd * randomness) - G_npk * fee_payer_npk = - /// = G_rnd * randomness - /// 2) user_fingerprint = user_point - randomness_influence = - /// = (G_npk * user_npk + G_rnd * randomness) - G_rnd * randomness = - /// = G_npk * user_npk - /// 3) Then the second time the user would use this fee paying contract we would recover the same fingerprint - /// and link that the 2 transactions were made by the same user. Given that it's expected that only - /// a limited set of fee paying contracts will be used and they will be known, searching for fingerprints - /// by trying different fee payer npk values of these known contracts is a feasible attack. - /// - /// `fee_payer_point` and `user_point` above are public information because they are passed as args to the public - /// `complete_refund(...)` function. - #[aztec(private)] - fn setup_refund( - fee_payer: AztecAddress, // Address of the entity which will receive the fee note. - user: AztecAddress, // A user for which we are setting up the fee refund. - funded_amount: Field, // The amount the user funded the fee payer with (represents fee limit). - user_randomness: Field, // A randomness to mix in with the generated refund note for the sponsored user. - fee_payer_randomness: Field // A randomness to mix in with the generated fee note for the fee payer. - ) { - // 1. This function is called by fee paying contract (fee_payer) when setting up a refund so we need to support - // the authwit flow here and check that the user really permitted fee_payer to set up a refund on their behalf. - assert_current_call_valid_authwit(&mut context, user); - - // 2. Get all the relevant keys - let header = context.get_header(); - - let fee_payer_npk_m_hash = get_current_public_keys(&mut context, fee_payer).npk_m.hash(); - let user_keys = get_current_public_keys(&mut context, user); - let user_npk_m_hash = user_keys.npk_m.hash(); - - // 3. Deduct the funded amount from the user's balance - this is a maximum fee a user is willing to pay - // (called fee limit in aztec spec). The difference between fee limit and the actual tx fee will be refunded - // to the user in the `complete_refund(...)` function. - let change = subtract_balance( - &mut context, - storage, - user, - U128::from_integer(funded_amount), - INITIAL_TRANSFER_CALL_MAX_NOTES - ); - storage.balances.add(user, change).emit( - encode_and_encrypt_note_with_keys_unconstrained(&mut context, user_keys.ovpk_m, user_keys.ivpk_m, user) - ); - - // 4. We create the partial notes for the fee payer and the user. - // --> Called "partial" because they don't have the amount set yet (that will be done in `complete_refund(...)`). - let fee_payer_partial_note = TokenNote { - header: NoteHeader { - contract_address: AztecAddress::zero(), - nonce: 0, - storage_slot: storage.balances.map.at(fee_payer).storage_slot, - note_hash_counter: 0 - }, - amount: U128::zero(), - npk_m_hash: fee_payer_npk_m_hash, - randomness: fee_payer_randomness - }; - let user_partial_note = TokenNote { - header: NoteHeader { - contract_address: AztecAddress::zero(), - nonce: 0, - storage_slot: storage.balances.map.at(user).storage_slot, - note_hash_counter: 0 - }, - amount: U128::zero(), - npk_m_hash: user_npk_m_hash, - randomness: user_randomness - }; - - // 5. Now we get the note hiding points. - let mut fee_payer_point = fee_payer_partial_note.to_note_hiding_point(); - let mut user_point = user_partial_note.to_note_hiding_point(); - - // 6. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public - // function has access to the final transaction fee, which is needed to compute the actual refund amount. - context.set_public_teardown_function( - context.this_address(), - FunctionSelector::from_signature("complete_refund(((Field,Field,bool)),((Field,Field,bool)),Field)"), - [ - fee_payer_point.inner.x, fee_payer_point.inner.y, fee_payer_point.inner.is_infinite as Field, user_point.inner.x, user_point.inner.y, user_point.inner.is_infinite as Field, funded_amount - ] - ); - } - - // TODO(#7728): even though the funded_amount should be a U128, we can't have that type in a contract interface due - // to serialization issues. - #[aztec(public)] - #[aztec(internal)] - fn complete_refund( - // TODO(#7771): the following makes macros crash --> try getting it work once we migrate to metaprogramming - // mut fee_payer_point: TokenNoteHidingPoint, - // mut user_point: TokenNoteHidingPoint, - fee_payer_point_immutable: TokenNoteHidingPoint, - user_point_immutable: TokenNoteHidingPoint, - funded_amount: Field - ) { - // TODO(#7771): nuke the following 2 lines once we have mutable args - let mut fee_payer_point = fee_payer_point_immutable; - let mut user_point = user_point_immutable; - - // TODO(#7728): Remove the next line - let funded_amount = U128::from_integer(funded_amount); - let tx_fee = U128::from_integer(context.transaction_fee()); - - // 1. We check that user funded the fee payer contract with at least the transaction fee. - // TODO(#7796): we should try to prevent reverts here - assert(funded_amount >= tx_fee, "funded amount not enough to cover tx fee"); - - // 2. We compute the refund amount as the difference between funded amount and tx fee. - let refund_amount = funded_amount - tx_fee; - - // 3. We add fee to the fee payer point and refund amount to the user point. - fee_payer_point.add_amount(tx_fee); - user_point.add_amount(refund_amount); - - // 4. We finalize the hiding points to get the note hashes. - let fee_payer_note_hash = fee_payer_point.finalize(); - let user_note_hash = user_point.finalize(); - - // 5. At last we emit the note hashes. - context.push_note_hash(fee_payer_note_hash); - context.push_note_hash(user_note_hash); - // --> Once the tx is settled user and fee recipient can add the notes to their pixies. - } - // END OF REFUNDS SPECIFIC FUNCTIONALITY -} -// docs:end:token_all \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test.nr deleted file mode 100644 index f606967d1ac1..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test.nr +++ /dev/null @@ -1,2 +0,0 @@ -mod basic; -mod utils; diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr deleted file mode 100644 index 770c87dba8dc..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/test/utils.nr +++ /dev/null @@ -1,79 +0,0 @@ -use dep::aztec::{ - hash::compute_secret_hash, prelude::AztecAddress, - test::helpers::{cheatcodes, test_environment::TestEnvironment}, - oracle::{execution::get_contract_address, unsafe_rand::unsafe_rand} -}; - -use crate::{types::{token_note::TokenNote, transparent_note::TransparentNote}, TokenWithRefunds}; - -pub fn setup(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress) { - // Setup env, generate keys - let mut env = TestEnvironment::new(); - let (owner, recipient) = if with_account_contracts { - let owner = env.create_account_contract(1); - let recipient = env.create_account_contract(2); - // Deploy canonical auth registry - let _auth_registry = env.deploy("./@auth_registry_contract", "AuthRegistry").without_initializer(); - (owner, recipient) - } else { - let owner = env.create_account(); - let recipient = env.create_account(); - (owner, recipient) - }; - - // Start the test in the account contract address - env.impersonate(owner); - - // Deploy token contract - let initializer_call_interface = TokenWithRefunds::interface().constructor( - owner, - "TestToken0000000000000000000000", - "TT00000000000000000000000000000", - 18 - ); - let token_contract = env.deploy_self("TokenWithRefunds").with_public_initializer(initializer_call_interface); - let token_contract_address = token_contract.to_address(); - env.advance_block_by(1); - (&mut env, token_contract_address, owner, recipient) -} - -pub fn setup_and_mint(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, Field) { - // Setup - let (env, token_contract_address, owner, recipient) = setup(with_account_contracts); - let mint_amount = 10000; - // Mint some tokens - let secret = unsafe_rand(); - let secret_hash = compute_secret_hash(secret); - let mint_private_call_interface = TokenWithRefunds::at(token_contract_address).mint_private(mint_amount, secret_hash); - env.call_public(mint_private_call_interface); - - let mint_public_call_interface = TokenWithRefunds::at(token_contract_address).mint_public(owner, mint_amount); - env.call_public(mint_public_call_interface); - - // Time travel so we can read keys from the registry - env.advance_block_by(6); - - // docs:start:txe_test_store_note - // Store a note in the cache so we can redeem it - env.store_note_in_cache( - &mut TransparentNote::new(mint_amount, secret_hash), - TokenWithRefunds::storage().pending_shields.slot, - token_contract_address - ); - // docs:end:txe_test_store_note - - // Redeem our shielded tokens - let redeem_shield_call_interface = TokenWithRefunds::at(token_contract_address).redeem_shield(owner, mint_amount, secret); - env.call_private_void(redeem_shield_call_interface); - - (env, token_contract_address, owner, recipient, mint_amount) -} - -pub fn check_private_balance(token_contract_address: AztecAddress, address: AztecAddress, address_amount: Field) { - let current_contract_address = get_contract_address(); - cheatcodes::set_contract_address(token_contract_address); - // Direct call to unconstrained - let balance_of_private = TokenWithRefunds::balance_of_private(address); - assert(balance_of_private == address_amount, "Private balance is not correct"); - cheatcodes::set_contract_address(current_contract_address); -} diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr deleted file mode 100644 index e162c8cae130..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types.nr +++ /dev/null @@ -1,3 +0,0 @@ -mod balances_map; -mod token_note; -mod transparent_note; diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr deleted file mode 100644 index 0fece6964379..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/balances_map.nr +++ /dev/null @@ -1,153 +0,0 @@ -use dep::aztec::prelude::{AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateSet, Map}; -use dep::aztec::{ - context::{PrivateContext, UnconstrainedContext}, - protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, - note::{ - note_getter::view_notes, note_getter_options::SortOrder, - note_emission::{NoteEmission, OuterNoteEmission} -}, - keys::getters::get_current_public_keys -}; -use crate::types::{token_note::{TokenNote, OwnedNote}}; - -struct BalancesMap { - map: Map, Context> -} - -impl BalancesMap { - pub fn new(context: Context, storage_slot: Field) -> Self { - assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); - Self { - map: Map::new( - context, - storage_slot, - |context, slot| PrivateSet::new(context, slot) - ) - } - } -} - -impl BalancesMap { - unconstrained pub fn balance_of( - self: Self, - owner: AztecAddress - ) -> U128 where T: NoteInterface + OwnedNote { - self.balance_of_with_offset(owner, 0) - } - - unconstrained pub fn balance_of_with_offset( - self: Self, - owner: AztecAddress, - offset: u32 - ) -> U128 where T: NoteInterface + OwnedNote { - let mut balance = U128::from_integer(0); - // docs:start:view_notes - let mut options = NoteViewerOptions::new(); - let notes = self.map.at(owner).view_notes(options.set_offset(offset)); - // docs:end:view_notes - for i in 0..options.limit { - if i < notes.len() { - balance = balance + notes.get_unchecked(i).get_amount(); - } - } - if (notes.len() == options.limit) { - balance = balance + self.balance_of_with_offset(owner, offset + options.limit); - } - - balance - } -} - -impl BalancesMap { - pub fn add( - self: Self, - owner: AztecAddress, - addend: U128 - ) -> OuterNoteEmission where T: NoteInterface + OwnedNote + Eq { - if addend == U128::from_integer(0) { - OuterNoteEmission::new(Option::none()) - } else { - let context = self.map.context; - - // We fetch the nullifier public key hash from the registry / from our PXE - let owner_npk_m_hash = get_current_public_keys(context, owner).npk_m.hash(); - let mut addend_note = T::new(addend, owner_npk_m_hash); - - // docs:start:insert - OuterNoteEmission::new(Option::some(self.map.at(owner).insert(&mut addend_note))) - // docs:end:insert - } - } - - pub fn sub( - self: Self, - owner: AztecAddress, - amount: U128 - ) -> OuterNoteEmission where T: NoteInterface + OwnedNote + Eq { - let subtracted = self.try_sub(owner, amount, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL); - - // try_sub may have substracted more or less than amount. We must ensure that we subtracted at least as much as - // we needed, and then create a new note for the owner for the change (if any). - assert(subtracted >= amount, "Balance too low"); - self.add(owner, subtracted - amount) - } - - // Attempts to remove 'target_amount' from the owner's balance. try_sub returns how much was actually subtracted - // (i.e. the sum of the value of nullified notes), but this subtracted amount may be more or less than the target - // amount. - // This may seem odd, but is unfortunately unavoidable due to the number of notes available and their amounts being - // unknown. What try_sub does is a best-effort attempt to consume as few notes as possible that add up to more than - // `target_amount`. - // The `max_notes` parameter is used to fine-tune the number of constraints created by this function. The gate count - // scales relatively linearly with `max_notes`, but a lower `max_notes` parameter increases the likelihood of - // `try_sub` subtracting an amount smaller than `target_amount`. - pub fn try_sub( - self: Self, - owner: AztecAddress, - target_amount: U128, - max_notes: u32 - ) -> U128 where T: NoteInterface + OwnedNote + Eq { - // We are using a preprocessor here (filter applied in an unconstrained context) instead of a filter because - // we do not need to prove correct execution of the preprocessor. - // Because the `min_sum` notes is not constrained, users could choose to e.g. not call it. However, all this - // might result in is simply higher DA costs due to more nullifiers being emitted. Since we don't care - // about proving optimal note usage, we can save these constraints and make the circuit smaller. - let options = NoteGetterOptions::with_preprocessor(preprocess_notes_min_sum, target_amount).set_limit(max_notes); - let notes = self.map.at(owner).pop_notes(options); - - let mut subtracted = U128::from_integer(0); - for i in 0..options.limit { - if i < notes.len() { - let note = notes.get_unchecked(i); - subtracted = subtracted + note.get_amount(); - } - } - - subtracted - } -} - -// Computes the partial sum of the notes array, stopping once 'min_sum' is reached. This can be used to minimize the -// number of notes read that add to some value, e.g. when transferring some amount of tokens. -// The preprocessor (a filter applied in an unconstrained context) does not check if total sum is larger or equal to -// 'min_sum' - all it does is remove extra notes if it does reach that value. -// Note that proper usage of this preprocessor requires for notes to be sorted in descending order. -pub fn preprocess_notes_min_sum( - notes: [Option; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL], - min_sum: U128 -) -> [Option; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] where T: NoteInterface + OwnedNote { - let mut selected = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; - let mut sum = U128::from_integer(0); - for i in 0..notes.len() { - // Because we process notes in retrieved order, notes need to be sorted in descending amount order for this - // filter to be useful. Consider a 'min_sum' of 4, and a set of notes with amounts [3, 2, 1, 1, 1, 1, 1]. If - // sorted in descending order, the filter will only choose the notes with values 3 and 2, but if sorted in - // ascending order it will choose 4 notes of value 1. - if notes[i].is_some() & sum < min_sum { - let note = notes[i].unwrap_unchecked(); - selected[i] = Option::some(note); - sum = sum.add(note.get_amount()); - } - } - selected -} diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr deleted file mode 100644 index 4be78977dbfa..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/token_note.nr +++ /dev/null @@ -1,140 +0,0 @@ -use dep::aztec::{ - generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd, G_slot}, - prelude::{NoteHeader, NoteInterface, PrivateContext}, - protocol_types::{ - constants::GENERATOR_INDEX__NOTE_NULLIFIER, point::{Point, POINT_LENGTH}, scalar::Scalar, - hash::poseidon2_hash_with_separator, traits::Serialize -}, - note::utils::compute_note_hash_for_nullify, oracle::unsafe_rand::unsafe_rand, - keys::getters::get_nsk_app -}; -use dep::std::{embedded_curve_ops::multi_scalar_mul, hash::from_field_unsafe}; - -trait OwnedNote { - fn new(amount: U128, owner_npk_m_hash: Field) -> Self; - fn get_amount(self) -> U128; -} - -global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. -global TOKEN_NOTE_BYTES_LEN: Field = 3 * 32 + 64; - -#[aztec(note)] -struct TokenNote { - // The amount of tokens in the note - amount: U128, - // The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent. - npk_m_hash: Field, - // Randomness of the note to hide its contents - randomness: Field, -} - -impl NoteInterface for TokenNote { - // docs:start:nullifier - fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field { - let secret = context.request_nsk_app(self.npk_m_hash); - poseidon2_hash_with_separator([ - note_hash_for_nullify, - secret - ], - GENERATOR_INDEX__NOTE_NULLIFIER as Field, - ) - } - // docs:end:nullifier - - fn compute_nullifier_without_context(self) -> Field { - let note_hash_for_nullify = compute_note_hash_for_nullify(self); - let secret = get_nsk_app(self.npk_m_hash); - poseidon2_hash_with_separator([note_hash_for_nullify, secret],GENERATOR_INDEX__NOTE_NULLIFIER) - } - - fn compute_note_hiding_point(self) -> Point { - assert(self.header.storage_slot != 0, "Storage slot must be set before computing note hiding point"); - - // TODO(#7772): decompose amount with from_field_unsafe or constrain it fits into 1 limb - let amount_scalar = Scalar { - lo: self.amount.to_integer(), - hi: 0 - }; - // We use the unsafe version because the multi_scalar_mul will constrain the scalars. - let npk_m_hash_scalar = from_field_unsafe(self.npk_m_hash); - let randomness_scalar = from_field_unsafe(self.randomness); - let slot_scalar = from_field_unsafe(self.header.storage_slot); - // We compute the note hiding point as: - // `G_amt * amount + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot` - // instead of using pedersen or poseidon2 because it allows us to privately add and subtract from amount - // in public by leveraging homomorphism. - multi_scalar_mul( - [G_amt, G_npk, G_rnd, G_slot], - [amount_scalar, npk_m_hash_scalar, randomness_scalar, slot_scalar] - ) - } -} - -impl TokenNote { - // TODO: Merge this func with `compute_note_hiding_point`. I (benesjan) didn't do it in the initial PR to not have - // to modify macros and all the related funcs in it. - fn to_note_hiding_point(self) -> TokenNoteHidingPoint { - TokenNoteHidingPoint::new(self.compute_note_hiding_point()) - } -} - -struct TokenNoteHidingPoint { - inner: Point -} - -impl TokenNoteHidingPoint { - fn new(point: Point) -> Self { - Self { inner: point } - } - - fn add_amount(&mut self, amount: U128) { - // TODO(#7772): decompose amount with from_field_unsafe or constrain it fits into 1 limb - let amount_scalar = Scalar { lo: amount.to_integer(), hi: 0 }; - self.inner = multi_scalar_mul([G_amt], [amount_scalar]) + self.inner; - } - - fn add_npk_m_hash(&mut self, npk_m_hash: Field) { - self.inner = multi_scalar_mul([G_npk], [from_field_unsafe(npk_m_hash)]) + self.inner; - } - - fn add_randomness(&mut self, randomness: Field) { - self.inner = multi_scalar_mul([G_rnd], [from_field_unsafe(randomness)]) + self.inner; - } - - fn add_slot(&mut self, slot: Field) { - self.inner = multi_scalar_mul([G_slot], [from_field_unsafe(slot)]) + self.inner; - } - - fn finalize(self) -> Field { - self.inner.x - } -} - -impl Serialize for TokenNoteHidingPoint { - fn serialize(self) -> [Field; POINT_LENGTH] { - self.inner.serialize() - } -} - -impl Eq for TokenNote { - fn eq(self, other: Self) -> bool { - (self.amount == other.amount) & - (self.npk_m_hash == other.npk_m_hash) & - (self.randomness == other.randomness) - } -} - -impl OwnedNote for TokenNote { - fn new(amount: U128, owner_npk_m_hash: Field) -> Self { - Self { - amount, - npk_m_hash: owner_npk_m_hash, - randomness: unsafe_rand(), - header: NoteHeader::empty(), - } - } - - fn get_amount(self) -> U128 { - self.amount - } -} diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/transparent_note.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/transparent_note.nr deleted file mode 100644 index 1e543e3d4adf..000000000000 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/types/transparent_note.nr +++ /dev/null @@ -1,87 +0,0 @@ -// docs:start:token_types_all -use dep::aztec::{ - note::{note_getter_options::PropertySelector, utils::compute_note_hash_for_nullify}, - prelude::{NoteHeader, NoteInterface, PrivateContext}, - protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, hash::poseidon2_hash_with_separator} -}; - -global TRANSPARENT_NOTE_LEN: Field = 2; -// TRANSPARENT_NOTE_LEN * 32 + 32(storage_slot as bytes) + 32(note_type_id as bytes) -global TRANSPARENT_NOTE_BYTES_LEN: Field = 2 * 32 + 64; - -// Transparent note represents a note that is created in the clear (public execution), but can only be spent by those -// that know the preimage of the "secret_hash" (the secret). This is typically used when shielding a token balance. -// Owner of the tokens provides a "secret_hash" as an argument to the public "shield" function and then the tokens -// can be redeemed in private by presenting the preimage of the "secret_hash" (the secret). -#[aztec(note)] -struct TransparentNote { - amount: Field, - secret_hash: Field, -} - -struct TransparentNoteProperties { - amount: PropertySelector, - secret_hash: PropertySelector, -} - -impl NoteInterface for TransparentNote { - - // Custom serialization to avoid disclosing the secret field - fn serialize_content(self) -> [Field; TRANSPARENT_NOTE_LEN] { - [self.amount, self.secret_hash] - } - - // Custom deserialization since we don't have access to the secret plaintext - fn deserialize_content(serialized_note: [Field; TRANSPARENT_NOTE_LEN]) -> Self { - TransparentNote { - amount: serialized_note[0], - secret_hash: serialized_note[1], - header: NoteHeader::empty(), - } - } - - fn compute_nullifier(self, _context: &mut PrivateContext, _note_hash_for_nullify: Field) -> Field { - self.compute_nullifier_without_context() - } - - // Computing a nullifier in a transparent note is not guarded by making secret a part of the nullifier preimage (as - // is common in other cases) and instead is guarded by the functionality of "redeem_shield" function. There we do - // the following: - // 1) We pass the secret as an argument to the function and use it to compute a secret hash, - // 2) we fetch a note via the "get_notes" oracle which accepts the secret hash as an argument, - // 3) the "get_notes" oracle constrains that the secret hash in the returned note matches the one computed in - // circuit. - // This achieves that the note can only be spent by the party that knows the secret. - fn compute_nullifier_without_context(self) -> Field { - let note_hash_for_nullify = compute_note_hash_for_nullify(self); - poseidon2_hash_with_separator([ - note_hash_for_nullify, - ], - GENERATOR_INDEX__NOTE_NULLIFIER as Field, - ) - } -} - -impl TransparentNote { - // CONSTRUCTORS - pub fn new(amount: Field, secret_hash: Field) -> Self { - TransparentNote { amount, secret_hash, header: NoteHeader::empty() } - } - - // CUSTOM FUNCTIONS FOR THIS NOTE TYPE - // Custom serialization forces us to manually create the metadata struct and its getter - pub fn properties() -> TransparentNoteProperties { - TransparentNoteProperties { - amount: PropertySelector { index: 0, offset: 0, length: 32 }, - secret_hash: PropertySelector { index: 1, offset: 0, length: 32 } - } - } -} - -impl Eq for TransparentNote { - fn eq(self, other: Self) -> bool { - (self.amount == other.amount) & (self.secret_hash == other.secret_hash) - } -} - -// docs:end:token_types_all \ No newline at end of file diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index f284d5f1affe..29d838ca1afc 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -24,7 +24,7 @@ import { FPCContract, FeeJuiceContract, PrivateFPCContract, - TokenWithRefundsContract, + TokenContract, } from '@aztec/noir-contracts.js'; import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; @@ -70,7 +70,7 @@ export class FeesTest { public feeJuiceContract!: FeeJuiceContract; public bananaCoin!: BananaCoin; public bananaFPC!: FPCContract; - public tokenWithRefunds!: TokenWithRefundsContract; + public token!: TokenContract; public privateFPC!: PrivateFPCContract; public counterContract!: CounterContract; public subscriptionContract!: AppSubscriptionContract; @@ -80,7 +80,7 @@ export class FeesTest { public getGasBalanceFn!: BalancesFn; public getBananaPublicBalanceFn!: BalancesFn; public getBananaPrivateBalanceFn!: BalancesFn; - public getTokenWithRefundsBalanceFn!: BalancesFn; + public getTokenBalanceFn!: BalancesFn; public readonly INITIAL_GAS_BALANCE = BigInt(1e15); public readonly ALICE_INITIAL_BANANAS = BigInt(1e12); @@ -102,11 +102,11 @@ export class FeesTest { await this.snapshotManager.teardown(); } - /** Alice mints TokenWithRefunds */ - async mintTokenWithRefunds(amount: bigint) { - const balanceBefore = await this.tokenWithRefunds.methods.balance_of_private(this.aliceAddress).simulate(); - await this.tokenWithRefunds.methods.privately_mint_private_note(amount).send().wait(); - const balanceAfter = await this.tokenWithRefunds.methods.balance_of_private(this.aliceAddress).simulate(); + /** Alice mints Token */ + async mintToken(amount: bigint) { + const balanceBefore = await this.token.methods.balance_of_private(this.aliceAddress).simulate(); + await this.token.methods.privately_mint_private_note(amount).send().wait(); + const balanceAfter = await this.token.methods.balance_of_private(this.aliceAddress).simulate(); expect(balanceAfter).toEqual(balanceBefore + amount); } @@ -245,29 +245,23 @@ export class FeesTest { ); } - async applyTokenWithRefundsAndFPC() { + async applyTokenAndFPC() { await this.snapshotManager.snapshot( - 'token_with_refunds_and_private_fpc', + 'token_and_private_fpc', async context => { // Deploy token/fpc flavors for private refunds const feeJuiceContract = this.feeJuiceBridgeTestHarness.l2Token; expect(await context.pxe.isContractPubliclyDeployed(feeJuiceContract.address)).toBe(true); - const tokenWithRefunds = await TokenWithRefundsContract.deploy( - this.aliceWallet, - this.aliceAddress, - 'PVT', - 'PVT', - 18n, - ) + const token = await TokenContract.deploy(this.aliceWallet, this.aliceAddress, 'PVT', 'PVT', 18n) .send() .deployed(); - this.logger.info(`TokenWithRefunds deployed at ${tokenWithRefunds.address}`); + this.logger.info(`Token deployed at ${token.address}`); const privateFPCSent = PrivateFPCContract.deploy( this.bobWallet, - tokenWithRefunds.address, + token.address, this.bobWallet.getAddress(), ).send(); const privateFPC = await privateFPCSent.deployed(); @@ -280,20 +274,16 @@ export class FeesTest { ); return { - tokenWithRefundsAddress: tokenWithRefunds.address, + tokenAddress: token.address, privateFPCAddress: privateFPC.address, }; }, async data => { this.privateFPC = await PrivateFPCContract.at(data.privateFPCAddress, this.bobWallet); - this.tokenWithRefunds = await TokenWithRefundsContract.at(data.tokenWithRefundsAddress, this.aliceWallet); + this.token = await TokenContract.at(data.tokenAddress, this.aliceWallet); const logger = this.logger; - this.getTokenWithRefundsBalanceFn = getBalancesFn( - '🕵️.private', - this.tokenWithRefunds.methods.balance_of_private, - logger, - ); + this.getTokenBalanceFn = getBalancesFn('🕵️.private', this.token.methods.balance_of_private, logger); }, ); } @@ -362,7 +352,7 @@ export class FeesTest { await this.snapshotManager.snapshot( 'fund_alice_with_tokens', async () => { - await this.mintTokenWithRefunds(this.ALICE_INITIAL_BANANAS); + await this.mintToken(this.ALICE_INITIAL_BANANAS); }, () => Promise.resolve(), ); diff --git a/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts index 6f198f665399..417f83c4773b 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts @@ -10,7 +10,7 @@ import { import { Fr, type GasSettings } from '@aztec/circuits.js'; import { deriveStorageSlotInMap, siloNullifier } from '@aztec/circuits.js/hash'; import { FunctionSelector, FunctionType } from '@aztec/foundation/abi'; -import { type PrivateFPCContract, TokenWithRefundsContract } from '@aztec/noir-contracts.js'; +import { type PrivateFPCContract, TokenContract } from '@aztec/noir-contracts.js'; import { expectMapping } from '../fixtures/utils.js'; import { FeesTest } from './fees_test.js'; @@ -19,7 +19,7 @@ describe('e2e_fees/private_refunds', () => { let aliceWallet: AccountWallet; let aliceAddress: AztecAddress; let bobAddress: AztecAddress; - let tokenWithRefunds: TokenWithRefundsContract; + let token: TokenContract; let privateFPC: PrivateFPCContract; let initialAliceBalance: bigint; @@ -33,9 +33,9 @@ describe('e2e_fees/private_refunds', () => { await t.applyInitialAccountsSnapshot(); await t.applyPublicDeployAccountsSnapshot(); await t.applyDeployFeeJuiceSnapshot(); - await t.applyTokenWithRefundsAndFPC(); + await t.applyTokenAndFPC(); await t.applyFundAliceWithTokens(); - ({ aliceWallet, aliceAddress, bobAddress, privateFPC, tokenWithRefunds } = await t.setup()); + ({ aliceWallet, aliceAddress, bobAddress, privateFPC, token } = await t.setup()); t.logger.debug(`Alice address: ${aliceAddress}`); // We give Alice access to Bob's notes because Alice is used to check if balances are correct. @@ -48,7 +48,7 @@ describe('e2e_fees/private_refunds', () => { beforeEach(async () => { [[initialAliceBalance, initialBobBalance], [initialFPCGasBalance]] = await Promise.all([ - t.getTokenWithRefundsBalanceFn(aliceAddress, t.bobAddress), + t.getTokenBalanceFn(aliceAddress, t.bobAddress), t.getGasBalanceFn(privateFPC.address), ]); }); @@ -59,13 +59,13 @@ describe('e2e_fees/private_refunds', () => { const bobRandomness = siloNullifier(privateFPC.address, aliceRandomness); // Called fee_payer_randomness in contracts // 2. We call arbitrary `private_get_name(...)` function to check that the fee refund flow works. - const { txHash, transactionFee, debugInfo } = await tokenWithRefunds.methods + const { txHash, transactionFee, debugInfo } = await token.methods .private_get_name() .send({ fee: { gasSettings: t.gasSettings, paymentMethod: new PrivateRefundPaymentMethod( - tokenWithRefunds.address, + token.address, privateFPC.address, aliceWallet, aliceRandomness, @@ -97,9 +97,9 @@ describe('e2e_fees/private_refunds', () => { new ExtendedNote( aliceRefundNote, t.aliceAddress, - tokenWithRefunds.address, - deriveStorageSlotInMap(TokenWithRefundsContract.storage.balances.slot, t.aliceAddress), - TokenWithRefundsContract.notes.TokenNote.id, + token.address, + deriveStorageSlotInMap(TokenContract.storage.balances.slot, t.aliceAddress), + TokenContract.notes.TokenNote.id, txHash, ), ); @@ -116,9 +116,9 @@ describe('e2e_fees/private_refunds', () => { new ExtendedNote( bobFeeNote, t.bobAddress, - tokenWithRefunds.address, - deriveStorageSlotInMap(TokenWithRefundsContract.storage.balances.slot, t.bobAddress), - TokenWithRefundsContract.notes.TokenNote.id, + token.address, + deriveStorageSlotInMap(TokenContract.storage.balances.slot, t.bobAddress), + TokenContract.notes.TokenNote.id, txHash, ), ); @@ -127,7 +127,7 @@ describe('e2e_fees/private_refunds', () => { await expectMapping(t.getGasBalanceFn, [privateFPC.address], [initialFPCGasBalance - transactionFee!]); // ... and that the transaction fee was correctly transferred from Alice to Bob. await expectMapping( - t.getTokenWithRefundsBalanceFn, + t.getTokenBalanceFn, [aliceAddress, t.bobAddress], [initialAliceBalance - transactionFee!, initialBobBalance + transactionFee!], ); @@ -141,11 +141,11 @@ describe('e2e_fees/private_refunds', () => { // 2. We call arbitrary `private_get_name(...)` function to check that the fee refund flow works. await expect( - tokenWithRefunds.methods.private_get_name().prove({ + token.methods.private_get_name().prove({ fee: { gasSettings: t.gasSettings, paymentMethod: new PrivateRefundPaymentMethod( - tokenWithRefunds.address, + token.address, privateFPC.address, aliceWallet, aliceRandomness,