diff --git a/.circleci/config.yml b/.circleci/config.yml index cae0d54798f..85a41dff953 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -895,6 +895,18 @@ jobs: - run: name: "Test" command: AVM_ENABLED=1 cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_avm_simulator.test.ts + + e2e-fees: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_fees.test.ts + pxe: docker: - image: aztecprotocol/alpine-build-image @@ -916,7 +928,7 @@ jobs: - run: name: "Test" command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=cli_docs_sandbox.test.ts - + e2e-docs-examples: docker: - image: aztecprotocol/alpine-build-image @@ -926,7 +938,7 @@ jobs: - *setup_env - run: name: "Test" - command: AVM_ENABLED=1 cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=docs_examples_test.ts + command: AVM_ENABLED=1 cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=docs_examples_test.ts guides-writing-an-account-contract: docker: @@ -1362,6 +1374,7 @@ workflows: - e2e-browser: *e2e_test - e2e-card-game: *e2e_test - e2e-avm-simulator: *e2e_test + - e2e-fees: *e2e_test - pxe: *e2e_test - cli-docs-sandbox: *e2e_test - e2e-docs-examples: *e2e_test @@ -1406,6 +1419,7 @@ workflows: - e2e-browser - e2e-card-game - e2e-avm-simulator + - e2e-fees - pxe - boxes-blank - boxes-blank-react diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 1659c73b762..216fa3ec0cc 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -14,6 +14,7 @@ members = [ "contracts/easy_private_voting_contract", "contracts/ecdsa_account_contract", "contracts/escrow_contract", + "contracts/gas_token_contract", "contracts/import_test_contract", "contracts/inclusion_proofs_contract", "contracts/lending_contract", diff --git a/noir-projects/noir-contracts/contracts/gas_token_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/gas_token_contract/Nargo.toml new file mode 100644 index 00000000000..6bbdeb817f3 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/gas_token_contract/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gas_token_contract" +authors = [""] +compiler_version = ">=0.18.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } +safe_math = { path = "../../../aztec-nr/safe-math" } +authwit = { path = "../../../aztec-nr/authwit" } diff --git a/noir-projects/noir-contracts/contracts/gas_token_contract/src/fee.nr b/noir-projects/noir-contracts/contracts/gas_token_contract/src/fee.nr new file mode 100644 index 00000000000..708a6b08d23 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/gas_token_contract/src/fee.nr @@ -0,0 +1,6 @@ +use dep::safe_math::SafeU120; +use dep::aztec::context::PublicContext; + +pub fn calculate_fee(_context: PublicContext) -> SafeU120 { + SafeU120::new(1) +} diff --git a/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr new file mode 100644 index 00000000000..6fdf9b46d54 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr @@ -0,0 +1,63 @@ +mod fee; + +contract GasToken { + use dep::aztec::protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress}; + use dep::aztec::{hash::{compute_secret_hash}, state_vars::{public_state::PublicState, map::Map}}; + + use dep::safe_math::SafeU120; + + use crate::fee::calculate_fee; + + struct Storage { + balances: Map>, + } + + #[aztec(private)] + fn constructor() {} + + #[aztec(public)] + fn redeem_bridged_balance(amount: Field) { + // mock + let amount_u120 = SafeU120::new(amount); + let new_balance = storage.balances.at(context.msg_sender()).read().add(amount_u120); + storage.balances.at(context.msg_sender()).write(new_balance); + } + + #[aztec(public)] + fn check_balance(fee_limit: Field) { + let fee_limit_u120 = SafeU120::new(fee_limit); + assert(storage.balances.at(context.msg_sender()).read().ge(fee_limit_u120), "Balance too low"); + } + + #[aztec(public)] + fn pay_fee(fee_limit: Field) -> Field { + let fee_limit_u120 = SafeU120::new(fee_limit); + let fee = calculate_fee(context); + assert(fee.le(fee_limit_u120), "Fee too high"); + + let sender_new_balance = storage.balances.at(context.msg_sender()).read().sub(fee); + storage.balances.at(context.msg_sender()).write(sender_new_balance); + + let recipient_new_balance = storage.balances.at(context.fee_recipient()).read().add(fee); + storage.balances.at(context.fee_recipient()).write(recipient_new_balance); + + let rebate = fee_limit_u120.sub(fee); + rebate.value as Field + } + + // utility function for testing + unconstrained fn balance_of(owner: AztecAddress) -> pub Field { + storage.balances.at(owner).read().value as Field + } + + // TODO: remove this placeholder once https://github.com/AztecProtocol/aztec-packages/issues/2918 is implemented + unconstrained fn compute_note_hash_and_nullifier( + contract_address: AztecAddress, + nonce: Field, + storage_slot: Field, + note_type_id: Field, + serialized_note: [Field; 0] + ) -> pub [Field; 4] { + [0, 0, 0, 0] + } +} diff --git a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts index b0ace57c322..00b7f6e6920 100644 --- a/yarn-project/aztec.js/src/contract/base_contract_interaction.ts +++ b/yarn-project/aztec.js/src/contract/base_contract_interaction.ts @@ -1,5 +1,6 @@ import { PXE, Tx, TxExecutionRequest } from '@aztec/circuit-types'; +import { FeeOptions } from '../account/interface.js'; import { SentTx } from './sent_tx.js'; /** @@ -11,6 +12,11 @@ export type SendMethodOptions = { * Wether to skip the simulation of the public part of the transaction. */ skipPublicSimulation?: boolean; + + /** + * The fee options for the transaction. + */ + fee?: FeeOptions; }; /** diff --git a/yarn-project/aztec.js/src/contract/batch_call.ts b/yarn-project/aztec.js/src/contract/batch_call.ts index 52c3e869de7..4bfd2187952 100644 --- a/yarn-project/aztec.js/src/contract/batch_call.ts +++ b/yarn-project/aztec.js/src/contract/batch_call.ts @@ -1,7 +1,7 @@ import { FunctionCall, TxExecutionRequest } from '@aztec/circuit-types'; import { Wallet } from '../account/index.js'; -import { BaseContractInteraction } from './base_contract_interaction.js'; +import { BaseContractInteraction, SendMethodOptions } from './base_contract_interaction.js'; /** A batch of function calls to be sent as a single transaction through a wallet. */ export class BatchCall extends BaseContractInteraction { @@ -12,11 +12,12 @@ export class BatchCall extends BaseContractInteraction { /** * Create a transaction execution request that represents this batch, encoded and authenticated by the * user's wallet, ready to be simulated. + * @param opts - An optional object containing additional configuration for the transaction. * @returns A Promise that resolves to a transaction instance. */ - public async create(): Promise { + public async create(opts?: SendMethodOptions): Promise { if (!this.txRequest) { - this.txRequest = await this.wallet.createTxExecutionRequest(this.calls); + this.txRequest = await this.wallet.createTxExecutionRequest(this.calls, opts?.fee); } return this.txRequest; } diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index 1e7a30d33d6..f03b3340763 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -38,14 +38,15 @@ export class ContractFunctionInteraction extends BaseContractInteraction { /** * Create a transaction execution request that represents this call, encoded and authenticated by the * user's wallet, ready to be simulated. + * @param opts - An optional object containing additional configuration for the transaction. * @returns A Promise that resolves to a transaction instance. */ - public async create(): Promise { + public async create(opts?: SendMethodOptions): Promise { if (this.functionDao.functionType === FunctionType.UNCONSTRAINED) { throw new Error("Can't call `create` on an unconstrained function."); } if (!this.txRequest) { - this.txRequest = await this.wallet.createTxExecutionRequest([this.request()]); + this.txRequest = await this.wallet.createTxExecutionRequest([this.request()], opts?.fee); } return this.txRequest; } diff --git a/yarn-project/aztec.js/src/fee/native_fee_payment_method.ts b/yarn-project/aztec.js/src/fee/native_fee_payment_method.ts index 1e81c3024ef..0a579704fa9 100644 --- a/yarn-project/aztec.js/src/fee/native_fee_payment_method.ts +++ b/yarn-project/aztec.js/src/fee/native_fee_payment_method.ts @@ -1,8 +1,8 @@ import { FunctionCall } from '@aztec/circuit-types'; import { FunctionData } from '@aztec/circuits.js'; import { FunctionSelector } from '@aztec/foundation/abi'; -import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; +import { GasTokenAddress } from '@aztec/protocol-contracts/gas-token'; import { FeePaymentMethod } from './fee_payment_method.js'; @@ -10,8 +10,7 @@ import { FeePaymentMethod } from './fee_payment_method.js'; * Pay fee directly in the native gas token. */ export class NativeFeePaymentMethod implements FeePaymentMethod { - // TODO(fees) replace this with the address of the gas token when that's deployed. - static #GAS_TOKEN = AztecAddress.ZERO; + static #GAS_TOKEN = GasTokenAddress; constructor() {} diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index 9c2befce623..dbc05dadfcb 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -38,6 +38,7 @@ "@aztec/merkle-tree": "workspace:^", "@aztec/noir-contracts.js": "workspace:^", "@aztec/p2p": "workspace:^", + "@aztec/protocol-contracts": "workspace:^", "@aztec/pxe": "workspace:^", "@aztec/sequencer-client": "workspace:^", "@aztec/types": "workspace:^", diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index b6c21d3a070..0ca801c0611 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -109,6 +109,7 @@ EasyPrivateTokenContractArtifact EasyPrivateVotingContractArtifact EcdsaAccountContractArtifact EscrowContractArtifact +GasTokenContractArtifact ImportTestContractArtifact InclusionProofsContractArtifact LendingContractArtifact diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts new file mode 100644 index 00000000000..f4caf125222 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -0,0 +1,66 @@ +import { AztecAddress, ContractDeployer, NativeFeePaymentMethod } from '@aztec/aztec.js'; +import { GasTokenContract, TokenContract } from '@aztec/noir-contracts.js'; +import { getCanonicalGasToken } from '@aztec/protocol-contracts/gas-token'; + +import { setup } from './fixtures/utils.js'; + +describe('e2e_fees', () => { + let aliceAddress: AztecAddress; + let _bobAddress: AztecAddress; + let sequencerAddress: AztecAddress; + let gasTokenContract: GasTokenContract; + let testContract: TokenContract; + + beforeAll(async () => { + process.env.PXE_URL = ''; + const { accounts, aztecNode, wallet } = await setup(3); + + await aztecNode.setConfig({ + feeRecipient: accounts.at(-1)!.address, + }); + const canonicalGasToken = getCanonicalGasToken(); + const deployer = new ContractDeployer(canonicalGasToken.artifact, wallet); + const { contract } = await deployer + .deploy() + .send({ + contractAddressSalt: canonicalGasToken.instance.salt, + }) + .wait(); + + gasTokenContract = contract as GasTokenContract; + aliceAddress = accounts.at(0)!.address; + _bobAddress = accounts.at(1)!.address; + sequencerAddress = accounts.at(-1)!.address; + + testContract = await TokenContract.deploy(wallet, aliceAddress, 'Test', 'TEST', 1).send().deployed(); + + // Alice gets a balance of 1000 gas token + await gasTokenContract.methods.redeem_bridged_balance(1000).send().wait(); + }, 100_000); + + it('deploys gas token contract at canonical address', () => { + expect(gasTokenContract.address).toEqual(getCanonicalGasToken().address); + }); + + describe('NativeFeePaymentMethod', () => { + it('pays out the expected fee to the sequencer', async () => { + await testContract.methods + .mint_public(aliceAddress, 1000) + .send({ + fee: { + maxFee: 1, + paymentMethod: new NativeFeePaymentMethod(), + }, + }) + .wait(); + + const [sequencerBalance, aliceBalance] = await Promise.all([ + gasTokenContract.methods.balance_of(sequencerAddress).view(), + gasTokenContract.methods.balance_of(aliceAddress).view(), + ]); + + expect(sequencerBalance).toEqual(1n); + expect(aliceBalance).toEqual(999n); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index c3235d235cb..157ae78430a 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -192,6 +192,7 @@ async function setupWithRemoteEnvironment( }; const cheatCodes = CheatCodes.create(config.rpcUrl, pxeClient!); const teardown = () => Promise.resolve(); + return { aztecNode, sequencer: undefined, diff --git a/yarn-project/end-to-end/tsconfig.json b/yarn-project/end-to-end/tsconfig.json index 582567ec99f..62b11720085 100644 --- a/yarn-project/end-to-end/tsconfig.json +++ b/yarn-project/end-to-end/tsconfig.json @@ -48,6 +48,9 @@ { "path": "../p2p" }, + { + "path": "../protocol-contracts" + }, { "path": "../pxe" }, diff --git a/yarn-project/protocol-contracts/scripts/copy-contracts.sh b/yarn-project/protocol-contracts/scripts/copy-contracts.sh index 5c39b4fe786..ff20ea5aea0 100755 --- a/yarn-project/protocol-contracts/scripts/copy-contracts.sh +++ b/yarn-project/protocol-contracts/scripts/copy-contracts.sh @@ -2,10 +2,14 @@ set -euo pipefail mkdir -p ./src/artifacts -contracts=(contract_class_registerer_contract-ContractClassRegisterer contract_instance_deployer_contract-ContractInstanceDeployer) +contracts=( + contract_class_registerer_contract-ContractClassRegisterer + contract_instance_deployer_contract-ContractInstanceDeployer + gas_token_contract-GasToken +) for contract in "${contracts[@]}"; do cp "../noir-contracts.js/artifacts/$contract.json" ./src/artifacts/${contract#*-}.json done -yarn run -T prettier -w ./src/artifacts \ No newline at end of file +yarn run -T prettier -w ./src/artifacts diff --git a/yarn-project/protocol-contracts/src/gas-token/artifact.ts b/yarn-project/protocol-contracts/src/gas-token/artifact.ts new file mode 100644 index 00000000000..042b3701e49 --- /dev/null +++ b/yarn-project/protocol-contracts/src/gas-token/artifact.ts @@ -0,0 +1,6 @@ +import { loadContractArtifact } from '@aztec/types/abi'; +import { NoirCompiledContract } from '@aztec/types/noir'; + +import GasTokenJson from '../artifacts/GasToken.json' assert { type: 'json' }; + +export const GasTokenArtifact = loadContractArtifact(GasTokenJson as NoirCompiledContract); diff --git a/yarn-project/protocol-contracts/src/gas-token/index.test.ts b/yarn-project/protocol-contracts/src/gas-token/index.test.ts new file mode 100644 index 00000000000..5f293ab8fa2 --- /dev/null +++ b/yarn-project/protocol-contracts/src/gas-token/index.test.ts @@ -0,0 +1,8 @@ +import { GasTokenAddress, getCanonicalGasToken } from './index.js'; + +describe('GasToken', () => { + it('returns canonical protocol contract', () => { + const contract = getCanonicalGasToken(); + expect(contract.address.toString()).toEqual(GasTokenAddress.toString()); + }); +}); diff --git a/yarn-project/protocol-contracts/src/gas-token/index.ts b/yarn-project/protocol-contracts/src/gas-token/index.ts new file mode 100644 index 00000000000..b86cb89fba1 --- /dev/null +++ b/yarn-project/protocol-contracts/src/gas-token/index.ts @@ -0,0 +1,9 @@ +import { ProtocolContract, getCanonicalProtocolContract } from '../protocol_contract.js'; +import { GasTokenArtifact } from './artifact.js'; + +/** Returns the canonical deployment of the gas token. */ +export function getCanonicalGasToken(): ProtocolContract { + return getCanonicalProtocolContract(GasTokenArtifact, 1); +} + +export const GasTokenAddress = getCanonicalGasToken().address; diff --git a/yarn-project/pxe/src/pxe_service/create_pxe_service.ts b/yarn-project/pxe/src/pxe_service/create_pxe_service.ts index d3b21dc5299..92f9fbbc179 100644 --- a/yarn-project/pxe/src/pxe_service/create_pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/create_pxe_service.ts @@ -4,6 +4,7 @@ import { TestKeyStore } from '@aztec/key-store'; import { AztecLmdbStore } from '@aztec/kv-store/lmdb'; import { initStoreForRollup } from '@aztec/kv-store/utils'; import { getCanonicalClassRegisterer } from '@aztec/protocol-contracts/class-registerer'; +import { getCanonicalGasToken } from '@aztec/protocol-contracts/gas-token'; import { getCanonicalInstanceDeployer } from '@aztec/protocol-contracts/instance-deployer'; import { join } from 'path'; @@ -45,7 +46,7 @@ export async function createPXEService( const db = new KVPxeDatabase(await initStoreForRollup(AztecLmdbStore.open(pxeDbPath), l1Contracts.rollupAddress)); const server = new PXEService(keyStore, aztecNode, db, config, logSuffix); - await server.addContracts([getCanonicalClassRegisterer(), getCanonicalInstanceDeployer()]); + await server.addContracts([getCanonicalClassRegisterer(), getCanonicalInstanceDeployer(), getCanonicalGasToken()]); await server.start(); return server; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 0dd0c2eeb4b..5e177a93a81 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -387,6 +387,7 @@ __metadata: "@aztec/merkle-tree": "workspace:^" "@aztec/noir-contracts.js": "workspace:^" "@aztec/p2p": "workspace:^" + "@aztec/protocol-contracts": "workspace:^" "@aztec/pxe": "workspace:^" "@aztec/sequencer-client": "workspace:^" "@aztec/types": "workspace:^"