├── .env.example ├── src ├── escrow_contract │ ├── src │ │ ├── library.nr │ │ ├── types.nr │ │ ├── test.nr │ │ ├── test │ │ │ ├── test_logic_contract │ │ │ │ ├── src │ │ │ │ │ ├── test.nr │ │ │ │ │ ├── test │ │ │ │ │ │ ├── withdraw_nft.nr │ │ │ │ │ │ ├── withdraw.nr │ │ │ │ │ │ ├── share_escrow.nr │ │ │ │ │ │ ├── secret_keys_to_public_keys.nr │ │ │ │ │ │ ├── utils.nr │ │ │ │ │ │ └── get_escrow.nr │ │ │ │ │ └── main.nr │ │ │ │ └── Nargo.toml │ │ │ ├── withdraw_nft.nr │ │ │ ├── utils.nr │ │ │ └── withdraw.nr │ │ ├── types │ │ │ └── escrow_details_event.nr │ │ ├── main.nr │ │ └── library │ │ │ └── logic.nr │ ├── Nargo.toml │ └── README.md ├── nft_contract │ ├── src │ │ ├── types.nr │ │ ├── test.nr │ │ └── test │ │ │ ├── access_control.nr │ │ │ ├── view.nr │ │ │ ├── mint_to_public.nr │ │ │ ├── mint_to_private.nr │ │ │ ├── initialize_transfer_commitment.nr │ │ │ ├── burn_public.nr │ │ │ ├── transfer_public_to_private.nr │ │ │ ├── burn_private.nr │ │ │ ├── transfer_private_to_public.nr │ │ │ ├── transfer_public_to_public.nr │ │ │ ├── utils.nr │ │ │ └── transfer_private_to_private.nr │ └── Nargo.toml ├── token_contract │ ├── src │ │ ├── types.nr │ │ ├── test.nr │ │ ├── test │ │ │ ├── tokenized_vault │ │ │ │ ├── mod.nr │ │ │ │ ├── tokenized_vault_utils.nr │ │ │ │ └── deposit_public_to_public.nr │ │ │ ├── reading_constants.nr │ │ │ ├── initialize_transfer_commitment.nr │ │ │ ├── mint_to_private.nr │ │ │ ├── mint_to_public.nr │ │ │ ├── transfer_public_to_private.nr │ │ │ ├── transfer_private_to_commitment.nr │ │ │ ├── burn_public.nr │ │ │ ├── burn_private.nr │ │ │ ├── transfer_public_to_commitment.nr │ │ │ ├── mint_to_commitment.nr │ │ │ ├── transfer_public_to_public.nr │ │ │ └── transfer_private_to_private.nr │ │ └── types │ │ │ └── balance_set.nr │ └── Nargo.toml ├── dripper │ ├── Nargo.toml │ ├── src │ │ └── main.nr │ └── README.md └── deployments.json ├── .husky └── pre-commit ├── .github ├── pull_request_template.md └── workflows │ ├── tests-nr.yaml │ ├── tests-js.yaml │ ├── canary.yml │ ├── release.yml │ └── compare-benchmark.yml ├── .prettierrc ├── .gitignore ├── jest.integration.config.json ├── Nargo.toml ├── docker-compose.override.yml ├── tsconfig.json ├── .cursor └── rules │ └── Aztec │ ├── aztec-privacy.mdc │ ├── aztec-base.mdc │ ├── aztec-optimization.mdc │ ├── aztec-contract-patterns.mdc │ └── aztec-tests.mdc ├── LICENSE ├── package.json ├── benchmarks ├── nft_contract.benchmark.ts ├── logic_contract.benchmark.ts ├── escrow_contract.benchmark.ts └── token_contract.benchmark.ts └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | VERSION= 2 | BASE_PXE_URL= 3 | -------------------------------------------------------------------------------- /src/escrow_contract/src/library.nr: -------------------------------------------------------------------------------- 1 | pub mod logic; 2 | -------------------------------------------------------------------------------- /src/nft_contract/src/types.nr: -------------------------------------------------------------------------------- 1 | pub mod nft_note; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | aztec-nargo fmt 2 | yarn lint-staged 3 | -------------------------------------------------------------------------------- /src/token_contract/src/types.nr: -------------------------------------------------------------------------------- 1 | pub mod balance_set; 2 | -------------------------------------------------------------------------------- /src/escrow_contract/src/types.nr: -------------------------------------------------------------------------------- 1 | pub mod escrow_details_event; 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # 🤖 Linear 2 | 3 | Closes AZT-XXX 4 | 5 | ## Description -------------------------------------------------------------------------------- /src/escrow_contract/src/test.nr: -------------------------------------------------------------------------------- 1 | mod withdraw; 2 | mod withdraw_nft; 3 | 4 | pub mod utils; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "endOfLine": "lf", 5 | "printWidth": 120, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test.nr: -------------------------------------------------------------------------------- 1 | mod share_escrow; 2 | mod get_escrow; 3 | mod withdraw; 4 | mod withdraw_nft; 5 | mod secret_keys_to_public_keys; 6 | 7 | pub mod utils; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target/ 3 | artifacts/ 4 | log/ 5 | .vscode 6 | .DS_Store 7 | codegenCache.json 8 | .env 9 | benchmarks/*.json 10 | store 11 | store-* 12 | .cursor/* 13 | !.cursor/rules/ 14 | !.cursor/rules/** 15 | -------------------------------------------------------------------------------- /jest.integration.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "ts-jest/presets/default-esm", 3 | "transform": { 4 | "^.+\\.tsx?$": ["ts-jest", { "useESM": true }] 5 | }, 6 | "moduleNameMapper": { 7 | "^(\\.{1,2}/.*)\\.js$": "$1" 8 | }, 9 | "testRegex": "./src/.*\\.test\\.ts$", 10 | "rootDir": "./", 11 | "testTimeout": 30000 12 | } 13 | -------------------------------------------------------------------------------- /src/dripper/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dripper" 3 | authors = [""] 4 | compiler_version = ">=1.0.0" 5 | type = "contract" 6 | 7 | [dependencies] 8 | aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/aztec" } 9 | token = { path = "../token_contract" } 10 | -------------------------------------------------------------------------------- /src/escrow_contract/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "escrow_contract" 3 | authors = [""] 4 | compiler_version = ">=1.0.0" 5 | type = "contract" 6 | 7 | [dependencies] 8 | aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/aztec" } 9 | token = { path = "../token_contract" } 10 | nft = { path = "../nft_contract" } 11 | -------------------------------------------------------------------------------- /src/nft_contract/src/test.nr: -------------------------------------------------------------------------------- 1 | mod access_control; 2 | mod mint_to_public; 3 | mod mint_to_private; 4 | mod burn_public; 5 | mod burn_private; 6 | mod initialize_transfer_commitment; 7 | mod transfer_private_to_commitment; 8 | mod transfer_private_to_private; 9 | mod transfer_private_to_public_with_commitment; 10 | mod transfer_private_to_public; 11 | mod transfer_public_to_private; 12 | mod transfer_public_to_public; 13 | pub mod utils; 14 | mod view; 15 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/access_control.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | 4 | #[test] 5 | unconstrained fn nft_minter_is_set() { 6 | let (env, nft_contract_address, _, minter, _) = utils::setup_with_minter(false); 7 | env.public_context_at(nft_contract_address, |context| { 8 | let minter_address = context.storage_read(NFT::storage_layout().minter.slot); 9 | assert(minter_address == minter, "minter is not set"); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test_logic_contract" 3 | authors = [""] 4 | compiler_version = ">=1.0.0" 5 | type = "contract" 6 | 7 | [dependencies] 8 | aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/aztec" } 9 | escrow_contract = { path = "../../../" } 10 | token = { path = "../../../../token_contract" } 11 | nft = { path = "../../../../nft_contract" } 12 | -------------------------------------------------------------------------------- /Nargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/token_contract", 4 | "src/dripper", 5 | "src/nft_contract", 6 | "src/escrow_contract", 7 | "src/escrow_contract/src/test/test_logic_contract", 8 | ] 9 | 10 | [benchmark] 11 | token = "benchmarks/token_contract.benchmark.ts" 12 | nft = "benchmarks/nft_contract.benchmark.ts" 13 | tokenized_vault = "benchmarks/tokenized_vault_contract.benchmark.ts" 14 | escrow = "benchmarks/escrow_contract.benchmark.ts" 15 | logic = "benchmarks/logic_contract.benchmark.ts" 16 | -------------------------------------------------------------------------------- /src/token_contract/src/test.nr: -------------------------------------------------------------------------------- 1 | mod burn_private; 2 | mod burn_public; 3 | mod mint_to_private; 4 | mod mint_to_public; 5 | mod mint_to_commitment; 6 | mod initialize_transfer_commitment; 7 | // mod reading_constants; 8 | mod transfer_private_to_private; 9 | mod transfer_private_to_public; 10 | mod transfer_private_to_public_with_commitment; 11 | mod transfer_private_to_commitment; 12 | mod transfer_public_to_public; 13 | mod transfer_public_to_commitment; 14 | mod transfer_public_to_private; 15 | mod tokenized_vault; 16 | 17 | pub mod utils; 18 | -------------------------------------------------------------------------------- /src/nft_contract/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nft_contract" 3 | authors = [""] 4 | compiler_version = ">=1.0.0" 5 | type = "contract" 6 | 7 | [dependencies] 8 | aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/aztec" } 9 | compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/compressed-string" } 10 | contract_instance_registry = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/noir-contracts/contracts/protocol/contract_instance_registry" } 11 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | services: 2 | aztec: 3 | healthcheck: 4 | test: ["CMD", "curl", "-fsS", "http://aztec:8080/status"] 5 | interval: 1s 6 | timeout: 1s 7 | retries: 10 8 | 9 | alternative-pxe: 10 | image: "aztecprotocol/aztec:latest" 11 | ports: 12 | - "${PXE_PORT:-8081}:${PXE_PORT:-8081}" 13 | environment: 14 | LOG_LEVEL: '${LOG_LEVEL:-info; verbose: simulator:avm:debug_log}' 15 | HOST_WORKDIR: "${PWD}" 16 | VERSION: latest 17 | volumes: 18 | - ./pxe/log:/usr/src/yarn-project/aztec/log:rw 19 | depends_on: 20 | aztec: 21 | condition: service_healthy 22 | command: "start --port 8081 --pxe --pxe.nodeUrl=http://aztec:8080" 23 | -------------------------------------------------------------------------------- /src/token_contract/src/test/tokenized_vault/mod.nr: -------------------------------------------------------------------------------- 1 | mod deposit_public_to_public; 2 | mod deposit_public_to_private; 3 | mod deposit_private_to_private; 4 | mod deposit_private_to_public; 5 | mod deposit_public_to_private_exact; 6 | mod deposit_private_to_private_exact; 7 | mod issue_public_to_public; 8 | mod issue_public_to_private; 9 | mod issue_private_to_public_exact; 10 | mod issue_private_to_private_exact; 11 | mod redeem_public_to_public; 12 | mod redeem_private_to_public; 13 | mod redeem_private_to_private_exact; 14 | mod redeem_public_to_private_exact; 15 | mod withdraw_public_to_public; 16 | mod withdraw_public_to_private; 17 | mod withdraw_private_to_private; 18 | mod withdraw_private_to_public_exact; 19 | mod withdraw_private_to_private_exact; 20 | 21 | mod tokenized_vault_utils; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "dest", 5 | "tsBuildInfoFile": ".tsbuildinfo", 6 | "target": "es2020", 7 | "lib": ["esnext", "dom", "es2017.object"], 8 | "module": "NodeNext", 9 | "moduleResolution": "NodeNext", 10 | "strict": true, 11 | "declaration": true, 12 | "allowSyntheticDefaultImports": true, 13 | "allowJs": true, 14 | "esModuleInterop": true, 15 | "downlevelIteration": true, 16 | "inlineSourceMap": true, 17 | "declarationMap": true, 18 | "importHelpers": true, 19 | "resolveJsonModule": true, 20 | "composite": true, 21 | "skipLibCheck": true, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": ["src", "scripts/**/*", "src/**/*.json", "artifacts/**/*", "target/**/*"] 25 | } 26 | -------------------------------------------------------------------------------- /src/token_contract/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "token_contract" 3 | authors = [""] 4 | compiler_version = ">=1.0.0" 5 | type = "contract" 6 | 7 | [dependencies] 8 | aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/aztec" } 9 | uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/uint-note" } 10 | compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/aztec-nr/compressed-string" } 11 | contract_instance_registry = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.2", directory = "noir-projects/noir-contracts/contracts/protocol/contract_instance_registry" } 12 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/view.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use compressed_string::FieldCompressedString; 4 | 5 | #[test] 6 | unconstrained fn nft_name_is_set() { 7 | let (env, nft_contract_address, _, _, _) = utils::setup_with_minter(false); 8 | 9 | let name = env.view_public(NFT::at(nft_contract_address).public_get_name()); 10 | let expected_name = FieldCompressedString::from_string("TestNFT000000000000000000000000"); 11 | assert(name == expected_name, "name is not set correctly"); 12 | } 13 | 14 | #[test] 15 | unconstrained fn nft_symbol_is_set() { 16 | let (env, nft_contract_address, _, _, _) = utils::setup_with_minter(false); 17 | 18 | let symbol = env.view_public(NFT::at(nft_contract_address).public_get_symbol()); 19 | let expected_symbol = FieldCompressedString::from_string("TNFT000000000000000000000000000"); 20 | assert(symbol == expected_symbol, "symbol is not set correctly"); 21 | } 22 | -------------------------------------------------------------------------------- /src/token_contract/src/test/reading_constants.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils; 2 | use crate::Token; 3 | use aztec::test::helpers::cheatcodes; 4 | 5 | // It is not possible to deserialize strings in Noir ATM, so name and symbol cannot be checked yet. 6 | 7 | #[test] 8 | unconstrained fn check_decimals_private() { 9 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 10 | let (env, token_contract_address, _, _) = utils::setup(false); 11 | 12 | // Check decimals 13 | let result = env.view_public(Token::at(token_contract_address).private_get_decimals()); 14 | 15 | assert(result == 18); 16 | } 17 | 18 | #[test] 19 | unconstrained fn check_decimals_public() { 20 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 21 | let (env, token_contract_address, _, _) = utils::setup(false); 22 | 23 | // Check decimals 24 | let result = env.view_public(Token::at(token_contract_address).public_get_decimals()); 25 | 26 | assert(result == 18); 27 | } 28 | -------------------------------------------------------------------------------- /.cursor/rules/Aztec/aztec-privacy.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Aztec privacy and security guidelines 3 | globs: **/*.nr 4 | version: 1.0.0 5 | --- 6 | 7 | # Aztec Privacy & Security Guidelines 8 | 9 | ## Privacy Patterns 10 | - Implement both private and public versions of core functions 11 | - Use commitment pattern for privacy entrance (return Field from private functions) 12 | - Handle note encryption with encode_and_encrypt_note for recipient discovery 13 | - Separate private/public state transitions clearly 14 | - Use PartialUintNote for incomplete notes that complete in public 15 | 16 | ## Authorization 17 | - Always validate authorization with assert_current_call_valid_authwit 18 | - Implement dedicated validation functions: _validate_from_private, _validate_minter 19 | - Use authwit for both private and public contexts appropriately 20 | 21 | ## Note Management 22 | - Define note limits: INITIAL_TRANSFER_CALL_MAX_NOTES, RECURSIVE_TRANSFER_CALL_MAX_NOTES 23 | - Handle recursive balance operations when exceeding note limits 24 | - Always emit notes with proper encryption for recipient 25 | - Use preprocess_notes_min_sum for efficient note selection -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Wonderland 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.cursor/rules/Aztec/aztec-base.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Aztec Noir guidelines 3 | globs: **/*.nr 4 | version: 1.0.0 5 | --- 6 | 7 | # Aztec Noir Base Guidelines 8 | 9 | ## Code Structure 10 | - Order module declarations at the top (mod types; mod test;) 11 | - Import aztec macro immediately after module declarations 12 | - Inside contract: Aztec imports → External libraries → Custom types → Contract instances 13 | - Order functions: Initializers → Private → Public → View → Unconstrained/Utility → Internal 14 | - Use comment headers to separate function sections (// --------- Mintable ---------) 15 | 16 | ## Naming Conventions 17 | - Use snake_case for all variables and functions 18 | - All functions with the `#[contract_library_method]` attribute must be prefixed with `_` 19 | - Examples: `_increase_private_balance`, `_decrease_public_balance`, `_validate_minter` 20 | - Name transfers with clear directionality: transfer_private_to_public 21 | - Do NOT use underscore prefixes for regular internal functions 22 | - Suffix internal variants with _internal: increase_public_balance_internal 23 | - Use descriptive variable names: private_balances, public_owners, mint_amount -------------------------------------------------------------------------------- /.cursor/rules/Aztec/aztec-optimization.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Aztec gas and performance optimization guidelines 3 | globs: **/*.nr 4 | version: 1.0.0 5 | --- 6 | 7 | # Aztec Optimization Guidelines 8 | 9 | ## Note Efficiency 10 | - Set appropriate max notes limits (2 for initial, 8 for recursive typically) 11 | - Sort notes by value descending for optimal selection 12 | - Use recursive patterns for handling many notes 13 | - Minimize note reads with preprocess_notes_min_sum 14 | - Batch operations where possible to reduce note overhead 15 | 16 | ## Storage Optimization 17 | - Use PublicImmutable for deployment-time constants (name, symbol, decimals) 18 | - Use PublicMutable for changing state (balances, total_supply) 19 | - Pack related data into single storage slots where possible 20 | - Use Map for address-indexed storage 21 | - Prefer PrivateSet for private note collections 22 | 23 | ## Function Optimization 24 | - Extract common logic to #[contract_library_method] functions 25 | - Use internal function variants to avoid redundant validation 26 | - Minimize context switches between private and public 27 | - Batch public calls when transitioning from private -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test/withdraw_nft.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils as logic_utils; 2 | use crate::TestLogic; 3 | use escrow_contract::test::utils as escrow_utils; 4 | use nft::{NFT, test::utils as nft_utils}; 5 | 6 | // TODO: add test for successful withdraws via the logic contract. Currently it is not possible 7 | // because the address cannot be set in the salt nor we cannot modify the logic address to be the salt 8 | 9 | #[test(should_fail_with = "Not Authorized")] 10 | unconstrained fn withdraw_unauthorized() { 11 | let (mut env, escrow, _, nft, _, recipient, _minter) = 12 | escrow_utils::set_escrow_with_token_and_nft(false, 1); 13 | 14 | // Mint some tokens to escrow 15 | let token_id: Field = 1; 16 | env.call_private(_minter, NFT::at(nft).mint_to_private(escrow, token_id)); 17 | 18 | // Check the escrow received and can read the nft 19 | nft_utils::assert_owns_private_nft(env, nft, escrow, token_id); 20 | 21 | let logic = logic_utils::deploy_logic(&mut env, 1); 22 | 23 | let caller = env.create_light_account(); 24 | env.call_private(caller, TestLogic::at(logic).withdraw_nft(escrow, recipient, nft, token_id)); 25 | } 26 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test/withdraw.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils as logic_utils; 2 | use crate::TestLogic; 3 | use escrow_contract::test::utils as escrow_utils; 4 | use token::{test::utils as token_utils, Token}; 5 | 6 | // TODO: add test for successful withdraws via the logic contract. Currently it is not possible 7 | // because the address cannot be set in the salt nor we cannot modify the logic address to be the salt 8 | 9 | #[test(should_fail_with = "Not Authorized")] 10 | unconstrained fn withdraw_unauthorized() { 11 | let (mut env, escrow, token, _, _, recipient, minter) = 12 | escrow_utils::set_escrow_with_token_and_nft(false, 1); 13 | 14 | // Mint some tokens to escrow 15 | let amount: u128 = token_utils::mint_amount; 16 | env.call_private(minter, Token::at(token).mint_to_private(escrow, amount)); 17 | 18 | // Check the escrow received and can read the tokens 19 | token_utils::check_private_balance(env, token, escrow, amount); 20 | 21 | let logic = logic_utils::deploy_logic(&mut env, 1); 22 | 23 | let caller = env.create_light_account(); 24 | env.call_private(caller, TestLogic::at(logic).withdraw(escrow, recipient, token, amount)); 25 | } 26 | -------------------------------------------------------------------------------- /.cursor/rules/Aztec/aztec-contract-patterns.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Aztec contract design patterns 3 | globs: **/*.nr 4 | version: 1.0.0 5 | --- 6 | 7 | # Aztec Contract Design Patterns 8 | 9 | ## Contract Structure 10 | - Define storage struct with appropriate visibility modifiers 11 | - Group storage by type: immutable config, mutable state, private collections 12 | - Implement standard interfaces consistently (token, NFT patterns) 13 | - Use #[aztec] macro for contract definition 14 | - Apply appropriate function decorators: #[public], #[private], #[view], #[utility] 15 | 16 | ## Common Patterns 17 | - Commitment pattern: initialize_transfer_commitment returns Field 18 | - Validation pattern: separate _validate_* functions for reuse 19 | - Balance management: _increase_balance, _decrease_balance helpers 20 | - Recursive operations: recurse_subtract_balance_internal for note limits 21 | - Library methods: #[contract_library_method] for shared logic 22 | 23 | ## State Management 24 | - Private state uses note-based system (UintNote, NFTNote) 25 | - Public state uses traditional mappings 26 | - Support state transitions in both directions 27 | - Emit encrypted notes for private recipients 28 | - Complete partial notes in public context -------------------------------------------------------------------------------- /.cursor/rules/Aztec/aztec-tests.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Aztec test file guidelines 3 | globs: **/test/**/*.nr 4 | version: 1.0.0 5 | --- 6 | 7 | # Aztec Test Guidelines 8 | 9 | ## Test Organization 10 | - Create separate test files for each major functionality 11 | - Name test files by feature: mint_to_private.nr, transfer_public_to_public.nr 12 | - Use descriptive test names: mint_to_private_success, burn_public_fail_not_enough_balance 13 | - Group related tests in the same file 14 | 15 | ## Test Structure 16 | - Use utils module for common setup and assertions 17 | - Implement setup helpers: setup_with_minter, setup_with_initial_supply 18 | - Create balance checking utilities: check_private_balance, check_public_balance 19 | - Use TestEnvironment for consistent test isolation 20 | 21 | ## Test Patterns 22 | - Test both success and failure cases 23 | - Use #[test(should_fail_with = "error message")] for expected failures 24 | - Advance blocks after private operations: env.advance_block_by(1) 25 | - Impersonate users with env.impersonate(address) 26 | - Use the `authwit_cheatcodes` library for testing authorization cases 27 | - Always test both authorization success and failure scenarios 28 | 29 | ## Best Practices 30 | - Never skip tests - implement or remove them 31 | - Test edge cases explicitly (zero amounts, max values) 32 | - Verify state changes after operations 33 | - Test all privacy combinations (private→public, public→private, etc.) -------------------------------------------------------------------------------- /src/deployments.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "name": "WETH", 5 | "symbol": "WETH", 6 | "address": "0x2925b0b7212440baaace46ab05821ed589fad263fb5ff2243dd65eaaab84ab34", 7 | "decimals": 18, 8 | "minter": "0x1dd712303e81139c9ad77f15cd3a88a87946c5f821b78350bb9238122d9fe997", 9 | "upgrade_authority": "0x0000000000000000000000000000000000000000000000000000000000000000" 10 | }, 11 | { 12 | "name": "DAI", 13 | "symbol": "DAI", 14 | "address": "0x264f3d1dbe34e6428d5e958bb379bd290548ece1a2cd0a419127f4d9a16d2917", 15 | "decimals": 9, 16 | "minter": "0x1dd712303e81139c9ad77f15cd3a88a87946c5f821b78350bb9238122d9fe997", 17 | "upgrade_authority": "0x0000000000000000000000000000000000000000000000000000000000000000" 18 | }, 19 | { 20 | "name": "USDC", 21 | "symbol": "USDC", 22 | "address": "0x1d298948a788dee4992efad5300fc9520da043d0f18e925a2e8fc2bf7b83ee99", 23 | "decimals": 6, 24 | "minter": "0x1dd712303e81139c9ad77f15cd3a88a87946c5f821b78350bb9238122d9fe997", 25 | "upgrade_authority": "0x0000000000000000000000000000000000000000000000000000000000000000" 26 | } 27 | ], 28 | "dripper": { 29 | "address": "0x1dd712303e81139c9ad77f15cd3a88a87946c5f821b78350bb9238122d9fe997" 30 | } 31 | } -------------------------------------------------------------------------------- /src/dripper/src/main.nr: -------------------------------------------------------------------------------- 1 | use aztec::macros::aztec; 2 | 3 | #[aztec] 4 | pub contract Dripper { 5 | use aztec::macros::functions::{external, initializer}; 6 | use aztec::protocol_types::address::AztecAddress; 7 | use token::Token; 8 | 9 | #[external("public")] 10 | #[initializer] 11 | fn constructor() {} 12 | 13 | /// @notice Mints tokens into the public balance of the caller 14 | /// @dev Caller obtains `amount` tokens in their public balance 15 | /// @param token_address The address of the token contract 16 | /// @param amount The amount of tokens to mint (u64, converted to u128 internally) 17 | #[external("public")] 18 | fn drip_to_public(token_address: AztecAddress, amount: u64) { 19 | let token = Token::at(token_address); 20 | let msg_sender = context.msg_sender().unwrap(); 21 | token.mint_to_public(msg_sender, amount as u128).call(&mut context); 22 | } 23 | 24 | /// @notice Mints tokens into the private balance of the caller 25 | /// @dev Caller obtains `amount` tokens in their private balance 26 | /// @param token_address The address of the token contract 27 | /// @param amount The amount of tokens to mint (u64, converted to u128 internally) 28 | #[external("private")] 29 | fn drip_to_private(token_address: AztecAddress, amount: u64) { 30 | let token = Token::at(token_address); 31 | let msg_sender = context.msg_sender().unwrap(); 32 | token.mint_to_private(msg_sender, amount as u128).call(&mut context); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/dripper/README.md: -------------------------------------------------------------------------------- 1 | # Dripper Contract 2 | 3 | The `Dripper` contract provides a convenient faucet mechanism for minting tokens into private or public balances. Anyone can easily invoke the functions below to request tokens for testing or development purposes. 4 | 5 | > **Note**: This contract is designed for development and testing environments only. Do not use in production. 6 | 7 | ## Public Functions 8 | 9 | ### drip_to_public 10 | ```rust 11 | /// @notice Mints tokens into the public balance of the caller 12 | /// @dev Caller obtains `amount` tokens in their public balance 13 | /// @param token_address The address of the token contract 14 | /// @param amount The amount of tokens to mint (u64, converted to u128 internally) 15 | #[public] 16 | fn drip_to_public(token_address: AztecAddress, amount: u64) { /* ... */ } 17 | ``` 18 | 19 | ## Private Functions 20 | 21 | ### drip_to_private 22 | ```rust 23 | /// @notice Mints tokens into the private balance of the caller 24 | /// @dev Caller obtains `amount` tokens in their private balance 25 | /// @param token_address The address of the token contract 26 | /// @param amount The amount of tokens to mint (u64, converted to u128 internally) 27 | #[private] 28 | fn drip_to_private(token_address: AztecAddress, amount: u64) { /* ... */ } 29 | ``` 30 | 31 | ## Usage 32 | 33 | The Dripper contract is designed for testing and development environments where you need to quickly obtain tokens for experimentation. Simply call either function with the token contract address and the desired amount to receive tokens in your chosen balance type (public or private). 34 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test/share_escrow.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils as logic_utils; 2 | use crate::TestLogic; 3 | use aztec::{ 4 | oracle::get_contract_instance::get_contract_instance, protocol_types::traits::ToField, 5 | test::helpers::test_environment::TestEnvironment, 6 | }; 7 | use escrow_contract::test::utils as escrow_utils; 8 | 9 | // NOTE: This test is disabled because share_escrow validation requires the escrow salt 10 | // to match the logic contract address, which is not possible in v2.0.3 test environment. 11 | // The salt check will always fail with "Escrow salt mismatch". 12 | // Additionally, event emission modules are not accessible in Noir tests. 13 | // Full share_escrow functionality should be tested in TypeScript integration tests. 14 | // See: https://github.com/AztecProtocol/aztec-packages/issues/16656 15 | 16 | // #[test] 17 | #[allow(dead_code)] 18 | unconstrained fn share_escrow_success() { 19 | let mut env = TestEnvironment::new(); 20 | 21 | let recipient = env.create_light_account(); 22 | let sender = env.create_light_account(); 23 | 24 | let (_, msk, _, _) = logic_utils::get_test_vector(); 25 | 26 | let secret = 1; 27 | let escrow = escrow_utils::deploy_escrow_with_secret(&mut env, secret); 28 | 29 | // Get the actual escrow class id 30 | let escrow_instance = get_contract_instance(escrow); 31 | let escrow_class_id = escrow_instance.contract_class_id.to_field(); 32 | 33 | // Deploy logic contract with the correct escrow class id 34 | let logic = logic_utils::deploy_logic(&mut env, escrow_class_id); 35 | 36 | // This will fail with "Escrow salt mismatch" because we cannot make 37 | // the escrow salt equal the logic contract address in test environment 38 | env.call_private(sender, TestLogic::at(logic).share_escrow(recipient, escrow, msk)); 39 | } 40 | -------------------------------------------------------------------------------- /src/escrow_contract/src/types/escrow_details_event.nr: -------------------------------------------------------------------------------- 1 | use aztec::{ 2 | event::{event_interface::EventInterface, event_selector::EventSelector}, 3 | protocol_types::{address::AztecAddress, traits::{Serialize, ToField}}, 4 | }; 5 | 6 | /// @notice Master secret keys 7 | /// @dev These keys are used to derive the public keys for the escrow contract 8 | /// @param nsk_m Master Nullifier Secret Key 9 | /// @param ivsk_m Incoming Viewing Key 10 | /// @param ovsk_m Outgoing Viewing Key 11 | /// @param tsk_m Tagging Secret Key 12 | pub struct MasterSecretKeys { 13 | pub nsk_m: Field, 14 | pub ivsk_m: Field, 15 | pub ovsk_m: Field, 16 | pub tsk_m: Field, 17 | } 18 | 19 | impl Serialize for MasterSecretKeys { 20 | let N: u32 = 4; 21 | 22 | fn serialize(self) -> [Field; Self::N] { 23 | [self.nsk_m, self.ivsk_m, self.ovsk_m, self.tsk_m] 24 | } 25 | } 26 | 27 | /// @notice Escrow details log content 28 | /// @dev #[event] macro cannot be used in libraries, only in contracts. 29 | /// @param escrow The address of the escrow 30 | /// @param master_secret_keys The master secret keys 31 | pub struct EscrowDetailsLogContent { 32 | pub escrow: AztecAddress, 33 | pub master_secret_keys: MasterSecretKeys, 34 | } 35 | 36 | impl Serialize for EscrowDetailsLogContent { 37 | let N: u32 = 5; 38 | 39 | fn serialize(self) -> [Field; Self::N] { 40 | [ 41 | self.escrow.to_field(), 42 | self.master_secret_keys.nsk_m, 43 | self.master_secret_keys.ivsk_m, 44 | self.master_secret_keys.ovsk_m, 45 | self.master_secret_keys.tsk_m, 46 | ] 47 | } 48 | } 49 | 50 | impl EventInterface for EscrowDetailsLogContent { 51 | fn get_event_type_id() -> EventSelector { 52 | EventSelector::from_signature("EscrowDetailsLogContent((Field),(Field,Field,Field,Field))") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/mint_to_public.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | 4 | #[test] 5 | unconstrained fn nft_mint_to_public_success() { 6 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 7 | let (env, nft_contract_address, owner, minter, _) = utils::setup_with_minter(false); 8 | let token_id = 10000; 9 | 10 | env.call_public(minter, NFT::at(nft_contract_address).mint_to_public(owner, token_id)); 11 | 12 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 13 | } 14 | 15 | #[test(should_fail_with = "caller is not minter")] 16 | unconstrained fn nft_mint_to_public_fail_non_minter() { 17 | let (env, nft_contract_address, owner, _, recipient) = utils::setup_with_minter(false); 18 | 19 | let token_id = 10000; 20 | env.call_public(recipient, NFT::at(nft_contract_address).mint_to_public(owner, token_id)); 21 | } 22 | 23 | #[test(should_fail_with = "token already exists")] 24 | unconstrained fn nft_mint_to_public_fail_same_nft_twice() { 25 | let (env, nft_contract_address, owner, minter, _) = utils::setup_with_minter(false); 26 | 27 | let token_id = 10000; 28 | let mint_call_interface = NFT::at(nft_contract_address).mint_to_public(owner, token_id); 29 | env.call_public(minter, mint_call_interface); 30 | 31 | let actual_owner = env.view_public(NFT::at(nft_contract_address).public_owner_of(token_id)); 32 | assert(actual_owner == owner, "NFT not minted to correct owner"); 33 | 34 | utils::assert_nft_exists(env, nft_contract_address, token_id); 35 | 36 | // Second call should fail 37 | env.call_public(minter, mint_call_interface); 38 | } 39 | 40 | #[test(should_fail_with = "zero token ID not supported")] 41 | unconstrained fn nft_mint_to_public_fail_token_id_zero() { 42 | let (env, nft_contract_address, owner, minter, _) = utils::setup_with_minter(false); 43 | 44 | env.call_public(minter, NFT::at(nft_contract_address).mint_to_public(owner, 0)); 45 | } 46 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/mint_to_private.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::protocol_types::address::AztecAddress; 4 | 5 | #[test] 6 | unconstrained fn nft_mint_to_private_success() { 7 | let (env, nft_contract_address, owner, minter, _) = utils::setup_with_minter(false); 8 | 9 | let token_id = 10000; 10 | env.call_private(minter, NFT::at(nft_contract_address).mint_to_private(owner, token_id)); 11 | 12 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 13 | // Verify no public owner exists 14 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 15 | } 16 | 17 | #[test(should_fail_with = "caller is not minter")] 18 | unconstrained fn nft_mint_to_private_fail_non_minter() { 19 | let (env, nft_contract_address, owner, _, recipient) = utils::setup_with_minter(false); 20 | 21 | let token_id = 10000; 22 | env.call_private(recipient, NFT::at(nft_contract_address).mint_to_private(owner, token_id)); 23 | } 24 | 25 | #[test(should_fail_with = "token already exists")] 26 | unconstrained fn nft_mint_to_private_fail_same_nft_twice() { 27 | let (env, nft_contract_address, owner, minter, _) = utils::setup_with_minter(false); 28 | 29 | let token_id = 10000; 30 | let mint_call_interface = NFT::at(nft_contract_address).mint_to_private(owner, token_id); 31 | env.call_private(minter, mint_call_interface); 32 | 33 | // Verify the NFT was minted correctly 34 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 35 | utils::assert_nft_exists(env, nft_contract_address, token_id); 36 | 37 | // Second call should fail 38 | env.call_private(minter, mint_call_interface); 39 | } 40 | 41 | #[test(should_fail_with = "zero token ID not supported")] 42 | unconstrained fn nft_mint_to_private_fail_token_id_zero() { 43 | let (env, nft_contract_address, owner, minter, _) = utils::setup_with_minter(false); 44 | 45 | env.call_private(minter, NFT::at(nft_contract_address).mint_to_private(owner, 0)); 46 | } 47 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/withdraw_nft.nr: -------------------------------------------------------------------------------- 1 | use crate::Escrow; 2 | use crate::test::utils as escrow_utils; 3 | use aztec::oracle::get_contract_instance::get_contract_instance; 4 | use aztec::protocol_types::{address::AztecAddress, traits::FromField}; 5 | use nft::{NFT, test::utils as nft_utils}; 6 | 7 | // #[test] 8 | #[allow(dead_code)] 9 | unconstrained fn escrow_withdraw_nft_success() { 10 | let escrow_secret: Field = 123456; 11 | let (mut env, escrow_contract_address, _, nft_contract_address, _, recipient, minter) = 12 | escrow_utils::set_escrow_with_token_and_nft(false, escrow_secret); 13 | 14 | let escrow_instance = get_contract_instance(escrow_contract_address); 15 | let logic_contract_address = AztecAddress::from_field(escrow_instance.salt); 16 | 17 | // Mint some tokens to escrow 18 | let token_id: Field = 1; 19 | env.call_private( 20 | minter, 21 | NFT::at(nft_contract_address).mint_to_private(escrow_contract_address, token_id), 22 | ); 23 | 24 | // Check the escrow received and can read the nft 25 | nft_utils::assert_owns_private_nft( 26 | env, 27 | nft_contract_address, 28 | escrow_contract_address, 29 | token_id, 30 | ); 31 | 32 | env.call_private( 33 | logic_contract_address, 34 | Escrow::at(escrow_contract_address).withdraw_nft(nft_contract_address, token_id, recipient), 35 | ); 36 | 37 | // Check recipient got tokens 38 | nft_utils::assert_owns_private_nft(env, nft_contract_address, recipient, token_id); 39 | } 40 | 41 | #[test(should_fail_with = "Not Authorized")] 42 | unconstrained fn escrow_withdraw_nft_unauthorized() { 43 | let escrow_secret: Field = 123456; 44 | let (mut env, escrow_contract_address, _, nft_contract_address, owner, recipient, _) = 45 | escrow_utils::set_escrow_with_token_and_nft(false, escrow_secret); 46 | 47 | let token_id: Field = 1; 48 | env.call_private( 49 | owner, 50 | Escrow::at(escrow_contract_address).withdraw_nft(nft_contract_address, token_id, recipient), 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /src/token_contract/src/test/tokenized_vault/tokenized_vault_utils.nr: -------------------------------------------------------------------------------- 1 | use crate::Token; 2 | use aztec::{ 3 | protocol_types::address::AztecAddress, test::helpers::test_environment::TestEnvironment, 4 | }; 5 | use aztec::test::helpers::authwit as authwit_cheatcodes; 6 | 7 | pub unconstrained fn mint_and_deposit_public_to_public( 8 | env: TestEnvironment, 9 | minter: AztecAddress, 10 | asset_address: AztecAddress, 11 | vault_address: AztecAddress, 12 | recipient: AztecAddress, 13 | amount: u128, 14 | ) { 15 | env.call_public(minter, Token::at(asset_address).mint_to_public(minter, amount)); 16 | 17 | // Deposit assets to get shares 18 | // Authorize the vault to use the caller's assets 19 | let public_transfer_public_to_public_call_interface = 20 | Token::at(asset_address).transfer_public_to_public(minter, vault_address, amount, 0); 21 | authwit_cheatcodes::add_public_authwit_from_call_interface( 22 | env, 23 | minter, 24 | vault_address, 25 | public_transfer_public_to_public_call_interface, 26 | ); 27 | 28 | // Deposit 29 | env.call_public( 30 | minter, 31 | Token::at(vault_address).deposit_public_to_public(minter, recipient, amount, 0), 32 | ); 33 | } 34 | 35 | pub unconstrained fn mint_and_deposit_public_to_private( 36 | env: TestEnvironment, 37 | minter: AztecAddress, 38 | asset_address: AztecAddress, 39 | vault_address: AztecAddress, 40 | recipient: AztecAddress, 41 | amount: u128, 42 | ) { 43 | env.call_public(minter, Token::at(asset_address).mint_to_public(minter, amount)); 44 | 45 | // Deposit assets to get shares 46 | // Authorize the vault to use the caller's assets 47 | let public_transfer_public_to_public_call_interface = 48 | Token::at(asset_address).transfer_public_to_public(minter, vault_address, amount, 0); 49 | authwit_cheatcodes::add_public_authwit_from_call_interface( 50 | env, 51 | minter, 52 | vault_address, 53 | public_transfer_public_to_public_call_interface, 54 | ); 55 | 56 | // Deposit 57 | env.call_private( 58 | minter, 59 | Token::at(vault_address).deposit_public_to_private(minter, recipient, amount, amount, 0), 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/tests-nr.yaml: -------------------------------------------------------------------------------- 1 | name: Noir Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref_name }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | setup-and-run: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest-m] 18 | threads: [12] 19 | 20 | runs-on: 21 | labels: ${{ matrix.os }} 22 | timeout-minutes: 60 23 | 24 | concurrency: 25 | group: job-${{ github.workflow }}-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.threads }} 26 | cancel-in-progress: false 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 0 # Fetch all history for comparison 33 | 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: "22.17.0" 37 | 38 | - name: Set up Docker 39 | uses: docker/setup-buildx-action@v2 40 | 41 | - name: Detect Aztec version 42 | id: aztec-version 43 | run: | 44 | AZTEC_VERSION=$(node -p "require('./package.json').config.aztecVersion") 45 | echo "version=$AZTEC_VERSION" >> "$GITHUB_OUTPUT" 46 | echo "Aztec version is ${{ steps.aztec-version.outputs.version }}" 47 | 48 | - name: Install Aztec CLI 49 | run: | 50 | curl -s https://install.aztec.network > tmp.sh 51 | bash tmp.sh <<< yes "yes" 52 | 53 | - name: Update path 54 | run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH 55 | 56 | - name: Set Aztec version 57 | run: | 58 | VERSION=${{ steps.aztec-version.outputs.version }} aztec-up 59 | 60 | # This is a temporary hack to fix a problem with v3 releases. 61 | - name: Manually tag the aztec version as `latest` 62 | run: | 63 | docker tag aztecprotocol/aztec:${{ steps.aztec-version.outputs.version }} aztecprotocol/aztec:latest 64 | 65 | - name: Install project dependencies 66 | run: yarn --frozen-lockfile 67 | 68 | - name: Compile 69 | run: yarn compile 70 | 71 | - name: Run nr tests 72 | run: | 73 | script -e -c "aztec test --test-threads ${{ matrix.threads }}" 74 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/utils.nr: -------------------------------------------------------------------------------- 1 | use aztec::{ 2 | oracle::random::random, 3 | protocol_types::{address::AztecAddress, traits::ToField}, 4 | test::helpers::{ 5 | test_environment::TestEnvironment, txe_oracles as cheatcodes, utils::ContractDeployment, 6 | }, 7 | }; 8 | use nft::test::utils as nft_utils; 9 | use token::test::utils as token_utils; 10 | 11 | pub unconstrained fn set_escrow_with_token_and_nft( 12 | with_account_contracts: bool, 13 | escrow_secret: Field, 14 | ) -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { 15 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 16 | let (mut env, token_contract_address, owner, recipient, minter) = 17 | token_utils::setup_with_minter(with_account_contracts); 18 | 19 | let nft_contract_address = nft_utils::deploy_nft_with_minter(&mut env, owner, minter); 20 | 21 | // Deploy escrow contract 22 | let escrow_contract_address = deploy_escrow_with_secret(&mut env, escrow_secret); 23 | 24 | ( 25 | env, escrow_contract_address, token_contract_address, nft_contract_address, owner, 26 | recipient, minter, 27 | ) 28 | } 29 | 30 | pub unconstrained fn deploy_escrow_with_secret( 31 | env: &mut TestEnvironment, 32 | escrow_secret: Field, 33 | ) -> AztecAddress { 34 | // Create an account with the custom secret, which generates the public keys 35 | // This registers the public keys with the test environment so the escrow can decrypt notes 36 | let _escrow_account = cheatcodes::add_account(escrow_secret); 37 | 38 | // Deploy the Escrow contract using escrow_secret as the secret 39 | // In v1.2.1, deploy_with_public_keys accepted a secret 40 | // In v2.0.3, we manually create ContractDeployment with a secret 41 | // TODO: The salt will be stored in the contract instance and checked by _assert_msg_sender 42 | // The test extracts logic_contract_address from escrow_instance.salt 43 | let _escrow_contract = 44 | ContractDeployment { env: *env, path: "@escrow_contract/Escrow", secret: escrow_secret }; 45 | 46 | _escrow_contract.without_initializer() 47 | } 48 | 49 | pub unconstrained fn get_escrow_class_id() -> Field { 50 | let escrow_instance = cheatcodes::deploy("@escrow_contract/Escrow", "", &[], random()); 51 | escrow_instance.contract_class_id.to_field() 52 | } 53 | -------------------------------------------------------------------------------- /src/token_contract/src/test/initialize_transfer_commitment.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils; 2 | use crate::Token; 3 | use aztec::{oracle::random::random, protocol_types::{address::AztecAddress, traits::FromField}}; 4 | use uint_note::uint_note::PartialUintNote; 5 | 6 | #[test] 7 | unconstrained fn initialize_transfer_commitment() { 8 | let (env, token_contract_address, owner, recipient) = 9 | utils::setup_and_mint_to_private_without_minter(false); 10 | 11 | // Transfer tokens 12 | let commitment = env.call_private( 13 | owner, 14 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, owner), 15 | ); 16 | 17 | let validity_commitment = 18 | PartialUintNote::from_field(commitment).compute_validity_commitment(owner); 19 | assert( 20 | env.public_context(|context| { 21 | context.nullifier_exists(validity_commitment, token_contract_address) 22 | }), 23 | "validity nullifier should exist", 24 | ); 25 | } 26 | 27 | #[test(should_fail_with = "Invalid partial note or completer")] 28 | unconstrained fn initialize_transfer_commitment_and_complete_with_incorrect_completer() { 29 | let (env, token_contract_address, owner, recipient, minter) = 30 | utils::setup_and_mint_to_private_with_minter(false); 31 | 32 | // Generate an address using a random field 33 | let random_completer: AztecAddress = AztecAddress::from_field(random()); 34 | 35 | // Initialize a transfer commitment using a random completer 36 | let commitment = env.call_private( 37 | owner, 38 | Token::at(token_contract_address).initialize_transfer_commitment( 39 | recipient, 40 | random_completer, 41 | ), 42 | ); 43 | 44 | // Using an arbitrary completer should result in a non-existing nullifier 45 | let validity_commitment = 46 | PartialUintNote::from_field(commitment).compute_validity_commitment(recipient); 47 | assert( 48 | !env.public_context(|context| { 49 | context.nullifier_exists(validity_commitment, token_contract_address) 50 | }), 51 | "validity nullifier should not exist", 52 | ); 53 | 54 | // Minting to a commitment uses msg.sender as completer, which is the minter, and not the random completer 55 | let _ = env.call_public( 56 | minter, 57 | Token::at(token_contract_address).mint_to_commitment(commitment, 1 as u128), 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@defi-wonderland/aztec-standards", 3 | "version": "3.0.0-devnet.2-deployments", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/defi-wonderland/aztec-standards.git" 7 | }, 8 | "author": "Wonderland", 9 | "license": "MIT", 10 | "type": "module", 11 | "scripts": { 12 | "postinstall": "husky", 13 | "clean": "rm -rf ./artifacts ./target codegenCache.json", 14 | "codegen": "aztec codegen target --outdir artifacts --force", 15 | "compile": "aztec-nargo compile && aztec-postprocess-contract", 16 | "start:pxe": "docker compose -p sandbox -f ~/.aztec/docker-compose.sandbox.yml -f docker-compose.override.yml up", 17 | "test": "yarn test:nr && yarn test:js", 18 | "test:js": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --config jest.integration.config.json", 19 | "test:nr": "aztec test", 20 | "lint:prettier": "prettier '**/*.{js,ts}' --write", 21 | "bench": "NODE_NO_WARNINGS=1 aztec-benchmark --suffix _base", 22 | "ccc": "yarn clean && yarn compile && yarn codegen", 23 | "generate-release-artifacts": "tsc artifacts/*.ts --outDir dist/ --skipLibCheck --target es2020 --module nodenext --moduleResolution nodenext --resolveJsonModule --declaration" 24 | }, 25 | "lint-staged": { 26 | "*.ts": "prettier --write -u" 27 | }, 28 | "dependencies": { 29 | "@aztec/accounts": "3.0.0-devnet.2", 30 | "@aztec/aztec.js": "3.0.0-devnet.2", 31 | "@aztec/noir-contracts.js": "3.0.0-devnet.2", 32 | "@aztec/protocol-contracts": "3.0.0-devnet.2", 33 | "@aztec/pxe": "3.0.0-devnet.2", 34 | "@aztec/stdlib": "3.0.0-devnet.2", 35 | "@aztec/test-wallet": "3.0.0-devnet.2", 36 | "@defi-wonderland/aztec-benchmark": "3.0.0-devnet.2", 37 | "@types/node": "22.5.1" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "29.5.11", 41 | "@types/mocha": "10.0.6", 42 | "@types/node": "22.5.1", 43 | "husky": "9.1.7", 44 | "jest": "29.7.0", 45 | "lint-staged": "15.4.3", 46 | "prettier": "3.4.2", 47 | "ts-jest": "29.2.5", 48 | "ts-node": "10.9.2", 49 | "typescript": "5.7.2" 50 | }, 51 | "jest": { 52 | "testTimeout": 200000 53 | }, 54 | "config": { 55 | "aztecVersion": "3.0.0-devnet.2" 56 | }, 57 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 58 | } 59 | -------------------------------------------------------------------------------- /src/escrow_contract/src/main.nr: -------------------------------------------------------------------------------- 1 | pub mod test; 2 | pub mod library; 3 | pub mod types; 4 | 5 | use aztec::macros::aztec; 6 | 7 | #[aztec] 8 | pub contract Escrow { 9 | use aztec::{ 10 | context::PrivateContext, 11 | macros::functions::external, 12 | oracle::get_contract_instance::get_contract_instance, 13 | protocol_types::{ 14 | address::AztecAddress, contract_instance::ContractInstance, traits::FromField, 15 | }, 16 | }; 17 | use nft::NFT; 18 | use token::Token; 19 | 20 | /// @notice Withdraws an amount from the escrow's private balance to the 21 | /// recipient's private balance. 22 | /// @dev Can only be called by the corresponding Logic contract 23 | /// @param token The address of the token 24 | /// @param amount The amount of tokens to withdraw from the escrow 25 | /// @param recipient The address of the recipient 26 | #[external("private")] 27 | fn withdraw(token: AztecAddress, amount: u128, recipient: AztecAddress) { 28 | _assert_msg_sender(&mut context); 29 | 30 | Token::at(token) 31 | .transfer_private_to_private(context.this_address(), recipient, amount, 0) 32 | .call(&mut context); 33 | } 34 | 35 | /// @notice Withdraws a token of a given ID from the escrow's private balance to 36 | /// the recipient's private balance 37 | /// @dev Can only be called by the corresponding Logic contract 38 | /// @param nft The address of the NFT contract 39 | /// @param token_id The id of the token to withdraw from the escrow 40 | /// @param recipient The address of the recipient 41 | #[external("private")] 42 | fn withdraw_nft(nft: AztecAddress, token_id: Field, recipient: AztecAddress) { 43 | _assert_msg_sender(&mut context); 44 | 45 | NFT::at(nft) 46 | .transfer_private_to_private(context.this_address(), recipient, token_id, 0) 47 | .call(&mut context); 48 | } 49 | 50 | /// @notice Asserts that the caller is the one encoded into the contract instance salt 51 | #[contract_library_method] 52 | fn _assert_msg_sender(context: &mut PrivateContext) { 53 | let msg_sender = context.msg_sender(); 54 | let escrow_instance: ContractInstance = get_contract_instance(context.this_address()); 55 | assert( 56 | AztecAddress::from_field(escrow_instance.salt) == msg_sender.unwrap(), 57 | "Not Authorized", 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/initialize_transfer_commitment.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | 4 | #[test] 5 | unconstrained fn nft_initialize_transfer_commitment_success() { 6 | // Setup with NFT in private state 7 | let token_id = 10000; 8 | let (env, nft_contract_address, owner, _, recipient) = 9 | utils::setup_and_mint_to_private(false, token_id); 10 | 11 | // Initialize transfer commitment 12 | let commitment = env.call_private( 13 | owner, 14 | NFT::at(nft_contract_address).initialize_transfer_commitment(recipient, owner), 15 | ); 16 | 17 | // Verify commitment is stored 18 | utils::check_commitment_is_stored(env, nft_contract_address, commitment); 19 | } 20 | 21 | #[test] 22 | unconstrained fn nft_initialize_transfer_commitment_by_recipient_success() { 23 | // Setup with NFT in private state 24 | let token_id = 10000; 25 | let (env, nft_contract_address, _, _, recipient) = 26 | utils::setup_and_mint_to_private(false, token_id); 27 | 28 | // Initialize transfer commitment as recipient 29 | let commitment = env.call_private( 30 | recipient, 31 | NFT::at(nft_contract_address).initialize_transfer_commitment(recipient, recipient), 32 | ); 33 | 34 | // Verify commitment is stored 35 | utils::check_commitment_is_stored(env, nft_contract_address, commitment); 36 | } 37 | 38 | #[test] 39 | unconstrained fn nft_initialize_transfer_commitment_self_success() { 40 | // Setup with NFT in private state 41 | let token_id = 10000; 42 | let (env, nft_contract_address, owner, _, _) = 43 | utils::setup_and_mint_to_private(false, token_id); 44 | 45 | // Initialize transfer commitment to self 46 | let commitment = env.call_private( 47 | owner, 48 | NFT::at(nft_contract_address).initialize_transfer_commitment(owner, owner), 49 | ); 50 | 51 | // Verify commitment is stored 52 | utils::check_commitment_is_stored(env, nft_contract_address, commitment); 53 | } 54 | 55 | #[test] 56 | unconstrained fn nft_initialize_transfer_commitment_by_third_party_success() { 57 | // Setup with NFT in private state 58 | let token_id = 10000; 59 | let (env, nft_contract_address, _, minter, recipient) = 60 | utils::setup_and_mint_to_private(false, token_id); 61 | 62 | // Initialize transfer commitment as minter (third party) 63 | let commitment = env.call_private( 64 | minter, 65 | NFT::at(nft_contract_address).initialize_transfer_commitment(recipient, minter), 66 | ); 67 | 68 | // Verify commitment is stored 69 | utils::check_commitment_is_stored(env, nft_contract_address, commitment); 70 | } 71 | -------------------------------------------------------------------------------- /src/token_contract/src/test/mint_to_private.nr: -------------------------------------------------------------------------------- 1 | use crate::{test::utils, Token}; 2 | 3 | #[test] 4 | unconstrained fn mint_to_private_success() { 5 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 6 | let (mut env, token_contract_address, owner, _, minter) = utils::setup_with_minter(false); 7 | 8 | let mint_amount: u128 = 10_000; 9 | env.call_private(minter, Token::at(token_contract_address).mint_to_private(owner, mint_amount)); 10 | 11 | utils::check_private_balance(env, token_contract_address, owner, mint_amount); 12 | 13 | let total_supply = env.view_public(Token::at(token_contract_address).total_supply()); 14 | assert(total_supply == mint_amount); 15 | } 16 | 17 | #[test(should_fail_with = "Assertion failed: caller is not minter")] 18 | unconstrained fn mint_to_private_failure_as_non_minter() { 19 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 20 | let (mut env, token_contract_address, owner, recipient, _) = utils::setup_with_minter(false); 21 | 22 | let mint_amount: u128 = 10_000; 23 | let mint_to_private_call_interface = 24 | Token::at(token_contract_address).mint_to_private(owner, mint_amount); 25 | // As non-minter 26 | env.call_private(recipient, mint_to_private_call_interface); 27 | } 28 | 29 | #[test(should_fail_with = "Contract execution has reverted: Assertion failed: attempt to add with overflow 'total_supply.read() + amount'")] 30 | unconstrained fn mint_to_private_failure_recipient_balance_overflow() { 31 | let (mut env, token_contract_address, owner, recipient, minter) = 32 | utils::setup_with_minter(false); 33 | env.call_private( 34 | minter, 35 | Token::at(token_contract_address).mint_to_private(recipient, utils::max_u128()), 36 | ); 37 | 38 | // Recipient's balance overflows 39 | env.call_private(minter, Token::at(token_contract_address).mint_to_private(owner, 1 as u128)); 40 | } 41 | #[test(should_fail_with = "Contract execution has reverted: Assertion failed: attempt to add with overflow 'total_supply.read() + amount'")] 42 | unconstrained fn mint_to_private_failure_total_supply_overflow() { 43 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 44 | let (mut env, token_contract_address, owner, recipient, minter) = 45 | utils::setup_with_minter(false); 46 | 47 | env.call_private( 48 | minter, 49 | Token::at(token_contract_address).mint_to_private(recipient, utils::max_u128()), 50 | ); 51 | env.call_private(minter, Token::at(token_contract_address).mint_to_private(owner, 1 as u128)); 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/tests-js.yaml: -------------------------------------------------------------------------------- 1 | name: JS Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref_name }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | setup-and-run: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest-m] 18 | threads: [12] 19 | 20 | runs-on: 21 | labels: ${{ matrix.os }} 22 | timeout-minutes: 60 23 | 24 | concurrency: 25 | group: job-${{ github.workflow }}-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.threads }} 26 | cancel-in-progress: false 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v3 31 | with: 32 | fetch-depth: 0 # Fetch all history for comparison 33 | 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version: "22.17.0" 37 | 38 | - name: Set up Docker 39 | uses: docker/setup-buildx-action@v2 40 | 41 | - name: Detect Aztec version 42 | id: aztec-version 43 | run: | 44 | AZTEC_VERSION=$(node -p "require('./package.json').config.aztecVersion") 45 | echo "version=$AZTEC_VERSION" >> "$GITHUB_OUTPUT" 46 | echo "Aztec version is ${{ steps.aztec-version.outputs.version }}" 47 | 48 | - name: Install Aztec CLI 49 | run: | 50 | curl -s https://install.aztec.network > tmp.sh 51 | bash tmp.sh <<< yes "yes" 52 | 53 | - name: Update path 54 | run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH 55 | 56 | - name: Set Aztec version 57 | run: | 58 | VERSION=${{ steps.aztec-version.outputs.version }} aztec-up 59 | 60 | # This is a temporary hack to fix a problem with v3 releases. 61 | - name: Manually tag the aztec version as `latest` 62 | run: | 63 | docker tag aztecprotocol/aztec:${{ steps.aztec-version.outputs.version }} aztecprotocol/aztec:latest 64 | 65 | - name: Start sandbox 66 | run: | 67 | aztec start --sandbox & 68 | 69 | - name: Install project dependencies 70 | run: yarn --frozen-lockfile 71 | 72 | - name: Compile 73 | run: yarn compile 74 | 75 | - name: Codegen 76 | run: yarn codegen 77 | 78 | - name: Start PXE 79 | run: | 80 | VERSION=${{ steps.aztec-version.outputs.version }} aztec start --port 8081 --pxe --pxe.nodeUrl=http://localhost:8080/ --pxe.proverEnabled false & 81 | 82 | - name: Run js tests 83 | run: script -e -c "BASE_PXE_URL=http://localhost NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --config jest.integration.config.json" 84 | -------------------------------------------------------------------------------- /src/token_contract/src/test/mint_to_public.nr: -------------------------------------------------------------------------------- 1 | use crate::{test::utils, Token}; 2 | 3 | #[test] 4 | unconstrained fn mint_to_public_success() { 5 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 6 | let (mut env, token_contract_address, owner, _, minter) = utils::setup_with_minter(false); 7 | 8 | let token = Token::at(token_contract_address); 9 | 10 | let mint_amount: u128 = 10_000; 11 | env.call_public(minter, token.mint_to_public(owner, mint_amount)); 12 | 13 | utils::check_public_balance(env, token_contract_address, owner, mint_amount); 14 | 15 | let total_supply = env.view_public(token.total_supply()); 16 | assert(total_supply == mint_amount); 17 | } 18 | 19 | #[test(should_fail_with = "Contract execution has reverted: Assertion failed: caller is not minter 'minter.eq(sender)'")] 20 | unconstrained fn mint_to_public_failure_as_non_minter() { 21 | let (mut env, token_contract_address, owner, recipient, _) = utils::setup_with_minter(false); 22 | 23 | let mint_amount: u128 = 10_000; 24 | let mint_to_public_call_interface = 25 | Token::at(token_contract_address).mint_to_public(owner, mint_amount); 26 | 27 | // As non-minter 28 | env.call_public(recipient, mint_to_public_call_interface); 29 | } 30 | 31 | #[test(should_fail_with = "Contract execution has reverted: Assertion failed: attempt to add with overflow 'public_balances.at(to).read() + amount'")] 32 | unconstrained fn mint_to_public_failure_recipient_balance_overflow() { 33 | let (mut env, token_contract_address, owner, recipient, minter) = 34 | utils::setup_with_minter(false); 35 | 36 | // We have to do this in 2 steps because we have to pass in a valid u128 37 | let max_u128 = utils::max_u128(); 38 | env.call_public(minter, Token::at(token_contract_address).mint_to_public(recipient, max_u128)); 39 | 40 | utils::check_public_balance(env, token_contract_address, owner, 0); 41 | utils::check_total_supply(env, token_contract_address, max_u128); 42 | 43 | // Overflow recipient's balance 44 | env.call_public(minter, Token::at(token_contract_address).mint_to_public(recipient, 1 as u128)); 45 | } 46 | 47 | #[test(should_fail_with = "Contract execution has reverted: Assertion failed: attempt to add with overflow 'public_balances.at(to).read() + amount'")] 48 | unconstrained fn mint_to_public_failure_overflow_total_supply() { 49 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 50 | let (mut env, token_contract_address, owner, _, minter) = utils::setup_with_minter(false); 51 | 52 | let max_u128 = utils::max_u128(); 53 | let mint_to_public_call_interface = 54 | Token::at(token_contract_address).mint_to_public(owner, max_u128); 55 | 56 | env.call_public(minter, mint_to_public_call_interface); 57 | utils::check_public_balance(env, token_contract_address, owner, max_u128); 58 | 59 | let total_supply = env.view_public(Token::at(token_contract_address).total_supply()); 60 | assert(total_supply == max_u128); 61 | 62 | // Overflow total supply 63 | env.call_public(minter, Token::at(token_contract_address).mint_to_public(owner, 1 as u128)); 64 | } 65 | -------------------------------------------------------------------------------- /benchmarks/nft_contract.benchmark.ts: -------------------------------------------------------------------------------- 1 | import type { PXE } from '@aztec/pxe/server'; 2 | import type { Wallet } from '@aztec/aztec.js/wallet'; 3 | import { AztecAddress } from '@aztec/aztec.js/addresses'; 4 | import type { ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization'; 5 | 6 | // Import the new Benchmark base class and context 7 | import { Benchmark, BenchmarkContext } from '@defi-wonderland/aztec-benchmark'; 8 | 9 | import { NFTContract } from '../artifacts/NFT.js'; 10 | import { deployNFTWithMinter, setupTestSuite } from '../src/ts/test/utils.js'; 11 | 12 | // Extend the BenchmarkContext from the new package 13 | interface NFTBenchmarkContext extends BenchmarkContext { 14 | pxe: PXE; 15 | wallet: Wallet; 16 | deployer: AztecAddress; 17 | accounts: AztecAddress[]; 18 | nftContract: NFTContract; 19 | } 20 | 21 | // Use export default class extending Benchmark 22 | export default class NFTContractBenchmark extends Benchmark { 23 | /** 24 | * Sets up the benchmark environment for the NFTContract. 25 | * Creates PXE client, gets accounts, and deploys the contract. 26 | */ 27 | async setup(): Promise { 28 | const { pxe, wallet, accounts } = await setupTestSuite(); 29 | const [deployer] = accounts; 30 | const deployedBaseContract = await deployNFTWithMinter(wallet, deployer, { 31 | universalDeploy: true, 32 | from: deployer, 33 | }); 34 | const nftContract = await NFTContract.at(deployedBaseContract.address, wallet); 35 | return { pxe, wallet, deployer, accounts, nftContract }; 36 | } 37 | 38 | /** 39 | * Returns the list of NFTContract methods to be benchmarked. 40 | */ 41 | getMethods(context: NFTBenchmarkContext): ContractFunctionInteractionCallIntent[] { 42 | const { nftContract, accounts, wallet } = context; 43 | const [alice] = accounts; 44 | const owner = alice; 45 | const methods: ContractFunctionInteractionCallIntent[] = [ 46 | // Mint methods 47 | { 48 | caller: alice, 49 | action: nftContract.withWallet(wallet).methods.mint_to_private(owner, 1), 50 | }, 51 | { 52 | caller: alice, 53 | action: nftContract.withWallet(wallet).methods.mint_to_public(owner, 2), 54 | }, 55 | 56 | // Transfer methods 57 | { 58 | caller: alice, 59 | action: nftContract.withWallet(wallet).methods.transfer_private_to_public(owner, owner, 1, 0), 60 | }, 61 | { 62 | caller: alice, 63 | action: nftContract.withWallet(wallet).methods.transfer_public_to_private(owner, owner, 1, 0), 64 | }, 65 | { 66 | caller: alice, 67 | action: nftContract.withWallet(wallet).methods.transfer_private_to_private(owner, owner, 1, 0), 68 | }, 69 | { 70 | caller: alice, 71 | action: nftContract.withWallet(wallet).methods.transfer_public_to_public(owner, owner, 2, 0), 72 | }, 73 | 74 | // NOTE: don't have enough private NFT's to burn_private 75 | // nftContract.withWallet(alice).methods.transfer_private_to_public_with_commitment(owner, owner, 1, 0), 76 | 77 | // Burn methods 78 | { 79 | caller: alice, 80 | action: nftContract.withWallet(wallet).methods.burn_private(owner, 1, 0), 81 | }, 82 | { 83 | caller: alice, 84 | action: nftContract.withWallet(wallet).methods.burn_public(owner, 2, 0), 85 | }, 86 | ]; 87 | 88 | return methods.filter(Boolean); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test/secret_keys_to_public_keys.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils as logic_utils; 2 | use crate::TestLogic; 3 | use aztec::test::helpers::test_environment::TestEnvironment; 4 | use escrow_contract::library::logic::MasterSecretKeys; 5 | 6 | // We test public key derivation using two different secret keys. 7 | #[test] 8 | unconstrained fn secret_keys_to_public_keys_success() { 9 | let mut env = TestEnvironment::new(); 10 | 11 | let logic_contract_address = logic_utils::deploy_logic(&mut env, 1); 12 | 13 | let (vector_public_keys_1, vector_secret_keys_1, vector_public_keys_2, vector_secret_keys_2) = 14 | logic_utils::get_test_vector(); 15 | 16 | let caller = env.create_light_account(); 17 | let public_keys_1 = env.call_private( 18 | caller, 19 | TestLogic::at(logic_contract_address).secret_keys_to_public_keys(vector_secret_keys_1), 20 | ); 21 | 22 | assert_eq(vector_public_keys_1.npk_m.inner.x, public_keys_1.npk_m.inner.x); 23 | assert_eq(vector_public_keys_1.npk_m.inner.y, public_keys_1.npk_m.inner.y); 24 | assert_eq(vector_public_keys_1.ivpk_m.inner.x, public_keys_1.ivpk_m.inner.x); 25 | assert_eq(vector_public_keys_1.ivpk_m.inner.y, public_keys_1.ivpk_m.inner.y); 26 | assert_eq(vector_public_keys_1.ovpk_m.inner.x, public_keys_1.ovpk_m.inner.x); 27 | assert_eq(vector_public_keys_1.ovpk_m.inner.y, public_keys_1.ovpk_m.inner.y); 28 | assert_eq(vector_public_keys_1.tpk_m.inner.x, public_keys_1.tpk_m.inner.x); 29 | assert_eq(vector_public_keys_1.tpk_m.inner.y, public_keys_1.tpk_m.inner.y); 30 | assert_eq(vector_public_keys_1, public_keys_1); 31 | 32 | let public_keys_2 = env.call_private( 33 | caller, 34 | TestLogic::at(logic_contract_address).secret_keys_to_public_keys(vector_secret_keys_2), 35 | ); 36 | 37 | assert_eq(vector_public_keys_2.npk_m.inner.x, public_keys_2.npk_m.inner.x); 38 | assert_eq(vector_public_keys_2.npk_m.inner.y, public_keys_2.npk_m.inner.y); 39 | assert_eq(vector_public_keys_2.ivpk_m.inner.x, public_keys_2.ivpk_m.inner.x); 40 | assert_eq(vector_public_keys_2.ivpk_m.inner.y, public_keys_2.ivpk_m.inner.y); 41 | assert_eq(vector_public_keys_2.ovpk_m.inner.x, public_keys_2.ovpk_m.inner.x); 42 | assert_eq(vector_public_keys_2.ovpk_m.inner.y, public_keys_2.ovpk_m.inner.y); 43 | assert_eq(vector_public_keys_2.tpk_m.inner.x, public_keys_2.tpk_m.inner.x); 44 | assert_eq(vector_public_keys_2.tpk_m.inner.y, public_keys_2.tpk_m.inner.y); 45 | assert_eq(vector_public_keys_2, public_keys_2); 46 | } 47 | 48 | // Note that this test will fail since we modify one of the secret keys to produce a different public key than expected 49 | #[test(should_fail)] 50 | unconstrained fn secret_keys_to_public_keys_fail() { 51 | let mut env = TestEnvironment::new(); 52 | 53 | let logic_contract_address = logic_utils::deploy_logic(&mut env, 1); 54 | 55 | let (vector_public_keys_1, vector_secret_keys_1, _, _) = logic_utils::get_test_vector(); 56 | 57 | // we modify the first secret key to be 1 greater than the original, which should derive a different public key 58 | let modified_secret_keys: MasterSecretKeys = MasterSecretKeys { 59 | nsk_m: vector_secret_keys_1.nsk_m + 1, 60 | ivsk_m: vector_secret_keys_1.ivsk_m + 1, 61 | ovsk_m: vector_secret_keys_1.ovsk_m + 1, 62 | tsk_m: vector_secret_keys_1.tsk_m + 1, 63 | }; 64 | 65 | let caller = env.create_light_account(); 66 | let public_keys_1 = env.call_private( 67 | caller, 68 | TestLogic::at(logic_contract_address).secret_keys_to_public_keys(modified_secret_keys), 69 | ); 70 | 71 | assert_eq(vector_public_keys_1, public_keys_1); 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aztec Standards 2 | 3 | [![npm version](https://img.shields.io/npm/v/@defi-wonderland/aztec-standards.svg)](https://www.npmjs.com/package/@defi-wonderland/aztec-standards) 4 | 5 | Aztec Standards is a comprehensive collection of reusable, standardized contracts for the Aztec Network. It provides a robust foundation of token primitives and utilities that support both private and public operations, empowering developers to build innovative privacy-preserving applications with ease. 6 | 7 | ## Table of Contents 8 | - [Dripper Contract](#dripper-contract) 9 | - [Token Contract](#token-contract) 10 | - [Tokenized Vault Contract](#tokenized-vault-contract) 11 | - [NFT Contract](#nft-contract) 12 | - [Escrow Standard Contract & Library](#escrow-standard-contract--library) 13 | - [Future Contracts](#future-contracts) 14 | 15 | ## Dripper Contract 16 | 17 | The `Dripper` contract provides a convenient faucet mechanism for minting tokens into private or public balances. Anyone can easily invoke the functions to request tokens for testing or development purposes. 18 | 19 | 📖 **[View detailed Dripper documentation](src/dripper/README.md)** 20 | 21 | ## Token Contract 22 | 23 | The `Token` contract implements an ERC-20-like token with Aztec-specific privacy extensions. It supports transfers and interactions explicitly through private balances and public balances, offering full coverage of Aztec's confidentiality features. 24 | 25 | We published the [AIP-20 Aztec Token Standard](https://forum.aztec.network/t/request-for-comments-aip-20-aztec-token-standard/7737) to the forum. Feel free to review and discuss the specification there. 26 | 27 | 📖 **[View detailed Token documentation](src/token_contract/README.md)** 28 | 29 | ## Tokenized Vault Contract 30 | 31 | The `Token` contract can be configured to function as a Tokenized Vault, allowing it to issue yield-bearing shares that represent deposits of an underlying asset. To enable this mode, deploy the contract using the `constructor_with_asset()` initializer. The underlying `asset` contract must be an AIP-20–compliant token, and the vault itself issues AIP-20–compliant share tokens to depositors. 32 | 33 | We published the [AIP-4626: Tokenized Vault Standard](https://forum.aztec.network/t/request-for-comments-aip-4626-tokenized-vault/8079) to the forum. Feel free to review and discuss the specification there. 34 | 35 | 📖 **[View detailed Tokenized Vault documentation](src/token_contract/README.md#aip-4626-aztec-tokenized-vault-standard)** 36 | 37 | ## NFT Contract 38 | 39 | The `NFT` contract implements an ERC-721-like non-fungible token with Aztec-specific privacy extensions. It supports transfers and interactions through both private and public ownership, offering full coverage of Aztec's confidentiality features for unique digital assets. 40 | 41 | 📖 **[View detailed NFT documentation](src/nft_contract/README.md)** 42 | 43 | ## Escrow Standard Contract & Library 44 | 45 | The Escrow Standard contains two elements: 46 | - Escrow Contract: a minimal private contract designed to have keys with which authorized callers can spend private balances of tokens and NFTs compliants with AIP-20 and AIP-721, respectively. 47 | - Logic Library: a set of contract library methods that standardizes and facilitates the management of Escrow contracts from another contract, a.k.a. the Logic contract. 48 | 49 | 📖 **[View detailed Escrow documentation](src/escrow_contract/README.md)** 50 | 51 | To see examples of Logic contract implementations, such as a linear vesting contract or a clawback escrow contract, go to [aztec-escrow-extensions](https://github.com/defi-wonderland/aztec-escrow-extensions). 52 | 53 | ## Future Contracts 54 | 55 | Additional standardized contracts (e.g., staking, governance, pools) will be added under this repository, with descriptions and function lists. -------------------------------------------------------------------------------- /src/token_contract/src/test/transfer_public_to_private.nr: -------------------------------------------------------------------------------- 1 | use crate::{test::utils::{self, mint_amount}, Token}; 2 | use aztec::test::helpers::authwit as authwit_cheatcodes; 3 | 4 | #[test] 5 | unconstrained fn transfer_public_to_private_success() { 6 | let (mut env, token_contract_address, sender, recipient) = 7 | utils::setup_and_mint_to_public_without_minter(false); 8 | 9 | utils::check_public_balance(env, token_contract_address, sender, mint_amount); 10 | utils::check_private_balance(env, token_contract_address, recipient, 0); 11 | 12 | let nonce = 0; 13 | let transfer_amount = mint_amount / 2; 14 | 15 | env.call_private( 16 | sender, 17 | Token::at(token_contract_address).transfer_public_to_private( 18 | sender, 19 | recipient, 20 | transfer_amount, 21 | nonce, 22 | ), 23 | ); 24 | 25 | // sender public balance decreases 26 | utils::check_public_balance(env, token_contract_address, sender, transfer_amount); 27 | assert( 28 | env.view_public(Token::at(token_contract_address).balance_of_public(sender)) 29 | == transfer_amount, 30 | "public balance is not correct", 31 | ); 32 | // recipient private balance incre ases 33 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 34 | } 35 | 36 | #[test(should_fail_with = "Assertion failed: attempt to subtract with overflow 'public_balances.at(from).read() - amount'")] 37 | unconstrained fn transfer_public_to_private_not_enough_balance() { 38 | let (mut env, token_contract_address, sender, recipient) = 39 | utils::setup_and_mint_to_public_without_minter(false); 40 | 41 | utils::check_public_balance(env, token_contract_address, sender, mint_amount); 42 | utils::check_private_balance(env, token_contract_address, recipient, 0); 43 | 44 | let nonce = 0; 45 | let transfer_amount = mint_amount * 2; 46 | 47 | env.call_private( 48 | sender, 49 | Token::at(token_contract_address).transfer_public_to_private( 50 | sender, 51 | recipient, 52 | transfer_amount, 53 | nonce, 54 | ), 55 | ); 56 | } 57 | 58 | #[test] 59 | unconstrained fn transfer_public_to_private_authwitness_success() { 60 | let (mut env, token_contract_address, sender, recipient) = 61 | utils::setup_and_mint_to_public_without_minter(true); 62 | 63 | let transfer_amount = mint_amount / 2; 64 | 65 | let transfer_public_to_private_call_interface = Token::at(token_contract_address) 66 | .transfer_public_to_private(sender, recipient, transfer_amount, 0); 67 | authwit_cheatcodes::add_private_authwit_from_call_interface( 68 | env, 69 | sender, 70 | recipient, 71 | transfer_public_to_private_call_interface, 72 | ); 73 | 74 | env.call_private(recipient, transfer_public_to_private_call_interface); 75 | 76 | // Check balances changes as expected 77 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 78 | utils::check_public_balance(env, token_contract_address, sender, transfer_amount); 79 | } 80 | 81 | #[test(should_fail_with = "Unknown auth witness for message hash")] 82 | unconstrained fn transfer_public_to_private_authwitness_unauthorized() { 83 | let (mut env, token_contract_address, sender, recipient) = 84 | utils::setup_and_mint_to_public_without_minter(true); 85 | 86 | let transfer_amount = mint_amount / 2; 87 | 88 | let transfer_public_to_private_call_interface = Token::at(token_contract_address) 89 | .transfer_public_to_private(sender, recipient, transfer_amount, 0); 90 | 91 | // no authwit added 92 | 93 | env.call_private(recipient, transfer_public_to_private_call_interface); 94 | } 95 | -------------------------------------------------------------------------------- /src/token_contract/src/test/transfer_private_to_commitment.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils::{self, mint_amount}; 2 | use crate::Token; 3 | use aztec::test::helpers::authwit as authwit_cheatcodes; 4 | 5 | #[test] 6 | unconstrained fn transfer_private_to_commitment() { 7 | let (mut env, token_contract_address, owner, recipient) = 8 | utils::setup_and_mint_to_private_without_minter(false); 9 | 10 | utils::check_private_balance(env, token_contract_address, owner, mint_amount); 11 | utils::check_private_balance(env, token_contract_address, recipient, 0 as u128); 12 | 13 | // Prepare commitment 14 | let commitment = env.call_private( 15 | owner, 16 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, owner), 17 | ); 18 | 19 | // Transfer tokens 20 | let transfer_amount = mint_amount; 21 | env.call_private( 22 | owner, 23 | Token::at(token_contract_address).transfer_private_to_commitment( 24 | owner, 25 | commitment, 26 | transfer_amount, 27 | 0, 28 | ), 29 | ); 30 | 31 | // Check balances 32 | utils::check_private_balance(env, token_contract_address, owner, 0 as u128); 33 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 34 | } 35 | 36 | #[test] 37 | unconstrained fn transfer_private_to_commitment_on_behalf_of_other() { 38 | let (mut env, token_contract_address, owner, recipient) = 39 | utils::setup_and_mint_to_private_without_minter(true); 40 | 41 | utils::check_private_balance(env, token_contract_address, owner, mint_amount); 42 | utils::check_private_balance(env, token_contract_address, recipient, 0 as u128); 43 | 44 | let transfer_amount = mint_amount; 45 | 46 | // Prepare commitment 47 | let commitment = env.call_private( 48 | owner, 49 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, recipient), 50 | ); 51 | 52 | let transfer_to_commitment_call_interface = Token::at(token_contract_address) 53 | .transfer_private_to_commitment(owner, commitment, transfer_amount, 0); 54 | 55 | authwit_cheatcodes::add_private_authwit_from_call_interface( 56 | env, 57 | owner, 58 | recipient, 59 | transfer_to_commitment_call_interface, 60 | ); 61 | 62 | // Transfer tokens 63 | env.call_private(recipient, transfer_to_commitment_call_interface); 64 | 65 | // Check balances 66 | utils::check_private_balance(env, token_contract_address, owner, 0 as u128); 67 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 68 | } 69 | 70 | #[test(should_fail_with = "Nullifier witness not found for nullifier")] 71 | unconstrained fn transfer_private_to_commitment_wrong_completer() { 72 | let (env, token_contract_address, owner, recipient) = 73 | utils::setup_and_mint_to_private_without_minter(false); 74 | 75 | utils::check_private_balance(env, token_contract_address, owner, mint_amount); 76 | utils::check_private_balance(env, token_contract_address, recipient, 0 as u128); 77 | 78 | let commitment = env.call_private( 79 | owner, 80 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, recipient), 81 | ); 82 | 83 | // Transfer tokens 84 | let transfer_amount = mint_amount; 85 | env.call_private( 86 | owner, 87 | Token::at(token_contract_address).transfer_private_to_commitment( 88 | owner, 89 | commitment, 90 | transfer_amount, 91 | 0, 92 | ), 93 | ); 94 | 95 | // Check balances 96 | utils::check_private_balance(env, token_contract_address, owner, 0 as u128); 97 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 98 | } 99 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/burn_public.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::{protocol_types::address::AztecAddress, test::helpers::authwit as authwit_cheatcodes}; 4 | 5 | #[test] 6 | unconstrained fn nft_burn_public_success() { 7 | // Setup and mint NFT to owner in public state 8 | let token_id = 10000; 9 | let (env, nft_contract_address, owner, _, _) = utils::setup_and_mint_to_public(false, token_id); 10 | 11 | // Verify initial state 12 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 13 | utils::assert_nft_exists(env, nft_contract_address, token_id); 14 | 15 | // Burn the NFT 16 | env.call_public(owner, NFT::at(nft_contract_address).burn_public(owner, token_id, 0)); 17 | 18 | // Verify NFT is burned (owner is zero address and NFT doesn't exist) 19 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 20 | utils::assert_nft_does_not_exist(env, nft_contract_address, token_id); 21 | } 22 | 23 | #[test] 24 | unconstrained fn nft_burn_public_authorized_success() { 25 | // Setup with account contracts and mint NFT to owner 26 | let token_id = 10000; 27 | let (env, nft_contract_address, owner, _, recipient) = 28 | utils::setup_and_mint_to_public(true, token_id); 29 | 30 | // Create burn call interface with non-zero nonce 31 | let burn_call_interface = NFT::at(nft_contract_address).burn_public(owner, token_id, 1); 32 | 33 | // Add authorization witness from owner to recipient 34 | authwit_cheatcodes::add_public_authwit_from_call_interface( 35 | env, 36 | owner, 37 | recipient, 38 | burn_call_interface, 39 | ); 40 | 41 | // Impersonate recipient to perform the authorized burn 42 | env.call_public(recipient, burn_call_interface); 43 | 44 | // Verify NFT is burned 45 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 46 | utils::assert_nft_does_not_exist(env, nft_contract_address, token_id); 47 | } 48 | 49 | #[test(should_fail_with = "unauthorized")] 50 | unconstrained fn nft_burn_public_unauthorized_fail() { 51 | // Setup with account contracts 52 | let token_id = 10000; 53 | let (env, nft_contract_address, owner, _, recipient) = 54 | utils::setup_and_mint_to_public(true, token_id); 55 | 56 | // Create burn interface with non-zero nonce 57 | let burn_call_interface = NFT::at(nft_contract_address).burn_public(owner, token_id, 1); 58 | 59 | // Impersonate recipient but DON'T add authorization witness 60 | env.call_public(recipient, burn_call_interface); 61 | } 62 | 63 | #[test(should_fail_with = "unauthorized")] 64 | unconstrained fn nft_burn_public_wrong_authwit_fail() { 65 | // Setup with account contracts 66 | let token_id = 10000; 67 | let (env, nft_contract_address, owner, _, recipient) = 68 | utils::setup_and_mint_to_public(true, token_id); 69 | 70 | // Create burn interface with non-zero nonce 71 | let burn_call_interface = NFT::at(nft_contract_address).burn_public(owner, token_id, 1); 72 | 73 | // Add authorization witness but to the wrong address (owner instead of recipient) 74 | authwit_cheatcodes::add_public_authwit_from_call_interface( 75 | env, 76 | owner, 77 | owner, // Wrong address - should be recipient 78 | burn_call_interface, 79 | ); 80 | 81 | // Impersonate recipient 82 | env.call_public(recipient, burn_call_interface); 83 | } 84 | 85 | #[test(should_fail_with = "caller is not owner")] 86 | unconstrained fn nft_burn_public_non_existent_fail() { 87 | // Setup but don't mint any NFT 88 | let (env, nft_contract_address, owner, _, _) = utils::setup_with_minter(false); 89 | let non_existent_token_id = 12345; 90 | 91 | // Attempt to burn non-existent NFT 92 | env.call_public( 93 | owner, 94 | NFT::at(nft_contract_address).burn_public(owner, non_existent_token_id, 0), 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/transfer_public_to_private.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::{ 4 | oracle::random::random, protocol_types::address::AztecAddress, 5 | test::helpers::authwit as authwit_cheatcodes, 6 | }; 7 | 8 | #[test] 9 | unconstrained fn nft_transfer_public_to_private_success() { 10 | // Setup and mint NFT to owner in public state 11 | let token_id = 10000; 12 | let (env, nft_contract_address, owner, _, recipient) = 13 | utils::setup_and_mint_to_public(false, token_id); 14 | 15 | // Verify initial ownership state 16 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 17 | 18 | // Transfer NFT from public to private state 19 | env.call_private( 20 | owner, 21 | NFT::at(nft_contract_address).transfer_public_to_private(owner, recipient, token_id, 0), 22 | ); 23 | 24 | // Verify the NFT is no longer owned publicly by owner 25 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 26 | // Verify the NFT is now owned privately by recipient 27 | utils::assert_owns_private_nft(env, nft_contract_address, recipient, token_id); 28 | } 29 | 30 | #[test] 31 | unconstrained fn nft_transfer_public_to_private_authorized_success() { 32 | // Setup with account contracts (needed for authwits) and mint NFT to owner 33 | let token_id = 10000; 34 | let (env, nft_contract_address, owner, _, recipient) = 35 | utils::setup_and_mint_to_public(true, token_id); 36 | 37 | // Create the transfer call interface 38 | let transfer_call_interface = 39 | NFT::at(nft_contract_address).transfer_public_to_private(owner, recipient, token_id, 1); 40 | 41 | // Add authorization witness from owner to recipient 42 | authwit_cheatcodes::add_private_authwit_from_call_interface( 43 | env, 44 | owner, 45 | recipient, 46 | transfer_call_interface, 47 | ); 48 | 49 | // Impersonate recipient to perform the authorized transfer 50 | env.call_private(recipient, transfer_call_interface); 51 | 52 | // Verify the NFT is no longer owned publicly by owner 53 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 54 | // Verify the NFT is now owned privately by recipient 55 | utils::assert_owns_private_nft(env, nft_contract_address, recipient, token_id); 56 | } 57 | 58 | #[test(should_fail_with = "caller is not owner")] 59 | unconstrained fn nft_transfer_public_to_private_not_owned_fail() { 60 | // Setup environment but don't mint the NFT 61 | let (env, nft_contract_address, owner, _, recipient) = utils::setup_with_minter(false); 62 | let non_existent_token_id = 12345; 63 | 64 | // Attempt to transfer a token that doesn't exist / isn't owned 65 | env.call_private( 66 | owner, 67 | NFT::at(nft_contract_address).transfer_public_to_private( 68 | owner, 69 | recipient, 70 | non_existent_token_id, 71 | 0, 72 | ), 73 | ); 74 | } 75 | 76 | #[test(should_fail_with = "Unknown auth witness for message hash")] 77 | unconstrained fn nft_transfer_public_to_private_unauthorized_fail() { 78 | // Setup with account contracts for proper authorization testing 79 | let token_id = 10000; 80 | let (env, nft_contract_address, owner, _, recipient) = 81 | utils::setup_and_mint_to_public(true, token_id); 82 | 83 | // Create transfer interface with non-zero nonce (indicating authorization needed) 84 | let transfer_call_interface = NFT::at(nft_contract_address).transfer_public_to_private( 85 | owner, 86 | recipient, 87 | token_id, 88 | random(), 89 | ); 90 | 91 | // Impersonate recipient but DON'T add authorization witness 92 | // This test verifies that without an authorization witness (authwit), 93 | // the recipient cannot transfer the NFT on behalf of the owner 94 | env.call_private(recipient, transfer_call_interface); 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/canary.yml: -------------------------------------------------------------------------------- 1 | name: Canary Release 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | export: 7 | name: Generate Canary Release 8 | environment: Development 9 | runs-on: ubuntu-latest 10 | 11 | env: 12 | PROJECT_NAME: '@defi-wonderland/aztec-standards' 13 | 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Install Node 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: "22.17.0" 22 | registry-url: "https://registry.npmjs.org" 23 | cache: "yarn" 24 | 25 | - name: Set up Docker 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Detect Aztec version 29 | id: aztec-version 30 | run: | 31 | AZTEC_VERSION=$(node -p "require('./package.json').config.aztecVersion") 32 | echo "version=$AZTEC_VERSION" >> "$GITHUB_OUTPUT" 33 | echo "Aztec version is $AZTEC_VERSION" 34 | 35 | - name: Install Aztec CLI 36 | run: | 37 | curl -s https://install.aztec.network > tmp.sh 38 | bash tmp.sh <<< yes "yes" 39 | - name: Update path 40 | run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH 41 | 42 | - name: Set Aztec version 43 | run: | 44 | VERSION=${{ steps.aztec-version.outputs.version }} aztec-up 45 | 46 | # This is a temporary hack to fix a problem with v3 releases. 47 | - name: Manually tag the aztec version as `latest` 48 | run: | 49 | docker tag aztecprotocol/aztec:${{ steps.aztec-version.outputs.version }} aztecprotocol/aztec:latest 50 | 51 | - name: Install dependencies 52 | run: yarn --frozen-lockfile 53 | 54 | - name: Compile 55 | run: yarn compile 56 | 57 | - name: Codegen 58 | run: aztec codegen target --outdir artifacts 59 | 60 | - name: Compile artifacts to JS 61 | run: | 62 | mkdir -p dist/ 63 | yarn tsc artifacts/*.ts --outDir dist/ --skipLibCheck --target es2020 --module nodenext --moduleResolution nodenext --resolveJsonModule --declaration 64 | 65 | - name: Update version 66 | run: yarn version --new-version "0.0.0-${GITHUB_SHA::8}" --no-git-tag-version 67 | 68 | - name: Inspect contracts 69 | run: | 70 | for f in target/*.json; do 71 | [ -f "$f" ] || continue 72 | aztec inspect-contract "$f" 73 | done 74 | 75 | # TODO: We do several things here: 76 | # 1. Create artifacts directory 77 | # 2. Copy compiled JS artifacts to artifacts/ 78 | # 3. Copy compiled Noir contracts (target/) to package root 79 | # 4. Copy deployments.json to package root (if exists) 80 | # 5. Copy README.md and LICENSE to package root 81 | # 6. Create trimmed package.json 82 | - name: Prepare files for release 83 | run: | 84 | mkdir -p export/${{ env.PROJECT_NAME }}/artifacts 85 | 86 | # Copy the compiled JS files to artifacts/ 87 | cp -r dist/artifacts/* export/${{ env.PROJECT_NAME }}/artifacts/ 88 | 89 | # Copy compiled Noir contracts to package root 90 | cp -r target export/${{ env.PROJECT_NAME }}/ 91 | 92 | # Copy deployments.json if it exists 93 | if [ -f "src/deployments.json" ]; then 94 | cp src/deployments.json export/${{ env.PROJECT_NAME }}/ 95 | else 96 | echo "src/deployments.json not found, skipping copy" 97 | fi 98 | 99 | cp README.md export/${{ env.PROJECT_NAME }}/ 100 | cp LICENSE export/${{ env.PROJECT_NAME }}/ 101 | 102 | cat package.json | jq 'del(.scripts, .jest, ."lint-staged", .packageManager, .devDependencies, .dependencies, .engines, .resolutions)' > export/${{ env.PROJECT_NAME }}/package.json 103 | - name: Publish to NPM 104 | run: cd export/${{ env.PROJECT_NAME }} && npm publish --access public --tag canary 105 | env: 106 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /src/token_contract/src/test/burn_public.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils::{self, mint_amount}; 2 | use crate::Token; 3 | use aztec::{oracle::random::random, test::helpers::authwit as authwit_cheatcodes}; 4 | 5 | #[test] 6 | unconstrained fn burn_public_success() { 7 | let (mut env, token_contract_address, owner, _) = 8 | utils::setup_and_mint_to_public_without_minter(false); 9 | let burn_amount = mint_amount / 10 as u128; 10 | 11 | // Burn less than balance 12 | env.call_public(owner, Token::at(token_contract_address).burn_public(owner, burn_amount, 0)); 13 | utils::check_public_balance( 14 | env, 15 | token_contract_address, 16 | owner, 17 | mint_amount - burn_amount, 18 | ); 19 | } 20 | 21 | #[test] 22 | unconstrained fn burn_public_decrease_total_supply() { 23 | let (mut env, token_contract_address, owner, _) = 24 | utils::setup_and_mint_to_public_without_minter(false); 25 | let burn_amount = mint_amount / 10 as u128; 26 | 27 | utils::check_total_supply(env, token_contract_address, mint_amount); 28 | 29 | // Burn less than balance 30 | env.call_public(owner, Token::at(token_contract_address).burn_public(owner, burn_amount, 0)); 31 | utils::check_public_balance( 32 | env, 33 | token_contract_address, 34 | owner, 35 | mint_amount - burn_amount, 36 | ); 37 | utils::check_total_supply(env, token_contract_address, mint_amount - burn_amount); 38 | } 39 | 40 | #[test] 41 | unconstrained fn burn_public_on_behalf_of_other() { 42 | let (mut env, token_contract_address, owner, recipient) = 43 | utils::setup_and_mint_to_public_without_minter(true); 44 | let burn_amount = mint_amount / 10 as u128; 45 | 46 | // Burn on behalf of other 47 | let burn_call_interface = 48 | Token::at(token_contract_address).burn_public(owner, burn_amount, random()); 49 | authwit_cheatcodes::add_public_authwit_from_call_interface( 50 | env, 51 | owner, 52 | recipient, 53 | burn_call_interface, 54 | ); 55 | // Burn tokens 56 | env.call_public(recipient, burn_call_interface); 57 | utils::check_public_balance( 58 | env, 59 | token_contract_address, 60 | owner, 61 | mint_amount - burn_amount, 62 | ); 63 | } 64 | 65 | #[test(should_fail_with = "attempt to subtract with overflow 'public_balances.at(from).read() - amount'")] 66 | unconstrained fn burn_public_failure_more_than_balance() { 67 | let (mut env, token_contract_address, owner, _) = 68 | utils::setup_and_mint_to_public_without_minter(false); 69 | 70 | // Burn more than balance 71 | let burn_amount = mint_amount * 10 as u128; 72 | // Try to burn 73 | env.call_public(owner, Token::at(token_contract_address).burn_public(owner, burn_amount, 0)); 74 | } 75 | 76 | #[test(should_fail_with = "unauthorized")] 77 | unconstrained fn burn_public_failure_on_behalf_of_other_without_approval() { 78 | let (mut env, token_contract_address, owner, recipient) = 79 | utils::setup_and_mint_to_public_without_minter(true); 80 | 81 | // Burn on behalf of other without approval 82 | let burn_amount = mint_amount / 10 as u128; 83 | let burn_call_interface = 84 | Token::at(token_contract_address).burn_public(owner, burn_amount, random()); 85 | env.call_public(recipient, burn_call_interface); 86 | } 87 | 88 | #[test(should_fail_with = "unauthorized")] 89 | unconstrained fn burn_public_failure_on_behalf_of_other_wrong_caller() { 90 | let (mut env, token_contract_address, owner, recipient) = 91 | utils::setup_and_mint_to_public_without_minter(true); 92 | 93 | // Burn on behalf of other, wrong designated caller 94 | let burn_amount = mint_amount / (10 as u128); 95 | let burn_call_interface = 96 | Token::at(token_contract_address).burn_public(owner, burn_amount, random()); 97 | authwit_cheatcodes::add_public_authwit_from_call_interface( 98 | env, 99 | owner, 100 | owner, 101 | burn_call_interface, 102 | ); 103 | env.call_public(recipient, burn_call_interface); 104 | } 105 | -------------------------------------------------------------------------------- /benchmarks/logic_contract.benchmark.ts: -------------------------------------------------------------------------------- 1 | // Import Aztec dependencies 2 | import { Fr } from '@aztec/aztec.js/fields'; 3 | import type { PXE } from '@aztec/pxe/server'; 4 | import { deriveKeys } from '@aztec/stdlib/keys'; 5 | import type { Wallet } from '@aztec/aztec.js/wallet'; 6 | import { AztecAddress } from '@aztec/aztec.js/addresses'; 7 | import { getContractClassFromArtifact } from '@aztec/aztec.js/contracts'; 8 | import type { ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization'; 9 | 10 | // Import the new Benchmark base class and context 11 | import { Benchmark, BenchmarkContext } from '@defi-wonderland/aztec-benchmark'; 12 | 13 | // Import artifacts 14 | import { EscrowContract, EscrowContractArtifact } from '../artifacts/Escrow.js'; 15 | import { TestLogicContract } from '../artifacts/TestLogic.js'; 16 | 17 | // Import test utilities 18 | import { 19 | setupTestSuite, 20 | deployLogic, 21 | deployEscrowWithPublicKeysAndSalt, 22 | grumpkinScalarToFr, 23 | } from '../src/ts/test/utils.js'; 24 | 25 | // Extend the BenchmarkContext from the new package 26 | interface LogicBenchmarkContext extends BenchmarkContext { 27 | pxe: PXE; 28 | wallet: Wallet; 29 | deployer: AztecAddress; 30 | accounts: AztecAddress[]; 31 | logicContract: TestLogicContract; 32 | escrowContract: EscrowContract; 33 | secretKeys: { 34 | nsk_m: Fr; 35 | ivsk_m: Fr; 36 | ovsk_m: Fr; 37 | tsk_m: Fr; 38 | }; 39 | } 40 | 41 | // Use export default class extending Benchmark 42 | export default class LogicContractBenchmark extends Benchmark { 43 | /** 44 | * Sets up the benchmark environment for the TokenContract. 45 | * Creates PXE client, gets accounts, and deploys the contract. 46 | */ 47 | async setup(): Promise { 48 | const { pxe, wallet, accounts } = await setupTestSuite('bench-logic'); 49 | const [deployer] = accounts; 50 | 51 | const escrowClassId = (await getContractClassFromArtifact(EscrowContractArtifact)).id; 52 | 53 | // Deploy logic contract 54 | const logicContract = (await deployLogic(wallet, deployer, escrowClassId)) as TestLogicContract; 55 | 56 | // Setup escrow 57 | const escrowSk = Fr.random(); 58 | const escrowKeys = await deriveKeys(escrowSk); 59 | const secretKeys = { 60 | nsk_m: grumpkinScalarToFr(escrowKeys.masterNullifierSecretKey), 61 | ivsk_m: grumpkinScalarToFr(escrowKeys.masterIncomingViewingSecretKey), 62 | ovsk_m: grumpkinScalarToFr(escrowKeys.masterOutgoingViewingSecretKey), 63 | tsk_m: grumpkinScalarToFr(escrowKeys.masterTaggingSecretKey), 64 | }; 65 | const escrowSalt = new Fr(logicContract.instance.address.toBigInt()); 66 | const escrowContract = (await deployEscrowWithPublicKeysAndSalt( 67 | escrowKeys.publicKeys, 68 | wallet, 69 | deployer, 70 | escrowSalt, 71 | )) as EscrowContract; 72 | 73 | return { 74 | pxe, 75 | wallet, 76 | deployer, 77 | accounts, 78 | logicContract, 79 | escrowContract, 80 | secretKeys, 81 | }; 82 | } 83 | 84 | /** 85 | * Returns the list of TokenContract methods to be benchmarked. 86 | */ 87 | getMethods(context: LogicBenchmarkContext): ContractFunctionInteractionCallIntent[] { 88 | const { accounts, escrowContract, deployer, logicContract, secretKeys, wallet } = context; 89 | const recipient = accounts[2]; 90 | 91 | const methods: ContractFunctionInteractionCallIntent[] = [ 92 | // Derive public keys from secret keys 93 | { 94 | caller: deployer, 95 | action: logicContract.withWallet(wallet).methods.secret_keys_to_public_keys(secretKeys), 96 | }, 97 | // Check escrow correctness 98 | { 99 | caller: deployer, 100 | action: logicContract.withWallet(wallet).methods.get_escrow(secretKeys), 101 | }, 102 | // Share escrow 103 | { 104 | caller: deployer, 105 | action: logicContract.withWallet(wallet).methods.share_escrow(recipient, escrowContract.address, secretKeys), 106 | }, 107 | ]; 108 | 109 | return methods.filter(Boolean); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/token_contract/src/test/burn_private.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils::{self, mint_amount}; 2 | use crate::Token; 3 | use aztec::{oracle::random::random, test::helpers::authwit as authwit_cheatcodes}; 4 | 5 | #[test] 6 | unconstrained fn burn_private_on_behalf_of_self() { 7 | let (env, token_contract_address, owner, _) = 8 | utils::setup_and_mint_to_private_without_minter(false); 9 | let burn_amount = mint_amount / 10; 10 | 11 | // Burn less than balance 12 | env.call_private(owner, Token::at(token_contract_address).burn_private(owner, burn_amount, 0)); 13 | utils::check_private_balance( 14 | env, 15 | token_contract_address, 16 | owner, 17 | mint_amount - burn_amount, 18 | ); 19 | } 20 | 21 | #[test] 22 | unconstrained fn burn_private_on_behalf_of_other() { 23 | let (mut env, token_contract_address, owner, recipient) = 24 | utils::setup_and_mint_to_private_without_minter(true); 25 | let burn_amount = mint_amount / 10; 26 | 27 | // Burn on behalf of other 28 | let burn_call_interface = 29 | Token::at(token_contract_address).burn_private(owner, burn_amount, random()); 30 | authwit_cheatcodes::add_private_authwit_from_call_interface( 31 | env, 32 | owner, 33 | recipient, 34 | burn_call_interface, 35 | ); 36 | // Burn tokens 37 | env.call_private(recipient, burn_call_interface); 38 | utils::check_private_balance( 39 | env, 40 | token_contract_address, 41 | owner, 42 | mint_amount - burn_amount, 43 | ); 44 | } 45 | 46 | #[test(should_fail_with = "Balance too low")] 47 | unconstrained fn burn_private_failure_more_than_balance() { 48 | let (mut env, token_contract_address, owner, _) = 49 | utils::setup_and_mint_to_public_without_minter(false); 50 | 51 | // Burn more than balance 52 | let burn_amount = mint_amount * 10; 53 | env.call_private(owner, Token::at(token_contract_address).burn_private(owner, burn_amount, 0)); 54 | } 55 | 56 | #[test(should_fail_with = "Balance too low")] 57 | unconstrained fn burn_private_failure_on_behalf_of_other_more_than_balance() { 58 | let (mut env, token_contract_address, owner, recipient) = 59 | utils::setup_and_mint_to_public_without_minter(true); 60 | 61 | // Burn more than balance 62 | let burn_amount = mint_amount * (10 as u128); 63 | // Burn on behalf of other 64 | let burn_call_interface = 65 | Token::at(token_contract_address).burn_private(owner, burn_amount, random()); 66 | authwit_cheatcodes::add_private_authwit_from_call_interface( 67 | env, 68 | owner, 69 | recipient, 70 | burn_call_interface, 71 | ); 72 | env.call_private(recipient, burn_call_interface); 73 | } 74 | #[test(should_fail_with = "Unknown auth witness for message hash")] 75 | unconstrained fn burn_private_failure_on_behalf_of_other_without_approval() { 76 | let (mut env, token_contract_address, owner, recipient) = 77 | utils::setup_and_mint_to_public_without_minter(true); 78 | 79 | // // Burn more than balance 80 | let burn_amount = mint_amount / (10 as u128); 81 | // // Burn on behalf of other 82 | let burn_call_interface = 83 | Token::at(token_contract_address).burn_private(owner, burn_amount, random()); 84 | env.call_private(recipient, burn_call_interface); 85 | } 86 | 87 | #[test(should_fail_with = "Unknown auth witness for message hash")] 88 | unconstrained fn burn_private_failure_on_behalf_of_other_wrong_designated_caller() { 89 | let (mut env, token_contract_address, owner, recipient) = 90 | utils::setup_and_mint_to_public_without_minter(true); 91 | 92 | // Burn more than balance 93 | let burn_amount = mint_amount / (10 as u128); 94 | // Burn on behalf of other 95 | let burn_call_interface = 96 | Token::at(token_contract_address).burn_private(owner, burn_amount, random()); 97 | authwit_cheatcodes::add_private_authwit_from_call_interface( 98 | env, 99 | owner, 100 | owner, 101 | burn_call_interface, 102 | ); 103 | env.call_private(recipient, burn_call_interface); 104 | } 105 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test/utils.nr: -------------------------------------------------------------------------------- 1 | use crate::TestLogic; 2 | use aztec::{ 3 | protocol_types::{ 4 | address::AztecAddress, 5 | point::Point, 6 | public_keys::{IvpkM, NpkM, OvpkM, PublicKeys, TpkM}, 7 | }, 8 | test::helpers::test_environment::TestEnvironment, 9 | }; 10 | use escrow_contract::library::logic::MasterSecretKeys; 11 | 12 | pub unconstrained fn deploy_logic( 13 | env: &mut TestEnvironment, 14 | escrow_class_id: Field, 15 | ) -> AztecAddress { 16 | let owner = env.create_light_account(); 17 | 18 | // Deploy logic contract 19 | let initializer_call_interface = TestLogic::interface().constructor(escrow_class_id); 20 | let logic_contract = env.deploy("@test_logic_contract/TestLogic").with_public_initializer( 21 | owner, 22 | initializer_call_interface, 23 | ); 24 | 25 | logic_contract 26 | } 27 | 28 | pub unconstrained fn get_test_vector() -> (PublicKeys, MasterSecretKeys, PublicKeys, MasterSecretKeys) { 29 | // secret keys derived using 1 as the seed 30 | let master_secret_keys_1 = MasterSecretKeys { 31 | nsk_m: 0x1c46232974b84af1ea0f3f8e02d68b205fa0fd765839bdd298d270c6f4d87190, 32 | ivsk_m: 0x23077d282aa597e7a1c6ffb4a1069cffd9743e861451076740cf42cb632fec84, 33 | ovsk_m: 0x0fd5dc80151e24e20e402fa82437be7483431347ca4aabde674cdb12598d0fc0, 34 | tsk_m: 0x0d104e3f086a9c3127b4152bac929180c21c06f9539b62843f4971753dd4ecf9, 35 | }; 36 | 37 | let npk_x_1: Field = 0x0b6111daac402252e041933aad127a26c9851acb11ced405d8563435bb33e0b8; 38 | let npk_y_1: Field = 0x0ecf620002229630f89c4f06638c3155f7e331a98e3304d92f2b7e213250d3f6; 39 | let ivpk_x_1: Field = 0x228e3645559e65ec052b25919ca2e8e9b610fcc1b61490a2fd7fc335909b018e; 40 | let ivpk_y_1: Field = 0x01ebaf7375bbeff41e6a41271ba3be92081b188654c773a76205b49265bb423c; 41 | let ovpk_x_1: Field = 0x1be0ff304b9be7acedfdf137a5e369576a19246f181602f7f53ba3ec319e18c8; 42 | let ovpk_y_1: Field = 0x19660b865a39e2196cc0ab6c0b901c23a30d275b8ec8f7d327478b24bb8629bc; 43 | let tpk_x_1: Field = 0x1ec58e51af858e5aca8a6f4ec35016cf3df68154d87d0fc235bedf94d1418e7e; 44 | let tpk_y_1: Field = 0x0e76360c52a5695c666495a6094032a486c04cf71cc9cf997bf92b916bed1493; 45 | 46 | let public_keys_1 = PublicKeys { 47 | npk_m: NpkM { inner: Point { x: npk_x_1, y: npk_y_1, is_infinite: false } }, 48 | ivpk_m: IvpkM { inner: Point { x: ivpk_x_1, y: ivpk_y_1, is_infinite: false } }, 49 | ovpk_m: OvpkM { inner: Point { x: ovpk_x_1, y: ovpk_y_1, is_infinite: false } }, 50 | tpk_m: TpkM { inner: Point { x: tpk_x_1, y: tpk_y_1, is_infinite: false } }, 51 | }; 52 | 53 | // secret keys derived using string "RANDOM_SECRET_KEY" left-padded to 32 bytes with spaces as the seed: 54 | // 0x20202020202020202020202020202052414e444f4d5f5345435245545f4b4559 55 | let master_secret_keys_2 = MasterSecretKeys { 56 | nsk_m: 0x0e9600b209142cfe453ee5082e0eb5916b381f81a1967556afe1049c87cbb2ae, 57 | ivsk_m: 0x01243994b0481f12305a17f6f12ed689e38c62d116efa1e2ff61718459f1d9b3, 58 | ovsk_m: 0x14958e327b2cbdfd7c18add0d5fc376e533f9ee47f18f47259c862a0429deaa4, 59 | tsk_m: 0x23c16c7e7fba57703aaf07ba75076c74d93747f2610e28689350b5e2f9061417, 60 | }; 61 | 62 | let npk_x_2: Field = 0x00ecf1384fa9a5acdaddcf0ca577d12d80309aab813dcf96ea79fad43f3daad9; 63 | let npk_y_2: Field = 0x24c81884aece11096a3058433339dd0180756c44b4372951f169c939f569f4fb; 64 | let ivpk_x_2: Field = 0x1d352885fed013e664fecb7cba4b2866f247616160d7b83218dfdba95302c16d; 65 | let ivpk_y_2: Field = 0x0c99f03d15323e6a59211db9ac5e81027671f8892030f06c9f541876974407ca; 66 | let ovpk_x_2: Field = 0x022d55fad39b3889ddecf7ab7634030c15da637e86e3ed550c8148076055e31a; 67 | let ovpk_y_2: Field = 0x27b58d54d8f22b769382eb8753181e3d56b0e2a247af7df7c79c28f01f168cc1; 68 | let tpk_x_2: Field = 0x2c3a360ec75935a2f3a12761bd764fe6fd97acaec5d085144795a05cbdf0b01b; 69 | let tpk_y_2: Field = 0x304512f4fad762bccf727a1cb5b1654211a6fdb7c09535faa1a786dd935bff55; 70 | 71 | let public_keys_2 = PublicKeys { 72 | npk_m: NpkM { inner: Point { x: npk_x_2, y: npk_y_2, is_infinite: false } }, 73 | ivpk_m: IvpkM { inner: Point { x: ivpk_x_2, y: ivpk_y_2, is_infinite: false } }, 74 | ovpk_m: OvpkM { inner: Point { x: ovpk_x_2, y: ovpk_y_2, is_infinite: false } }, 75 | tpk_m: TpkM { inner: Point { x: tpk_x_2, y: tpk_y_2, is_infinite: false } }, 76 | }; 77 | 78 | (public_keys_1, master_secret_keys_1, public_keys_2, master_secret_keys_2) 79 | } 80 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/main.nr: -------------------------------------------------------------------------------- 1 | mod test; 2 | 3 | use aztec::macros::aztec; 4 | 5 | #[aztec] 6 | contract TestLogic { 7 | // aztec library 8 | use aztec::{ 9 | macros::{events::event, functions::{external, initializer}, storage::storage}, 10 | protocol_types::{address::AztecAddress, public_keys::PublicKeys}, 11 | state_vars::public_immutable::PublicImmutable, 12 | }; 13 | use escrow_contract::library::logic::{ 14 | _get_escrow, _secret_keys_to_public_keys, _share_escrow, _withdraw, _withdraw_nft, 15 | MasterSecretKeys, 16 | }; 17 | 18 | /// @notice Data privately emitted to an account that will use the escrow 19 | /// @dev We declare the event struct here to expose it in the abi as macro event cannot be declared in libraries, only in contracts 20 | /// @dev See to library/logic.nr _share_escrow function for the event emission implementation 21 | /// @dev #[allow(dead_code)] added because is used because EscrowDetailsLogContent is defined for logging but not directly instantiated. 22 | /// @param escrow The address of the escrow 23 | /// @param master_secret_keys The master secret keys 24 | #[event] 25 | #[allow(dead_code)] 26 | struct EscrowDetailsLogContent { 27 | escrow: AztecAddress, 28 | master_secret_keys: MasterSecretKeys, 29 | } 30 | 31 | // @param escrow_class_id The contract class id of the escrow contract 32 | #[storage] 33 | struct Storage { 34 | escrow_class_id: PublicImmutable, 35 | } 36 | 37 | /// @dev Initialize the contract 38 | /// @param escrow_class_id The contract class id of the escrow contract 39 | #[external("public")] 40 | #[initializer] 41 | fn constructor(escrow_class_id: Field) { 42 | storage.escrow_class_id.initialize(escrow_class_id); 43 | } 44 | 45 | /// @notice Returns the escrow address that corresponds to the given master secret keys and class ID. 46 | /// @param master_secret_keys The master secret keys 47 | /// @return The escrow address 48 | #[external("private")] 49 | fn get_escrow(master_secret_keys: MasterSecretKeys) -> AztecAddress { 50 | _get_escrow( 51 | &mut context, 52 | storage.escrow_class_id.read(), 53 | master_secret_keys, 54 | ) 55 | } 56 | 57 | /// @notice Shares the escrow details needed to find and use the escrow contract 58 | /// @dev Emits a private log with the escrow details 59 | /// @param account The address of the account that will receive the escrow details 60 | /// @param escrow The address of the escrow 61 | /// @param master_secret_keys The master secret keys 62 | #[external("private")] 63 | fn share_escrow( 64 | account: AztecAddress, 65 | escrow: AztecAddress, 66 | master_secret_keys: MasterSecretKeys, 67 | ) { 68 | _share_escrow(&mut context, account, escrow, master_secret_keys); 69 | } 70 | 71 | /// @notice Withdraws an amount of tokens from the provided escrow. 72 | /// @param escrow The address of the escrow 73 | /// @param account The address of the account that will receive the tokens 74 | /// @param token The address of the token 75 | /// @param amount The amount of tokens to withdraw from the escrow 76 | #[external("private")] 77 | fn withdraw(escrow: AztecAddress, account: AztecAddress, token: AztecAddress, amount: u128) { 78 | _withdraw(&mut context, escrow, account, token, amount); 79 | } 80 | 81 | /// @notice Withdraws an NFT from the provided escrow. 82 | /// @param escrow The address of the escrow 83 | /// @param account The address of the account that will receive the NFT 84 | /// @param nft The address of the NFT contract 85 | /// @param token_id The id of the token to withdraw from the escrow 86 | #[external("private")] 87 | fn withdraw_nft( 88 | escrow: AztecAddress, 89 | account: AztecAddress, 90 | nft: AztecAddress, 91 | token_id: Field, 92 | ) { 93 | _withdraw_nft(&mut context, escrow, account, nft, token_id); 94 | } 95 | 96 | /// @notice Derives public keys from secret keys. 97 | /// @param master_secret_keys The master secret keys 98 | /// @return PublicKeys containing the derived public keys. 99 | #[external("private")] 100 | fn secret_keys_to_public_keys(master_secret_keys: MasterSecretKeys) -> PublicKeys { 101 | _secret_keys_to_public_keys(master_secret_keys) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /benchmarks/escrow_contract.benchmark.ts: -------------------------------------------------------------------------------- 1 | // Import Aztec dependencies 2 | import { Fr } from '@aztec/aztec.js/fields'; 3 | import type { PXE } from '@aztec/pxe/server'; 4 | import { deriveKeys } from '@aztec/stdlib/keys'; 5 | import type { Wallet } from '@aztec/aztec.js/wallet'; 6 | import { AztecAddress } from '@aztec/aztec.js/addresses'; 7 | import type { ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization'; 8 | 9 | // Import the new Benchmark base class and context 10 | import { Benchmark, BenchmarkContext } from '@defi-wonderland/aztec-benchmark'; 11 | import type { NamedBenchmarkedInteraction } from '@defi-wonderland/aztec-benchmark/dist/types.js'; 12 | 13 | // Import artifacts 14 | import { TokenContract } from '../artifacts/Token.js'; 15 | import { EscrowContract, EscrowContractArtifact } from '../artifacts/Escrow.js'; 16 | import { NFTContract } from '../artifacts/NFT.js'; 17 | 18 | // Import test utilities 19 | import { setupTestSuite, deployEscrow, deployTokenWithMinter, deployNFTWithMinter } from '../src/ts/test/utils.js'; 20 | 21 | // Extend the BenchmarkContext from the new package 22 | interface TokenBenchmarkContext extends BenchmarkContext { 23 | pxe: PXE; 24 | wallet: Wallet; 25 | deployer: AztecAddress; 26 | accounts: AztecAddress[]; 27 | tokenContract: TokenContract; 28 | nftContract: NFTContract; 29 | escrowContract: EscrowContract; 30 | tokenAmount: number; 31 | tokenId: number; 32 | } 33 | 34 | // Use export default class extending Benchmark 35 | export default class TokenContractBenchmark extends Benchmark { 36 | /** 37 | * Sets up the benchmark environment for the TokenContract. 38 | * Creates PXE client, gets accounts, and deploys the contract. 39 | */ 40 | async setup(): Promise { 41 | const { pxe, wallet, accounts } = await setupTestSuite('bench-escrow'); 42 | const [deployer, logicMock] = accounts; 43 | 44 | // Setup escrow 45 | const escrowSk = Fr.random(); 46 | const escrowKeys = await deriveKeys(escrowSk); 47 | const escrowSalt = new Fr(logicMock.toBigInt()); 48 | const escrowContract = (await deployEscrow(escrowKeys.publicKeys, wallet, deployer, escrowSalt)) as EscrowContract; 49 | await wallet.registerContract(escrowContract.instance, EscrowContractArtifact, escrowSk); 50 | 51 | // Deploy token and NFT contracts 52 | const deployedTokenContract = await deployTokenWithMinter(wallet, deployer); 53 | const tokenContract = await TokenContract.at(deployedTokenContract.address, wallet); 54 | const deployedNFTContract = await deployNFTWithMinter(wallet, deployer); 55 | const nftContract = await NFTContract.at(deployedNFTContract.address, wallet); 56 | 57 | // Mint tokens and NFT to the escrow contract 58 | const tokenAmount = 100; 59 | await tokenContract 60 | .withWallet(wallet) 61 | .methods.mint_to_private(escrowContract.address, tokenAmount) 62 | .send({ from: deployer }) 63 | .wait(); 64 | const tokenId = 1; 65 | await nftContract 66 | .withWallet(wallet) 67 | .methods.mint_to_private(escrowContract.address, tokenId) 68 | .send({ from: deployer }) 69 | .wait(); 70 | 71 | return { pxe, wallet, deployer, accounts, tokenContract, nftContract, escrowContract, tokenAmount, tokenId }; 72 | } 73 | 74 | /** 75 | * Returns the list of TokenContract methods to be benchmarked. 76 | */ 77 | getMethods( 78 | context: TokenBenchmarkContext, 79 | ): Array { 80 | const { accounts, tokenContract, nftContract, escrowContract, tokenId, wallet } = context; 81 | const logicMock = accounts[1]; 82 | const recipient = accounts[2]; 83 | const halfAmount = 50; 84 | 85 | const methods: Array = [ 86 | // Partial token withdrawal (with change) 87 | { 88 | interaction: { 89 | caller: logicMock, 90 | action: escrowContract.withWallet(wallet).methods.withdraw(tokenContract.address, halfAmount, recipient), 91 | }, 92 | name: '(partial) withdraw', 93 | }, 94 | // Full token withdrawal 95 | { 96 | caller: logicMock, 97 | action: escrowContract.withWallet(wallet).methods.withdraw(tokenContract.address, halfAmount, recipient), 98 | }, 99 | // NFT withdrawal 100 | { 101 | caller: logicMock, 102 | action: escrowContract.withWallet(wallet).methods.withdraw_nft(nftContract.address, tokenId, recipient), 103 | }, 104 | ]; 105 | 106 | return methods.filter(Boolean); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/withdraw.nr: -------------------------------------------------------------------------------- 1 | use crate::Escrow; 2 | use crate::test::utils as escrow_utils; 3 | use aztec::oracle::get_contract_instance::get_contract_instance; 4 | use aztec::protocol_types::{address::AztecAddress, traits::FromField}; 5 | use token::{test::utils as token_utils, Token}; 6 | 7 | // TODO: Fix error "No public key registered for address 0x0000000000000000000000000000000000000000000000000000000000000001." 8 | // Previously, with env.impersonate(any address) we could impersonate any address even if it didn't have keys, 9 | // but now with env.call_private(any address, ...) it is not possible. 10 | // The sender must have keys and the address must be registered in PXE like this. 11 | // Because the escrow contract cannot be deployed with the salt that we want yet, 12 | // it defaults to address 0x1 which we don't know the secret of. 13 | // #[test] 14 | #[allow(dead_code)] 15 | unconstrained fn escrow_withdraw_success() { 16 | let escrow_secret: Field = 123456; 17 | let (mut env, escrow_contract_address, token_contract_address, _, _, recipient, minter) = 18 | escrow_utils::set_escrow_with_token_and_nft(false, escrow_secret); 19 | 20 | let escrow_instance = get_contract_instance(escrow_contract_address); 21 | let logic_contract_address = AztecAddress::from_field(escrow_instance.salt); 22 | 23 | // Mint some tokens to escrow 24 | let amount: u128 = token_utils::mint_amount; 25 | env.call_private( 26 | minter, 27 | Token::at(token_contract_address).mint_to_private(escrow_contract_address, amount), 28 | ); 29 | 30 | // Check the escrow received and can read the tokens 31 | token_utils::check_private_balance( 32 | env, 33 | token_contract_address, 34 | escrow_contract_address, 35 | amount, 36 | ); 37 | 38 | env.call_private( 39 | logic_contract_address, 40 | Escrow::at(escrow_contract_address).withdraw(token_contract_address, amount, recipient), 41 | ); 42 | 43 | // Check recipient got tokens 44 | token_utils::check_private_balance(env, token_contract_address, recipient, amount); 45 | } 46 | 47 | // #[test] 48 | #[allow(dead_code)] 49 | unconstrained fn escrow_withdraw_twice_success() { 50 | let escrow_secret: Field = 123456; 51 | let (mut env, escrow_contract_address, token_contract_address, _, _, recipient, minter) = 52 | escrow_utils::set_escrow_with_token_and_nft(false, escrow_secret); 53 | 54 | let escrow_instance = get_contract_instance(escrow_contract_address); 55 | let logic_contract_address = AztecAddress::from_field(escrow_instance.salt); 56 | 57 | // Mint some tokens to escrow 58 | let total_amount: u128 = token_utils::mint_amount * 2; 59 | env.call_private( 60 | minter, 61 | Token::at(token_contract_address).mint_to_private(escrow_contract_address, total_amount), 62 | ); 63 | 64 | // Check the escrow received and can read the tokens 65 | token_utils::check_private_balance( 66 | env, 67 | token_contract_address, 68 | escrow_contract_address, 69 | total_amount, 70 | ); 71 | 72 | let amount: u128 = token_utils::mint_amount; 73 | // Partial withdrawal 74 | env.call_private( 75 | logic_contract_address, 76 | Escrow::at(escrow_contract_address).withdraw(token_contract_address, amount, recipient), 77 | ); 78 | 79 | // Check balances 80 | token_utils::check_private_balance(env, token_contract_address, recipient, amount); 81 | token_utils::check_private_balance( 82 | env, 83 | token_contract_address, 84 | escrow_contract_address, 85 | amount, 86 | ); 87 | 88 | // Complete withdrawal 89 | env.call_private( 90 | logic_contract_address, 91 | Escrow::at(escrow_contract_address).withdraw(token_contract_address, amount, recipient), 92 | ); 93 | 94 | // Check balances 95 | token_utils::check_private_balance(env, token_contract_address, recipient, total_amount); 96 | token_utils::check_private_balance(env, token_contract_address, escrow_contract_address, 0); 97 | } 98 | 99 | #[test(should_fail_with = "Not Authorized")] 100 | unconstrained fn escrow_withdraw_unauthorized() { 101 | let escrow_secret: Field = 123456; 102 | let (mut env, escrow_contract_address, token_contract_address, _, owner, recipient, _) = 103 | escrow_utils::set_escrow_with_token_and_nft(false, escrow_secret); 104 | 105 | let amount: u128 = token_utils::mint_amount; 106 | env.call_private( 107 | owner, 108 | Escrow::at(escrow_contract_address).withdraw(token_contract_address, amount, recipient), 109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/escrow_contract/src/test/test_logic_contract/src/test/get_escrow.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils as logic_utils; 2 | use crate::TestLogic; 3 | use aztec::{ 4 | oracle::get_contract_instance::get_contract_instance, 5 | protocol_types::{ 6 | address::AztecAddress, 7 | contract_instance::ContractInstance, 8 | traits::{FromField, ToField}, 9 | }, 10 | }; 11 | use aztec::test::helpers::test_environment::TestEnvironment; 12 | use escrow_contract::test::utils as escrow_utils; 13 | 14 | // TODO: Testing that get_escrow succeeds with a deployed escrow contract is not possible given that custom salt 15 | // is not supported yet in the test environment. Testing deployer and initialization hash has the same problem, 16 | // it is not possible to deploy a contract with public keys and constructor/deployer 17 | // https://github.com/AztecProtocol/aztec-packages/issues/16656 18 | #[test(should_fail_with = "Incorrect escrow deployed")] 19 | unconstrained fn get_escrow_correct_instance() { 20 | let mut env = TestEnvironment::new(); 21 | 22 | // Secret keys derived using a different seed than 1, hence the public keys will not match 23 | let secret = 1; 24 | let (public_keys, msk, _, _) = logic_utils::get_test_vector(); 25 | 26 | let deployed_escrow = escrow_utils::deploy_escrow_with_secret(&mut env, secret); 27 | 28 | let escrow_class_id = escrow_utils::get_escrow_class_id(); 29 | 30 | let logic = logic_utils::deploy_logic(&mut env, escrow_class_id); 31 | 32 | let caller = env.create_light_account(); 33 | let escrow = env.call_private(caller, TestLogic::at(logic).get_escrow(msk)); 34 | assert_eq(deployed_escrow, escrow, "Incorrect escrow deployed"); 35 | 36 | let escrow_instance: ContractInstance = get_contract_instance(escrow); 37 | 38 | assert_eq(escrow_instance.salt, logic.to_field(), "Salt is not the logic contract"); 39 | assert_eq(escrow_instance.deployer, AztecAddress::from_field(0), "Deployer is not zero"); 40 | assert_eq( 41 | escrow_instance.contract_class_id.to_field(), 42 | escrow_class_id, 43 | "Escrow class id mismatch", 44 | ); 45 | assert_eq(escrow_instance.initialization_hash, 0, "Initialization hash is not zero"); 46 | assert_eq(escrow_instance.public_keys, public_keys, "Public keys mismatch"); 47 | } 48 | 49 | #[test(should_fail_with = "Escrow address mismatch")] 50 | unconstrained fn get_escrow_public_keys_mismatch() { 51 | let mut env = TestEnvironment::new(); 52 | 53 | // Secret key derived using 1 as the seed 54 | let secret = 1; 55 | let (_, _, _, incorrect_msk) = logic_utils::get_test_vector(); 56 | 57 | let escrow = escrow_utils::deploy_escrow_with_secret(&mut env, secret); 58 | 59 | let escrow_class_id = escrow_utils::get_escrow_class_id(); 60 | 61 | let logic = logic_utils::deploy_logic(&mut env, escrow_class_id); 62 | 63 | let caller = env.create_light_account(); 64 | let expected_escrow_address = 65 | env.call_private(caller, TestLogic::at(logic).get_escrow(incorrect_msk)); 66 | 67 | assert_eq(escrow, expected_escrow_address, "Escrow address mismatch"); 68 | } 69 | 70 | #[test(should_fail_with = "Escrow address mismatch")] 71 | unconstrained fn get_escrow_class_id_mismatch() { 72 | let mut env = TestEnvironment::new(); 73 | 74 | // Secret key derived using 1 as the seed 75 | let secret = 1; 76 | let (_, msk, _, _) = logic_utils::get_test_vector(); 77 | 78 | let escrow = escrow_utils::deploy_escrow_with_secret(&mut env, secret); 79 | 80 | let escrow_class_id = escrow_utils::get_escrow_class_id(); 81 | 82 | // We deploy with a different class id, so the test should fail 83 | let logic = logic_utils::deploy_logic(&mut env, escrow_class_id + 1); 84 | 85 | let owner = env.create_light_account(); 86 | let expected_escrow_address = env.call_private(owner, TestLogic::at(logic).get_escrow(msk)); 87 | 88 | assert_eq(escrow, expected_escrow_address, "Escrow address mismatch"); 89 | } 90 | 91 | #[test(should_fail_with = "Escrow address mismatch")] 92 | unconstrained fn get_escrow_salt_mismatch() { 93 | let mut env = TestEnvironment::new(); 94 | 95 | // Secret key derived using 1 as the seed 96 | let secret = 1; 97 | let (_, msk, _, _) = logic_utils::get_test_vector(); 98 | 99 | let escrow = escrow_utils::deploy_escrow_with_secret(&mut env, secret); 100 | 101 | let escrow_class_id = escrow_utils::get_escrow_class_id(); 102 | 103 | let logic = logic_utils::deploy_logic(&mut env, escrow_class_id); 104 | 105 | let owner = env.create_light_account(); 106 | let expected_escrow_address = env.call_private(owner, TestLogic::at(logic).get_escrow(msk)); 107 | 108 | assert_eq(escrow, expected_escrow_address, "Escrow address mismatch"); 109 | } 110 | -------------------------------------------------------------------------------- /src/token_contract/src/test/transfer_public_to_commitment.nr: -------------------------------------------------------------------------------- 1 | use crate::{test::utils::{self, mint_amount}, Token}; 2 | use aztec::oracle::random::random; 3 | use std::test::OracleMock; 4 | 5 | /// Internal orchestration means that the calls to `initialize_transfer_commitment` 6 | /// and `transfer_public_to_commitment` are done by the TOKEN contract itself. 7 | /// In this test's case this is done by the `Token::transfer_public_to_commitment(...)` function called 8 | /// in `utils::setup_mint_and_transfer_public_to_commitment`. 9 | #[test] 10 | unconstrained fn transfer_public_to_commitment_internal_orchestration() { 11 | // The transfer to private is done in `utils::setup_and_mint_to_private_without_minter` and for this reason 12 | // in this test we just call it and check the outcome. 13 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 14 | let (mut env, token_contract_address, user, _) = 15 | utils::setup_and_mint_to_private_without_minter(false); 16 | 17 | // User's private balance should be equal to the amount 18 | utils::check_private_balance(env, token_contract_address, user, mint_amount); 19 | } 20 | 21 | /// External orchestration means that the calls to prepare and finalize are not done by the Token contract. This flow 22 | /// will typically be used by a DEX. 23 | #[test] 24 | unconstrained fn transfer_public_to_commitment_external_orchestration() { 25 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 26 | let (mut env, token_contract_address, owner, recipient) = 27 | utils::setup_and_mint_to_public_without_minter(false); 28 | 29 | let note_randomness = random(); 30 | 31 | // We mock the Oracle to return the note randomness such that later on we can manually add the note 32 | let _ = OracleMock::mock("getRandomField").returns(note_randomness); 33 | 34 | // We prepare the transfer 35 | let commitment_result = env.call_private( 36 | owner, 37 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, owner), 38 | ); 39 | let commitment = commitment_result; 40 | 41 | // Finalize the transfer of the tokens (message sender owns the tokens in public) 42 | env.call_public( 43 | owner, 44 | Token::at(token_contract_address).transfer_public_to_commitment( 45 | owner, 46 | commitment, 47 | mint_amount, 48 | 0, 49 | ), 50 | ); 51 | 52 | // Recipient's private balance should be equal to the amount 53 | utils::check_private_balance(env, token_contract_address, recipient, mint_amount); 54 | } 55 | 56 | #[test(should_fail_with = "Invalid partial note or completer 'context.nullifier_exists(validity_commitment, context.this_address())'")] 57 | unconstrained fn transfer_public_to_commitment_transfer_not_initialized() { 58 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 59 | let (mut env, token_contract_address, owner, _) = 60 | utils::setup_and_mint_to_public_without_minter(false); 61 | 62 | // Transfer was not prepared, so we can use a random value for the commitment 63 | let commitment = random(); 64 | 65 | // Try finalizing the transfer without preparing it first 66 | env.call_public( 67 | owner, 68 | Token::at(token_contract_address).transfer_public_to_commitment( 69 | owner, 70 | commitment, 71 | mint_amount, 72 | 0, 73 | ), 74 | ); 75 | } 76 | 77 | #[test(should_fail_with = "attempt to subtract with overflow 'public_balances.at(from).read() - amount'")] 78 | unconstrained fn transfer_public_to_commitment_failure_not_an_owner() { 79 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 80 | let (mut env, token_contract_address, owner, not_owner) = 81 | utils::setup_and_mint_to_public_without_minter(false); 82 | 83 | // (For this specific test we could set a random value for the commitment and not do the call to `prepare...` 84 | // as the token balance check is before we use the value but that would made the test less robust against changes 85 | // in the contract.) 86 | let commitment_result = env.call_private( 87 | owner, 88 | Token::at(token_contract_address).initialize_transfer_commitment(owner, owner), 89 | ); 90 | let commitment = commitment_result; 91 | 92 | // Try transferring someone else's token balance 93 | env.call_public( 94 | not_owner, 95 | Token::at(token_contract_address).transfer_public_to_commitment( 96 | not_owner, 97 | commitment, 98 | mint_amount, 99 | 0, 100 | ), 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/burn_private.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::test::helpers::authwit as authwit_cheatcodes; 4 | 5 | #[test] 6 | unconstrained fn nft_burn_private_self_success() { 7 | // Setup and mint NFT to owner in private state 8 | let token_id = 10000; 9 | let (env, nft_contract_address, owner, _, _) = 10 | utils::setup_and_mint_to_private(false, token_id); 11 | 12 | // Verify initial state 13 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 14 | utils::assert_nft_exists(env, nft_contract_address, token_id); 15 | 16 | // Burn the NFT 17 | env.call_private(owner, NFT::at(nft_contract_address).burn_private(owner, token_id, 0)); 18 | 19 | // Verify NFT is burned (no longer in private notes and marked as non-existent) 20 | utils::assert_private_nft_nullified(env, nft_contract_address, owner, token_id); 21 | utils::assert_nft_does_not_exist(env, nft_contract_address, token_id); 22 | } 23 | 24 | #[test] 25 | unconstrained fn nft_burn_private_authorized_success() { 26 | // Setup with account contracts and mint NFT to owner 27 | let token_id = 10000; 28 | let (env, nft_contract_address, owner, _, recipient) = 29 | utils::setup_and_mint_to_private(true, token_id); 30 | 31 | // Verify initial state 32 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 33 | utils::assert_nft_exists(env, nft_contract_address, token_id); 34 | 35 | // Create burn call interface with non-zero nonce 36 | let burn_call_interface = NFT::at(nft_contract_address).burn_private(owner, token_id, 1); 37 | 38 | // Add authorization witness from owner to recipient 39 | authwit_cheatcodes::add_private_authwit_from_call_interface( 40 | env, 41 | owner, 42 | recipient, 43 | burn_call_interface, 44 | ); 45 | 46 | // Impersonate recipient to perform the authorized burn 47 | env.call_private(recipient, burn_call_interface); 48 | 49 | // Verify NFT is burned 50 | utils::assert_private_nft_nullified(env, nft_contract_address, owner, token_id); 51 | utils::assert_nft_does_not_exist(env, nft_contract_address, token_id); 52 | } 53 | 54 | #[test(should_fail_with = "nft not found in private to public")] 55 | unconstrained fn nft_burn_private_non_existent_fail() { 56 | // Setup but don't mint any NFT 57 | let (env, nft_contract_address, owner, _, _) = utils::setup_with_minter(false); 58 | let non_existent_token_id = 12345; 59 | 60 | // Attempt to burn non-existent NFT 61 | env.call_private( 62 | owner, 63 | NFT::at(nft_contract_address).burn_private(owner, non_existent_token_id, 0), 64 | ); 65 | } 66 | 67 | #[test(should_fail_with = "Unknown auth witness for message hash")] 68 | unconstrained fn nft_burn_private_unauthorized_fail() { 69 | // Setup with account contracts 70 | let token_id = 10000; 71 | let (env, nft_contract_address, owner, _, recipient) = 72 | utils::setup_and_mint_to_private(true, token_id); 73 | 74 | // Create burn interface with non-zero nonce 75 | let burn_call_interface = NFT::at(nft_contract_address).burn_private(owner, token_id, 1); 76 | 77 | // Impersonate recipient but DON'T add authorization witness 78 | env.call_private(recipient, burn_call_interface); 79 | } 80 | 81 | #[test(should_fail_with = "Unknown auth witness for message hash")] 82 | unconstrained fn nft_burn_private_wrong_authwit_fail() { 83 | // Setup with account contracts 84 | let token_id = 10000; 85 | let (env, nft_contract_address, owner, _, recipient) = 86 | utils::setup_and_mint_to_private(true, token_id); 87 | 88 | // Create burn interface with non-zero nonce 89 | let burn_call_interface = NFT::at(nft_contract_address).burn_private(owner, token_id, 1); 90 | 91 | // Add authorization witness but to the wrong address (owner instead of recipient) 92 | authwit_cheatcodes::add_private_authwit_from_call_interface( 93 | env, 94 | owner, 95 | owner, // Wrong address - should be recipient 96 | burn_call_interface, 97 | ); 98 | 99 | // Impersonate recipient 100 | env.call_private(recipient, burn_call_interface); 101 | } 102 | 103 | #[test(should_fail_with = "nft not found in private to public")] 104 | unconstrained fn nft_burn_private_public_nft_fail() { 105 | // Setup and mint NFT to owner in public state 106 | let token_id = 10000; 107 | let (env, nft_contract_address, owner, _, _) = utils::setup_and_mint_to_public(false, token_id); 108 | 109 | // Verify NFT is in public state 110 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 111 | 112 | // Attempt to burn from private state when NFT is actually in public state 113 | env.call_private(owner, NFT::at(nft_contract_address).burn_private(owner, token_id, 0)); 114 | } 115 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/transfer_private_to_public.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::{protocol_types::address::AztecAddress, test::helpers::authwit as authwit_cheatcodes}; 4 | 5 | #[test] 6 | unconstrained fn nft_transfer_private_to_public_self_success() { 7 | // Setup and mint NFT to owner in private state 8 | let token_id = 10000; 9 | let (env, nft_contract_address, owner, _, _) = 10 | utils::setup_and_mint_to_private(false, token_id); 11 | 12 | // Verify initial ownership state 13 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 14 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 15 | 16 | // Transfer NFT from private to public state (self-transfer) 17 | env.call_private( 18 | owner, 19 | NFT::at(nft_contract_address).transfer_private_to_public(owner, owner, token_id, 0), 20 | ); 21 | 22 | // Verify the NFT is now owned publicly by owner 23 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 24 | 25 | // Verify the NFT is no longer owned privately 26 | utils::assert_private_nft_nullified(env, nft_contract_address, owner, token_id); 27 | } 28 | 29 | #[test] 30 | unconstrained fn nft_transfer_private_to_public_authorized_success() { 31 | // Setup with account contracts (needed for authwits) and mint NFT to owner 32 | let token_id = 10000; 33 | let (env, nft_contract_address, owner, _, recipient) = 34 | utils::setup_and_mint_to_private(true, token_id); 35 | 36 | // Verify initial ownership state 37 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 38 | utils::assert_owns_public_nft(env, nft_contract_address, AztecAddress::zero(), token_id); 39 | 40 | // Create the transfer call interface 41 | let transfer_call_interface = 42 | NFT::at(nft_contract_address).transfer_private_to_public(owner, recipient, token_id, 0); 43 | 44 | // Add authorization witness from owner to recipient 45 | authwit_cheatcodes::add_private_authwit_from_call_interface( 46 | env, 47 | owner, 48 | recipient, 49 | transfer_call_interface, 50 | ); 51 | 52 | // Impersonate recipient to perform the authorized transfer 53 | let _ = env.call_private(recipient, transfer_call_interface); 54 | 55 | // Verify the NFT is now owned publicly by recipient 56 | utils::assert_owns_public_nft(env, nft_contract_address, recipient, token_id); 57 | utils::assert_private_nft_nullified(env, nft_contract_address, recipient, token_id); 58 | 59 | // Verify the NFT is no longer owned privately by owner 60 | utils::assert_private_nft_nullified(env, nft_contract_address, owner, token_id); 61 | } 62 | 63 | #[test(should_fail_with = "Unknown auth witness for message hash")] 64 | unconstrained fn nft_transfer_private_to_public_unauthorized_fail() { 65 | // Setup with account contracts for proper authorization testing 66 | let token_id = 10000; 67 | let (env, nft_contract_address, owner, _, recipient) = 68 | utils::setup_and_mint_to_private(true, token_id); 69 | 70 | // Create transfer interface with non-zero nonce (indicating authorization needed) 71 | let transfer_call_interface = 72 | NFT::at(nft_contract_address).transfer_private_to_public(owner, recipient, token_id, 0); 73 | 74 | // Impersonate recipient but DON'T add authorization witness 75 | // Should fail because recipient has no authorization witness from owner 76 | env.call_private(recipient, transfer_call_interface); 77 | } 78 | 79 | #[test(should_fail_with = "nft not found in private to public")] 80 | unconstrained fn nft_transfer_private_to_public_non_existent_nft_fail() { 81 | // Setup environment but don't mint any NFT 82 | let (env, nft_contract_address, owner, _, recipient) = utils::setup_with_minter(false); 83 | let non_existent_token_id = 12345; 84 | 85 | // Attempt to transfer a non-existent NFT from private to public 86 | env.call_private( 87 | owner, 88 | NFT::at(nft_contract_address).transfer_private_to_public( 89 | owner, 90 | recipient, 91 | non_existent_token_id, 92 | 0, 93 | ), 94 | ); 95 | } 96 | 97 | #[test(should_fail_with = "nft not found in private to public")] 98 | unconstrained fn nft_transfer_private_to_public_already_public_fail() { 99 | // Setup and mint NFT to owner in public state 100 | let token_id = 10000; 101 | let (env, nft_contract_address, owner, _, recipient) = 102 | utils::setup_and_mint_to_public(false, token_id); 103 | 104 | // Verify NFT is in public state 105 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 106 | 107 | // Attempt to transfer from private state when NFT is actually in public state 108 | env.call_private( 109 | owner, 110 | NFT::at(nft_contract_address).transfer_private_to_public(owner, recipient, token_id, 0), 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/escrow_contract/src/library/logic.nr: -------------------------------------------------------------------------------- 1 | // aztec library 2 | use aztec::{ 3 | context::PrivateContext, 4 | event::event_emission::emit_event_in_private, 5 | messages::message_delivery::MessageDelivery, 6 | protocol_types::{ 7 | address::AztecAddress, 8 | contract_class_id::ContractClassId, 9 | contract_instance::ContractInstance, 10 | public_keys::{IvpkM, NpkM, OvpkM, PublicKeys, TpkM}, 11 | traits::{FromField, ToField}, 12 | }, 13 | }; 14 | // standard library 15 | use std::embedded_curve_ops::{EmbeddedCurvePoint, EmbeddedCurveScalar, fixed_base_scalar_mul}; 16 | // escrow contract 17 | use crate::Escrow; 18 | // escrow details event 19 | pub use crate::types::escrow_details_event::{EscrowDetailsLogContent, MasterSecretKeys}; 20 | 21 | /** ========================================================== 22 | * =================== LOGIC LIBRARIES ======================= 23 | * ======================================================== */ 24 | 25 | /// @notice Returns the escrow address that corresponds to the given master secret keys and class ID. 26 | /// @param context The private context 27 | /// @param escrow_class_id The contract class id of the escrow contract 28 | /// @param master_secret_keys The master secret keys 29 | /// @return The escrow address 30 | #[contract_library_method] 31 | pub fn _get_escrow( 32 | context: &mut PrivateContext, 33 | escrow_class_id: Field, 34 | master_secret_keys: MasterSecretKeys, 35 | ) -> AztecAddress { 36 | // Compute the public keys from the secret keys 37 | let computed_public_keys: PublicKeys = _secret_keys_to_public_keys(master_secret_keys); 38 | 39 | let escrow_instance = ContractInstance { 40 | salt: context.this_address().to_field(), 41 | deployer: AztecAddress::from_field(0), 42 | contract_class_id: ContractClassId::from_field(escrow_class_id), 43 | initialization_hash: 0, 44 | public_keys: computed_public_keys, 45 | }; 46 | 47 | escrow_instance.to_address() 48 | } 49 | 50 | /// @notice Shares the escrow details needed to find and use the escrow contract 51 | /// @dev Emits a private log with the escrow details 52 | /// @param context The private context 53 | /// @param account The address of the account that will use the escrow 54 | /// @param escrow The address of the escrow 55 | /// @param master_secret_keys The master secret keys 56 | #[contract_library_method] 57 | pub fn _share_escrow( 58 | context: &mut PrivateContext, 59 | account: AztecAddress, 60 | escrow: AztecAddress, 61 | master_secret_keys: MasterSecretKeys, 62 | ) { 63 | let event_struct = EscrowDetailsLogContent { escrow, master_secret_keys }; 64 | 65 | emit_event_in_private( 66 | event_struct, 67 | context, 68 | account, 69 | MessageDelivery.CONSTRAINED_ONCHAIN, 70 | ); 71 | } 72 | 73 | /// @notice Withdraws an amount of tokens from the provided escrow. 74 | /// @param context The private context 75 | /// @param escrow The address of the escrow 76 | /// @param account The address of the account that will receive the tokens 77 | /// @param token The address of the token 78 | /// @param amount The amount of tokens to withdraw from the escrow 79 | #[contract_library_method] 80 | pub fn _withdraw( 81 | context: &mut PrivateContext, 82 | escrow: AztecAddress, 83 | account: AztecAddress, 84 | token: AztecAddress, 85 | amount: u128, 86 | ) { 87 | Escrow::at(escrow).withdraw(token, amount, account).call(context); 88 | } 89 | 90 | /// @notice Withdraws an NFT from the provided escrow. 91 | /// @param context The private context 92 | /// @param escrow The address of the escrow 93 | /// @param account The address of the account that will receive the NFT 94 | /// @param nft The address of the NFT contract 95 | /// @param token_id The id of the token to withdraw from the escrow 96 | #[contract_library_method] 97 | pub fn _withdraw_nft( 98 | context: &mut PrivateContext, 99 | escrow: AztecAddress, 100 | account: AztecAddress, 101 | nft: AztecAddress, 102 | token_id: Field, 103 | ) { 104 | Escrow::at(escrow).withdraw_nft(nft, token_id, account).call(context); 105 | } 106 | 107 | /// @notice Derives public keys from secret keys. 108 | /// @param master_secret_keys The master secret keys 109 | /// @return PublicKeys containing the derived public keys. 110 | #[contract_library_method] 111 | pub fn _secret_keys_to_public_keys(master_secret_keys: MasterSecretKeys) -> PublicKeys { 112 | let npk_m: EmbeddedCurvePoint = 113 | fixed_base_scalar_mul(EmbeddedCurveScalar::from_field(master_secret_keys.nsk_m)); 114 | let ivpk_m: EmbeddedCurvePoint = 115 | fixed_base_scalar_mul(EmbeddedCurveScalar::from_field(master_secret_keys.ivsk_m)); 116 | let ovpk_m: EmbeddedCurvePoint = 117 | fixed_base_scalar_mul(EmbeddedCurveScalar::from_field(master_secret_keys.ovsk_m)); 118 | let tpk_m: EmbeddedCurvePoint = 119 | fixed_base_scalar_mul(EmbeddedCurveScalar::from_field(master_secret_keys.tsk_m)); 120 | 121 | PublicKeys { 122 | npk_m: NpkM { inner: npk_m }, 123 | ivpk_m: IvpkM { inner: ivpk_m }, 124 | ovpk_m: OvpkM { inner: ovpk_m }, 125 | tpk_m: TpkM { inner: tpk_m }, 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /benchmarks/token_contract.benchmark.ts: -------------------------------------------------------------------------------- 1 | import type { PXE } from '@aztec/pxe/server'; 2 | import type { Wallet } from '@aztec/aztec.js/wallet'; 3 | import { AztecAddress } from '@aztec/aztec.js/addresses'; 4 | import type { ContractFunctionInteractionCallIntent } from '@aztec/aztec.js/authorization'; 5 | 6 | import { parseUnits } from 'viem'; 7 | 8 | // Import the new Benchmark base class and context 9 | import { Benchmark, BenchmarkContext } from '@defi-wonderland/aztec-benchmark'; 10 | 11 | import { TokenContract } from '../artifacts/Token.js'; 12 | import { deployTokenWithMinter, initializeTransferCommitment, setupTestSuite } from '../src/ts/test/utils.js'; 13 | 14 | // Extend the BenchmarkContext from the new package 15 | interface TokenBenchmarkContext extends BenchmarkContext { 16 | pxe: PXE; 17 | wallet: Wallet; 18 | deployer: AztecAddress; 19 | accounts: AztecAddress[]; 20 | tokenContract: TokenContract; 21 | commitments: bigint[]; 22 | } 23 | 24 | // --- Helper Functions --- 25 | 26 | function amt(x: bigint | number | string) { 27 | // Using 18 decimals as standard for Token examples 28 | return parseUnits(x.toString(), 18); 29 | } 30 | 31 | // Use export default class extending Benchmark 32 | export default class TokenContractBenchmark extends Benchmark { 33 | /** 34 | * Sets up the benchmark environment for the TokenContract. 35 | * Creates PXE client, gets accounts, and deploys the contract. 36 | */ 37 | 38 | async setup(): Promise { 39 | const { pxe, wallet, accounts } = await setupTestSuite(); 40 | const [deployer] = accounts; 41 | const deployedBaseContract = await deployTokenWithMinter(wallet, deployer); 42 | const tokenContract = await TokenContract.at(deployedBaseContract.address, wallet); 43 | 44 | // Initialize partial notes 45 | const [alice] = accounts; 46 | const owner = alice; 47 | // We need an account manager to decrypt the private logs in the initializeTransferCommitment function 48 | const commitmentRecipientAccountManager = await wallet.createAccount(); 49 | const commitment_1 = await initializeTransferCommitment( 50 | tokenContract, 51 | alice, 52 | commitmentRecipientAccountManager, 53 | owner, 54 | ); 55 | const commitment_2 = await initializeTransferCommitment( 56 | tokenContract, 57 | alice, 58 | commitmentRecipientAccountManager, 59 | owner, 60 | ); 61 | 62 | const commitments = [commitment_1, commitment_2]; 63 | 64 | return { pxe, wallet, deployer, accounts, tokenContract, commitments }; 65 | } 66 | 67 | /** 68 | * Returns the list of TokenContract methods to be benchmarked. 69 | */ 70 | getMethods(context: TokenBenchmarkContext): ContractFunctionInteractionCallIntent[] { 71 | const { tokenContract, accounts, wallet, commitments } = context; 72 | const [alice, bob] = accounts; 73 | const owner = alice; 74 | 75 | const methods: ContractFunctionInteractionCallIntent[] = [ 76 | // Mint methods 77 | { 78 | caller: alice, 79 | action: tokenContract.withWallet(wallet).methods.mint_to_private(owner, amt(100)), 80 | }, 81 | { 82 | caller: alice, 83 | action: tokenContract.withWallet(wallet).methods.mint_to_public(owner, amt(100)), 84 | }, 85 | // Transfer methods 86 | { 87 | caller: alice, 88 | action: tokenContract.withWallet(wallet).methods.transfer_private_to_public(owner, bob, amt(10), 0), 89 | }, 90 | { 91 | caller: alice, 92 | action: tokenContract 93 | .withWallet(wallet) 94 | .methods.transfer_private_to_public_with_commitment(owner, bob, amt(10), 0), 95 | }, 96 | { 97 | caller: alice, 98 | action: tokenContract.withWallet(wallet).methods.transfer_private_to_private(owner, bob, amt(10), 0), 99 | }, 100 | { 101 | caller: alice, 102 | action: tokenContract.withWallet(wallet).methods.transfer_public_to_private(owner, bob, amt(10), 0), 103 | }, 104 | { 105 | caller: alice, 106 | action: tokenContract.withWallet(wallet).methods.transfer_public_to_public(owner, bob, amt(10), 0), 107 | }, 108 | 109 | // Burn methods 110 | { 111 | caller: alice, 112 | action: tokenContract.withWallet(wallet).methods.burn_private(owner, amt(10), 0), 113 | }, 114 | { 115 | caller: alice, 116 | action: tokenContract.withWallet(wallet).methods.burn_public(owner, amt(10), 0), 117 | }, 118 | 119 | // Partial notes methods 120 | { 121 | caller: alice, 122 | action: tokenContract.withWallet(wallet).methods.initialize_transfer_commitment(bob, owner), 123 | }, 124 | { 125 | caller: alice, 126 | action: tokenContract 127 | .withWallet(wallet) 128 | .methods.transfer_private_to_commitment(owner, commitments[0], amt(10), 0), 129 | }, 130 | { 131 | caller: alice, 132 | action: tokenContract 133 | .withWallet(wallet) 134 | .methods.transfer_public_to_commitment(owner, commitments[1], amt(10), 0), 135 | }, 136 | ]; 137 | 138 | return methods.filter(Boolean); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/transfer_public_to_public.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::test::helpers::authwit as authwit_cheatcodes; 4 | 5 | #[test] 6 | unconstrained fn nft_transfer_public_to_public_success() { 7 | // Setup and mint NFT to owner 8 | let token_id = 10000; 9 | let (env, nft_contract_address, owner, _, recipient) = 10 | utils::setup_and_mint_to_public(false, token_id); 11 | 12 | // Transfer NFT from owner to recipient 13 | env.call_public( 14 | owner, 15 | NFT::at(nft_contract_address).transfer_public_to_public(owner, recipient, token_id, 0), 16 | ); 17 | 18 | // Verify recipient now owns the NFT 19 | utils::assert_owns_public_nft(env, nft_contract_address, recipient, token_id); 20 | } 21 | 22 | #[test] 23 | unconstrained fn nft_transfer_public_to_public_self_success() { 24 | // Setup and mint NFT to owner 25 | let token_id = 10000; 26 | let (env, nft_contract_address, owner, _, _) = utils::setup_and_mint_to_public(false, token_id); 27 | 28 | // Transfer NFT from owner to self 29 | env.call_public( 30 | owner, 31 | NFT::at(nft_contract_address).transfer_public_to_public(owner, owner, token_id, 0), 32 | ); 33 | 34 | // Verify owner still owns the NFT 35 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 36 | } 37 | 38 | #[test] 39 | unconstrained fn nft_transfer_public_to_public_authorized_success() { 40 | // Setup with account contracts (needed for authwits) and mint NFT to owner 41 | let token_id = 10000; 42 | let (env, nft_contract_address, owner, _, recipient) = 43 | utils::setup_and_mint_to_public(true, token_id); 44 | 45 | // Create the transfer call interface 46 | let transfer_call_interface = 47 | NFT::at(nft_contract_address).transfer_public_to_public(owner, recipient, token_id, 1); 48 | 49 | // Add authorization witness from owner to recipient 50 | authwit_cheatcodes::add_public_authwit_from_call_interface( 51 | env, 52 | owner, 53 | recipient, 54 | transfer_call_interface, 55 | ); 56 | 57 | // Impersonate recipient to perform the authorized transfer 58 | env.call_public(recipient, transfer_call_interface); 59 | 60 | // Verify recipient now owns the NFT 61 | utils::assert_owns_public_nft(env, nft_contract_address, recipient, token_id); 62 | // Verify the NFT is no longer owned by the previous owner 63 | let cur_owner = env.view_public(NFT::at(nft_contract_address).public_owner_of(token_id)); 64 | assert(owner != cur_owner, "incorrect NFT owner"); 65 | } 66 | 67 | #[test(should_fail_with = "caller is not owner")] 68 | unconstrained fn nft_transfer_public_to_public_non_existent_fail() { 69 | // Setup environment but don't mint the NFT 70 | let (env, nft_contract_address, owner, _, recipient) = utils::setup_with_minter(false); 71 | let non_existent_token_id = 12345; 72 | 73 | // Attempt to transfer a non-existent NFT 74 | env.call_public( 75 | owner, 76 | NFT::at(nft_contract_address).transfer_public_to_public( 77 | owner, 78 | recipient, 79 | non_existent_token_id, 80 | 0, 81 | ), 82 | ); 83 | } 84 | 85 | #[test(should_fail_with = "unauthorized")] 86 | unconstrained fn nft_transfer_public_to_public_unauthorized_fail() { 87 | // Setup with account contracts for proper authorization testing 88 | let token_id = 10000; 89 | let (env, nft_contract_address, owner, _, recipient) = 90 | utils::setup_and_mint_to_public(true, token_id); 91 | 92 | // Create transfer interface with non-zero nonce (indicating authorization needed) 93 | let transfer_call_interface = 94 | NFT::at(nft_contract_address).transfer_public_to_public(owner, recipient, token_id, 1); 95 | 96 | // Impersonate recipient but DON'T add authorization witness 97 | // This test verifies that without an authorization witness (authwit), 98 | // the recipient cannot transfer the NFT on behalf of the owner 99 | // Should fail because recipient has no authorization witness from owner 100 | env.call_public(recipient, transfer_call_interface); 101 | } 102 | 103 | #[test(should_fail_with = "unauthorized")] 104 | unconstrained fn nft_transfer_public_to_public_wrong_authwit_fail() { 105 | // Setup with account contracts for proper authorization testing 106 | let token_id = 10000; 107 | let (env, nft_contract_address, owner, _, recipient) = 108 | utils::setup_and_mint_to_public(true, token_id); 109 | 110 | // Create transfer interface with non-zero nonce (indicating authorization needed) 111 | let transfer_call_interface = 112 | NFT::at(nft_contract_address).transfer_public_to_public(owner, recipient, token_id, 1); 113 | 114 | // Add authorization witness but to the OWNER instead of the recipient 115 | // This simulates giving authorization to the wrong address 116 | authwit_cheatcodes::add_public_authwit_from_call_interface( 117 | env, 118 | owner, 119 | owner, // Wrong address - should be recipient 120 | transfer_call_interface, 121 | ); 122 | 123 | // Impersonate recipient 124 | // Should fail because the authorization witness was given to the wrong address 125 | env.call_public(recipient, transfer_call_interface); 126 | } 127 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Production Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version from package.json (e.g., 1.0.0 or 1.0.0-beta.1). Do NOT include the "v" prefix.' 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | environment: Production 15 | runs-on: ubuntu-latest 16 | if: github.ref == 'refs/heads/main' 17 | permissions: 18 | contents: write # Required to create and push tags 19 | 20 | env: 21 | PROJECT_NAME: '@defi-wonderland/aztec-standards' 22 | 23 | steps: 24 | - name: Checkout Repo 25 | uses: actions/checkout@v4 26 | 27 | - name: Install Node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: "22.17.0" 31 | registry-url: "https://registry.npmjs.org" 32 | cache: "yarn" 33 | 34 | # TODO: Automate changing package.json version from the workflow input 35 | # (e.g., open a PR or push a signed commit) so manual edits are not required. 36 | - name: Validate input version matches package.json 37 | run: | 38 | INPUT_VERSION="${{ github.event.inputs.version }}" 39 | PKG_VERSION=$(node -p "require('./package.json').version") 40 | echo "Input version: $INPUT_VERSION" 41 | echo "package.json version: $PKG_VERSION" 42 | if [ "$INPUT_VERSION" != "$PKG_VERSION" ]; then 43 | echo "::error::Input version ($INPUT_VERSION) does not match package.json version ($PKG_VERSION). Update package.json or re-run with the matching version (no 'v' prefix)." 44 | exit 1 45 | fi 46 | 47 | - name: Set up Docker 48 | uses: docker/setup-buildx-action@v2 49 | 50 | - name: Detect Aztec version 51 | id: aztec-version 52 | run: | 53 | AZTEC_VERSION=$(node -p "require('./package.json').config.aztecVersion") 54 | echo "version=$AZTEC_VERSION" >> "$GITHUB_OUTPUT" 55 | echo "Aztec version is $version" 56 | 57 | - name: Install Aztec CLI 58 | run: | 59 | curl -s https://install.aztec.network > tmp.sh 60 | bash tmp.sh <<< yes "yes" 61 | - name: Update path 62 | run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH 63 | 64 | - name: Set Aztec version 65 | run: | 66 | VERSION=${{ steps.aztec-version.outputs.version }} aztec-up 67 | 68 | # This is a temporary hack to fix a problem with v3 releases. 69 | - name: Manually tag the aztec version as `latest` 70 | run: | 71 | docker tag aztecprotocol/aztec:${{ steps.aztec-version.outputs.version }} aztecprotocol/aztec:latest 72 | 73 | - name: Install dependencies 74 | run: yarn --frozen-lockfile 75 | 76 | - name: Compile 77 | run: yarn compile 78 | 79 | - name: Codegen 80 | run: aztec codegen target --outdir artifacts 81 | 82 | - name: Compile artifacts to JS 83 | run: | 84 | mkdir -p dist/ 85 | yarn tsc artifacts/*.ts --outDir dist/ --skipLibCheck --target es2020 --module nodenext --moduleResolution nodenext --resolveJsonModule --declaration 86 | 87 | - name: Inspect contracts 88 | run: | 89 | for f in target/*.json; do 90 | [ -f "$f" ] || continue 91 | aztec inspect-contract "$f" 92 | done 93 | 94 | # TODO: We do several things here: 95 | # 1. Create artifacts directory 96 | # 2. Copy compiled JS artifacts to artifacts/ 97 | # 3. Copy compiled Noir contracts (target/) to package root 98 | # 4. Copy deployments.json to package root (if exists) 99 | # 5. Copy README.md and LICENSE to package root 100 | # 6. Create trimmed package.json 101 | - name: Prepare files for release 102 | run: | 103 | VERSION=${{ github.event.inputs.version }} 104 | mkdir -p export/${{ env.PROJECT_NAME }}/artifacts 105 | 106 | # Copy the compiled JS files to artifacts/ 107 | cp -r dist/artifacts/* export/${{ env.PROJECT_NAME }}/artifacts/ 108 | 109 | # Copy compiled Noir contracts to package root 110 | cp -r target export/${{ env.PROJECT_NAME }}/ 111 | 112 | # Copy deployments.json if it exists 113 | if [ -f "src/deployments.json" ]; then 114 | cp src/deployments.json export/${{ env.PROJECT_NAME }}/ 115 | else 116 | echo "src/deployments.json not found, skipping copy" 117 | fi 118 | 119 | cp README.md export/${{ env.PROJECT_NAME }}/ 120 | cp LICENSE export/${{ env.PROJECT_NAME }}/ 121 | 122 | cat package.json | jq --arg v "$VERSION" 'del(.scripts, .jest, ."lint-staged", .packageManager, .devDependencies, .dependencies, .engines, .resolutions) | .version=$v' > export/${{ env.PROJECT_NAME }}/package.json 123 | 124 | - name: Publish to NPM 125 | run: cd export/${{ env.PROJECT_NAME }} && npm publish --access public --tag latest 126 | env: 127 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 128 | 129 | - name: Create GitHub Release Tag (adds 'v' prefix to input) 130 | run: | 131 | TAG_NAME="v${{ github.event.inputs.version }}" 132 | # Check if tag already exists 133 | if git ls-remote --tags origin | grep -q "refs/tags/${TAG_NAME}$"; then 134 | echo "::error::Tag ${TAG_NAME} already exists. Please use a different version." 135 | exit 1 136 | fi 137 | git tag "${TAG_NAME}" 138 | git push origin "${TAG_NAME}" 139 | env: 140 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/nft_contract/src/test/utils.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use aztec::{ 3 | protocol_types::{address::AztecAddress, storage::map::derive_storage_slot_in_map}, 4 | test::helpers::test_environment::TestEnvironment, 5 | }; 6 | 7 | pub unconstrained fn setup_with_minter( 8 | with_account_contracts: bool, 9 | ) -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { 10 | // Setup env, generate keys 11 | let mut env = TestEnvironment::new(); 12 | let (owner, minter, recipient) = if with_account_contracts { 13 | let owner = env.create_contract_account(); 14 | let minter = env.create_contract_account(); 15 | let recipient = env.create_contract_account(); 16 | (owner, minter, recipient) 17 | } else { 18 | // For simple tests without authorization, use regular accounts 19 | let owner = env.create_light_account(); 20 | let minter = env.create_light_account(); 21 | let recipient = env.create_light_account(); 22 | (owner, minter, recipient) 23 | }; 24 | 25 | let nft_contract_address = deploy_nft_with_minter(&mut env, owner, minter); 26 | 27 | (env, nft_contract_address, owner, minter, recipient) 28 | } 29 | 30 | pub unconstrained fn deploy_nft_with_minter( 31 | env: &mut TestEnvironment, 32 | owner: AztecAddress, 33 | minter: AztecAddress, 34 | ) -> AztecAddress { 35 | // Deploy token contract 36 | let initializer_call_interface = NFT::interface().constructor_with_minter( 37 | "TestNFT000000000000000000000000", 38 | "TNFT000000000000000000000000000", 39 | minter, 40 | AztecAddress::zero(), 41 | ); 42 | let nft_contract_address = 43 | env.deploy("@nft_contract/NFT").with_public_initializer(owner, initializer_call_interface); 44 | 45 | nft_contract_address 46 | } 47 | 48 | /// @dev Setup and mint to owner in public 49 | pub unconstrained fn setup_and_mint_to_public( 50 | with_account_contracts: bool, 51 | token_id: Field, 52 | ) -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { 53 | let (env, nft_contract_address, owner, minter, recipient) = 54 | setup_with_minter(with_account_contracts); 55 | 56 | env.call_public(minter, NFT::at(nft_contract_address).mint_to_public(owner, token_id)); 57 | 58 | (env, nft_contract_address, owner, minter, recipient) 59 | } 60 | 61 | pub unconstrained fn setup_and_mint_to_private( 62 | with_account_contracts: bool, 63 | token_id: Field, 64 | ) -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { 65 | let (env, nft_contract_address, owner, minter, recipient) = 66 | setup_with_minter(with_account_contracts); 67 | 68 | env.call_private(minter, NFT::at(nft_contract_address).mint_to_private(owner, token_id)); 69 | 70 | (env, nft_contract_address, owner, minter, recipient) 71 | } 72 | 73 | pub unconstrained fn assert_nft_exists( 74 | env: TestEnvironment, 75 | nft_contract_address: AztecAddress, 76 | token_id: Field, 77 | ) { 78 | env.public_context_at(nft_contract_address, |context| { 79 | let nft_exists_slot = NFT::storage_layout().nft_exists.slot; 80 | let nft_slot = derive_storage_slot_in_map(nft_exists_slot, token_id); 81 | let nft_exists = context.storage_read(nft_slot); 82 | assert(nft_exists, "NFT does not exist"); 83 | }); 84 | } 85 | 86 | pub unconstrained fn assert_nft_does_not_exist( 87 | env: TestEnvironment, 88 | nft_contract_address: AztecAddress, 89 | token_id: Field, 90 | ) { 91 | env.public_context_at(nft_contract_address, |context| { 92 | let nft_exists_slot = NFT::storage_layout().nft_exists.slot; 93 | let nft_slot = derive_storage_slot_in_map(nft_exists_slot, token_id); 94 | let nft_exists = context.storage_read(nft_slot); 95 | assert(!nft_exists, "NFT exists when it should not"); 96 | }); 97 | } 98 | 99 | pub unconstrained fn assert_owns_public_nft( 100 | env: TestEnvironment, 101 | nft_contract_address: AztecAddress, 102 | owner: AztecAddress, 103 | token_id: Field, 104 | ) { 105 | let obtained_owner = env.view_public(NFT::at(nft_contract_address).public_owner_of(token_id)); 106 | assert(owner == obtained_owner, "incorrect NFT owner"); 107 | } 108 | 109 | pub unconstrained fn assert_owns_private_nft( 110 | env: TestEnvironment, 111 | nft_contract_address: AztecAddress, 112 | owner: AztecAddress, 113 | token_id: Field, 114 | ) { 115 | let nft_found = owns_private_nft(env, nft_contract_address, owner, token_id); 116 | assert(nft_found, "NFT not found in private nfts"); 117 | } 118 | 119 | pub unconstrained fn assert_private_nft_nullified( 120 | env: TestEnvironment, 121 | nft_contract_address: AztecAddress, 122 | owner: AztecAddress, 123 | token_id: Field, 124 | ) { 125 | let nft_found = owns_private_nft(env, nft_contract_address, owner, token_id); 126 | assert(!nft_found, "NFT found in private notes when it should have been nullified"); 127 | } 128 | 129 | pub unconstrained fn owns_private_nft( 130 | env: TestEnvironment, 131 | nft_contract_address: AztecAddress, 132 | owner: AztecAddress, 133 | token_id: Field, 134 | ) -> bool { 135 | // Direct call to unconstrained 136 | let (private_nfts, _) = 137 | env.simulate_utility(NFT::at(nft_contract_address).get_private_nfts(owner, 0)); 138 | 139 | let mut nft_found = false; 140 | for obtained_token_id in private_nfts { 141 | if obtained_token_id == token_id { 142 | nft_found = true; 143 | } 144 | } 145 | nft_found 146 | } 147 | 148 | pub unconstrained fn check_commitment_is_stored( 149 | env: TestEnvironment, 150 | nft_contract_address: AztecAddress, 151 | commitment: Field, 152 | ) { 153 | env.public_context_at(nft_contract_address, |context| { 154 | let stored = context.storage_read(commitment); 155 | assert_eq(stored, true); 156 | }); 157 | } 158 | -------------------------------------------------------------------------------- /src/nft_contract/src/test/transfer_private_to_private.nr: -------------------------------------------------------------------------------- 1 | use crate::NFT; 2 | use crate::test::utils; 3 | use aztec::test::helpers::authwit as authwit_cheatcodes; 4 | 5 | #[test] 6 | unconstrained fn nft_transfer_private_to_private_success() { 7 | // Setup and mint NFT to owner in private state 8 | let token_id = 10000; 9 | let (env, nft_contract_address, owner, _, recipient) = 10 | utils::setup_and_mint_to_private(false, token_id); 11 | 12 | // Verify initial ownership 13 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 14 | 15 | // Transfer NFT from owner to recipient 16 | env.call_private( 17 | owner, 18 | NFT::at(nft_contract_address).transfer_private_to_private(owner, recipient, token_id, 0), 19 | ); 20 | 21 | // Verify ownership transfer 22 | utils::assert_private_nft_nullified(env, nft_contract_address, owner, token_id); 23 | utils::assert_owns_private_nft(env, nft_contract_address, recipient, token_id); 24 | } 25 | 26 | #[test] 27 | unconstrained fn nft_transfer_private_to_private_self_success() { 28 | // Setup and mint NFT to owner in private state 29 | let token_id = 10000; 30 | let (env, nft_contract_address, owner, _, _) = 31 | utils::setup_and_mint_to_private(false, token_id); 32 | 33 | // Verify initial ownership 34 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 35 | 36 | // Transfer NFT from owner to self 37 | env.call_private( 38 | owner, 39 | NFT::at(nft_contract_address).transfer_private_to_private(owner, owner, token_id, 0), 40 | ); 41 | 42 | // Verify owner still owns the NFT 43 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 44 | } 45 | 46 | #[test] 47 | unconstrained fn nft_transfer_private_to_private_authorized_success() { 48 | // Setup with account contracts and mint NFT to owner 49 | let token_id = 10000; 50 | let (env, nft_contract_address, owner, _, recipient) = 51 | utils::setup_and_mint_to_private(true, token_id); 52 | 53 | // Verify initial ownership 54 | utils::assert_owns_private_nft(env, nft_contract_address, owner, token_id); 55 | 56 | // Create transfer call interface with non-zero nonce 57 | let transfer_call_interface = 58 | NFT::at(nft_contract_address).transfer_private_to_private(owner, recipient, token_id, 1); 59 | 60 | // Add authorization witness from owner to recipient 61 | authwit_cheatcodes::add_private_authwit_from_call_interface( 62 | env, 63 | owner, 64 | recipient, 65 | transfer_call_interface, 66 | ); 67 | 68 | // Impersonate recipient to perform the authorized transfer 69 | env.call_private(recipient, transfer_call_interface); 70 | 71 | // Verify ownership transfer 72 | utils::assert_private_nft_nullified(env, nft_contract_address, owner, token_id); 73 | utils::assert_owns_private_nft(env, nft_contract_address, recipient, token_id); 74 | } 75 | 76 | #[test(should_fail_with = "nft not found in private to public")] 77 | unconstrained fn nft_transfer_private_to_private_non_existent_fail() { 78 | // Setup but don't mint any NFT 79 | let (env, nft_contract_address, owner, _, recipient) = utils::setup_with_minter(false); 80 | let non_existent_token_id = 12345; 81 | 82 | // Attempt to transfer non-existent NFT 83 | env.call_private( 84 | owner, 85 | NFT::at(nft_contract_address).transfer_private_to_private( 86 | owner, 87 | recipient, 88 | non_existent_token_id, 89 | 0, 90 | ), 91 | ); 92 | } 93 | 94 | #[test(should_fail_with = "Unknown auth witness for message hash")] 95 | unconstrained fn nft_transfer_private_to_private_unauthorized_fail() { 96 | // Setup with account contracts 97 | let token_id = 10000; 98 | let (env, nft_contract_address, owner, _, recipient) = 99 | utils::setup_and_mint_to_private(true, token_id); 100 | 101 | // Create transfer interface with non-zero nonce 102 | let transfer_call_interface = 103 | NFT::at(nft_contract_address).transfer_private_to_private(owner, recipient, token_id, 1); 104 | 105 | // Impersonate recipient but DON'T add authorization witness 106 | env.call_private(recipient, transfer_call_interface); 107 | } 108 | 109 | #[test(should_fail_with = "Unknown auth witness for message hash")] 110 | unconstrained fn nft_transfer_private_to_private_wrong_authwit_fail() { 111 | // Setup with account contracts 112 | let token_id = 10000; 113 | let (env, nft_contract_address, owner, _, recipient) = 114 | utils::setup_and_mint_to_private(true, token_id); 115 | 116 | // Create transfer interface with non-zero nonce 117 | let transfer_call_interface = 118 | NFT::at(nft_contract_address).transfer_private_to_private(owner, recipient, token_id, 1); 119 | 120 | // Add authorization witness but to the wrong address (owner instead of recipient) 121 | authwit_cheatcodes::add_private_authwit_from_call_interface( 122 | env, 123 | owner, 124 | owner, // Wrong address - should be recipient 125 | transfer_call_interface, 126 | ); 127 | 128 | // Impersonate recipient 129 | env.call_private(recipient, transfer_call_interface); 130 | } 131 | 132 | #[test(should_fail_with = "nft not found in private to public")] 133 | unconstrained fn nft_transfer_private_to_private_public_nft_fail() { 134 | // Setup and mint NFT to owner in public state 135 | let token_id = 10000; 136 | let (env, nft_contract_address, owner, _, recipient) = 137 | utils::setup_and_mint_to_public(false, token_id); 138 | 139 | // Verify NFT is in public state 140 | utils::assert_owns_public_nft(env, nft_contract_address, owner, token_id); 141 | 142 | // Attempt to transfer from private state when NFT is actually in public state 143 | env.call_private( 144 | owner, 145 | NFT::at(nft_contract_address).transfer_private_to_private(owner, recipient, token_id, 0), 146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /src/token_contract/src/test/mint_to_commitment.nr: -------------------------------------------------------------------------------- 1 | use crate::{test::utils, Token}; 2 | use aztec::oracle::random::random; 3 | 4 | #[test] 5 | unconstrained fn mint_to_commitment_success() { 6 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 7 | let (mut env, token_contract_address, _, recipient, minter) = utils::setup_with_minter(false); 8 | 9 | let commitment = env.call_private( 10 | minter, 11 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, minter), 12 | ); 13 | 14 | let mint_amount: u128 = 10_000; 15 | env.call_public( 16 | minter, 17 | Token::at(token_contract_address).mint_to_commitment(commitment, mint_amount), 18 | ); 19 | 20 | utils::check_private_balance(env, token_contract_address, recipient, mint_amount); 21 | 22 | let total_supply = env.view_public(Token::at(token_contract_address).total_supply()); 23 | assert(total_supply == mint_amount); 24 | } 25 | 26 | #[test(should_fail_with = "caller is not minter")] 27 | unconstrained fn mint_to_private_failure_unauthorized() { 28 | let (mut env, token_contract_address, _, recipient, _) = utils::setup_with_minter(false); 29 | 30 | let commitment = env.call_private( 31 | recipient, 32 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, recipient), 33 | ); 34 | 35 | let mint_amount: u128 = 10_000; 36 | env.call_public( 37 | recipient, 38 | Token::at(token_contract_address).mint_to_commitment(commitment, mint_amount), 39 | ); 40 | } 41 | 42 | #[test(should_fail_with = "attempt to add with overflow 'total_supply.read() + amount'")] 43 | unconstrained fn mint_to_private_failure_balance_overflow() { 44 | let (mut env, token_contract_address, owner, recipient, minter) = 45 | utils::setup_with_minter(false); 46 | 47 | let commitment = env.call_private( 48 | minter, 49 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, minter), 50 | ); 51 | 52 | let max_u128 = utils::max_u128(); 53 | env.call_public( 54 | minter, 55 | Token::at(token_contract_address).mint_to_commitment(commitment, max_u128), 56 | ); 57 | 58 | env.call_public( 59 | minter, 60 | Token::at(token_contract_address).mint_to_commitment(commitment, 2 as u128), 61 | ); 62 | // env.assert_private_call_fails(mint_to_private_call_interface); 63 | 64 | utils::check_private_balance(env, token_contract_address, owner, 0); 65 | utils::check_total_supply(env, token_contract_address, max_u128); 66 | } 67 | 68 | #[test(should_fail_with = "attempt to add with overflow 'total_supply.read() + amount'")] 69 | unconstrained fn mint_to_private_failure_total_supply_overflow() { 70 | let (mut env, token_contract_address, owner, recipient, minter) = 71 | utils::setup_with_minter(false); 72 | 73 | let commitment = env.call_private( 74 | minter, 75 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, minter), 76 | ); 77 | 78 | let mint_amount: u128 = 10_000; 79 | let max_u128 = utils::max_u128(); 80 | env.call_public( 81 | minter, 82 | Token::at(token_contract_address).mint_to_commitment(commitment, max_u128), 83 | ); 84 | 85 | utils::check_private_balance(env, token_contract_address, owner, 0); 86 | 87 | let another_commitment = env.call_private( 88 | minter, 89 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, minter), 90 | ); 91 | 92 | env.call_public( 93 | minter, 94 | Token::at(token_contract_address).mint_to_commitment(another_commitment, mint_amount), 95 | ); 96 | } 97 | 98 | #[test(should_fail_with = "Invalid partial note or completer 'context.nullifier_exists(validity_commitment, context.this_address())'")] 99 | unconstrained fn mint_to_private_failure_invalid_commitments_random() { 100 | let (mut env, token_contract_address, _, _, minter) = utils::setup_with_minter(false); 101 | 102 | let mint_amount: u128 = 10_000; 103 | 104 | env.call_public( 105 | minter, 106 | Token::at(token_contract_address).mint_to_commitment(random(), mint_amount), 107 | ); 108 | } 109 | 110 | #[test(should_fail_with = "Invalid partial note or completer 'context.nullifier_exists(validity_commitment, context.this_address())'")] 111 | unconstrained fn mint_to_private_failure_invalid_commitments_zero() { 112 | let (mut env, token_contract_address, _, _, minter) = utils::setup_with_minter(false); 113 | 114 | let mint_amount: u128 = 10_000; 115 | env.call_public(minter, Token::at(token_contract_address).mint_to_commitment(0, mint_amount)); 116 | } 117 | 118 | #[test(should_fail_with = "Got 2 logs for tag")] 119 | unconstrained fn mint_to_private_failure_already_completed_commitment() { 120 | let (mut env, token_contract_address, _, recipient, minter) = utils::setup_with_minter(false); 121 | 122 | let commitment = env.call_private( 123 | minter, 124 | Token::at(token_contract_address).initialize_transfer_commitment(recipient, minter), 125 | ); 126 | 127 | let mint_amount: u128 = 10_000; 128 | 129 | // mint to a commitment once 130 | env.call_public( 131 | minter, 132 | Token::at(token_contract_address).mint_to_commitment(commitment, mint_amount), 133 | ); 134 | 135 | // balance should be minted 136 | utils::check_private_balance(env, token_contract_address, recipient, mint_amount); 137 | // balance should be equal to total supply 138 | utils::check_total_supply(env, token_contract_address, mint_amount); 139 | // mint to the same commitment again 140 | env.call_public( 141 | minter, 142 | Token::at(token_contract_address).mint_to_commitment(commitment, mint_amount), 143 | ); 144 | 145 | // this will revert because the TXE will now notice that a tag has duplicate logs 146 | utils::check_private_balance(env, token_contract_address, recipient, mint_amount); 147 | } 148 | -------------------------------------------------------------------------------- /src/token_contract/src/test/transfer_public_to_public.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils::{self, mint_amount}; 2 | use crate::Token; 3 | use aztec::test::helpers::authwit as authwit_cheatcodes; 4 | 5 | #[test] 6 | unconstrained fn public_transfer() { 7 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 8 | let (mut env, token_contract_address, owner, recipient) = 9 | utils::setup_and_mint_to_public_without_minter(false); 10 | // Transfer tokens 11 | let transfer_amount = mint_amount / (10 as u128); 12 | env.call_public( 13 | owner, 14 | Token::at(token_contract_address).transfer_public_to_public( 15 | owner, 16 | recipient, 17 | transfer_amount, 18 | 0, 19 | ), 20 | ); 21 | 22 | // Check balances 23 | utils::check_public_balance( 24 | env, 25 | token_contract_address, 26 | owner, 27 | mint_amount - transfer_amount, 28 | ); 29 | utils::check_public_balance(env, token_contract_address, recipient, transfer_amount); 30 | } 31 | 32 | #[test] 33 | unconstrained fn public_transfer_to_self() { 34 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 35 | let (mut env, token_contract_address, owner, _) = 36 | utils::setup_and_mint_to_public_without_minter(false); 37 | // Transfer tokens 38 | let transfer_amount = mint_amount / (10 as u128); 39 | env.call_public( 40 | owner, 41 | Token::at(token_contract_address).transfer_public_to_public( 42 | owner, 43 | owner, 44 | transfer_amount, 45 | 0, 46 | ), 47 | ); 48 | // Check balances 49 | utils::check_public_balance(env, token_contract_address, owner, mint_amount); 50 | } 51 | 52 | #[test] 53 | unconstrained fn public_transfer_on_behalf_of_other() { 54 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 55 | let (mut env, token_contract_address, owner, recipient) = 56 | utils::setup_and_mint_to_public_without_minter(true); 57 | let transfer_amount = mint_amount / (10 as u128); 58 | let public_transfer_private_to_private_call_interface = Token::at(token_contract_address) 59 | .transfer_public_to_public(owner, recipient, transfer_amount, 1); 60 | authwit_cheatcodes::add_public_authwit_from_call_interface( 61 | env, 62 | owner, 63 | recipient, 64 | public_transfer_private_to_private_call_interface, 65 | ); 66 | // Impersonate recipient to perform the call 67 | // Transfer tokens 68 | env.call_public(recipient, public_transfer_private_to_private_call_interface); 69 | // Check balances 70 | utils::check_public_balance( 71 | env, 72 | token_contract_address, 73 | owner, 74 | mint_amount - transfer_amount, 75 | ); 76 | utils::check_public_balance(env, token_contract_address, recipient, transfer_amount); 77 | } 78 | 79 | #[test(should_fail_with = "attempt to subtract with overflow 'public_balances.at(from).read() - amount'")] 80 | unconstrained fn public_transfer_failure_more_than_balance() { 81 | // Setup without account contracts. We are not using authwits here, so dummy accounts are enough 82 | let (mut env, token_contract_address, owner, recipient) = 83 | utils::setup_and_mint_to_public_without_minter(false); 84 | // Transfer tokens 85 | let transfer_amount = mint_amount + (1 as u128); 86 | let public_transfer_call_interface = Token::at(token_contract_address) 87 | .transfer_public_to_public(owner, recipient, transfer_amount, 0); 88 | // Try to transfer tokens 89 | env.call_public(owner, public_transfer_call_interface); 90 | } 91 | 92 | #[test(should_fail_with = "unauthorized")] 93 | unconstrained fn public_transfer_failure_on_behalf_of_other_without_approval() { 94 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 95 | let (mut env, token_contract_address, owner, recipient) = 96 | utils::setup_and_mint_to_public_without_minter(true); 97 | let transfer_amount = mint_amount / (10 as u128); 98 | let public_transfer_private_to_private_call_interface = Token::at(token_contract_address) 99 | .transfer_public_to_public(owner, recipient, transfer_amount, 1); 100 | // Impersonate recipient to perform the call 101 | // Try to transfer tokens 102 | env.call_public(recipient, public_transfer_private_to_private_call_interface); 103 | } 104 | 105 | #[test(should_fail_with = "attempt to subtract with overflow 'public_balances.at(from).read() - amount'")] 106 | unconstrained fn public_transfer_failure_on_behalf_of_other_more_than_balance() { 107 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 108 | let (mut env, token_contract_address, owner, recipient) = 109 | utils::setup_and_mint_to_public_without_minter(true); 110 | let transfer_amount = mint_amount + (1 as u128); 111 | let public_transfer_private_to_private_call_interface = Token::at(token_contract_address) 112 | .transfer_public_to_public(owner, recipient, transfer_amount, 1); 113 | authwit_cheatcodes::add_public_authwit_from_call_interface( 114 | env, 115 | owner, 116 | recipient, 117 | public_transfer_private_to_private_call_interface, 118 | ); 119 | // Impersonate recipient to perform the call 120 | // Try to transfer tokens 121 | env.call_public(recipient, public_transfer_private_to_private_call_interface); 122 | } 123 | 124 | #[test(should_fail_with = "unauthorized")] 125 | unconstrained fn public_transfer_failure_on_behalf_of_other_wrong_caller() { 126 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 127 | let (mut env, token_contract_address, owner, recipient) = 128 | utils::setup_and_mint_to_public_without_minter(true); 129 | let transfer_amount = mint_amount / (10 as u128); 130 | let public_transfer_private_to_private_call_interface = Token::at(token_contract_address) 131 | .transfer_public_to_public(owner, recipient, transfer_amount, 1); 132 | authwit_cheatcodes::add_public_authwit_from_call_interface( 133 | env, 134 | owner, 135 | owner, 136 | public_transfer_private_to_private_call_interface, 137 | ); 138 | // Impersonate recipient to perform the call 139 | // Try to transfer tokens 140 | env.call_public(recipient, public_transfer_private_to_private_call_interface); 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/compare-benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Aztec Benchmark Diff 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }} 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: read 12 | pull-requests: write 13 | issues: write 14 | 15 | env: 16 | BENCH_DIR: ./benchmarks 17 | 18 | jobs: 19 | benchmark: 20 | strategy: 21 | matrix: 22 | os: [ubuntu-latest-m] 23 | threads: [12] 24 | 25 | runs-on: 26 | labels: ${{ matrix.os }} 27 | timeout-minutes: 60 28 | 29 | steps: 30 | # ────────────────────────────────────────────────────────────── 31 | # 0️⃣ SHARED TOOLING – Buildx + Aztec CLI skeleton 32 | # ────────────────────────────────────────────────────────────── 33 | - name: Checkout repo (full history) 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | 38 | - name: Set up Docker Buildx (Aztec sandbox needs BuildKit) 39 | uses: docker/setup-buildx-action@v2 40 | 41 | - name: Install Aztec CLI 42 | run: | 43 | curl -s https://install.aztec.network > tmp.sh 44 | bash tmp.sh <<< yes "yes" 45 | 46 | - name: Update path 47 | run: echo "/home/runner/.aztec/bin" >> $GITHUB_PATH 48 | 49 | # ────────────────────────────────────────────────────────────── 50 | # 1️⃣ BENCHMARK BASE COMMIT 51 | # ────────────────────────────────────────────────────────────── 52 | - name: Checkout BASE branch 53 | uses: actions/checkout@v4 54 | with: 55 | ref: ${{ github.event.pull_request.base.sha }} 56 | 57 | - uses: actions/setup-node@v4 58 | with: 59 | node-version: "22.17.0" 60 | 61 | - name: Detect Aztec version (BASE) 62 | id: basever 63 | run: | 64 | VER=$(node -p "require('./package.json').config.aztecVersion") 65 | echo "ver=$VER" >> "$GITHUB_OUTPUT" 66 | echo "Base Aztec version is $VER" 67 | 68 | - name: Switch CLI to BASE version 69 | run: | 70 | VERSION=${{ steps.basever.outputs.ver }} aztec-up 71 | 72 | - name: Start sandbox (BASE, background) 73 | run: aztec start --sandbox & 74 | 75 | - name: Start PXE node (BASE, background) 76 | run: | 77 | VERSION=${{ steps.basever.outputs.ver }} aztec \ 78 | start --port 8081 --pxe --pxe.nodeUrl=http://localhost:8080/ \ 79 | --pxe.proverEnabled false & 80 | 81 | # This is a temporary hack to fix a problem with v3 releases. 82 | - name: Manually tag the aztec version as `latest` 83 | run: | 84 | docker tag aztecprotocol/aztec:${{ steps.basever.outputs.ver }} aztecprotocol/aztec:latest 85 | 86 | - name: Install deps (BASE) 87 | run: yarn --frozen-lockfile 88 | 89 | - name: Compile contracts (BASE) 90 | run: yarn compile 91 | 92 | - name: Codegen wrappers (BASE) 93 | run: yarn codegen 94 | 95 | - name: Benchmark (BASE) 96 | run: | 97 | npx aztec-benchmark --suffix _base --output-dir ${{ env.BENCH_DIR }} 98 | 99 | # This is required to avoid the benchmark results being removed by the Checkout PR step 100 | - name: Store base benchmark results 101 | run: | 102 | mkdir -p ../benchmarks_base && mv ${{ env.BENCH_DIR }}/*.json ../benchmarks_base/ 103 | 104 | # ────────────────────────────────────────────────────────────── 105 | # 2️⃣ BENCHMARK PR HEAD (github.event.pull_request.head.sha) 106 | # ────────────────────────────────────────────────────────────── 107 | 108 | # clean does not work correctly and is removing the benchmark results anyways 109 | # https://github.com/actions/checkout/issues/1201 110 | - name: Checkout PR branch 111 | uses: actions/checkout@v4 112 | with: 113 | ref: ${{ github.event.pull_request.head.sha }} 114 | clean: false 115 | 116 | # Restore the base benchmark results to avoid the benchmark results being removed by the Checkout PR step 117 | - name: Restore base benchmark results 118 | run: mv ../benchmarks_base/* ${{ env.BENCH_DIR }}/ 119 | 120 | - name: Detect Aztec version (PR) 121 | id: prver 122 | run: | 123 | VER=$(node -p "require('./package.json').config.aztecVersion") 124 | echo "PR Aztec version is $VER" 125 | if [ "${{ steps.basever.outputs.ver }}" != "$VER" ]; then 126 | echo "ver_diff=true" >> "$GITHUB_OUTPUT" 127 | else 128 | echo "ver_diff=false" >> "$GITHUB_OUTPUT" 129 | fi 130 | echo "ver=$VER" >> "$GITHUB_OUTPUT" 131 | 132 | - name: Kill BASE services 133 | if: steps.prver.outputs.ver_diff == 'true' 134 | run: | 135 | pkill -f "aztec.*--sandbox" || true 136 | pkill -f "aztec.*--pxe.*8081" || true 137 | sleep 5 138 | 139 | - name: Switch CLI to PR version 140 | if: steps.prver.outputs.ver_diff == 'true' 141 | run: | 142 | VERSION=${{ steps.prver.outputs.ver }} aztec-up 143 | 144 | - name: Start sandbox (PR, background) 145 | if: steps.prver.outputs.ver_diff == 'true' 146 | run: aztec start --sandbox & 147 | 148 | - name: Start PXE node (PR, background) 149 | if: steps.prver.outputs.ver_diff == 'true' 150 | run: | 151 | VERSION=${{ steps.prver.outputs.ver }} aztec \ 152 | start --port 8081 --pxe --pxe.nodeUrl=http://localhost:8080/ \ 153 | --pxe.proverEnabled false & 154 | 155 | # This is a temporary hack to fix a problem with v3 releases. 156 | - name: Manually tag the aztec version as `latest` 157 | run: | 158 | docker tag aztecprotocol/aztec:${{ steps.prver.outputs.ver }} aztecprotocol/aztec:latest 159 | 160 | - name: Install deps (PR) 161 | run: yarn --frozen-lockfile 162 | 163 | - name: Compile contracts (PR) 164 | run: yarn compile 165 | 166 | - name: Codegen wrappers (PR) 167 | run: yarn codegen 168 | 169 | # ────────────────────────────────────────────────────────────── 170 | # 3️⃣ DIFF & COMMENT 171 | # ────────────────────────────────────────────────────────────── 172 | - name: Generate Markdown diff 173 | uses: defi-wonderland/aztec-benchmark/action@main 174 | with: 175 | base_suffix: '_base' 176 | current_suffix: '_pr' 177 | 178 | - name: Comment diff 179 | uses: peter-evans/create-or-update-comment@v4 180 | with: 181 | issue-number: ${{ github.event.pull_request.number }} 182 | body-file: benchmark-comparison.md 183 | -------------------------------------------------------------------------------- /src/token_contract/src/types/balance_set.nr: -------------------------------------------------------------------------------- 1 | use aztec::{ 2 | context::{PrivateContext, UtilityContext}, 3 | note::{ 4 | note_emission::OuterNoteEmission, 5 | note_getter_options::{NoteGetterOptions, SortOrder}, 6 | note_interface::NoteProperties, 7 | note_viewer_options::NoteViewerOptions, 8 | retrieved_note::RetrievedNote, 9 | }, 10 | protocol_types::{address::AztecAddress, constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL}, 11 | state_vars::{PrivateSet, storage::HasStorageSlot}, 12 | }; 13 | use std::ops::Add; 14 | use uint_note::uint_note::UintNote; 15 | 16 | pub struct BalanceSet { 17 | pub set: PrivateSet, 18 | } 19 | 20 | // TODO(#13824): remove this impl once we allow structs to hold state variables. 21 | impl HasStorageSlot<1> for BalanceSet { 22 | fn get_storage_slot(self) -> Field { 23 | self.set.get_storage_slot() 24 | } 25 | } 26 | 27 | impl BalanceSet { 28 | pub fn new(context: Context, storage_slot: Field) -> Self { 29 | assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1."); 30 | Self { set: PrivateSet::new(context, storage_slot) } 31 | } 32 | } 33 | 34 | impl BalanceSet { 35 | pub unconstrained fn balance_of(self: Self) -> u128 { 36 | self.balance_of_with_offset(0) 37 | } 38 | 39 | pub unconstrained fn balance_of_with_offset(self: Self, offset: u32) -> u128 { 40 | let mut balance = 0 as u128; 41 | let mut options = NoteViewerOptions::new(); 42 | let notes = self.set.view_notes(options.set_offset(offset)); 43 | for i in 0..options.limit { 44 | if i < notes.len() { 45 | balance = balance + notes.get_unchecked(i).get_value(); 46 | } 47 | } 48 | if (notes.len() == options.limit) { 49 | balance = balance + self.balance_of_with_offset(offset + options.limit); 50 | } 51 | 52 | balance 53 | } 54 | } 55 | 56 | impl BalanceSet<&mut PrivateContext> { 57 | pub fn add(self: Self, owner: AztecAddress, addend: u128) -> OuterNoteEmission { 58 | let content = if addend == 0 as u128 { 59 | Option::none() 60 | } else { 61 | // We fetch the nullifier public key hash from the registry / from our PXE 62 | let mut addend_note = UintNote::new(addend, owner); 63 | 64 | Option::some(self.set.insert(addend_note).content) 65 | }; 66 | 67 | OuterNoteEmission::new(content, self.set.context) 68 | } 69 | 70 | pub fn sub(self: Self, owner: AztecAddress, amount: u128) -> OuterNoteEmission { 71 | let subtracted = self.try_sub(amount, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL); 72 | 73 | // try_sub may have subtracted more or less than amount. We must ensure that we subtracted at least as much as 74 | // we needed, and then create a new note for the owner for the change (if any). 75 | assert(subtracted >= amount, "Balance too low"); 76 | self.add(owner, subtracted - amount) 77 | } 78 | 79 | // Attempts to remove 'target_amount' from the owner's balance. try_sub returns how much was actually subtracted 80 | // (i.e. the sum of the value of nullified notes), but this subtracted amount may be more or less than the target 81 | // amount. 82 | // This may seem odd, but is unfortunately unavoidable due to the number of notes available and their amounts being 83 | // unknown. What try_sub does is a best-effort attempt to consume as few notes as possible that add up to more than 84 | // `target_amount`. 85 | // The `max_notes` parameter is used to fine-tune the number of constraints created by this function. The gate count 86 | // scales relatively linearly with `max_notes`, but a lower `max_notes` parameter increases the likelihood of 87 | // `try_sub` subtracting an amount smaller than `target_amount`. 88 | pub fn try_sub(self: Self, target_amount: u128, max_notes: u32) -> u128 { 89 | // We are using a preprocessor here (filter applied in an unconstrained context) instead of a filter because 90 | // we do not need to prove correct execution of the preprocessor. 91 | // Because the `min_sum` notes is not constrained, users could choose to e.g. not call it. However, all this 92 | // might result in is simply higher DA costs due to more nullifiers being emitted. Since we don't care 93 | // about proving optimal note usage, we can save these constraints and make the circuit smaller. 94 | let options = NoteGetterOptions::with_preprocessor(preprocess_notes_min_sum, target_amount) 95 | .sort(UintNote::properties().value, SortOrder.DESC) 96 | .set_limit(max_notes); 97 | let notes = self.set.pop_notes(options); 98 | 99 | let mut subtracted = 0 as u128; 100 | for i in 0..options.limit { 101 | if i < notes.len() { 102 | let note = notes.get_unchecked(i); 103 | subtracted = subtracted + note.get_value(); 104 | } 105 | } 106 | 107 | subtracted 108 | } 109 | } 110 | 111 | // Computes the partial sum of the notes array, stopping once 'min_sum' is reached. This can be used to minimize the 112 | // number of notes read that add to some value, e.g. when transferring some amount of tokens. 113 | // The preprocessor (a filter applied in an unconstrained context) does not check if total sum is larger or equal to 114 | // 'min_sum' - all it does is remove extra notes if it does reach that value. 115 | // Note that proper usage of this preprocessor requires for notes to be sorted in descending order. 116 | pub fn preprocess_notes_min_sum( 117 | notes: [Option>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL], 118 | min_sum: u128, 119 | ) -> [Option>; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] { 120 | let mut selected = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; 121 | let mut sum = 0 as u128; 122 | for i in 0..notes.len() { 123 | // Because we process notes in retrieved order, notes need to be sorted in descending amount order for this 124 | // 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 125 | // sorted in descending order, the filter will only choose the notes with values 3 and 2, but if sorted in 126 | // ascending order it will choose 4 notes of value 1. 127 | if notes[i].is_some() & sum < min_sum { 128 | let retrieved_note = notes[i].unwrap_unchecked(); 129 | selected[i] = Option::some(retrieved_note); 130 | sum = sum.add(retrieved_note.note.get_value()); 131 | } 132 | } 133 | selected 134 | } 135 | -------------------------------------------------------------------------------- /src/escrow_contract/README.md: -------------------------------------------------------------------------------- 1 | # Escrow Standard 2 | 3 | The Escrow Standard defines a minimal, reusable on-chain Escrow contract that safely holds private balances while delegating release logic, key distribution, and participant discovery to a separate Logic contract. 4 | 5 | Since encryption and nullification keys are needed to read and spend private balances, respectively, the Escrow contract must have keys. Logic contracts should implement a key- and escrow-sharing mechanism, for which a Logic library with helper functions is provided. 6 | 7 | Logic contract implementations may vary greatly among use cases, but the basic patterns presented here, available in the logic library, should be used carefully, so that privacy is preserved. Examples of logic contracts can be found [here](https://github.com/defi-wonderland/aztec-escrow-extensions). 8 | 9 | ## Escrow Contract 10 | 11 | The Escrow contract is a minimally designed private contract with the following important characteristics: 12 | - Needs to be setup with keys. This allows the Escrow to hold private balances. 13 | - Does not need to be publicly deployed. 14 | - Has only two methods - `withdraw()` and `withdraw_nft()` - that allows the owner of the Escrow to spend private balances of tokens or NFTs compliant with AIP-20 and AIP-721, respectively. The keys are needed for these. 15 | - Is fully private. Tokens and NFTs can only be withdrawn from the Escrow to another private balance, which does not leak any information. 16 | - Only the owner can interact with the Escrow. 17 | - Does not have storage nor needs initialization. The owner of the Escrow is defined as an `AztecAddress` encoded into the contract instance salt, which means that its immutable and the Escrow address is determined by it. 18 | 19 | ## Private Functions 20 | 21 | ### withdraw 22 | ```rust 23 | /// @notice Withdraws an amount from the escrow's private balance to the 24 | /// recipient's private balance. 25 | /// @dev Can only be called by the corresponding Logic contract 26 | /// @param token The address of the token 27 | /// @param amount The amount of tokens to withdraw from the escrow 28 | /// @param recipient The address of the recipient 29 | #[private] 30 | fn withdraw(token: AztecAddress, amount: u128, recipient: AztecAddress) { /* ... */ } 31 | ``` 32 | 33 | ### withdraw_nft 34 | ```rust 35 | /// @notice Withdraws a token of a given ID from the escrow's private balance to 36 | /// the recipient's private balance 37 | /// @dev Can only be called by the corresponding Logic contract 38 | /// @param nft The address of the NFT contract 39 | /// @param token_id The id of the token to withdraw from the escrow 40 | /// @param recipient The address of the recipient 41 | #[private] 42 | fn withdraw_nft(nft: AztecAddress, token_id: Field, recipient: AztecAddress) { /* ... */ } 43 | ``` 44 | 45 | ## Logic Library 46 | 47 | The Logic library provides functions that standardize and facilitate the implementation of Logic contracts. We call Logic contract any contract that owns one or multiple Escrows. Unlike the Escrow contract, a Logic contract implements the policy for a specific escrow use case and therefore can vary significantly between applications. For example, a trivial Logic that always releases tokens needs a much simpler interface than one that supports clawbacks, vesting schedules, or milestone conditions. 48 | 49 | Usually, a Logic contract will have the following features: 50 | 51 | - Manages how escrow details, including keys, are shared to escrow's participants. 52 | - Ensures that the escrow details are valid. 53 | - Assigns roles to participants (recipients, owner, etc.) and set any additional conditions (start timestamps, amounts, expiration, etc.). 54 | - Manages Escrow withdrawals. 55 | 56 | ## Library Functions 57 | 58 | The library functions guarantee that escrow's keys, contract class ID and setup are correct while standardizing the correct private sharing of keys and escrow address to participants. 59 | 60 | > ⚠️ **WARNING — Private Balance Loss** 61 | > 62 | > It's still the job of the Logic contract implementation to handle information safely and privately, while avoiding malicious attempts of withdrawing funds from Escrow contracts. This library facilitates this but cannot ensure Logic contracts are implemented correctly. Use carefully. 63 | 64 | ### _get_escrow 65 | ```rust 66 | /// @notice Returns the escrow address that corresponds to the given master secret keys and class ID. 67 | /// @param context The private context 68 | /// @param escrow_class_id The contract class id of the escrow contract 69 | /// @param master_secret_keys The master secret keys 70 | /// @return The escrow address 71 | #[contract_library_method] 72 | pub fn _get_escrow( 73 | context: &mut PrivateContext, 74 | escrow_class_id: Field, 75 | master_secret_keys: MasterSecretKeys, 76 | ) { /* ... */ } 77 | ``` 78 | 79 | ### _share_escrow 80 | ```rust 81 | /// @notice Shares the escrow details needed to find and use the escrow contract 82 | /// @dev Emits a private log with the escrow details 83 | /// @param context The private context 84 | /// @param account The address of the account that will use the escrow 85 | /// @param escrow The address of the escrow 86 | /// @param master_secret_keys The master secret keys 87 | #[contract_library_method] 88 | pub fn _share_escrow( 89 | context: &mut PrivateContext, 90 | account: AztecAddress, 91 | escrow: AztecAddress, 92 | master_secret_keys: MasterSecretKeys, 93 | ) { /* ... */ } 94 | ``` 95 | 96 | ### _withdraw 97 | ```rust 98 | /// @notice Withdraws an amount of tokens from the provided escrow. 99 | /// @param context The private context 100 | /// @param escrow The address of the escrow 101 | /// @param account The address of the account that will receive the tokens 102 | /// @param token The address of the token 103 | /// @param amount The amount of tokens to withdraw from the escrow 104 | #[contract_library_method] 105 | pub fn _withdraw( 106 | context: &mut PrivateContext, 107 | escrow: AztecAddress, 108 | account: AztecAddress, 109 | token: AztecAddress, 110 | amount: u128, 111 | ) { /* ... */ } 112 | ``` 113 | 114 | ### _withdraw_nft 115 | ```rust 116 | /// @notice Withdraws an NFT from the provided escrow. 117 | /// @param context The private context 118 | /// @param escrow The address of the escrow 119 | /// @param account The address of the account that will receive the NFT 120 | /// @param nft The address of the NFT contract 121 | /// @param token_id The id of the token to withdraw from the escrow 122 | #[contract_library_method] 123 | pub fn _withdraw_nft( 124 | context: &mut PrivateContext, 125 | escrow: AztecAddress, 126 | account: AztecAddress, 127 | nft: AztecAddress, 128 | token_id: Field, 129 | ){ /* ... */ } 130 | ``` 131 | 132 | ### _secret_keys_to_public_keys 133 | ```rust 134 | /// @notice Derives public keys from secret keys. 135 | /// @param master_secret_keys The master secret keys 136 | /// @return PublicKeys containing the derived public keys. 137 | #[contract_library_method] 138 | pub fn _secret_keys_to_public_keys(master_secret_keys: MasterSecretKeys) -> PublicKeys { /* ... */ } 139 | ``` 140 | -------------------------------------------------------------------------------- /src/token_contract/src/test/transfer_private_to_private.nr: -------------------------------------------------------------------------------- 1 | use crate::test::utils::{self, mint_amount}; 2 | use crate::Token; 3 | use aztec::{note::constants::MAX_NOTES_PER_PAGE, test::helpers::authwit as authwit_cheatcodes}; 4 | use uint_note::uint_note::UintNote; 5 | 6 | #[test] 7 | unconstrained fn transfer_private_on_behalf_of_other() { 8 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 9 | let (mut env, token_contract_address, owner, recipient) = 10 | utils::setup_and_mint_to_private_without_minter(true); 11 | // Add authwit 12 | let transfer_amount = (1000 as u128); 13 | let transfer_private_from_call_interface = Token::at(token_contract_address) 14 | .transfer_private_to_private(owner, recipient, transfer_amount, 1); 15 | authwit_cheatcodes::add_private_authwit_from_call_interface( 16 | env, 17 | owner, 18 | recipient, 19 | transfer_private_from_call_interface, 20 | ); 21 | // Transfer tokens using new API 22 | env.call_private(recipient, transfer_private_from_call_interface); 23 | // Check balances 24 | utils::check_private_balance( 25 | env, 26 | token_contract_address, 27 | owner, 28 | mint_amount - transfer_amount, 29 | ); 30 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 31 | } 32 | 33 | #[test] 34 | unconstrained fn transfer_private_zero_amount() { 35 | let (mut env, token_contract_address, owner, recipient) = 36 | utils::setup_and_mint_to_private_without_minter(false); 37 | 38 | env.call_private( 39 | owner, 40 | Token::at(token_contract_address).transfer_private_to_private(owner, recipient, 0, 0), 41 | ); 42 | 43 | // Check balances 44 | utils::check_private_balance(env, token_contract_address, owner, mint_amount); 45 | utils::check_private_balance(env, token_contract_address, recipient, 0); 46 | } 47 | 48 | #[test] 49 | unconstrained fn transfer_private_multiple_notes_recursively() { 50 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 51 | let (mut env, token_contract_address, owner, recipient, minter) = 52 | utils::setup_with_minter(true); 53 | 54 | let notes_amount: u128 = 1000; 55 | let notes_count: u128 = 12; 56 | let total_amount = notes_amount * notes_count; 57 | 58 | for _ in 0..notes_count { 59 | utils::mint_to_private(env, token_contract_address, owner, notes_amount, minter); 60 | } 61 | 62 | // Transfer tokens 63 | // Transfer will require 11 notes with change, which requires 2 recursive calls: 2 + 8 + 1 64 | // transfer amount is 10999 of the 12000 total 65 | let transfer_amount = total_amount - notes_amount - (1 as u128); 66 | let transfer_private_from_call_interface = Token::at(token_contract_address) 67 | .transfer_private_to_private(owner, recipient, transfer_amount, 0); 68 | env.call_private(owner, transfer_private_from_call_interface); 69 | 70 | // NOTE: Removing this check makes the test fail. 71 | let recipient_balance = utils::get_private_balance(env, token_contract_address, recipient); 72 | assert(recipient_balance == transfer_amount, "Incorrect recipient balance"); 73 | 74 | // Check that the notes still owned by the owner are correct 75 | let final_owner_notes: BoundedVec = 76 | utils::get_private_balance_notes(env, token_contract_address, owner, 0); 77 | assert(final_owner_notes.len() == 2, "Incorrect note count"); // 1000 UintNote x1 and 1 UintNote x1 78 | assert(final_owner_notes.get(0).get_value() == notes_amount, "Incorrect note amount"); 79 | assert(final_owner_notes.get(1).get_value() == (1 as u128), "Incorrect note change amount"); 80 | 81 | // Check that the notes generated to the recipient are correct 82 | let recipient_notes: BoundedVec = 83 | utils::get_private_balance_notes(env, token_contract_address, recipient, 0); 84 | assert(recipient_notes.len() == 1, "Incorrect transferred note count"); // 8999 UintNote x1 85 | assert( 86 | recipient_notes.get(0).get_value() == transfer_amount, 87 | "Incorrect transferred note amount", 88 | ); 89 | 90 | // Check balances 91 | utils::check_private_balance( 92 | env, 93 | token_contract_address, 94 | owner, 95 | total_amount - transfer_amount, 96 | ); 97 | utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); 98 | } 99 | 100 | #[test(should_fail_with = "Balance too low")] 101 | unconstrained fn transfer_private_failure_on_behalf_of_more_than_balance() { 102 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 103 | let (mut env, token_contract_address, owner, recipient) = 104 | utils::setup_and_mint_to_private_without_minter(true); 105 | // Add authwit 106 | let transfer_amount = mint_amount + (1 as u128); 107 | let transfer_private_from_call_interface = Token::at(token_contract_address) 108 | .transfer_private_to_private(owner, recipient, transfer_amount, 1); 109 | authwit_cheatcodes::add_private_authwit_from_call_interface( 110 | env, 111 | owner, 112 | recipient, 113 | transfer_private_from_call_interface, 114 | ); 115 | // Transfer tokens using new API 116 | env.call_private(recipient, transfer_private_from_call_interface); 117 | } 118 | 119 | // Unknown auth witness for message hash 0x055a9af747d60526794cfa8d3cf0b506831f34a202f85f6576ac67c429962b01 120 | #[test(should_fail_with = "Unknown auth witness for message hash ")] 121 | unconstrained fn transfer_private_failure_on_behalf_of_other_without_approval() { 122 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 123 | let (mut env, token_contract_address, owner, recipient) = 124 | utils::setup_and_mint_to_private_without_minter(true); 125 | // Add authwit 126 | let transfer_amount = (1000 as u128); 127 | let transfer_private_from_call_interface = Token::at(token_contract_address) 128 | .transfer_private_to_private(owner, recipient, transfer_amount, 1); 129 | // Transfer tokens using new API 130 | env.call_private(recipient, transfer_private_from_call_interface); 131 | } 132 | 133 | #[test(should_fail_with = "Unknown auth witness for message hash")] 134 | unconstrained fn transfer_private_failure_on_behalf_of_other_wrong_caller() { 135 | // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. 136 | let (mut env, token_contract_address, owner, recipient) = 137 | utils::setup_and_mint_to_private_without_minter(true); 138 | // Add authwit 139 | let transfer_amount: u128 = 1000; 140 | let transfer_private_from_call_interface = Token::at(token_contract_address) 141 | .transfer_private_to_private(owner, recipient, transfer_amount, 1); 142 | authwit_cheatcodes::add_private_authwit_from_call_interface( 143 | env, 144 | owner, 145 | owner, 146 | transfer_private_from_call_interface, 147 | ); 148 | // Transfer tokens using new API 149 | env.call_private(recipient, transfer_private_from_call_interface); 150 | } 151 | -------------------------------------------------------------------------------- /src/token_contract/src/test/tokenized_vault/deposit_public_to_public.nr: -------------------------------------------------------------------------------- 1 | use crate::{test::utils::{self, mint_amount}, Token}; 2 | use aztec::test::helpers::authwit as authwit_cheatcodes; 3 | 4 | #[test(should_fail_with = "Trying to read from uninitialized PublicImmutable")] 5 | unconstrained fn deposit_public_to_public_without_asset() { 6 | let (env, vault_address, owner, recipient, _) = utils::setup_with_minter(false); 7 | 8 | // Deposit should fail because the PublicImmutable asset was not initialized 9 | env.call_public( 10 | owner, 11 | Token::at(vault_address).deposit_public_to_public(owner, recipient, mint_amount, 0), 12 | ); 13 | } 14 | 15 | #[test] 16 | unconstrained fn deposit_public_to_public_success() { 17 | // Setup with asset token 18 | let (env, vault_address, owner, recipient, asset_address) = utils::setup_with_asset(false); 19 | 20 | // Mint some asset tokens to owner 21 | let deposit_amount: u128 = mint_amount; 22 | env.call_public(owner, Token::at(asset_address).mint_to_public(owner, deposit_amount)); 23 | 24 | // Deposit assets to get shares 25 | // Authorize the vault to use the caller's assets 26 | utils::authorize_transfer_public_to_public( 27 | env, 28 | asset_address, 29 | vault_address, 30 | owner, 31 | deposit_amount, 32 | 0, 33 | ); 34 | 35 | // Deposit 36 | env.call_public( 37 | owner, 38 | Token::at(vault_address).deposit_public_to_public(owner, recipient, deposit_amount, 0), 39 | ); 40 | 41 | // Check recipient got shares 42 | // At the first deposit 1 share = 1 asset 43 | utils::check_public_balance(env, vault_address, recipient, deposit_amount); 44 | 45 | // Check the total supply got updated 46 | let total_supply = env.view_public(Token::at(vault_address).total_supply()); 47 | assert(total_supply == deposit_amount, "Incorrect shares total supply"); 48 | 49 | // Check vault has assets 50 | utils::check_public_balance(env, asset_address, vault_address, deposit_amount); 51 | } 52 | 53 | #[test] 54 | unconstrained fn deposit_public_to_public_after_yield() { 55 | // Setup with asset token 56 | let (env, vault_address, owner, recipient, asset_address) = utils::setup_with_asset(false); 57 | 58 | // Mint some asset tokens to the vault contract 59 | let yield_amount: u128 = 1; 60 | env.call_public(owner, Token::at(asset_address).mint_to_public(vault_address, yield_amount)); 61 | 62 | // Mint some asset tokens to owner 63 | let deposit_amount: u128 = mint_amount; 64 | env.call_public(owner, Token::at(asset_address).mint_to_public(owner, deposit_amount)); 65 | 66 | // Deposit assets to get shares 67 | // Authorize the vault to use the caller's assets 68 | utils::authorize_transfer_public_to_public( 69 | env, 70 | asset_address, 71 | vault_address, 72 | owner, 73 | deposit_amount, 74 | 0, 75 | ); 76 | 77 | // Deposit 78 | env.call_public( 79 | owner, 80 | Token::at(vault_address).deposit_public_to_public(owner, recipient, deposit_amount, 0), 81 | ); 82 | 83 | // Check recipient got shares 84 | let expected_shares: u128 = deposit_amount / 2; // The initial rate, when shares' supply is still 0, is given by the amount of assets held by the vault + 1. Since yield = 1, the rate is 1:2. 85 | utils::check_public_balance(env, vault_address, recipient, expected_shares); 86 | 87 | // Check the total supply got updated 88 | let total_supply = env.view_public(Token::at(vault_address).total_supply()); 89 | assert(total_supply == expected_shares, "Incorrect shares total supply"); 90 | 91 | // Check vault has assets 92 | utils::check_public_balance( 93 | env, 94 | asset_address, 95 | vault_address, 96 | deposit_amount + yield_amount, 97 | ); 98 | } 99 | 100 | #[test(should_fail_with = "unauthorized")] 101 | unconstrained fn deposit_public_to_public_without_asset_approval() { 102 | // Setup with asset token 103 | let (env, vault_address, owner, recipient, asset_address) = utils::setup_with_asset(false); 104 | 105 | // Mint some asset tokens to owner 106 | let deposit_amount: u128 = mint_amount; 107 | env.call_public(owner, Token::at(asset_address).mint_to_public(owner, deposit_amount)); 108 | 109 | // Deposit 110 | env.call_public( 111 | owner, 112 | Token::at(vault_address).deposit_public_to_public(owner, recipient, deposit_amount, 0), 113 | ); 114 | } 115 | 116 | #[test(should_fail_with = "unauthorized")] 117 | unconstrained fn deposit_public_to_public_on_behalf_of_wrong_caller() { 118 | // Setup with asset token 119 | let (env, vault_address, owner, recipient, asset_address) = utils::setup_with_asset(false); 120 | 121 | // Mint some asset tokens to owner 122 | let deposit_amount: u128 = mint_amount; 123 | env.call_public(owner, Token::at(asset_address).mint_to_public(owner, deposit_amount)); 124 | 125 | // Deposit assets to get shares 126 | // Authorize the vault to use the caller's assets 127 | utils::authorize_transfer_public_to_public( 128 | env, 129 | asset_address, 130 | vault_address, 131 | owner, 132 | deposit_amount, 133 | 0, 134 | ); 135 | 136 | // Deposit 137 | env.call_public( 138 | recipient, 139 | Token::at(vault_address).deposit_public_to_public(owner, recipient, deposit_amount, 0), 140 | ); 141 | } 142 | 143 | #[test] 144 | unconstrained fn deposit_public_to_public_on_behalf_of_other_success() { 145 | // Setup with asset token 146 | let (env, vault_address, owner, recipient, asset_address) = utils::setup_with_asset(false); 147 | 148 | // Mint some asset tokens to owner 149 | let deposit_amount: u128 = mint_amount; 150 | env.call_public(owner, Token::at(asset_address).mint_to_public(owner, deposit_amount)); 151 | 152 | // Authorize the vault to use the caller's assets 153 | utils::authorize_transfer_public_to_public( 154 | env, 155 | asset_address, 156 | vault_address, 157 | owner, 158 | deposit_amount, 159 | 0, 160 | ); 161 | 162 | // Authorize the recipient to call deposit_public_to_public 163 | let deposit_public_to_public_call_interface = 164 | Token::at(vault_address).deposit_public_to_public(owner, recipient, deposit_amount, 0); 165 | authwit_cheatcodes::add_public_authwit_from_call_interface( 166 | env, 167 | owner, 168 | recipient, 169 | deposit_public_to_public_call_interface, 170 | ); 171 | 172 | // Deposit 173 | env.call_public( 174 | recipient, 175 | Token::at(vault_address).deposit_public_to_public(owner, recipient, deposit_amount, 0), 176 | ); 177 | 178 | // Check recipient got shares 179 | // At the first deposit 1 share = 1 asset 180 | utils::check_public_balance(env, vault_address, recipient, deposit_amount); 181 | 182 | // Check the total supply got updated 183 | let total_supply = env.view_public(Token::at(vault_address).total_supply()); 184 | assert(total_supply == deposit_amount, "Incorrect shares total supply"); 185 | 186 | // Check vault has assets 187 | utils::check_public_balance(env, asset_address, vault_address, deposit_amount); 188 | } 189 | --------------------------------------------------------------------------------