diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr index 4fc172d63fc..f2e54e26133 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/main.nr @@ -20,7 +20,7 @@ contract TokenBlacklist { use crate::types::{transparent_note::TransparentNote, token_note::TokenNote, balances_map::BalancesMap, roles::UserFlags}; // Changing an address' roles has a certain block delay before it goes into effect. - global CHANGE_ROLES_DELAY_BLOCKS = 5; + global CHANGE_ROLES_DELAY_BLOCKS = 2; #[aztec(storage)] struct Storage { diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts deleted file mode 100644 index 05c2b994a91..00000000000 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts +++ /dev/null @@ -1,1095 +0,0 @@ -import { - type AccountWallet, - AztecAddress, - type DebugLogger, - ExtendedNote, - Fr, - FunctionSelector, - Note, - type TxHash, - type Wallet, - computeAuthWitMessageHash, - computeSecretHash, -} from '@aztec/aztec.js'; -import { TokenBlacklistContract, type TokenContract } from '@aztec/noir-contracts.js'; - -import { jest } from '@jest/globals'; - -import { BITSIZE_TOO_BIG_ERROR, U128_OVERFLOW_ERROR, U128_UNDERFLOW_ERROR } from './fixtures/fixtures.js'; -import { publicDeployAccounts, setup } from './fixtures/utils.js'; -import { TokenSimulator } from './simulators/token_simulator.js'; - -const TIMEOUT = 120_000; - -describe('e2e_blacklist_token_contract', () => { - jest.setTimeout(TIMEOUT); - - let teardown: () => Promise; - let wallets: AccountWallet[]; - let logger: DebugLogger; - - let asset: TokenBlacklistContract; - - let admin: Wallet; - let other: Wallet; - let blacklisted: Wallet; - - let tokenSim: TokenSimulator; - - const DELAY = 5; - - async function mineBlock() { - await asset.methods.get_roles(admin.getAddress()).send().wait(); - } - - async function mineBlocks(amount: number) { - for (let i = 0; i < amount; ++i) { - await mineBlock(); - } - } - - class Role { - private isAdmin = false; - private isMinter = false; - private isBlacklisted = false; - - withAdmin() { - this.isAdmin = true; - return this; - } - - withMinter() { - this.isMinter = true; - return this; - } - - withBlacklisted() { - this.isBlacklisted = true; - return this; - } - - toNoirStruct() { - // We need to use lowercase identifiers as those are what the noir interface expects - // eslint-disable-next-line camelcase - return { is_admin: this.isAdmin, is_minter: this.isMinter, is_blacklisted: this.isBlacklisted }; - } - } - - const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote( - note, - wallets[accountIndex].getAddress(), - asset.address, - TokenBlacklistContract.storage.pending_shields.slot, - TokenBlacklistContract.notes.TransparentNote.id, - txHash, - ); - await wallets[accountIndex].addNote(extendedNote); - }; - - beforeAll(async () => { - ({ teardown, logger, wallets } = await setup(4)); - admin = wallets[0]; - other = wallets[1]; - blacklisted = wallets[2]; - - await publicDeployAccounts(admin, wallets.slice(0, 3)); - - const deployTx = TokenBlacklistContract.deploy(admin, admin.getAddress()).send(); - const receipt = await deployTx.wait(); - asset = receipt.contract; - - await mineBlocks(DELAY); // This gets us past the block of change - - expect(await asset.methods.get_roles(admin.getAddress()).simulate()).toEqual(new Role().withAdmin().toNoirStruct()); - - logger.info(`Token deployed to ${asset.address}`); - tokenSim = new TokenSimulator( - asset as unknown as TokenContract, - logger, - wallets.map(a => a.getAddress()), - ); - - asset.artifact.functions.forEach(fn => { - logger.info( - `Function ${fn.name} has ${fn.bytecode.length} bytes and the selector: ${FunctionSelector.fromNameAndParameters( - fn.name, - fn.parameters, - )}`, - ); - }); - }, 100_000); - - afterAll(() => teardown()); - - afterEach(async () => { - await tokenSim.check(); - }, TIMEOUT); - - describe('Access controlled functions', () => { - it('grant mint permission to the admin', async () => { - const adminMinterRole = new Role().withAdmin().withMinter(); - await asset - .withWallet(admin) - .methods.update_roles(admin.getAddress(), adminMinterRole.toNoirStruct()) - .send() - .wait(); - - await mineBlocks(DELAY); // This gets us past the block of change - - expect(await asset.methods.get_roles(admin.getAddress()).simulate()).toEqual(adminMinterRole.toNoirStruct()); - }); - - it('create a new admin', async () => { - const adminRole = new Role().withAdmin(); - await asset.withWallet(admin).methods.update_roles(other.getAddress(), adminRole.toNoirStruct()).send().wait(); - - await mineBlocks(DELAY); // This gets us past the block of change - - expect(await asset.methods.get_roles(other.getAddress()).simulate()).toEqual(adminRole.toNoirStruct()); - }); - - it('revoke the new admin', async () => { - const noRole = new Role(); - await asset.withWallet(admin).methods.update_roles(other.getAddress(), noRole.toNoirStruct()).send().wait(); - - await mineBlocks(DELAY); // This gets us past the block of change - - expect(await asset.methods.get_roles(other.getAddress()).simulate()).toEqual(noRole.toNoirStruct()); - }); - - it('blacklist account', async () => { - const blacklistRole = new Role().withBlacklisted(); - await asset - .withWallet(admin) - .methods.update_roles(blacklisted.getAddress(), blacklistRole.toNoirStruct()) - .send() - .wait(); - - await mineBlocks(DELAY); // This gets us past the block of change - - expect(await asset.methods.get_roles(blacklisted.getAddress()).simulate()).toEqual(blacklistRole.toNoirStruct()); - }); - - describe('failure cases', () => { - it('set roles from non admin', async () => { - const newRole = new Role().withAdmin().withAdmin(); - await expect( - asset.withWallet(other).methods.update_roles(AztecAddress.random(), newRole.toNoirStruct()).prove(), - ).rejects.toThrow("Assertion failed: caller is not admin 'caller_roles.is_admin'"); - }); - - it('revoke minter from non admin', async () => { - const noRole = new Role(); - await expect( - asset.withWallet(other).methods.update_roles(admin.getAddress(), noRole.toNoirStruct()).prove(), - ).rejects.toThrow("Assertion failed: caller is not admin 'caller_roles.is_admin'"); - }); - }); - }); - - describe('Minting', () => { - describe('Public', () => { - it('as minter', async () => { - const amount = 10000n; - await asset.methods.mint_public(wallets[0].getAddress(), amount).send().wait(); - - tokenSim.mintPublic(wallets[0].getAddress(), amount); - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual( - tokenSim.balanceOfPublic(wallets[0].getAddress()), - ); - expect(await asset.methods.total_supply().simulate()).toEqual(tokenSim.totalSupply); - }); - - describe('failure cases', () => { - it('as non-minter', async () => { - const amount = 10000n; - await expect( - asset.withWallet(wallets[1]).methods.mint_public(wallets[0].getAddress(), amount).prove(), - ).rejects.toThrow('Assertion failed: caller is not minter'); - }); - - it('mint >u128 tokens to overflow', async () => { - const amount = 2n ** 128n; // U128::max() + 1; - await expect(asset.methods.mint_public(wallets[0].getAddress(), amount).prove()).rejects.toThrow( - BITSIZE_TOO_BIG_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPublic(wallets[0].getAddress()); - await expect(asset.methods.mint_public(wallets[0].getAddress(), amount).prove()).rejects.toThrow( - U128_OVERFLOW_ERROR, - ); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPublic(wallets[0].getAddress()); - await expect(asset.methods.mint_public(wallets[1].getAddress(), amount).prove()).rejects.toThrow( - U128_OVERFLOW_ERROR, - ); - }); - - it('mint to blacklisted entity', async () => { - await expect( - asset.withWallet(wallets[1]).methods.mint_public(blacklisted.getAddress(), 1n).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); - }); - }); - }); - - describe('Private', () => { - const secret = Fr.random(); - const amount = 10000n; - let secretHash: Fr; - let txHash: TxHash; - - beforeAll(() => { - secretHash = computeSecretHash(secret); - }); - - describe('Mint flow', () => { - it('mint_private as minter', async () => { - const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); - tokenSim.mintPrivate(amount); - txHash = receipt.txHash; - }); - - it('redeem as recipient', async () => { - await addPendingShieldNoteToPXE(0, amount, secretHash, txHash); - - const receiptClaim = await asset.methods - .redeem_shield(wallets[0].getAddress(), amount, secret) - .send() - .wait({ debug: true }); - - tokenSim.redeemShield(wallets[0].getAddress(), amount); - // 1 note should be created containing `amount` of tokens - const { visibleNotes } = receiptClaim.debugInfo!; - expect(visibleNotes.length).toBe(1); - expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); - }); - }); - - describe('failure cases', () => { - it('try to redeem as recipient (double-spend) [REVERTS]', async () => { - await expect(addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( - 'The note has been destroyed.', - ); - await expect(asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).prove()).rejects.toThrow( - `Assertion failed: Cannot return zero notes`, - ); - }); - - it('mint_private as non-minter', async () => { - await expect(asset.withWallet(wallets[1]).methods.mint_private(amount, secretHash).prove()).rejects.toThrow( - 'Assertion failed: caller is not minter', - ); - }); - - it('mint >u128 tokens to overflow', async () => { - const amount = 2n ** 128n; // U128::max() + 1; - await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(BITSIZE_TOO_BIG_ERROR); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.balanceOfPrivate(wallets[0].getAddress()); - expect(amount).toBeLessThan(2n ** 128n); - await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(U128_OVERFLOW_ERROR); - }); - - it('mint u128', async () => { - const amount = 2n ** 128n - tokenSim.totalSupply; - await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(U128_OVERFLOW_ERROR); - }); - - it('mint and try to redeem at blacklist', async () => { - await expect(asset.methods.redeem_shield(blacklisted.getAddress(), amount, secret).prove()).rejects.toThrow( - "Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'", - ); - }); - }); - }); - }); - - describe('Transfer', () => { - describe('public', () => { - it('transfer less than balance', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).send().wait(); - - tokenSim.transferPublic(wallets[0].getAddress(), wallets[1].getAddress(), amount); - }); - - it('transfer to self', async () => { - const balance = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer_public(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); - - tokenSim.transferPublic(wallets[0].getAddress(), wallets[0].getAddress(), amount); - }); - - it('transfer on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - // docs:start:authwit_public_transfer_example - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - // docs:end:authwit_public_transfer_example - - // Perform the transfer - await action.send().wait(); - - tokenSim.transferPublic(wallets[0].getAddress(), wallets[1].getAddress(), amount); - - // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txReplay = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); - - describe('failure cases', () => { - it('transfer more than balance', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = 0; - await expect( - asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce).prove(), - ).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('transfer on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - const nonce = 1; - await expect( - asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce).prove(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('transfer on behalf of other without "approval"', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - await expect( - asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) - .prove(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('transfer more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - // We need to compute the message we want to sign and add it to the wallet as approved - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); - - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); - const amount = balance0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); - - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); - const amount = balance0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); - - expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it.skip('transfer into account to overflow', () => { - // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not - // a way to get funds enough to overflow. - // Require direct storage manipulation for us to perform a nice explicit case though. - // See https://github.com/AztecProtocol/aztec-packages/issues/1259 - }); - - it('transfer from a blacklisted account', async () => { - await expect( - asset.methods.transfer_public(blacklisted.getAddress(), wallets[0].getAddress(), 1n, 0n).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); - }); - - it('transfer to a blacklisted account', async () => { - await expect( - asset.methods.transfer_public(wallets[0].getAddress(), blacklisted.getAddress(), 1n, 0n).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); - }); - }); - }); - - describe('private', () => { - it('transfer less than balance', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).send().wait(); - tokenSim.transferPrivate(wallets[0].getAddress(), wallets[1].getAddress(), amount); - }); - - it('transfer to self', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - - await asset.methods.transfer(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); - tokenSim.transferPrivate(wallets[0].getAddress(), wallets[0].getAddress(), amount); - }); - - it('transfer on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - // docs:start:authwit_transfer_example - // docs:start:authwit_computeAuthWitMessageHash - const action = asset - .withWallet(wallets[1]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - // docs:end:authwit_computeAuthWitMessageHash - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - // docs:end:authwit_transfer_example - - // Perform the transfer - await action.send().wait(); - tokenSim.transferPrivate(wallets[0].getAddress(), wallets[1].getAddress(), amount); - - // Perform the transfer again, should fail - const txReplay = asset - .withWallet(wallets[1]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); - - describe('failure cases', () => { - it('transfer more than balance', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).prove(), - ).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('transfer on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 1).prove(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('transfer more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const balance1 = await asset.methods.balance_of_private(wallets[1].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - // Perform the transfer - await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); - expect(await asset.methods.balance_of_private(wallets[0].getAddress()).simulate()).toEqual(balance0); - expect(await asset.methods.balance_of_private(wallets[1].getAddress()).simulate()).toEqual(balance1); - }); - - it.skip('transfer into account to overflow', () => { - // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not - // a way to get funds enough to overflow. - // Require direct storage manipulation for us to perform a nice explicit case though. - // See https://github.com/AztecProtocol/aztec-packages/issues/1259 - }); - - it('transfer on behalf of other without approval', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const messageHash = computeAuthWitMessageHash( - wallets[1].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${messageHash.toString()}`, - ); - }); - - it('transfer on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[2]) - .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - wallets[2].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - expect(await asset.methods.balance_of_private(wallets[0].getAddress()).simulate()).toEqual(balance0); - }); - - it('transfer from a blacklisted account', async () => { - await expect( - asset.methods.transfer(blacklisted.getAddress(), wallets[0].getAddress(), 1n, 0).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); - }); - - it('transfer to a blacklisted account', async () => { - await expect( - asset.methods.transfer(wallets[0].getAddress(), blacklisted.getAddress(), 1n, 0).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); - }); - }); - }); - }); - - describe('Shielding (shield + redeem_shield)', () => { - const secret = Fr.random(); - let secretHash: Fr; - - beforeAll(() => { - secretHash = computeSecretHash(secret); - }); - - it('on behalf of self', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub / 2n; - expect(amount).toBeGreaterThan(0n); - - const receipt = await asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).send().wait(); - - tokenSim.shield(wallets[0].getAddress(), amount); - await tokenSim.check(); - - // Redeem it - await addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); - - tokenSim.redeemShield(wallets[0].getAddress(), amount); - }); - - it('on behalf of other', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - const receipt = await action.send().wait(); - - tokenSim.shield(wallets[0].getAddress(), amount); - await tokenSim.check(); - - // Check that replaying the shield should fail! - const txReplay = asset - .withWallet(wallets[1]) - .methods.shield(wallets[0].getAddress(), amount, secretHash, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - - // Redeem it - await addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); - await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); - - tokenSim.redeemShield(wallets[0].getAddress(), amount); - }); - - describe('failure cases', () => { - it('on behalf of self (more than balance)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect(asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).prove()).rejects.toThrow( - U128_UNDERFLOW_ERROR, - ); - }); - - it('on behalf of self (invalid nonce)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect(asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 1).prove()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('on behalf of other (more than balance)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('on behalf of other (wrong designated caller)', async () => { - const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balancePub + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[2]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('on behalf of other (without approval)', async () => { - const balance = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce).prove(), - ).rejects.toThrow(`Assertion failed: Message not authorized by account`); - }); - - it('shielding from blacklisted account', async () => { - await expect( - asset.withWallet(blacklisted).methods.shield(blacklisted.getAddress(), 1n, secretHash, 0).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); - }); - }); - }); - - describe('Unshielding', () => { - it('on behalf of self', async () => { - const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv / 2n; - expect(amount).toBeGreaterThan(0n); - - await asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); - - tokenSim.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount); - }); - - it('on behalf of other', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await action.send().wait(); - tokenSim.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount); - - // Perform the transfer again, should fail - const txReplay = asset - .withWallet(wallets[1]) - .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - // @todo @LHerskind This error is weird? - }); - - describe('failure cases', () => { - it('on behalf of self (more than balance)', async () => { - const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).prove(), - ).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('on behalf of self (invalid nonce)', async () => { - const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv + 1n; - expect(amount).toBeGreaterThan(0n); - - await expect( - asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 1).prove(), - ).rejects.toThrow('Assertion failed: invalid nonce'); - }); - - it('on behalf of other (more than balance)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[1]) - .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('on behalf of other (invalid designated caller)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset - .withWallet(wallets[2]) - .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - wallets[2].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - }); - - it('unshield from blacklisted account', async () => { - await expect( - asset.methods.unshield(blacklisted.getAddress(), wallets[0].getAddress(), 1n, 0).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); - }); - - it('unshield to blacklisted account', async () => { - await expect( - asset.methods.unshield(wallets[0].getAddress(), blacklisted.getAddress(), 1n, 0).prove(), - ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); - }); - }); - }); - - describe('Burn', () => { - describe('public', () => { - it('burn less than balance', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.burn_public(wallets[0].getAddress(), amount, 0).send().wait(); - - tokenSim.burnPublic(wallets[0].getAddress(), amount); - }); - - it('burn on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - const nonce = Fr.random(); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await action.send().wait(); - - tokenSim.burnPublic(wallets[0].getAddress(), amount); - - // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. - const txReplay = asset - .withWallet(wallets[1]) - .methods.burn_public(wallets[0].getAddress(), amount, nonce) - .send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); - - describe('failure cases', () => { - it('burn more than balance', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = 0; - await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow( - U128_UNDERFLOW_ERROR, - ); - }); - - it('burn on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - const nonce = 1; - await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('burn on behalf of other without "approval"', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - await expect( - asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('burn more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); - - await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); - }); - - it('burn on behalf of other, wrong designated caller', async () => { - const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); - const amount = balance0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce); - await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); - - await expect( - asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(), - ).rejects.toThrow('Assertion failed: Message not authorized by account'); - }); - - it('burn from blacklisted account', async () => { - await expect(asset.methods.burn_public(blacklisted.getAddress(), 1n, 0).prove()).rejects.toThrow( - "Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'", - ); - }); - }); - }); - - describe('private', () => { - it('burn less than balance', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - expect(amount).toBeGreaterThan(0n); - await asset.methods.burn(wallets[0].getAddress(), amount, 0).send().wait(); - tokenSim.burnPrivate(wallets[0].getAddress(), amount); - }); - - it('burn on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce).send().wait(); - tokenSim.burnPrivate(wallets[0].getAddress(), amount); - - // Perform the transfer again, should fail - const txReplay = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce).send(); - await expect(txReplay.wait()).rejects.toThrow('Transaction '); - }); - - describe('failure cases', () => { - it('burn more than balance', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - expect(amount).toBeGreaterThan(0n); - await expect(asset.methods.burn(wallets[0].getAddress(), amount, 0).prove()).rejects.toThrow( - 'Assertion failed: Balance too low', - ); - }); - - it('burn on behalf of self with non-zero nonce', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 - 1n; - expect(amount).toBeGreaterThan(0n); - await expect(asset.methods.burn(wallets[0].getAddress(), amount, 1).prove()).rejects.toThrow( - 'Assertion failed: invalid nonce', - ); - }); - - it('burn more than balance on behalf of other', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 + 1n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce); - - // Both wallets are connected to same node and PXE so we could just insert directly - // But doing it in two actions to show the flow. - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[1].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); - }); - - it('burn on behalf of other without approval', async () => { - const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balance0 / 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce); - const messageHash = computeAuthWitMessageHash( - wallets[1].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${messageHash.toString()}`, - ); - }); - - it('on behalf of other (invalid designated caller)', async () => { - const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); - const amount = balancePriv0 + 2n; - const nonce = Fr.random(); - expect(amount).toBeGreaterThan(0n); - - // We need to compute the message we want to sign and add it to the wallet as approved - const action = asset.withWallet(wallets[2]).methods.burn(wallets[0].getAddress(), amount, nonce); - const expectedMessageHash = computeAuthWitMessageHash( - wallets[2].getAddress(), - wallets[0].getChainId(), - wallets[0].getVersion(), - action.request(), - ); - - const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); - await wallets[2].addAuthWitness(witness); - - await expect(action.prove()).rejects.toThrow( - `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, - ); - }); - - it('burn from blacklisted account', async () => { - await expect(asset.methods.burn(blacklisted.getAddress(), 1n, 0).prove()).rejects.toThrow( - "Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'", - ); - }); - }); - }); - }); -}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts new file mode 100644 index 00000000000..4c1400caffe --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/access_control.test.ts @@ -0,0 +1,86 @@ +import { AztecAddress } from '@aztec/aztec.js'; + +import { BlacklistTokenContractTest, Role } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract access control', () => { + const t = new BlacklistTokenContractTest('access_control'); + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.setup(); + }); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + it('grant mint permission to the admin', async () => { + const adminMinterRole = new Role().withAdmin().withMinter(); + await t.asset + .withWallet(t.admin) + .methods.update_roles(t.admin.getAddress(), adminMinterRole.toNoirStruct()) + .send() + .wait(); + + await t.mineBlocks(); // This gets us past the block of change + + expect(await t.asset.methods.get_roles(t.admin.getAddress()).simulate()).toEqual(adminMinterRole.toNoirStruct()); + }); + + it('create a new admin', async () => { + const adminRole = new Role().withAdmin(); + await t.asset + .withWallet(t.admin) + .methods.update_roles(t.other.getAddress(), adminRole.toNoirStruct()) + .send() + .wait(); + + await t.mineBlocks(); // This gets us past the block of change + + expect(await t.asset.methods.get_roles(t.other.getAddress()).simulate()).toEqual(adminRole.toNoirStruct()); + }); + + it('revoke the new admin', async () => { + const noRole = new Role(); + await t.asset.withWallet(t.admin).methods.update_roles(t.other.getAddress(), noRole.toNoirStruct()).send().wait(); + + await t.mineBlocks(); // This gets us past the block of change + + expect(await t.asset.methods.get_roles(t.other.getAddress()).simulate()).toEqual(noRole.toNoirStruct()); + }); + + it('blacklist account', async () => { + const blacklistRole = new Role().withBlacklisted(); + await t.asset + .withWallet(t.admin) + .methods.update_roles(t.blacklisted.getAddress(), blacklistRole.toNoirStruct()) + .send() + .wait(); + + await t.mineBlocks(); // This gets us past the block of change + + expect(await t.asset.methods.get_roles(t.blacklisted.getAddress()).simulate()).toEqual( + blacklistRole.toNoirStruct(), + ); + }); + + describe('failure cases', () => { + it('set roles from non admin', async () => { + const newRole = new Role().withAdmin().withAdmin(); + await expect( + t.asset.withWallet(t.other).methods.update_roles(AztecAddress.random(), newRole.toNoirStruct()).prove(), + ).rejects.toThrow("Assertion failed: caller is not admin 'caller_roles.is_admin'"); + }); + + it('revoke minter from non admin', async () => { + const noRole = new Role(); + await expect( + t.asset.withWallet(t.other).methods.update_roles(t.admin.getAddress(), noRole.toNoirStruct()).prove(), + ).rejects.toThrow("Assertion failed: caller is not admin 'caller_roles.is_admin'"); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts new file mode 100644 index 00000000000..c0872969c69 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/blacklist_token_contract_test.ts @@ -0,0 +1,234 @@ +import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { + type AccountWallet, + type CompleteAddress, + type DebugLogger, + ExtendedNote, + Fr, + Note, + type TxHash, + computeSecretHash, + createDebugLogger, +} from '@aztec/aztec.js'; +import { DocsExampleContract, TokenBlacklistContract, type TokenContract } from '@aztec/noir-contracts.js'; + +import { + SnapshotManager, + type SubsystemsContext, + addAccounts, + publicDeployAccounts, +} from '../fixtures/snapshot_manager.js'; +import { TokenSimulator } from '../simulators/token_simulator.js'; + +const { E2E_DATA_PATH: dataPath } = process.env; + +export class Role { + private isAdmin = false; + private isMinter = false; + private isBlacklisted = false; + + withAdmin() { + this.isAdmin = true; + return this; + } + + withMinter() { + this.isMinter = true; + return this; + } + + withBlacklisted() { + this.isBlacklisted = true; + return this; + } + + toNoirStruct() { + // We need to use lowercase identifiers as those are what the noir interface expects + // eslint-disable-next-line camelcase + return { is_admin: this.isAdmin, is_minter: this.isMinter, is_blacklisted: this.isBlacklisted }; + } +} + +export class BlacklistTokenContractTest { + // A low delay is really poor ux, but we need to keep it low for the tests to run "quickly". + // This value MUST match the same value that we have in the contract + static DELAY = 2; + + private snapshotManager: SnapshotManager; + logger: DebugLogger; + wallets: AccountWallet[] = []; + accounts: CompleteAddress[] = []; + asset!: TokenBlacklistContract; + tokenSim!: TokenSimulator; + badAccount!: DocsExampleContract; + + admin!: AccountWallet; + other!: AccountWallet; + blacklisted!: AccountWallet; + + constructor(testName: string) { + this.logger = createDebugLogger(`aztec:e2e_blacklist_token_contract:${testName}`); + this.snapshotManager = new SnapshotManager(`e2e_blacklist_token_contract/${testName}`, dataPath); + } + + async mineBlocks(amount: number = BlacklistTokenContractTest.DELAY) { + for (let i = 0; i < amount; ++i) { + await this.asset.methods.get_roles(this.admin.getAddress()).send().wait(); + } + } + + /** + * Adds two state shifts to snapshot manager. + * 1. Add 3 accounts. + * 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.admin = this.wallets[0]; + this.other = this.wallets[1]; + this.blacklisted = this.wallets[2]; + // this.accounts = this.wallets.map(a => a.getCompleteAddress()); + this.accounts = await pxe.getRegisteredAccounts(); + this.wallets.forEach((w, i) => this.logger.verbose(`Wallet ${i} address: ${w.getAddress()}`)); + this.accounts.forEach((w, i) => this.logger.verbose(`Account ${i} address: ${w.address}`)); + }); + + await this.snapshotManager.snapshot( + 'e2e_blacklist_token_contract', + async () => { + // Create the token contract state. + // Move this account thing to addAccounts above? + this.logger.verbose(`Public deploy accounts...`); + await publicDeployAccounts(this.wallets[0], this.accounts.slice(0, 3)); + + this.logger.verbose(`Deploying TokenContract...`); + this.asset = await TokenBlacklistContract.deploy(this.admin, this.admin.getAddress()).send().deployed(); + this.logger.verbose(`Token deployed to ${this.asset.address}`); + + this.logger.verbose(`Deploying bad account...`); + this.badAccount = await DocsExampleContract.deploy(this.wallets[0]).send().deployed(); + this.logger.verbose(`Deployed to ${this.badAccount.address}.`); + + await this.mineBlocks(); + + return { tokenContractAddress: this.asset.address, badAccountAddress: this.badAccount.address }; + }, + async ({ tokenContractAddress, badAccountAddress }) => { + // Restore the token contract state. + this.asset = await TokenBlacklistContract.at(tokenContractAddress, this.wallets[0]); + this.logger.verbose(`Token contract address: ${this.asset.address}`); + + this.tokenSim = new TokenSimulator( + this.asset as unknown as TokenContract, + this.logger, + this.accounts.map(a => a.address), + ); + + this.badAccount = await DocsExampleContract.at(badAccountAddress, this.wallets[0]); + this.logger.verbose(`Bad account address: ${this.badAccount.address}`); + + expect(await this.asset.methods.get_roles(this.admin.getAddress()).simulate()).toEqual( + new Role().withAdmin().toNoirStruct(), + ); + }, + ); + } + + async setup() { + await this.snapshotManager.setup(); + } + + snapshot = ( + name: string, + apply: (context: SubsystemsContext) => Promise, + restore: (snapshotData: T, context: SubsystemsContext) => Promise = () => Promise.resolve(), + ): Promise => this.snapshotManager.snapshot(name, apply, restore); + + async teardown() { + await this.snapshotManager.teardown(); + } + + async addPendingShieldNoteToPXE(accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) { + const note = new Note([new Fr(amount), secretHash]); + const extendedNote = new ExtendedNote( + note, + this.accounts[accountIndex].address, + this.asset.address, + TokenBlacklistContract.storage.pending_shields.slot, + TokenBlacklistContract.notes.TransparentNote.id, + txHash, + ); + await this.wallets[accountIndex].addNote(extendedNote); + } + + async applyMintSnapshot() { + await this.snapshotManager.snapshot( + 'mint', + async () => { + const { asset, accounts } = this; + const amount = 10000n; + + const adminMinterRole = new Role().withAdmin().withMinter(); + await this.asset + .withWallet(this.admin) + .methods.update_roles(this.admin.getAddress(), adminMinterRole.toNoirStruct()) + .send() + .wait(); + + const blacklistRole = new Role().withBlacklisted(); + await this.asset + .withWallet(this.admin) + .methods.update_roles(this.blacklisted.getAddress(), blacklistRole.toNoirStruct()) + .send() + .wait(); + + await this.mineBlocks(); // This gets us past the block of change + + expect(await this.asset.methods.get_roles(this.admin.getAddress()).simulate()).toEqual( + adminMinterRole.toNoirStruct(), + ); + + this.logger.verbose(`Minting ${amount} publicly...`); + await asset.methods.mint_public(accounts[0].address, amount).send().wait(); + + this.logger.verbose(`Minting ${amount} privately...`); + const secret = Fr.random(); + const secretHash = computeSecretHash(secret); + const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); + + await this.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); + await txClaim.wait({ debug: true }); + this.logger.verbose(`Minting complete.`); + + return { amount }; + }, + async ({ amount }) => { + const { + asset, + accounts: [{ address }], + tokenSim, + } = this; + tokenSim.mintPublic(address, amount); + + const publicBalance = await asset.methods.balance_of_public(address).simulate(); + this.logger.verbose(`Public balance of wallet 0: ${publicBalance}`); + expect(publicBalance).toEqual(this.tokenSim.balanceOfPublic(address)); + + tokenSim.mintPrivate(amount); + tokenSim.redeemShield(address, amount); + const privateBalance = await asset.methods.balance_of_private(address).simulate(); + this.logger.verbose(`Private balance of wallet 0: ${privateBalance}`); + expect(privateBalance).toEqual(tokenSim.balanceOfPrivate(address)); + + const totalSupply = await asset.methods.total_supply().simulate(); + this.logger.verbose(`Total supply: ${totalSupply}`); + expect(totalSupply).toEqual(tokenSim.totalSupply); + + return Promise.resolve(); + }, + ); + } +} diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts new file mode 100644 index 00000000000..ffe828172f4 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/burn.test.ts @@ -0,0 +1,236 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; +import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract burn', () => { + const t = new BlacklistTokenContractTest('burn'); + let { asset, tokenSim, wallets, blacklisted } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + // Beware that we are adding the admin as minter here, which is very slow because it needs multiple blocks. + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, tokenSim, wallets, blacklisted } = t); + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + describe('public', () => { + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.burn_public(wallets[0].getAddress(), amount, 0).send().wait(); + + tokenSim.burnPublic(wallets[0].getAddress(), amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + + await action.send().wait(); + + tokenSim.burnPublic(wallets[0].getAddress(), amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = 0; + await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow( + U128_UNDERFLOW_ERROR, + ); + }); + + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + const nonce = 1; + await expect(asset.methods.burn_public(wallets[0].getAddress(), amount, nonce).prove()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + + await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); + + it('burn on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); + + await expect( + asset.withWallet(wallets[1]).methods.burn_public(wallets[0].getAddress(), amount, nonce).prove(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('burn from blacklisted account', async () => { + await expect(asset.methods.burn_public(blacklisted.getAddress(), 1n, 0).prove()).rejects.toThrow( + "Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'", + ); + }); + }); + }); + + describe('private', () => { + it('burn less than balance', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.burn(wallets[0].getAddress(), amount, 0).send().wait(); + tokenSim.burnPrivate(wallets[0].getAddress(), amount); + }); + + it('burn on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[1].addAuthWitness(witness); + + await asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce).send().wait(); + tokenSim.burnPrivate(wallets[0].getAddress(), amount); + + // Perform the transfer again, should fail + const txReplay = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce).send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('burn more than balance', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn(wallets[0].getAddress(), amount, 0).prove()).rejects.toThrow( + 'Assertion failed: Balance too low', + ); + }); + + it('burn on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + await expect(asset.methods.burn(wallets[0].getAddress(), amount, 1).prove()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('burn more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[1].addAuthWitness(witness); + + await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('burn on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.burn(wallets[0].getAddress(), amount, nonce); + const messageHash = computeAuthWitMessageHash( + wallets[1].getAddress(), + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await expect(action.prove()).rejects.toThrow(`Unknown auth witness for message hash ${messageHash.toString()}`); + }); + + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[2]).methods.burn(wallets[0].getAddress(), amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + wallets[2].getAddress(), + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.prove()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + }); + + it('burn from blacklisted account', async () => { + await expect(asset.methods.burn(blacklisted.getAddress(), 1n, 0).prove()).rejects.toThrow( + "Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'", + ); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts new file mode 100644 index 00000000000..b003195c393 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/minting.test.ts @@ -0,0 +1,147 @@ +import { Fr, type TxHash, computeSecretHash } from '@aztec/aztec.js'; + +import { BITSIZE_TOO_BIG_ERROR, U128_OVERFLOW_ERROR } from '../fixtures/index.js'; +import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract mint', () => { + const t = new BlacklistTokenContractTest('mint'); + let { asset, tokenSim, wallets, blacklisted } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + // Beware that we are adding the admin as minter here, which is very slow because it needs multiple blocks. + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, tokenSim, wallets, blacklisted } = t); + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + beforeEach(async () => { + await t.tokenSim.check(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + describe('Public', () => { + it('as minter', async () => { + const amount = 10000n; + tokenSim.mintPublic(wallets[0].getAddress(), amount); + await asset.methods.mint_public(wallets[0].getAddress(), amount).send().wait(); + }); + + describe('failure cases', () => { + it('as non-minter', async () => { + const amount = 10000n; + await expect( + asset.withWallet(wallets[1]).methods.mint_public(wallets[0].getAddress(), amount).prove(), + ).rejects.toThrow('Assertion failed: caller is not minter'); + }); + + it('mint >u128 tokens to overflow', async () => { + const amount = 2n ** 128n; // U128::max() + 1; + await expect(asset.methods.mint_public(wallets[0].getAddress(), amount).prove()).rejects.toThrow( + BITSIZE_TOO_BIG_ERROR, + ); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPublic(wallets[0].getAddress()); + await expect(asset.methods.mint_public(wallets[0].getAddress(), amount).prove()).rejects.toThrow( + U128_OVERFLOW_ERROR, + ); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPublic(wallets[0].getAddress()); + await expect(asset.methods.mint_public(wallets[1].getAddress(), amount).prove()).rejects.toThrow( + U128_OVERFLOW_ERROR, + ); + }); + + it('mint to blacklisted entity', async () => { + await expect( + asset.withWallet(wallets[1]).methods.mint_public(blacklisted.getAddress(), 1n).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); + }); + }); + }); + + describe('Private', () => { + const secret = Fr.random(); + const amount = 10000n; + let secretHash: Fr; + let txHash: TxHash; + + beforeAll(() => { + secretHash = computeSecretHash(secret); + }); + + describe('Mint flow', () => { + it('mint_private as minter', async () => { + const receipt = await asset.methods.mint_private(amount, secretHash).send().wait(); + tokenSim.mintPrivate(amount); + txHash = receipt.txHash; + }); + + it('redeem as recipient', async () => { + await t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash); + + const receiptClaim = await asset.methods + .redeem_shield(wallets[0].getAddress(), amount, secret) + .send() + .wait({ debug: true }); + + tokenSim.redeemShield(wallets[0].getAddress(), amount); + // 1 note should be created containing `amount` of tokens + const { visibleNotes } = receiptClaim.debugInfo!; + expect(visibleNotes.length).toBe(1); + expect(visibleNotes[0].note.items[0].toBigInt()).toBe(amount); + }); + }); + + describe('failure cases', () => { + it('try to redeem as recipient (double-spend) [REVERTS]', async () => { + await expect(t.addPendingShieldNoteToPXE(0, amount, secretHash, txHash)).rejects.toThrow( + 'The note has been destroyed.', + ); + await expect(asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).prove()).rejects.toThrow( + `Assertion failed: Cannot return zero notes`, + ); + }); + + it('mint_private as non-minter', async () => { + await expect(asset.withWallet(wallets[1]).methods.mint_private(amount, secretHash).prove()).rejects.toThrow( + 'Assertion failed: caller is not minter', + ); + }); + + it('mint >u128 tokens to overflow', async () => { + const amount = 2n ** 128n; // U128::max() + 1; + await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(BITSIZE_TOO_BIG_ERROR); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.balanceOfPrivate(wallets[0].getAddress()); + expect(amount).toBeLessThan(2n ** 128n); + await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(U128_OVERFLOW_ERROR); + }); + + it('mint u128', async () => { + const amount = 2n ** 128n - tokenSim.totalSupply; + await expect(asset.methods.mint_private(amount, secretHash).prove()).rejects.toThrow(U128_OVERFLOW_ERROR); + }); + + it('mint and try to redeem at blacklist', async () => { + await expect(asset.methods.redeem_shield(blacklisted.getAddress(), amount, secret).prove()).rejects.toThrow( + "Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'", + ); + }); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts new file mode 100644 index 00000000000..0414e3c5742 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/shielding.test.ts @@ -0,0 +1,143 @@ +import { Fr, computeSecretHash } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; +import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract shield + redeem_shield', () => { + const t = new BlacklistTokenContractTest('shield'); + let { asset, tokenSim, wallets, blacklisted } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + await t.applyMintSnapshot(); // Beware that we are adding the admin as minter here + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, tokenSim, wallets, blacklisted } = t); + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + const secret = Fr.random(); + let secretHash: Fr; + + beforeAll(() => { + secretHash = computeSecretHash(secret); + }); + + it('on behalf of self', async () => { + const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balancePub / 2n; + expect(amount).toBeGreaterThan(0n); + + const receipt = await asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).send().wait(); + + tokenSim.shield(wallets[0].getAddress(), amount); + await tokenSim.check(); + + // Redeem it + await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); + + tokenSim.redeemShield(wallets[0].getAddress(), amount); + }); + + it('on behalf of other', async () => { + const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balancePub / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + + const receipt = await action.send().wait(); + + tokenSim.shield(wallets[0].getAddress(), amount); + await tokenSim.check(); + + // Check that replaying the shield should fail! + const txReplay = asset + .withWallet(wallets[1]) + .methods.shield(wallets[0].getAddress(), amount, secretHash, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + + // Redeem it + await t.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); + await asset.methods.redeem_shield(wallets[0].getAddress(), amount, secret).send().wait(); + + tokenSim.redeemShield(wallets[0].getAddress(), amount); + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect(asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 0).prove()).rejects.toThrow( + U128_UNDERFLOW_ERROR, + ); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balancePub + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect(asset.methods.shield(wallets[0].getAddress(), amount, secretHash, 1).prove()).rejects.toThrow( + 'Assertion failed: invalid nonce', + ); + }); + + it('on behalf of other (more than balance)', async () => { + const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + + await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); + + it('on behalf of other (wrong designated caller)', async () => { + const balancePub = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balancePub + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset.withWallet(wallets[2]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + + await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('on behalf of other (without approval)', async () => { + const balance = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.withWallet(wallets[1]).methods.shield(wallets[0].getAddress(), amount, secretHash, nonce).prove(), + ).rejects.toThrow(`Assertion failed: Message not authorized by account`); + }); + + it('shielding from blacklisted account', async () => { + await expect( + asset.withWallet(blacklisted).methods.shield(blacklisted.getAddress(), 1n, secretHash, 0).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts new file mode 100644 index 00000000000..6ac2f3b6af1 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_private.test.ts @@ -0,0 +1,182 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract transfer private', () => { + const t = new BlacklistTokenContractTest('transfer_private'); + let { asset, tokenSim, wallets, blacklisted } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + // Beware that we are adding the admin as minter here, which is very slow because it needs multiple blocks. + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, tokenSim, wallets, blacklisted } = t); + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).send().wait(); + tokenSim.transferPrivate(wallets[0].getAddress(), wallets[1].getAddress(), amount); + }); + + it('transfer to self', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + + await asset.methods.transfer(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); + tokenSim.transferPrivate(wallets[0].getAddress(), wallets[0].getAddress(), amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + // docs:start:authwit_transfer_example + // docs:start:authwit_computeAuthWitMessageHash + const action = asset + .withWallet(wallets[1]) + .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + // docs:end:authwit_computeAuthWitMessageHash + + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[1].addAuthWitness(witness); + // docs:end:authwit_transfer_example + + // Perform the transfer + await action.send().wait(); + tokenSim.transferPrivate(wallets[0].getAddress(), wallets[1].getAddress(), amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).prove(), + ).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 - 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, 1).prove(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const balance1 = await asset.methods.balance_of_private(wallets[1].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[1].addAuthWitness(witness); + + // Perform the transfer + await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); + expect(await asset.methods.balance_of_private(wallets[0].getAddress()).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_private(wallets[1].getAddress()).simulate()).toEqual(balance1); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + + it('transfer on behalf of other without approval', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + const messageHash = computeAuthWitMessageHash( + wallets[1].getAddress(), + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + await expect(action.prove()).rejects.toThrow(`Unknown auth witness for message hash ${messageHash.toString()}`); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[2]) + .methods.transfer(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + wallets[2].getAddress(), + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.prove()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + expect(await asset.methods.balance_of_private(wallets[0].getAddress()).simulate()).toEqual(balance0); + }); + + it('transfer from a blacklisted account', async () => { + await expect( + asset.methods.transfer(blacklisted.getAddress(), wallets[0].getAddress(), 1n, 0).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); + }); + + it('transfer to a blacklisted account', async () => { + await expect( + asset.methods.transfer(wallets[0].getAddress(), blacklisted.getAddress(), 1n, 0).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts new file mode 100644 index 00000000000..bc452f28f11 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/transfer_public.test.ts @@ -0,0 +1,184 @@ +import { Fr } from '@aztec/aztec.js'; + +import { U128_UNDERFLOW_ERROR } from '../fixtures/index.js'; +import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract transfer public', () => { + const t = new BlacklistTokenContractTest('transfer_public'); + let { asset, tokenSim, wallets, blacklisted } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + // Beware that we are adding the admin as minter here, which is very slow because it needs multiple blocks. + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, tokenSim, wallets, blacklisted } = t); + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + it('transfer less than balance', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, 0).send().wait(); + + tokenSim.transferPublic(wallets[0].getAddress(), wallets[1].getAddress(), amount); + }); + + it('transfer to self', async () => { + const balance = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance / 2n; + expect(amount).toBeGreaterThan(0n); + await asset.methods.transfer_public(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); + + tokenSim.transferPublic(wallets[0].getAddress(), wallets[0].getAddress(), amount); + }); + + it('transfer on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 / 2n; + expect(amount).toBeGreaterThan(0n); + const nonce = Fr.random(); + + // docs:start:authwit_public_transfer_example + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + // docs:end:authwit_public_transfer_example + + // Perform the transfer + await action.send().wait(); + + tokenSim.transferPublic(wallets[0].getAddress(), wallets[1].getAddress(), amount); + + // Check that the message hash is no longer valid. Need to try to send since nullifiers are handled by sequencer. + const txReplay = asset + .withWallet(wallets[1]) + .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + }); + + describe('failure cases', () => { + it('transfer more than balance', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = 0; + await expect( + asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce).prove(), + ).rejects.toThrow(U128_UNDERFLOW_ERROR); + }); + + it('transfer on behalf of self with non-zero nonce', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 - 1n; + const nonce = 1; + await expect( + asset.methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce).prove(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('transfer on behalf of other without "approval"', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + await expect( + asset + .withWallet(wallets[1]) + .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) + .prove(), + ).rejects.toThrow('Assertion failed: Message not authorized by account'); + }); + + it('transfer more than balance on behalf of other', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); + const amount = balance0 + 1n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + + // We need to compute the message we want to sign and add it to the wallet as approved + await wallets[0].setPublicAuthWit({ caller: wallets[1].getAddress(), action }, true).send().wait(); + + // Perform the transfer + await expect(action.prove()).rejects.toThrow(U128_UNDERFLOW_ERROR); + + expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + + await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); + + // Perform the transfer + await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); + + expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); + }); + + it('transfer on behalf of other, wrong designated caller', async () => { + const balance0 = await asset.methods.balance_of_public(wallets[0].getAddress()).simulate(); + const balance1 = await asset.methods.balance_of_public(wallets[1].getAddress()).simulate(); + const amount = balance0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.transfer_public(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + await wallets[0].setPublicAuthWit({ caller: wallets[0].getAddress(), action }, true).send().wait(); + + // Perform the transfer + await expect(action.prove()).rejects.toThrow('Assertion failed: Message not authorized by account'); + + expect(await asset.methods.balance_of_public(wallets[0].getAddress()).simulate()).toEqual(balance0); + expect(await asset.methods.balance_of_public(wallets[1].getAddress()).simulate()).toEqual(balance1); + }); + + it.skip('transfer into account to overflow', () => { + // This should already be covered by the mint case earlier. e.g., since we cannot mint to overflow, there is not + // a way to get funds enough to overflow. + // Require direct storage manipulation for us to perform a nice explicit case though. + // See https://github.com/AztecProtocol/aztec-packages/issues/1259 + }); + + it('transfer from a blacklisted account', async () => { + await expect( + asset.methods.transfer_public(blacklisted.getAddress(), wallets[0].getAddress(), 1n, 0n).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); + }); + + it('transfer to a blacklisted account', async () => { + await expect( + asset.methods.transfer_public(wallets[0].getAddress(), blacklisted.getAddress(), 1n, 0n).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts new file mode 100644 index 00000000000..4a26ad118b9 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract/unshielding.test.ts @@ -0,0 +1,143 @@ +import { Fr, computeAuthWitMessageHash } from '@aztec/aztec.js'; + +import { BlacklistTokenContractTest } from './blacklist_token_contract_test.js'; + +describe('e2e_blacklist_token_contract unshielding', () => { + const t = new BlacklistTokenContractTest('unshielding'); + let { asset, tokenSim, wallets, blacklisted } = t; + + beforeAll(async () => { + await t.applyBaseSnapshots(); + // Beware that we are adding the admin as minter here, which is very slow because it needs multiple blocks. + await t.applyMintSnapshot(); + await t.setup(); + // Have to destructure again to ensure we have latest refs. + ({ asset, tokenSim, wallets, blacklisted } = t); + }, 200_000); + + afterAll(async () => { + await t.teardown(); + }); + + afterEach(async () => { + await t.tokenSim.check(); + }); + + it('on behalf of self', async () => { + const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv / 2n; + expect(amount).toBeGreaterThan(0n); + + await asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).send().wait(); + + tokenSim.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount); + }); + + it('on behalf of other', async () => { + const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv0 / 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[1].addAuthWitness(witness); + + await action.send().wait(); + tokenSim.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount); + + // Perform the transfer again, should fail + const txReplay = asset + .withWallet(wallets[1]) + .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce) + .send(); + await expect(txReplay.wait()).rejects.toThrow('Transaction '); + // @todo @LHerskind This error is weird? + }); + + describe('failure cases', () => { + it('on behalf of self (more than balance)', async () => { + const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 0).prove(), + ).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('on behalf of self (invalid nonce)', async () => { + const balancePriv = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv + 1n; + expect(amount).toBeGreaterThan(0n); + + await expect( + asset.methods.unshield(wallets[0].getAddress(), wallets[0].getAddress(), amount, 1).prove(), + ).rejects.toThrow('Assertion failed: invalid nonce'); + }); + + it('on behalf of other (more than balance)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[1]) + .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[1].addAuthWitness(witness); + + await expect(action.prove()).rejects.toThrow('Assertion failed: Balance too low'); + }); + + it('on behalf of other (invalid designated caller)', async () => { + const balancePriv0 = await asset.methods.balance_of_private(wallets[0].getAddress()).simulate(); + const amount = balancePriv0 + 2n; + const nonce = Fr.random(); + expect(amount).toBeGreaterThan(0n); + + // We need to compute the message we want to sign and add it to the wallet as approved + const action = asset + .withWallet(wallets[2]) + .methods.unshield(wallets[0].getAddress(), wallets[1].getAddress(), amount, nonce); + const expectedMessageHash = computeAuthWitMessageHash( + wallets[2].getAddress(), + wallets[0].getChainId(), + wallets[0].getVersion(), + action.request(), + ); + + // Both wallets are connected to same node and PXE so we could just insert directly + // But doing it in two actions to show the flow. + const witness = await wallets[0].createAuthWit({ caller: wallets[1].getAddress(), action }); + await wallets[2].addAuthWitness(witness); + + await expect(action.prove()).rejects.toThrow( + `Unknown auth witness for message hash ${expectedMessageHash.toString()}`, + ); + }); + + it('unshield from blacklisted account', async () => { + await expect( + asset.methods.unshield(blacklisted.getAddress(), wallets[0].getAddress(), 1n, 0).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Sender '!from_roles.is_blacklisted'"); + }); + + it('unshield to blacklisted account', async () => { + await expect( + asset.methods.unshield(wallets[0].getAddress(), blacklisted.getAddress(), 1n, 0).prove(), + ).rejects.toThrow("Assertion failed: Blacklisted: Recipient '!to_roles.is_blacklisted'"); + }); + }); +}); diff --git a/yarn-project/end-to-end/src/simulators/token_simulator.ts b/yarn-project/end-to-end/src/simulators/token_simulator.ts index eacb27e201d..1cfd44eb6f4 100644 --- a/yarn-project/end-to-end/src/simulators/token_simulator.ts +++ b/yarn-project/end-to-end/src/simulators/token_simulator.ts @@ -3,8 +3,8 @@ import { type AztecAddress, type DebugLogger } from '@aztec/aztec.js'; import { type TokenContract } from '@aztec/noir-contracts.js/Token'; export class TokenSimulator { - private balancesPrivate: Map = new Map(); - private balancePublic: Map = new Map(); + private balancesPrivate: Map = new Map(); + private balancePublic: Map = new Map(); public totalSupply: bigint = 0n; constructor(protected token: TokenContract, protected logger: DebugLogger, protected accounts: AztecAddress[]) {} @@ -15,69 +15,69 @@ export class TokenSimulator { public mintPublic(to: AztecAddress, amount: bigint) { this.totalSupply += amount; - const value = this.balancePublic.get(to) || 0n; - this.balancePublic.set(to, value + amount); + const value = this.balancePublic.get(to.toString()) || 0n; + this.balancePublic.set(to.toString(), value + amount); } public transferPublic(from: AztecAddress, to: AztecAddress, amount: bigint) { - const fromBalance = this.balancePublic.get(from) || 0n; - this.balancePublic.set(from, fromBalance - amount); + const fromBalance = this.balancePublic.get(from.toString()) || 0n; + this.balancePublic.set(from.toString(), fromBalance - amount); expect(fromBalance).toBeGreaterThanOrEqual(amount); - const toBalance = this.balancePublic.get(to) || 0n; - this.balancePublic.set(to, toBalance + amount); + const toBalance = this.balancePublic.get(to.toString()) || 0n; + this.balancePublic.set(to.toString(), toBalance + amount); } public transferPrivate(from: AztecAddress, to: AztecAddress, amount: bigint) { - const fromBalance = this.balancesPrivate.get(from) || 0n; + const fromBalance = this.balancesPrivate.get(from.toString()) || 0n; expect(fromBalance).toBeGreaterThanOrEqual(amount); - this.balancesPrivate.set(from, fromBalance - amount); + this.balancesPrivate.set(from.toString(), fromBalance - amount); - const toBalance = this.balancesPrivate.get(to) || 0n; - this.balancesPrivate.set(to, toBalance + amount); + const toBalance = this.balancesPrivate.get(to.toString()) || 0n; + this.balancesPrivate.set(to.toString(), toBalance + amount); } public shield(from: AztecAddress, amount: bigint) { - const fromBalance = this.balancePublic.get(from) || 0n; + const fromBalance = this.balancePublic.get(from.toString()) || 0n; expect(fromBalance).toBeGreaterThanOrEqual(amount); - this.balancePublic.set(from, fromBalance - amount); + this.balancePublic.set(from.toString(), fromBalance - amount); } public redeemShield(to: AztecAddress, amount: bigint) { - const toBalance = this.balancesPrivate.get(to) || 0n; - this.balancesPrivate.set(to, toBalance + amount); + const toBalance = this.balancesPrivate.get(to.toString()) || 0n; + this.balancesPrivate.set(to.toString(), toBalance + amount); } public unshield(from: AztecAddress, to: AztecAddress, amount: bigint) { - const fromBalance = this.balancesPrivate.get(from) || 0n; - const toBalance = this.balancePublic.get(to) || 0n; + const fromBalance = this.balancesPrivate.get(from.toString()) || 0n; + const toBalance = this.balancePublic.get(to.toString()) || 0n; expect(fromBalance).toBeGreaterThanOrEqual(amount); - this.balancesPrivate.set(from, fromBalance - amount); - this.balancePublic.set(to, toBalance + amount); + this.balancesPrivate.set(from.toString(), fromBalance - amount); + this.balancePublic.set(to.toString(), toBalance + amount); } public burnPrivate(from: AztecAddress, amount: bigint) { - const fromBalance = this.balancesPrivate.get(from) || 0n; + const fromBalance = this.balancesPrivate.get(from.toString()) || 0n; expect(fromBalance).toBeGreaterThanOrEqual(amount); - this.balancesPrivate.set(from, fromBalance - amount); + this.balancesPrivate.set(from.toString(), fromBalance - amount); this.totalSupply -= amount; } public burnPublic(from: AztecAddress, amount: bigint) { - const fromBalance = this.balancePublic.get(from) || 0n; + const fromBalance = this.balancePublic.get(from.toString()) || 0n; expect(fromBalance).toBeGreaterThanOrEqual(amount); - this.balancePublic.set(from, fromBalance - amount); + this.balancePublic.set(from.toString(), fromBalance - amount); this.totalSupply -= amount; } public balanceOfPublic(address: AztecAddress) { - return this.balancePublic.get(address) || 0n; + return this.balancePublic.get(address.toString()) || 0n; } public balanceOfPrivate(address: AztecAddress) { - return this.balancesPrivate.get(address) || 0n; + return this.balancesPrivate.get(address.toString()) || 0n; } public async check() { @@ -85,10 +85,10 @@ export class TokenSimulator { // Check that all our public matches for (const address of this.accounts) { - expect(await this.token.methods.balance_of_public({ address }).simulate()).toEqual(this.balanceOfPublic(address)); - expect(await this.token.methods.balance_of_private({ address }).simulate()).toEqual( - this.balanceOfPrivate(address), - ); + const actualPublicBalance = await this.token.methods.balance_of_public(address).simulate(); + expect(actualPublicBalance).toEqual(this.balanceOfPublic(address)); + const actualPrivateBalance = await this.token.methods.balance_of_private({ address }).simulate(); + expect(actualPrivateBalance).toEqual(this.balanceOfPrivate(address)); } } }