Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: native fee payment #4543

Merged
merged 15 commits into from
Feb 19, 2024
Merged
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
Loading