Skip to content

Commit

Permalink
feat: native fee payment (#4543)
Browse files Browse the repository at this point in the history
This PR adds a gas payment contract.

---------

Co-authored-by: Rahul Kothari <rahul.kothari.201@gmail.com>
Co-authored-by: Mitchell Tracy <mitchell@aztecprotocol.com>
  • Loading branch information
3 people authored Feb 19, 2024
1 parent 5920012 commit 5d4702b
Show file tree
Hide file tree
Showing 20 changed files with 215 additions and 13 deletions.
18 changes: 16 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1406,6 +1419,7 @@ workflows:
- e2e-browser
- e2e-card-game
- e2e-avm-simulator
- e2e-fees
- pxe
- boxes-blank
- boxes-blank-react
Expand Down
1 change: 1 addition & 0 deletions noir-projects/noir-contracts/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use dep::safe_math::SafeU120;
use dep::aztec::context::PublicContext;

pub fn calculate_fee(_context: PublicContext) -> SafeU120 {
SafeU120::new(1)
}
Original file line number Diff line number Diff line change
@@ -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<AztecAddress, PublicState<SafeU120>>,
}

#[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]
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PXE, Tx, TxExecutionRequest } from '@aztec/circuit-types';

import { FeeOptions } from '../account/interface.js';
import { SentTx } from './sent_tx.js';

/**
Expand All @@ -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;
};

/**
Expand Down
7 changes: 4 additions & 3 deletions yarn-project/aztec.js/src/contract/batch_call.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<TxExecutionRequest> {
public async create(opts?: SendMethodOptions): Promise<TxExecutionRequest> {
if (!this.txRequest) {
this.txRequest = await this.wallet.createTxExecutionRequest(this.calls);
this.txRequest = await this.wallet.createTxExecutionRequest(this.calls, opts?.fee);
}
return this.txRequest;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TxExecutionRequest> {
public async create(opts?: SendMethodOptions): Promise<TxExecutionRequest> {
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;
}
Expand Down
5 changes: 2 additions & 3 deletions yarn-project/aztec.js/src/fee/native_fee_payment_method.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
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';

/**
* 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() {}

Expand Down
1 change: 1 addition & 0 deletions yarn-project/end-to-end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
1 change: 1 addition & 0 deletions yarn-project/end-to-end/src/cli_docs_sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ EasyPrivateTokenContractArtifact
EasyPrivateVotingContractArtifact
EcdsaAccountContractArtifact
EscrowContractArtifact
GasTokenContractArtifact
ImportTestContractArtifact
InclusionProofsContractArtifact
LendingContractArtifact
Expand Down
66 changes: 66 additions & 0 deletions yarn-project/end-to-end/src/e2e_fees.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
1 change: 1 addition & 0 deletions yarn-project/end-to-end/src/fixtures/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ async function setupWithRemoteEnvironment(
};
const cheatCodes = CheatCodes.create(config.rpcUrl, pxeClient!);
const teardown = () => Promise.resolve();

return {
aztecNode,
sequencer: undefined,
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/end-to-end/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
{
"path": "../p2p"
},
{
"path": "../protocol-contracts"
},
{
"path": "../pxe"
},
Expand Down
8 changes: 6 additions & 2 deletions yarn-project/protocol-contracts/scripts/copy-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
yarn run -T prettier -w ./src/artifacts
6 changes: 6 additions & 0 deletions yarn-project/protocol-contracts/src/gas-token/artifact.ts
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions yarn-project/protocol-contracts/src/gas-token/index.test.ts
Original file line number Diff line number Diff line change
@@ -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());
});
});
9 changes: 9 additions & 0 deletions yarn-project/protocol-contracts/src/gas-token/index.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion yarn-project/pxe/src/pxe_service/create_pxe_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions yarn-project/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:^"
Expand Down

0 comments on commit 5d4702b

Please sign in to comment.