diff --git a/circuits/cpp/src/aztec3/circuits/rollup/base/.test.cpp b/circuits/cpp/src/aztec3/circuits/rollup/base/.test.cpp index 9e14512f24c..57cf5a2f5c3 100644 --- a/circuits/cpp/src/aztec3/circuits/rollup/base/.test.cpp +++ b/circuits/cpp/src/aztec3/circuits/rollup/base/.test.cpp @@ -854,44 +854,47 @@ TEST_F(base_rollup_tests, native_multiple_public_state_read_writes) // run_cbind(inputs, outputs); } -TEST_F(base_rollup_tests, native_invalid_public_state_read) -{ - DummyCircuitBuilder builder = DummyCircuitBuilder("base_rollup_tests__native_invalid_public_state_read"); - MemoryStore private_data_tree_store; - MerkleTree private_data_tree(private_data_tree_store, PRIVATE_DATA_TREE_HEIGHT); - - MemoryStore contract_tree_store; - MerkleTree contract_tree(contract_tree_store, CONTRACT_TREE_HEIGHT); - - MemoryStore public_data_tree_store; - MerkleTree public_data_tree(public_data_tree_store, PUBLIC_DATA_TREE_HEIGHT); - - MemoryStore l1_to_l2_messages_tree_store; - MerkleTree l1_to_l2_messages_tree(l1_to_l2_messages_tree_store, L1_TO_L2_MSG_TREE_HEIGHT); - - auto data_read = abis::PublicDataRead{ - .leaf_index = fr(1), - .value = fr(42), - }; - - std::array, 2> kernel_data = { get_empty_kernel(), get_empty_kernel() }; - kernel_data[0].public_inputs.end.public_data_reads[0] = data_read; - auto inputs = test_utils::utils::base_rollup_inputs_from_kernels( - kernel_data, private_data_tree, contract_tree, public_data_tree, l1_to_l2_messages_tree); - - // We change the initial tree root so the read value does not match - public_data_tree.update_element(1, fr(43)); - inputs.start_public_data_tree_root = public_data_tree.root(); - - BaseOrMergeRollupPublicInputs outputs = - aztec3::circuits::rollup::native_base_rollup::base_rollup_circuit(builder, inputs); - - ASSERT_EQ(outputs.start_public_data_tree_root, inputs.start_public_data_tree_root); - ASSERT_EQ(outputs.end_public_data_tree_root, public_data_tree.root()); - ASSERT_EQ(outputs.end_public_data_tree_root, outputs.start_public_data_tree_root); - ASSERT_TRUE(builder.failed()); - // TODO(1998): see above - // run_cbind(inputs, outputs, true, false); -} +// TODO(#2521) - data read validation should happen against the current state of the tx and not the start state. +// https://aztecprotocol.slack.com/archives/C02M7VC7TN0/p1695809629015719?thread_ts=1695653252.007339&cid=C02M7VC7TN0 + +// TEST_F(base_rollup_tests, native_invalid_public_state_read) +// { +// DummyCircuitBuilder builder = DummyCircuitBuilder("base_rollup_tests__native_invalid_public_state_read"); +// MemoryStore private_data_tree_store; +// MerkleTree private_data_tree(private_data_tree_store, PRIVATE_DATA_TREE_HEIGHT); + +// MemoryStore contract_tree_store; +// MerkleTree contract_tree(contract_tree_store, CONTRACT_TREE_HEIGHT); + +// MemoryStore public_data_tree_store; +// MerkleTree public_data_tree(public_data_tree_store, PUBLIC_DATA_TREE_HEIGHT); + +// MemoryStore l1_to_l2_messages_tree_store; +// MerkleTree l1_to_l2_messages_tree(l1_to_l2_messages_tree_store, L1_TO_L2_MSG_TREE_HEIGHT); + +// auto data_read = abis::PublicDataRead{ +// .leaf_index = fr(1), +// .value = fr(42), +// }; + +// std::array, 2> kernel_data = { get_empty_kernel(), get_empty_kernel() }; +// kernel_data[0].public_inputs.end.public_data_reads[0] = data_read; +// auto inputs = test_utils::utils::base_rollup_inputs_from_kernels( +// kernel_data, private_data_tree, contract_tree, public_data_tree, l1_to_l2_messages_tree); + +// // We change the initial tree root so the read value does not match +// public_data_tree.update_element(1, fr(43)); +// inputs.start_public_data_tree_root = public_data_tree.root(); + +// BaseOrMergeRollupPublicInputs outputs = +// aztec3::circuits::rollup::native_base_rollup::base_rollup_circuit(builder, inputs); + +// ASSERT_EQ(outputs.start_public_data_tree_root, inputs.start_public_data_tree_root); +// ASSERT_EQ(outputs.end_public_data_tree_root, public_data_tree.root()); +// ASSERT_EQ(outputs.end_public_data_tree_root, outputs.start_public_data_tree_root); +// ASSERT_TRUE(builder.failed()); +// // TODO(1998): see above +// // run_cbind(inputs, outputs, true, false); +// } } // namespace aztec3::circuits::rollup::base::native_base_rollup_circuit diff --git a/circuits/cpp/src/aztec3/circuits/rollup/base/native_base_rollup_circuit.cpp b/circuits/cpp/src/aztec3/circuits/rollup/base/native_base_rollup_circuit.cpp index efed2fb3d5d..d47cc27ce27 100644 --- a/circuits/cpp/src/aztec3/circuits/rollup/base/native_base_rollup_circuit.cpp +++ b/circuits/cpp/src/aztec3/circuits/rollup/base/native_base_rollup_circuit.cpp @@ -418,12 +418,17 @@ void validate_public_data_reads( fr validate_and_process_public_state(DummyBuilder& builder, BaseRollupInputs const& baseRollupInputs) { + // TODO(#2521) - data read validation should happen against the current state of the tx and not the start state. + // Blocks all interesting usecases that read and write to the same public state in the same tx. + // https://aztecprotocol.slack.com/archives/C02M7VC7TN0/p1695809629015719?thread_ts=1695653252.007339&cid=C02M7VC7TN0 + + // Process public data reads and public data update requests for left input - validate_public_data_reads(builder, - baseRollupInputs.start_public_data_tree_root, - baseRollupInputs.kernel_data[0].public_inputs.end.public_data_reads, - 0, - baseRollupInputs.new_public_data_reads_sibling_paths); + // validate_public_data_reads(builder, + // baseRollupInputs.start_public_data_tree_root, + // baseRollupInputs.kernel_data[0].public_inputs.end.public_data_reads, + // 0, + // baseRollupInputs.new_public_data_reads_sibling_paths); auto mid_public_data_tree_root = insert_public_data_update_requests( builder, @@ -432,13 +437,19 @@ fr validate_and_process_public_state(DummyBuilder& builder, BaseRollupInputs con 0, baseRollupInputs.new_public_data_update_requests_sibling_paths); + + // TODO(#2521) - data read validation should happen against the current state of the tx and not the start state. + // Blocks all interesting usecases that read and write to the same public state in the same tx. + // https://aztecprotocol.slack.com/archives/C02M7VC7TN0/p1695809629015719?thread_ts=1695653252.007339&cid=C02M7VC7TN0 + + // Process public data reads and public data update requests for right input using the resulting tree root from the // left one - validate_public_data_reads(builder, - mid_public_data_tree_root, - baseRollupInputs.kernel_data[1].public_inputs.end.public_data_reads, - MAX_PUBLIC_DATA_READS_PER_TX, - baseRollupInputs.new_public_data_reads_sibling_paths); + // validate_public_data_reads(builder, + // mid_public_data_tree_root, + // baseRollupInputs.kernel_data[1].public_inputs.end.public_data_reads, + // MAX_PUBLIC_DATA_READS_PER_TX, + // baseRollupInputs.new_public_data_reads_sibling_paths); auto end_public_data_tree_root = insert_public_data_update_requests( builder, diff --git a/l1-contracts/test/portals/TokenPortal.sol b/l1-contracts/test/portals/TokenPortal.sol index e9deaa92596..37d5f5cb472 100644 --- a/l1-contracts/test/portals/TokenPortal.sol +++ b/l1-contracts/test/portals/TokenPortal.sol @@ -25,19 +25,19 @@ contract TokenPortal { // docs:start:deposit_public /** * @notice Deposit funds into the portal and adds an L2 message which can only be consumed publicly on Aztec - * @param _to - The aztec address of the recipient * @param _amount - The amount to deposit + * @param _to - The aztec address of the recipient + * @param _canceller - The address that can cancel the L1 to L2 message * @param _deadline - The timestamp after which the entry can be cancelled * @param _secretHash - The hash of the secret consumable message. The hash should be 254 bits (so it can fit in a Field element) - * @param _canceller - The address that can cancel the L1 to L2 message * @return The key of the entry in the Inbox */ function depositToAztecPublic( - bytes32 _to, uint256 _amount, + bytes32 _to, + address _canceller, uint32 _deadline, - bytes32 _secretHash, - address _canceller + bytes32 _secretHash ) external payable returns (bytes32) { // Preamble IInbox inbox = registry.getInbox(); @@ -59,18 +59,18 @@ contract TokenPortal { /** * @notice Deposit funds into the portal and adds an L2 message which can only be consumed privately on Aztec * @param _amount - The amount to deposit + * @param _secretHashForRedeemingMintedNotes - The hash of the secret to redeem minted notes privately on Aztec. The hash should be 254 bits (so it can fit in a Field element) + * @param _canceller - The address that can cancel the L1 to L2 message * @param _deadline - The timestamp after which the entry can be cancelled * @param _secretHashForL2MessageConsumption - The hash of the secret consumable L1 to L2 message. The hash should be 254 bits (so it can fit in a Field element) - * @param _secretHashForL2MessageConsumption - The hash of the secret to redeem minted notes privately on Aztec. The hash should be 254 bits (so it can fit in a Field element) - * @param _canceller - The address that can cancel the L1 to L2 message * @return The key of the entry in the Inbox */ function depositToAztecPrivate( uint256 _amount, - uint32 _deadline, - bytes32 _secretHashForL2MessageConsumption, bytes32 _secretHashForRedeemingMintedNotes, - address _canceller + address _canceller, + uint32 _deadline, + bytes32 _secretHashForL2MessageConsumption ) external payable returns (bytes32) { // Preamble IInbox inbox = registry.getInbox(); @@ -99,16 +99,16 @@ contract TokenPortal { /** * @notice Cancel a public depositToAztec L1 to L2 message * @dev only callable by the `canceller` of the message - * @param _to - The aztec address of the recipient in the original message * @param _amount - The amount to deposit per the original message + * @param _to - The aztec address of the recipient in the original message * @param _deadline - The timestamp after which the entry can be cancelled * @param _secretHash - The hash of the secret consumable message in the original message * @param _fee - The fee paid to the sequencer * @return The key of the entry in the Inbox */ function cancelL1ToAztecMessagePublic( - bytes32 _to, uint256 _amount, + bytes32 _to, uint32 _deadline, bytes32 _secretHash, uint64 _fee @@ -137,17 +137,17 @@ contract TokenPortal { * @notice Cancel a private depositToAztec L1 to L2 message * @dev only callable by the `canceller` of the message * @param _amount - The amount to deposit per the original message + * @param _secretHashForRedeemingMintedNotes - The hash of the secret to redeem minted notes privately on Aztec * @param _deadline - The timestamp after which the entry can be cancelled * @param _secretHashForL2MessageConsumption - The hash of the secret consumable L1 to L2 message - * @param _secretHashForL2MessageConsumption - The hash of the secret to redeem minted notes privately on Aztec * @param _fee - The fee paid to the sequencer * @return The key of the entry in the Inbox */ function cancelL1ToAztecMessagePrivate( uint256 _amount, + bytes32 _secretHashForRedeemingMintedNotes, uint32 _deadline, bytes32 _secretHashForL2MessageConsumption, - bytes32 _secretHashForRedeemingMintedNotes, uint64 _fee ) external returns (bytes32) { IInbox inbox = registry.getInbox(); diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index d914124e607..4b3d35e32a2 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -143,10 +143,10 @@ contract TokenPortalTest is Test { // Perform op bytes32 entryKey = tokenPortal.depositToAztecPrivate{value: bid}( amount, - deadline, - secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, - address(this) + address(this), + deadline, + secretHashForL2MessageConsumption ); assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); @@ -185,7 +185,7 @@ contract TokenPortalTest is Test { // Perform op bytes32 entryKey = tokenPortal.depositToAztecPublic{value: bid}( - to, amount, deadline, secretHashForL2MessageConsumption, address(this) + amount, to, address(this), deadline, secretHashForL2MessageConsumption ); assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); @@ -210,7 +210,7 @@ contract TokenPortalTest is Test { abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) ); tokenPortal.cancelL1ToAztecMessagePublic( - to, amount, deadline, secretHashForL2MessageConsumption, bid + amount, to, deadline, secretHashForL2MessageConsumption, bid ); vm.stopPrank(); @@ -221,7 +221,7 @@ contract TokenPortalTest is Test { abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) ); tokenPortal.cancelL1ToAztecMessagePrivate( - amount, deadline, secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, bid + amount, secretHashForRedeemingMintedNotes, deadline, secretHashForL2MessageConsumption, bid ); // actually cancel the message @@ -231,7 +231,7 @@ contract TokenPortalTest is Test { emit L1ToL2MessageCancelled(expectedEntryKey); // perform op bytes32 entryKey = tokenPortal.cancelL1ToAztecMessagePublic( - to, amount, deadline, secretHashForL2MessageConsumption, bid + amount, to, deadline, secretHashForL2MessageConsumption, bid ); assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); @@ -257,7 +257,7 @@ contract TokenPortalTest is Test { abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) ); tokenPortal.cancelL1ToAztecMessagePrivate( - amount, deadline, secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, bid + amount, secretHashForRedeemingMintedNotes, deadline, secretHashForL2MessageConsumption, bid ); vm.stopPrank(); @@ -268,7 +268,7 @@ contract TokenPortalTest is Test { abi.encodeWithSelector(Errors.Inbox__NothingToConsume.selector, expectedWrongEntryKey) ); tokenPortal.cancelL1ToAztecMessagePublic( - to, amount, deadline, secretHashForL2MessageConsumption, bid + amount, to, deadline, secretHashForL2MessageConsumption, bid ); // actually cancel the message @@ -278,7 +278,7 @@ contract TokenPortalTest is Test { emit L1ToL2MessageCancelled(expectedEntryKey); // perform op bytes32 entryKey = tokenPortal.cancelL1ToAztecMessagePrivate( - amount, deadline, secretHashForL2MessageConsumption, secretHashForRedeemingMintedNotes, bid + amount, secretHashForRedeemingMintedNotes, deadline, secretHashForL2MessageConsumption, bid ); assertEq(entryKey, expectedEntryKey, "returned entry key and calculated entryKey should match"); diff --git a/l1-contracts/test/portals/UniswapPortal.sol b/l1-contracts/test/portals/UniswapPortal.sol index f96f46bc4f0..76b0287c2fb 100644 --- a/l1-contracts/test/portals/UniswapPortal.sol +++ b/l1-contracts/test/portals/UniswapPortal.sol @@ -36,7 +36,7 @@ contract UniswapPortal { // docs:start:solidity_uniswap_swap /** - * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again + * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again publicly * @dev `msg.value` indicates fee to submit message to inbox. Currently, anyone can call this method on your behalf. * They could call it with 0 fee causing the sequencer to never include in the rollup. * In this case, you will have to cancel the message and then make the deposit later @@ -46,20 +46,20 @@ contract UniswapPortal { * @param _outputTokenPortal - The ethereum address of the output token portal * @param _amountOutMinimum - The minimum amount of output assets to receive from the swap (slippage protection) * @param _aztecRecipient - The aztec address to receive the output assets - * @param _secretHash - The hash of the secret consumable message + * @param _secretHashForL1ToL2Message - The hash of the secret consumable message. The hash should be 254 bits (so it can fit in a Field element) * @param _deadlineForL1ToL2Message - deadline for when the L1 to L2 message (to mint outpiut assets in L2) must be consumed by * @param _canceller - The ethereum address that can cancel the deposit * @param _withCaller - When true, using `msg.sender` as the caller, otherwise address(0) * @return The entryKey of the deposit transaction in the Inbox */ - function swap( + function swapPublic( address _inputTokenPortal, uint256 _inAmount, uint24 _uniswapFeeTier, address _outputTokenPortal, uint256 _amountOutMinimum, bytes32 _aztecRecipient, - bytes32 _secretHash, + bytes32 _secretHashForL1ToL2Message, uint32 _deadlineForL1ToL2Message, address _canceller, bool _withCaller @@ -75,14 +75,14 @@ contract UniswapPortal { // prevent stack too deep errors vars.contentHash = Hash.sha256ToField( abi.encodeWithSignature( - "swap(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)", + "swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)", _inputTokenPortal, _inAmount, _uniswapFeeTier, _outputTokenPortal, _amountOutMinimum, _aztecRecipient, - _secretHash, + _secretHashForL1ToL2Message, _deadlineForL1ToL2Message, _canceller, _withCaller ? msg.sender : address(0) @@ -121,11 +121,106 @@ contract UniswapPortal { // Note, safeApprove was deprecated from Oz vars.outputAsset.approve(address(_outputTokenPortal), amountOut); - // Deposit the output asset to the L2 via its portal] - // TODO(2167) - Update UniswapPortal properly with new portal standard. + // Deposit the output asset to the L2 via its portal return TokenPortal(_outputTokenPortal).depositToAztecPublic{value: msg.value}( - _aztecRecipient, amountOut, _deadlineForL1ToL2Message, _secretHash, _canceller + amountOut, _aztecRecipient, _canceller, _deadlineForL1ToL2Message, _secretHashForL1ToL2Message ); } // docs:end:solidity_uniswap_swap + + /** + * @notice Exit with funds from L2, perform swap on L1 and deposit output asset to L2 again privately + * @dev `msg.value` indicates fee to submit message to inbox. Currently, anyone can call this method on your behalf. + * They could call it with 0 fee causing the sequencer to never include in the rollup. + * In this case, you will have to cancel the message and then make the deposit later + * @param _inputTokenPortal - The ethereum address of the input token portal + * @param _inAmount - The amount of assets to swap (same amount as withdrawn from L2) + * @param _uniswapFeeTier - The fee tier for the swap on UniswapV3 + * @param _outputTokenPortal - The ethereum address of the output token portal + * @param _amountOutMinimum - The minimum amount of output assets to receive from the swap (slippage protection) + * @param _secretHashForRedeemingMintedNotes - The hash of the secret to redeem minted notes privately on Aztec. The hash should be 254 bits (so it can fit in a Field element) + * @param _secretHashForL1ToL2Message - The hash of the secret consumable message. The hash should be 254 bits (so it can fit in a Field element) + * @param _deadlineForL1ToL2Message - deadline for when the L1 to L2 message (to mint outpiut assets in L2) must be consumed by + * @param _canceller - The ethereum address that can cancel the deposit + * @param _withCaller - When true, using `msg.sender` as the caller, otherwise address(0) + * @return The entryKey of the deposit transaction in the Inbox + */ + function swapPrivate( + address _inputTokenPortal, + uint256 _inAmount, + uint24 _uniswapFeeTier, + address _outputTokenPortal, + uint256 _amountOutMinimum, + bytes32 _secretHashForRedeemingMintedNotes, + bytes32 _secretHashForL1ToL2Message, + uint32 _deadlineForL1ToL2Message, + address _canceller, + bool _withCaller + ) public payable returns (bytes32) { + LocalSwapVars memory vars; + + vars.inputAsset = TokenPortal(_inputTokenPortal).underlying(); + vars.outputAsset = TokenPortal(_outputTokenPortal).underlying(); + + // Withdraw the input asset from the portal + TokenPortal(_inputTokenPortal).withdraw(_inAmount, address(this), true); + { + // prevent stack too deep errors + vars.contentHash = Hash.sha256ToField( + abi.encodeWithSignature( + "swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)", + _inputTokenPortal, + _inAmount, + _uniswapFeeTier, + _outputTokenPortal, + _amountOutMinimum, + _secretHashForRedeemingMintedNotes, + _secretHashForL1ToL2Message, + _deadlineForL1ToL2Message, + _canceller, + _withCaller ? msg.sender : address(0) + ) + ); + } + + // Consume the message from the outbox + registry.getOutbox().consume( + DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2UniswapAddress, 1), + recipient: DataStructures.L1Actor(address(this), block.chainid), + content: vars.contentHash + }) + ); + + // Perform the swap + ISwapRouter.ExactInputSingleParams memory swapParams; + { + swapParams = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(vars.inputAsset), + tokenOut: address(vars.outputAsset), + fee: _uniswapFeeTier, + recipient: address(this), + deadline: block.timestamp, + amountIn: _inAmount, + amountOutMinimum: _amountOutMinimum, + sqrtPriceLimitX96: 0 + }); + } + // Note, safeApprove was deprecated from Oz + vars.inputAsset.approve(address(ROUTER), _inAmount); + uint256 amountOut = ROUTER.exactInputSingle(swapParams); + + // approve the output token portal to take funds from this contract + // Note, safeApprove was deprecated from Oz + vars.outputAsset.approve(address(_outputTokenPortal), amountOut); + + // Deposit the output asset to the L2 via its portal + return TokenPortal(_outputTokenPortal).depositToAztecPrivate{value: msg.value}( + amountOut, + _secretHashForRedeemingMintedNotes, + _canceller, + _deadlineForL1ToL2Message, + _secretHashForL1ToL2Message + ); + } } diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index e2724611745..02318a95c84 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -40,6 +40,7 @@ contract UniswapPortalTest is Test { uint256 internal amountOutMinimum = 0; uint32 internal deadlineForL1ToL2Message; // set after fork is activated bytes32 internal aztecRecipient = bytes32(uint256(0x3)); + bytes32 internal secretHashForRedeemingMintedNotes = bytes32(uint256(0x4)); function setUp() public { // fork mainnet @@ -93,7 +94,7 @@ contract UniswapPortalTest is Test { * @param _caller - designated caller on L1 that will call the swap function - typically address(this) * Set to address(0) if anyone can call. */ - function _createUniswapSwapMessage(bytes32 _aztecRecipient, address _caller) + function _createUniswapSwapMessagePublic(bytes32 _aztecRecipient, address _caller) internal view returns (bytes32 entryKey) @@ -103,7 +104,7 @@ contract UniswapPortalTest is Test { recipient: DataStructures.L1Actor(address(uniswapPortal), block.chainid), content: Hash.sha256ToField( abi.encodeWithSignature( - "swap(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)", + "swap_public(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)", address(daiTokenPortal), amount, uniswapFeePool, @@ -120,6 +121,38 @@ contract UniswapPortalTest is Test { entryKey = outbox.computeEntryKey(message); } + /** + * L2 to L1 message to be added to the outbox - + * @param _secretHashForRedeemingMintedNotes - The hash of the secret to redeem minted notes privately on Aztec + * @param _caller - designated caller on L1 that will call the swap function - typically address(this) + * Set to address(0) if anyone can call. + */ + function _createUniswapSwapMessagePrivate( + bytes32 _secretHashForRedeemingMintedNotes, + address _caller + ) internal view returns (bytes32 entryKey) { + DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({ + sender: DataStructures.L2Actor(l2UniswapAddress, 1), + recipient: DataStructures.L1Actor(address(uniswapPortal), block.chainid), + content: Hash.sha256ToField( + abi.encodeWithSignature( + "swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)", + address(daiTokenPortal), + amount, + uniswapFeePool, + address(wethTokenPortal), + amountOutMinimum, + _secretHashForRedeemingMintedNotes, + secretHash, + deadlineForL1ToL2Message, + address(this), + _caller + ) + ) + }); + entryKey = outbox.computeEntryKey(message); + } + function _addMessagesToOutbox(bytes32 daiWithdrawMessageKey, bytes32 swapMessageKey) internal { bytes32[] memory entryKeys = new bytes32[](2); entryKeys[0] = daiWithdrawMessageKey; @@ -139,7 +172,7 @@ contract UniswapPortalTest is Test { vm.expectRevert( abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) ); - uniswapPortal.swap( + uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -166,7 +199,7 @@ contract UniswapPortalTest is Test { vm.expectRevert( abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) ); - uniswapPortal.swap( + uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -183,16 +216,16 @@ contract UniswapPortalTest is Test { function testRevertIfSwapParamsDifferentToOutboxMessage() public { bytes32 daiWithdrawMsgKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); - bytes32 swapMsgKey = _createUniswapSwapMessage(aztecRecipient, address(this)); + bytes32 swapMsgKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); _addMessagesToOutbox(daiWithdrawMsgKey, swapMsgKey); bytes32 newAztecRecipient = bytes32(uint256(0x4)); bytes32 entryKeyPortalChecksAgainst = - _createUniswapSwapMessage(newAztecRecipient, address(this)); + _createUniswapSwapMessagePublic(newAztecRecipient, address(this)); vm.expectRevert( abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) ); - uniswapPortal.swap( + uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -209,10 +242,10 @@ contract UniswapPortalTest is Test { function testSwapWithDesignatedCaller() public { bytes32 daiWithdrawMsgKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); - bytes32 swapMsgKey = _createUniswapSwapMessage(aztecRecipient, address(this)); + bytes32 swapMsgKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); _addMessagesToOutbox(daiWithdrawMsgKey, swapMsgKey); - bytes32 l1ToL2MessageKey = uniswapPortal.swap( + bytes32 l1ToL2MessageKey = uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -240,12 +273,12 @@ contract UniswapPortalTest is Test { vm.assume(_caller != address(uniswapPortal)); bytes32 daiWithdrawMsgKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); - // don't set caller on swap() -> so anyone can call this method. - bytes32 swapMsgKey = _createUniswapSwapMessage(aztecRecipient, address(0)); + // don't set caller on swapPublic() -> so anyone can call this method. + bytes32 swapMsgKey = _createUniswapSwapMessagePublic(aztecRecipient, address(0)); _addMessagesToOutbox(daiWithdrawMsgKey, swapMsgKey); vm.prank(_caller); - bytes32 l1ToL2MessageKey = uniswapPortal.swap( + bytes32 l1ToL2MessageKey = uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -273,15 +306,15 @@ contract UniswapPortalTest is Test { vm.assume(_caller != address(this)); bytes32 daiWithdrawMsgKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); - bytes32 swapMsgKey = _createUniswapSwapMessage(aztecRecipient, address(this)); + bytes32 swapMsgKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); _addMessagesToOutbox(daiWithdrawMsgKey, swapMsgKey); vm.startPrank(_caller); - bytes32 entryKeyPortalChecksAgainst = _createUniswapSwapMessage(aztecRecipient, _caller); + bytes32 entryKeyPortalChecksAgainst = _createUniswapSwapMessagePublic(aztecRecipient, _caller); vm.expectRevert( abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) ); - uniswapPortal.swap( + uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -294,11 +327,11 @@ contract UniswapPortalTest is Test { true ); - entryKeyPortalChecksAgainst = _createUniswapSwapMessage(aztecRecipient, address(0)); + entryKeyPortalChecksAgainst = _createUniswapSwapMessagePublic(aztecRecipient, address(0)); vm.expectRevert( abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) ); - uniswapPortal.swap( + uniswapPortal.swapPublic( address(daiTokenPortal), amount, uniswapFeePool, @@ -320,10 +353,10 @@ contract UniswapPortalTest is Test { function testMessageToInboxIsCancellable() public { bytes32 daiWithdrawMsgKey = _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); - bytes32 swapMsgKey = _createUniswapSwapMessage(aztecRecipient, address(this)); + bytes32 swapMsgKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); _addMessagesToOutbox(daiWithdrawMsgKey, swapMsgKey); - bytes32 l1ToL2MessageKey = uniswapPortal.swap{value: 1 ether}( + bytes32 l1ToL2MessageKey = uniswapPortal.swapPublic{value: 1 ether}( address(daiTokenPortal), amount, uniswapFeePool, @@ -346,7 +379,7 @@ contract UniswapPortalTest is Test { // perform op // TODO(2167) - Update UniswapPortal properly with new portal standard. bytes32 entryKey = wethTokenPortal.cancelL1ToAztecMessagePublic( - aztecRecipient, wethAmountOut, deadlineForL1ToL2Message, secretHash, 1 ether + wethAmountOut, aztecRecipient, deadlineForL1ToL2Message, secretHash, 1 ether ); assertEq(entryKey, l1ToL2MessageKey, "returned entry key and calculated entryKey should match"); assertFalse(inbox.contains(entryKey), "entry still in inbox"); @@ -358,6 +391,33 @@ contract UniswapPortalTest is Test { assertEq(WETH9.balanceOf(address(wethTokenPortal)), 0, "portal should have no assets"); } + function testRevertIfSwapMessageWasForDifferentPublicOrPrivateFlow() public { + bytes32 daiWithdrawMsgKey = + _createDaiWithdrawMessage(address(uniswapPortal), address(uniswapPortal)); + + // Create message for `_isPrivateFlow`: + bytes32 swapMsgKey = _createUniswapSwapMessagePublic(aztecRecipient, address(this)); + _addMessagesToOutbox(daiWithdrawMsgKey, swapMsgKey); + + bytes32 entryKeyPortalChecksAgainst = + _createUniswapSwapMessagePrivate(secretHashForRedeemingMintedNotes, address(this)); + vm.expectRevert( + abi.encodeWithSelector(Errors.Outbox__NothingToConsume.selector, entryKeyPortalChecksAgainst) + ); + + uniswapPortal.swapPrivate( + address(daiTokenPortal), + amount, + uniswapFeePool, + address(wethTokenPortal), + amountOutMinimum, + secretHashForRedeemingMintedNotes, + secretHash, + deadlineForL1ToL2Message, + address(this), + true + ); + } // TODO(#887) - what if uniswap fails? // TODO(#887) - what happens if uniswap deadline is passed? } diff --git a/yarn-project/acir-simulator/src/public/index.test.ts b/yarn-project/acir-simulator/src/public/index.test.ts index 743cf6d3bdd..f65cc99acce 100644 --- a/yarn-project/acir-simulator/src/public/index.test.ts +++ b/yarn-project/acir-simulator/src/public/index.test.ts @@ -247,9 +247,7 @@ describe('ACIR public execution simulator', () => { const globalVariables = new GlobalVariables(new Fr(69), new Fr(420), new Fr(1), new Fr(7)); if (isInternal === undefined) { - await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError( - /ContractsDb don't contain isInternal for/, - ); + await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError(/Method not found -/); } else { const result = await executor.simulate(execution, globalVariables); diff --git a/yarn-project/acir-simulator/src/public/public_execution_context.ts b/yarn-project/acir-simulator/src/public/public_execution_context.ts index e208954ef8c..04094da9211 100644 --- a/yarn-project/acir-simulator/src/public/public_execution_context.ts +++ b/yarn-project/acir-simulator/src/public/public_execution_context.ts @@ -208,9 +208,7 @@ export class PublicExecutionContext extends TypedOracle { const portalAddress = (await this.contractsDb.getPortalContractAddress(targetContractAddress)) ?? EthAddress.ZERO; const isInternal = await this.contractsDb.getIsInternal(targetContractAddress, functionSelector); if (isInternal === undefined) { - throw new Error( - `ERR: ContractsDb don't contain isInternal for ${targetContractAddress.toString()}:${functionSelector.toString()}. Defaulting to false.`, - ); + throw new Error(`ERR: Method not found - ${targetContractAddress.toString()}:${functionSelector.toString()}`); } const acir = await this.contractsDb.getBytecode(targetContractAddress, functionSelector); diff --git a/yarn-project/aztec-sandbox/package.json b/yarn-project/aztec-sandbox/package.json index 11070a0946c..8e263193536 100644 --- a/yarn-project/aztec-sandbox/package.json +++ b/yarn-project/aztec-sandbox/package.json @@ -21,8 +21,7 @@ "formatting:fix": "run -T prettier -w ./src", "build:dev": "tsc -b --watch", "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests", - "run:example:token": "DEBUG='aztec:*' node ./dest/examples/private_token_contract.js", - "run:example:uniswap": "DEBUG='aztec:*' node ./dest/examples/uniswap_trade_on_l1_from_l2.js" + "run:example:token": "DEBUG='aztec:*' node ./dest/examples/private_token_contract.js" }, "inherits": [ "../package.common.json" diff --git a/yarn-project/aztec-sandbox/src/examples/uniswap_trade_on_l1_from_l2.ts b/yarn-project/aztec-sandbox/src/examples/uniswap_trade_on_l1_from_l2.ts deleted file mode 100644 index 2f2a05b6173..00000000000 --- a/yarn-project/aztec-sandbox/src/examples/uniswap_trade_on_l1_from_l2.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { - AccountWallet, - AztecAddress, - EthAddress, - Fr, - computeMessageSecretHash, - createPXEClient, - createRecipient, - getL1ContractAddresses, - getUnsafeSchnorrAccount, -} from '@aztec/aztec.js'; -import { GrumpkinScalar } from '@aztec/circuits.js'; -import { createDebugLogger } from '@aztec/foundation/log'; -import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; -import { NonNativeTokenContract, UniswapContract } from '@aztec/noir-contracts/types'; -import { PXE, TxStatus } from '@aztec/types'; - -import { createPublicClient, createWalletClient, getContract, http, parseEther } from 'viem'; -import { mnemonicToAccount } from 'viem/accounts'; -import { foundry } from 'viem/chains'; - -import { delay, deployAndInitializeNonNativeL2TokenContracts, deployL1Contract } from './util.js'; - -const logger = createDebugLogger('aztec:http-rpc-client'); - -export const MNEMONIC = 'test test test test test test test test test test test junk'; - -const INITIAL_BALANCE = 333n; -const wethAmountToBridge = parseEther('1'); - -const WETH9_ADDRESS = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); -const DAI_ADDRESS = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); - -const EXPECTED_FORKED_BLOCK = 17514288; - -const pxeRpcUrl = 'http://localhost:8080'; -const ethRpcUrl = 'http://localhost:8545'; - -const hdAccount = mnemonicToAccount(MNEMONIC); -const privateKey = GrumpkinScalar.fromBuffer(Buffer.from(hdAccount.getHdKey().privateKey!)); - -const walletClient = createWalletClient({ - account: hdAccount, - chain: foundry, - transport: http(ethRpcUrl), -}); -const publicClient = createPublicClient({ - chain: foundry, - transport: http(ethRpcUrl), -}); - -if (Number(await publicClient.getBlockNumber()) < EXPECTED_FORKED_BLOCK) { - throw new Error('This test must be run on a fork of mainnet with the expected fork block'); -} - -const ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); - -const pxe = createPXEClient(pxeRpcUrl); -let wallet: AccountWallet; - -/** - * Deploys all l1 / l2 contracts - * @param owner - Owner address. - */ -async function deployAllContracts(owner: AztecAddress) { - const l1ContractsAddresses = await getL1ContractAddresses(pxeRpcUrl); - logger('Deploying DAI Portal, initializing and deploying l2 contract...'); - const daiContracts = await deployAndInitializeNonNativeL2TokenContracts( - wallet, - walletClient, - publicClient, - l1ContractsAddresses!.registry, - INITIAL_BALANCE, - owner, - DAI_ADDRESS, - ); - const daiL2Contract = daiContracts.l2Contract; - const daiContract = daiContracts.underlyingERC20; - const daiTokenPortalAddress = daiContracts.tokenPortalAddress; - - logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - const wethContracts = await deployAndInitializeNonNativeL2TokenContracts( - wallet, - walletClient, - publicClient, - l1ContractsAddresses!.registry, - INITIAL_BALANCE, - owner, - WETH9_ADDRESS, - ); - const wethL2Contract = wethContracts.l2Contract; - const wethContract = wethContracts.underlyingERC20; - const wethTokenPortal = wethContracts.tokenPortal; - const wethTokenPortalAddress = wethContracts.tokenPortalAddress; - - logger('Deploy Uniswap portal on L1 and L2...'); - const uniswapPortalAddress = await deployL1Contract( - walletClient, - publicClient, - UniswapPortalAbi, - UniswapPortalBytecode, - ); - const uniswapPortal = getContract({ - address: uniswapPortalAddress.toString(), - abi: UniswapPortalAbi, - walletClient, - publicClient, - }); - - // deploy l2 uniswap contract and attach to portal - const tx = UniswapContract.deploy(pxe).send({ portalContract: uniswapPortalAddress }); - await tx.isMined({ interval: 0.5 }); - const receipt = await tx.getReceipt(); - const uniswapL2Contract = await UniswapContract.at(receipt.contractAddress!, wallet); - await uniswapL2Contract.attach(uniswapPortalAddress); - - await uniswapPortal.write.initialize( - [l1ContractsAddresses!.registry.toString(), uniswapL2Contract.address.toString()], - {} as any, - ); - - return { - daiL2Contract, - daiContract, - daiTokenPortalAddress, - wethL2Contract, - wethContract, - wethTokenPortal, - wethTokenPortalAddress, - uniswapL2Contract, - uniswapPortal, - }; -} - -const getL2BalanceOf = async (pxe: PXE, owner: AztecAddress, l2Contract: NonNativeTokenContract) => { - return await l2Contract.methods.getBalance(owner).view({ from: owner }); -}; - -const logExpectedBalanceOnL2 = async ( - pxe: PXE, - owner: AztecAddress, - expectedBalance: bigint, - l2Contract: NonNativeTokenContract, -) => { - const balance = await getL2BalanceOf(pxe, owner, l2Contract); - logger(`Account ${owner} balance: ${balance}. Expected to be: ${expectedBalance}`); -}; - -const transferWethOnL2 = async ( - _pxe: PXE, - wethL2Contract: NonNativeTokenContract, - ownerAddress: AztecAddress, - receiver: AztecAddress, - transferAmount: bigint, -) => { - const transferTx = wethL2Contract.methods.transfer(transferAmount, receiver).send(); - await transferTx.isMined({ interval: 0.5 }); - const transferReceipt = await transferTx.getReceipt(); - // expect(transferReceipt.status).toBe(TxStatus.MINED); - logger(`WETH to L2 Transfer Receipt status: ${transferReceipt.status} should be ${TxStatus.MINED}`); -}; - -/** - * main fn - */ -async function main() { - logger('Running L1/L2 messaging test on HTTP interface.'); - - wallet = await getUnsafeSchnorrAccount(pxe, privateKey).waitDeploy(); - const owner = wallet.getCompleteAddress(); - const receiver = await createRecipient(pxe); - - const result = await deployAllContracts(owner.address); - const { - daiL2Contract, - daiContract, - daiTokenPortalAddress, - wethL2Contract, - wethContract, - wethTokenPortal, - wethTokenPortalAddress, - uniswapL2Contract, - uniswapPortal, - } = result; - - // Give me some WETH so I can deposit to L2 and do the swap... - logger('Getting some weth'); - await walletClient.sendTransaction({ to: WETH9_ADDRESS.toString(), value: parseEther('1') }); - - const meBeforeBalance = await wethContract.read.balanceOf([ethAccount.toString()]); - // 1. Approve weth to be bridged - await wethContract.write.approve([wethTokenPortalAddress.toString(), wethAmountToBridge], {} as any); - - // 2. Deposit weth into the portal and move to L2 - // generate secret - const secret = Fr.random(); - const secretHash = await computeMessageSecretHash(secret); - const secretString = `0x${secretHash.toBuffer().toString('hex')}` as `0x${string}`; - const deadline = 2 ** 32 - 1; // max uint32 - 1 - logger('Sending messages to L1 portal'); - const args = [owner.toString(), wethAmountToBridge, deadline, secretString, ethAccount.toString()] as const; - const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztec(args, { - account: ethAccount.toString(), - } as any); - await wethTokenPortal.write.depositToAztec(args, {} as any); - // expect(await wethContract.read.balanceOf([ethAccount.toString()])).toBe(meBeforeBalance - wethAmountToBridge); - - const currentBalance = await wethContract.read.balanceOf([ethAccount.toString()]); - logger(`Current Balance: ${currentBalance}. Should be: ${meBeforeBalance - wethAmountToBridge}`); - const messageKey = Fr.fromString(messageKeyHex); - - // Wait for the archiver to process the message - await delay(5000); - // send a transfer tx to force through rollup with the message included - const transferAmount = 1n; - await transferWethOnL2(pxe, wethL2Contract, owner.address, receiver.address, transferAmount); - - // 3. Claim WETH on L2 - logger('Minting weth on L2'); - // Call the mint tokens function on the Aztec.nr contract - const consumptionTx = wethL2Contract.methods - .mint(wethAmountToBridge, owner.address, messageKey, secret, ethAccount.toField()) - .send(); - await consumptionTx.isMined({ interval: 0.5 }); - const consumptionReceipt = await consumptionTx.getReceipt(); - // expect(consumptionReceipt.status).toBe(TxStatus.MINED); - logger(`Consumption Receipt status: ${consumptionReceipt.status} should be ${TxStatus.MINED}`); - // await expectBalanceOnL2(ownerAddress, wethAmountToBridge + initialBalance - transferAmount, wethL2Contract); - - // Store balances - const wethBalanceBeforeSwap = await getL2BalanceOf(pxe, owner.address, wethL2Contract); - const daiBalanceBeforeSwap = await getL2BalanceOf(pxe, owner.address, daiL2Contract); - - // 4. Send L2 to L1 message to withdraw funds and another message to swap assets. - logger('Send L2 tx to withdraw WETH to uniswap portal and send message to swap assets on L1'); - // recipient is the uniswap portal - const minimumOutputAmount = 0n; - - const withdrawTx = uniswapL2Contract.methods - .swap( - wethL2Contract.address.toField(), - wethAmountToBridge, - new Fr(3000), - daiL2Contract.address.toField(), - new Fr(minimumOutputAmount), - owner.address, - owner.address, - secretHash, - new Fr(2 ** 32 - 1), - ethAccount.toField(), - ethAccount.toField(), - ) - .send(); - await withdrawTx.isMined({ interval: 0.5 }); - const withdrawReceipt = await withdrawTx.getReceipt(); - // expect(withdrawReceipt.status).toBe(TxStatus.MINED); - logger(`Withdraw receipt status: ${withdrawReceipt.status} should be ${TxStatus.MINED}`); - - // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) - await logExpectedBalanceOnL2(pxe, owner.address, INITIAL_BALANCE - transferAmount, wethL2Contract); - - // 5. Consume L2 to L1 message by calling uniswapPortal.swap() - logger('Execute withdraw and swap on the uniswapPortal!'); - const daiBalanceOfPortalBefore = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); - logger(`DAI balance of portal: ${daiBalanceOfPortalBefore}`); - const swapArgs = [ - wethTokenPortalAddress.toString(), - wethAmountToBridge, - 3000, - daiTokenPortalAddress.toString(), - minimumOutputAmount, - owner.address.toString(), - secretString, - deadline, - ethAccount.toString(), - true, - ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swap(swapArgs, { - account: ethAccount.toString(), - } as any); - // this should also insert a message into the inbox. - await uniswapPortal.write.swap(swapArgs, {} as any); - const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); - // weth was swapped to dai and send to portal - const daiBalanceOfPortalAfter = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); - // expect(daiBalanceOfPortalAfter).toBeGreaterThan(daiBalanceOfPortalBefore); - logger( - `DAI balance in Portal: ${daiBalanceOfPortalAfter} should be bigger than ${daiBalanceOfPortalBefore}. ${ - daiBalanceOfPortalAfter > daiBalanceOfPortalBefore - }`, - ); - const daiAmountToBridge = daiBalanceOfPortalAfter - daiBalanceOfPortalBefore; - - // Wait for the archiver to process the message - await delay(5000); - // send a transfer tx to force through rollup with the message included - await transferWethOnL2(pxe, wethL2Contract, owner.address, receiver.address, transferAmount); - - // 6. claim dai on L2 - logger('Consuming messages to mint dai on L2'); - // Call the mint tokens function on the Aztec.nr contract - const daiMintTx = daiL2Contract.methods - .mint(daiAmountToBridge, owner.address, depositDaiMessageKey, secret, ethAccount.toField()) - .send(); - await daiMintTx.isMined({ interval: 0.5 }); - const daiMintTxReceipt = await daiMintTx.getReceipt(); - // expect(daiMintTxReceipt.status).toBe(TxStatus.MINED); - logger(`DAI mint TX status: ${daiMintTxReceipt.status} should be ${TxStatus.MINED}`); - await logExpectedBalanceOnL2(pxe, owner.address, INITIAL_BALANCE + BigInt(daiAmountToBridge), daiL2Contract); - - const wethBalanceAfterSwap = await getL2BalanceOf(pxe, owner.address, wethL2Contract); - const daiBalanceAfterSwap = await getL2BalanceOf(pxe, owner.address, daiL2Contract); - - logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); - logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString()); - logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethBalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiBalanceAfterSwap.toString()); -} - -main() - .then(() => { - logger('Finished running successfully.'); - process.exit(0); - }) - .catch(err => { - logger.error('Error in main fn: ', err); - process.exit(1); - }); diff --git a/yarn-project/aztec.js/src/utils/l1_contracts.ts b/yarn-project/aztec.js/src/utils/l1_contracts.ts index 4ee7ef38739..7275756a579 100644 --- a/yarn-project/aztec.js/src/utils/l1_contracts.ts +++ b/yarn-project/aztec.js/src/utils/l1_contracts.ts @@ -1,50 +1,14 @@ -import { EthAddress } from '@aztec/circuits.js'; +import { L1ContractAddresses } from '@aztec/ethereum'; import { retryUntil } from '@aztec/foundation/retry'; -/** - * A dictionary of the Aztec-deployed L1 contracts. - */ -export type L1ContractAddresses = { - /** - * Address fo the main Aztec rollup contract. - */ - rollup: EthAddress; - /** - * Address of the contract that emits events on public contract deployment. - */ - contractDeploymentEmitter: EthAddress; - /** - * Address of the L1/L2 messaging inbox contract. - */ - inbox: EthAddress; - /** - * Address of the L1/L2 messaging outbox contract. - */ - outbox: EthAddress; - /** - * Address of the decoder helper contract - */ - decoderHelper?: EthAddress; - - /** - * Registry Address. - */ - registry: EthAddress; -}; - -/** - * string dictionary of aztec contract addresses that we receive over http. - */ -type L1ContractAddressesResp = { - [K in keyof L1ContractAddresses]: string; -}; +import { createPXEClient } from '../pxe_client.js'; export const getL1ContractAddresses = async (url: string): Promise => { - const reqUrl = new URL(`${url}/api/l1-contract-addresses`); + const pxeClient = createPXEClient(url); const response = await retryUntil( async () => { try { - return (await (await fetch(reqUrl.toString())).json()) as unknown as L1ContractAddressesResp; + return (await pxeClient.getNodeInfo()).l1ContractAddresses; } catch (err) { // do nothing } @@ -53,8 +17,5 @@ export const getL1ContractAddresses = async (url: string): Promise [key, EthAddress.fromString(value)]), - ); - return result as L1ContractAddresses; + return response; }; diff --git a/yarn-project/canary/package.json b/yarn-project/canary/package.json index 7e7d5eb48b0..138d9b5b098 100644 --- a/yarn-project/canary/package.json +++ b/yarn-project/canary/package.json @@ -21,6 +21,7 @@ "rootDir": "./src" }, "dependencies": { + "@aztec/circuits.js": "workspace:^", "@aztec/aztec.js": "workspace:^", "@aztec/cli": "workspace:^", "@aztec/end-to-end": "workspace:^", diff --git a/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts index f53765b96eb..0b7b71414c1 100644 --- a/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/canary/src/uniswap_trade_on_l1_from_l2.test.ts @@ -1,9 +1,9 @@ import { + AccountWallet, AztecAddress, EthAddress, Fr, TxStatus, - Wallet, computeMessageSecretHash, createDebugLogger, createPXEClient, @@ -12,8 +12,9 @@ import { sleep, waitForSandbox, } from '@aztec/aztec.js'; +import { FunctionSelector } from '@aztec/circuits.js'; import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; -import { NonNativeTokenContract, UniswapContract } from '@aztec/noir-contracts/types'; +import { TokenBridgeContract, TokenContract, UniswapContract } from '@aztec/noir-contracts/types'; import { HDAccount, @@ -29,7 +30,7 @@ import { import { mnemonicToAccount } from 'viem/accounts'; import { Chain, foundry } from 'viem/chains'; -import { deployAndInitializeNonNativeL2TokenContracts, deployL1Contract } from './utils.js'; +import { deployAndInitializeTokenAndBridgeContracts, deployL1Contract, hashPayload } from './utils.js'; const logger = createDebugLogger('aztec:canary'); @@ -37,9 +38,6 @@ const { SANDBOX_URL = 'http://localhost:8080', ETHEREUM_HOST = 'http://localhost export const MNEMONIC = 'test test test test test test test test test test test junk'; -const INITIAL_BALANCE = 333n; -const wethAmountToBridge = parseEther('1'); - const WETH9_ADDRESS = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); const DAI_ADDRESS = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); @@ -51,43 +49,48 @@ const ethRpcUrl = ETHEREUM_HOST; const hdAccount = mnemonicToAccount(MNEMONIC); const pxe = createPXEClient(pxeRpcUrl); -let wallet: Wallet; + +const wethAmountToBridge: bigint = parseEther('1'); +const uniswapFeeTier = 3000; +const minimumOutputAmount = 0n; +const deadline = 2 ** 32 - 1; // max uint32 - 1 /** * Deploys all l1 / l2 contracts * @param owner - Owner address. */ async function deployAllContracts( - owner: AztecAddress, + ownerWallet: AccountWallet, + ownerAddress: AztecAddress, publicClient: PublicClient, walletClient: WalletClient, ) { const l1ContractsAddresses = await getL1ContractAddresses(pxeRpcUrl); logger('Deploying DAI Portal, initializing and deploying l2 contract...'); - const daiContracts = await deployAndInitializeNonNativeL2TokenContracts( - wallet, + const daiContracts = await deployAndInitializeTokenAndBridgeContracts( + ownerWallet, walletClient, publicClient, - l1ContractsAddresses!.registry, - INITIAL_BALANCE, - owner, + l1ContractsAddresses!.registryAddress, + ownerAddress, DAI_ADDRESS, ); - const daiL2Contract = daiContracts.l2Contract; + const daiL2Contract = daiContracts.token; + const daiL2Bridge = daiContracts.bridge; const daiContract = daiContracts.underlyingERC20; const daiTokenPortalAddress = daiContracts.tokenPortalAddress; logger('Deploying WETH Portal, initializing and deploying l2 contract...'); - const wethContracts = await deployAndInitializeNonNativeL2TokenContracts( - wallet, + const wethContracts = await deployAndInitializeTokenAndBridgeContracts( + ownerWallet, walletClient, publicClient, - l1ContractsAddresses!.registry, - INITIAL_BALANCE, - owner, + l1ContractsAddresses!.registryAddress, + ownerAddress, WETH9_ADDRESS, ); - const wethL2Contract = wethContracts.l2Contract; + const wethL2Contract = wethContracts.token; + const wethL2Bridge = wethContracts.bridge; const wethContract = wethContracts.underlyingERC20; const wethTokenPortal = wethContracts.tokenPortal; const wethTokenPortalAddress = wethContracts.tokenPortalAddress; @@ -107,24 +110,23 @@ async function deployAllContracts( }); // deploy l2 uniswap contract and attach to portal - const tx = UniswapContract.deploy(pxe).send({ - portalContract: uniswapPortalAddress, - }); - await tx.isMined(); - const receipt = await tx.getReceipt(); - const uniswapL2Contract = await UniswapContract.at(receipt.contractAddress!, wallet); + const uniswapL2Contract = await UniswapContract.deploy(ownerWallet) + .send({ portalContract: uniswapPortalAddress }) + .deployed(); await uniswapL2Contract.attach(uniswapPortalAddress); await uniswapPortal.write.initialize( - [l1ContractsAddresses!.registry.toString(), uniswapL2Contract.address.toString()], + [l1ContractsAddresses!.registryAddress.toString(), uniswapL2Contract.address.toString()], {} as any, ); return { daiL2Contract, + daiL2Bridge, daiContract, daiTokenPortalAddress, wethL2Contract, + wethL2Bridge, wethContract, wethTokenPortal, wethTokenPortalAddress, @@ -134,34 +136,85 @@ async function deployAllContracts( }; } -const getL2BalanceOf = async (owner: AztecAddress, l2Contract: NonNativeTokenContract) => { - return await l2Contract.methods.getBalance(owner).view({ from: owner }); +const getL2PrivateBalanceOf = async (owner: AztecAddress, l2Contract: TokenContract) => { + return await l2Contract.methods.balance_of_private(owner).view({ from: owner }); +}; + +const getL2PublicBalanceOf = async (owner: AztecAddress, l2Contract: TokenContract) => { + return await l2Contract.methods.balance_of_public(owner).view(); +}; + +const expectPrivateBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: TokenContract) => { + const balance = await getL2PrivateBalanceOf(owner, l2Contract); + logger(`Account ${owner} balance: ${balance}. Expected to be: ${expectedBalance}`); + expect(balance).toBe(expectedBalance); }; -const expectBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: NonNativeTokenContract) => { - const balance = await getL2BalanceOf(owner, l2Contract); +const expectPublicBalanceOnL2 = async (owner: AztecAddress, expectedBalance: bigint, l2Contract: TokenContract) => { + const balance = await getL2PublicBalanceOf(owner, l2Contract); logger(`Account ${owner} balance: ${balance}. Expected to be: ${expectedBalance}`); expect(balance).toBe(expectedBalance); }; +const generateClaimSecret = async () => { + const secret = Fr.random(); + const secretHash = await computeMessageSecretHash(secret); + return [secret, secretHash]; +}; + const transferWethOnL2 = async ( - wethL2Contract: NonNativeTokenContract, + wethL2Contract: TokenContract, ownerAddress: AztecAddress, receiver: AztecAddress, transferAmount: bigint, ) => { - const transferTx = wethL2Contract.methods.transfer(transferAmount, receiver).send(); - await transferTx.isMined(); - const transferReceipt = await transferTx.getReceipt(); - expect(transferReceipt.status).toBe(TxStatus.MINED); - logger(`WETH to L2 Transfer Receipt status: ${transferReceipt.status}`); + const transferTx = wethL2Contract.methods.transfer_public(ownerAddress, receiver, transferAmount, 0).send(); + const receipt = await transferTx.wait(); + expect(receipt.status).toBe(TxStatus.MINED); +}; + +const consumeMessageOnAztecAndMintSecretly = async ( + l2Bridge: TokenBridgeContract, + bridgeAmount: bigint, + secretHashForRedeemingMintedNotes: Fr, + canceller: EthAddress, + messageKey: Fr, + secretForL2MessageConsumption: Fr, +) => { + logger('Consuming messages on L2 secretively'); + // Call the mint tokens function on the Aztec.nr contract + const consumptionTx = l2Bridge.methods + .claim_private( + bridgeAmount, + secretHashForRedeemingMintedNotes, + canceller, + messageKey, + secretForL2MessageConsumption, + ) + .send(); + const consumptionReceipt = await consumptionTx.wait(); + expect(consumptionReceipt.status).toBe(TxStatus.MINED); +}; + +const redeemShieldPrivatelyOnL2 = async ( + l2Contract: TokenContract, + to: AztecAddress, + shieldAmount: bigint, + secret: Fr, +) => { + logger('Spending commitment in private call'); + const privateTx = l2Contract.methods.redeem_shield(to, shieldAmount, secret).send(); + const privateReceipt = await privateTx.wait(); + expect(privateReceipt.status).toBe(TxStatus.MINED); }; -// TODO(2167) - Fix this! Adapt to new portal standard and new cross chain harness. -describe.skip('uniswap_trade_on_l1_from_l2', () => { - let ethAccount = EthAddress.ZERO; +describe('uniswap_trade_on_l1_from_l2', () => { let publicClient: PublicClient; let walletClient: WalletClient; + let ownerWallet: AccountWallet; + let ownerAddress: AztecAddress; + let ownerEthAddress: EthAddress; + beforeAll(async () => { await waitForSandbox(pxe); @@ -179,22 +232,24 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); } - ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); + ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); }, 60_000); it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { logger('Running L1/L2 messaging test on HTTP interface.'); - [wallet] = await getSandboxAccountsWallets(pxe); - const accounts = await wallet.getRegisteredAccounts(); - const owner = accounts[0].address; + [ownerWallet] = await getSandboxAccountsWallets(pxe); + const accounts = await ownerWallet.getRegisteredAccounts(); + ownerAddress = accounts[0].address; const receiver = accounts[1].address; - const result = await deployAllContracts(owner, publicClient, walletClient); + const result = await deployAllContracts(ownerWallet, ownerAddress, publicClient, walletClient); const { daiL2Contract, + daiL2Bridge, daiContract, daiTokenPortalAddress, wethL2Contract, + wethL2Bridge, wethContract, wethTokenPortal, wethTokenPortalAddress, @@ -202,7 +257,7 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { uniswapPortal, } = result; - const ownerInitialBalance = await wethL2Contract.methods.getBalance(owner).view(); + const ownerInitialBalance = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); logger(`Owner's initial L2 WETH balance: ${ownerInitialBalance}`); // Give me some WETH so I can deposit to L2 and do the swap... @@ -212,134 +267,153 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { value: parseEther('1'), }); - const meBeforeBalance = await wethContract.read.balanceOf([ethAccount.toString()]); + const wethL1BeforeBalance = await wethContract.read.balanceOf([ownerEthAddress.toString()]); // 1. Approve weth to be bridged await wethContract.write.approve([wethTokenPortalAddress.toString(), wethAmountToBridge], {} as any); // 2. Deposit weth into the portal and move to L2 // generate secret - const secret = Fr.random(); - const secretHash = await computeMessageSecretHash(secret); - const secretString = `0x${secretHash.toBuffer().toString('hex')}` as `0x${string}`; - const deadline = 2 ** 32 - 1; // max uint32 - 1 + const [secretForMintingWeth, secretHashForMintingWeth] = await generateClaimSecret(); + const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await generateClaimSecret(); logger('Sending messages to L1 portal'); - const args = [owner.toString(), wethAmountToBridge, deadline, secretString, ethAccount.toString()] as const; - const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztecPublic(args, { - account: ethAccount.toString(), + const args = [ + wethAmountToBridge, + secretHashForRedeemingWeth.toString(true), + ownerEthAddress.toString(), + deadline, + secretHashForMintingWeth.toString(true), + ] as const; + const { result: messageKeyHex } = await wethTokenPortal.simulate.depositToAztecPrivate(args, { + account: ownerEthAddress.toString(), } as any); - await wethTokenPortal.write.depositToAztecPublic(args, {} as any); + await wethTokenPortal.write.depositToAztecPrivate(args, {} as any); - const currentL1Balance = await wethContract.read.balanceOf([ethAccount.toString()]); - logger(`Initial Balance: ${currentL1Balance}. Should be: ${meBeforeBalance - wethAmountToBridge}`); - expect(currentL1Balance).toBe(meBeforeBalance - wethAmountToBridge); + const currentL1Balance = await wethContract.read.balanceOf([ownerEthAddress.toString()]); + logger(`Initial Balance: ${currentL1Balance}. Should be: ${wethL1BeforeBalance - wethAmountToBridge}`); + expect(currentL1Balance).toBe(wethL1BeforeBalance - wethAmountToBridge); const messageKey = Fr.fromString(messageKeyHex); // Wait for the archiver to process the message await sleep(5000); // send a transfer tx to force through rollup with the message included - const transferAmount = 1n; - await transferWethOnL2(wethL2Contract, owner, receiver, transferAmount); + await transferWethOnL2(wethL2Contract, ownerAddress, receiver, 0n); // 3. Claim WETH on L2 logger('Minting weth on L2'); - // Call the mint tokens function on the Aztec.nr contract - const consumptionTx = wethL2Contract.methods - .mint(wethAmountToBridge, owner, messageKey, secret, ethAccount.toField()) - .send(); - await consumptionTx.isMined(); - const consumptionReceipt = await consumptionTx.getReceipt(); - expect(consumptionReceipt.status).toBe(TxStatus.MINED); - logger(`Consumption Receipt status: ${consumptionReceipt.status}`); - await expectBalanceOnL2(owner, wethAmountToBridge + BigInt(ownerInitialBalance) - transferAmount, wethL2Contract); + await consumeMessageOnAztecAndMintSecretly( + wethL2Bridge, + wethAmountToBridge, + secretHashForRedeemingWeth, + ownerEthAddress, + messageKey, + secretForMintingWeth, + ); + await redeemShieldPrivatelyOnL2(wethL2Contract, ownerAddress, wethAmountToBridge, secretForRedeemingWeth); + await expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge + BigInt(ownerInitialBalance), wethL2Contract); // Store balances - const wethBalanceBeforeSwap = await getL2BalanceOf(owner, wethL2Contract); - const daiBalanceBeforeSwap = await getL2BalanceOf(owner, daiL2Contract); - - // 4. Send L2 to L1 message to withdraw funds and another message to swap assets. - logger('Send L2 tx to withdraw WETH to uniswap portal and send message to swap assets on L1'); - // recipient is the uniswap portal - const minimumOutputAmount = 0n; - - const withdrawTx = uniswapL2Contract.methods + const wethL2BalanceBeforeSwap = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); + const daiL2BalanceBeforeSwap = await getL2PrivateBalanceOf(ownerAddress, daiL2Contract); + + // 4. Owner gives uniswap approval to unshield funds to self on its behalf + logger('Approving uniswap to unshield funds to self on my behalf'); + const nonceForWETHUnshieldApproval = new Fr(2n); + const unshieldToUniswapMessageHash = await hashPayload([ + uniswapL2Contract.address.toField(), + wethL2Contract.address.toField(), + FunctionSelector.fromSignature('unshield((Field),(Field),Field,Field)').toField(), + ownerAddress.toField(), + uniswapL2Contract.address.toField(), + new Fr(wethAmountToBridge), + nonceForWETHUnshieldApproval, + ]); + await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); + + // 5. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. + logger('Withdrawing weth to L1 and sending message to swap to dai'); + + const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = await generateClaimSecret(); + const [secretForRedeemingDai, secretHashForRedeemingDai] = await generateClaimSecret(); + + const withdrawReceipt = await uniswapL2Contract.methods .swap( - wethL2Contract.address.toField(), + wethL2Contract.address, + wethL2Bridge.address, wethAmountToBridge, - new Fr(3000), - daiL2Contract.address.toField(), - new Fr(minimumOutputAmount), - owner, - owner, - secretHash, - new Fr(2 ** 32 - 1), - ethAccount.toField(), - ethAccount.toField(), + daiL2Bridge.address, + nonceForWETHUnshieldApproval, + uniswapFeeTier, + minimumOutputAmount, + secretHashForRedeemingDai, + secretHashForDepositingSwappedDai, + deadline, + ownerEthAddress, + ownerEthAddress, ) - .send(); - await withdrawTx.isMined(); - const withdrawReceipt = await withdrawTx.getReceipt(); + .send() + .wait(); expect(withdrawReceipt.status).toBe(TxStatus.MINED); - logger(`Withdraw receipt status: ${withdrawReceipt.status}`); + // ensure that user's funds were burnt + await expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge, wethL2Contract); + // ensure that uniswap contract didn't eat the funds. + await expectPublicBalanceOnL2(uniswapL2Contract.address, 0n, wethL2Contract); - // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) - await expectBalanceOnL2(owner, INITIAL_BALANCE - transferAmount, wethL2Contract); - - // 5. Consume L2 to L1 message by calling uniswapPortal.swap() + // 6. Consume L2 to L1 message by calling uniswapPortal.swap() logger('Execute withdraw and swap on the uniswapPortal!'); - const daiBalanceOfPortalBefore = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); - logger(`DAI balance of portal: ${daiBalanceOfPortalBefore}`); + const daiL1BalanceOfPortalBeforeSwap = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); const swapArgs = [ wethTokenPortalAddress.toString(), wethAmountToBridge, - 3000, + uniswapFeeTier, daiTokenPortalAddress.toString(), minimumOutputAmount, - owner.toString(), - secretString, + secretHashForRedeemingDai.toString(true), + secretHashForDepositingSwappedDai.toString(true), deadline, - ethAccount.toString(), + ownerEthAddress.toString(), true, ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swap(swapArgs, { - account: ethAccount.toString(), + const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPrivate(swapArgs, { + account: ownerEthAddress.toString(), } as any); + // this should also insert a message into the inbox. - await uniswapPortal.write.swap(swapArgs, {} as any); + await uniswapPortal.write.swapPrivate(swapArgs, {} as any); const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); + // weth was swapped to dai and send to portal - const daiBalanceOfPortalAfter = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); - expect(daiBalanceOfPortalAfter).toBeGreaterThan(daiBalanceOfPortalBefore); + const daiL1BalanceOfPortalAfter = await daiContract.read.balanceOf([daiTokenPortalAddress.toString()]); logger( - `DAI balance in Portal: ${daiBalanceOfPortalAfter} should be bigger than ${daiBalanceOfPortalBefore}. ${ - daiBalanceOfPortalAfter > daiBalanceOfPortalBefore - }`, + `DAI balance in Portal: balance after (${daiL1BalanceOfPortalAfter}) should be bigger than balance before (${daiL1BalanceOfPortalBeforeSwap})`, ); - const daiAmountToBridge = daiBalanceOfPortalAfter - daiBalanceOfPortalBefore; + expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); + const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); // Wait for the archiver to process the message await sleep(5000); // send a transfer tx to force through rollup with the message included - await transferWethOnL2(wethL2Contract, owner, receiver, transferAmount); + await transferWethOnL2(wethL2Contract, ownerAddress, receiver, 0n); - // 6. claim dai on L2 + // 7. claim dai on L2 logger('Consuming messages to mint dai on L2'); - // Call the mint tokens function on the Aztec.nr contract - const daiMintTx = daiL2Contract.methods - .mint(daiAmountToBridge, owner, depositDaiMessageKey, secret, ethAccount.toField()) - .send(); - await daiMintTx.isMined(); - const daiMintTxReceipt = await daiMintTx.getReceipt(); - expect(daiMintTxReceipt.status).toBe(TxStatus.MINED); - logger(`DAI mint TX status: ${daiMintTxReceipt.status}`); - await expectBalanceOnL2(owner, INITIAL_BALANCE + BigInt(daiAmountToBridge), daiL2Contract); - - const wethBalanceAfterSwap = await getL2BalanceOf(owner, wethL2Contract); - const daiBalanceAfterSwap = await getL2BalanceOf(owner, daiL2Contract); - - logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); - logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString()); + await consumeMessageOnAztecAndMintSecretly( + daiL2Bridge, + daiAmountToBridge, + secretHashForRedeemingDai, + ownerEthAddress, + depositDaiMessageKey, + secretForDepositingSwappedDai, + ); + await redeemShieldPrivatelyOnL2(daiL2Contract, ownerAddress, daiAmountToBridge, secretForRedeemingDai); + await expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge, daiL2Contract); + + const wethL2BalanceAfterSwap = await getL2PrivateBalanceOf(ownerAddress, wethL2Contract); + const daiL2BalanceAfterSwap = await getL2PrivateBalanceOf(ownerAddress, daiL2Contract); + + logger('WETH balance before swap: ', wethL2BalanceBeforeSwap.toString()); + logger('DAI balance before swap : ', daiL2BalanceBeforeSwap.toString()); logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethBalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiBalanceAfterSwap.toString()); - }, 240_000); + logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); + logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); + }, 140_000); }); diff --git a/yarn-project/canary/src/utils.ts b/yarn-project/canary/src/utils.ts index d5404a0fdae..4d89e1a7b4e 100644 --- a/yarn-project/canary/src/utils.ts +++ b/yarn-project/canary/src/utils.ts @@ -1,35 +1,55 @@ -import { AztecAddress, EthAddress, Wallet } from '@aztec/aztec.js'; +import { AztecAddress, EthAddress, Fr, TxStatus, Wallet } from '@aztec/aztec.js'; +import { CircuitsWasm, GeneratorIndex } from '@aztec/circuits.js'; +import { pedersenPlookupCompressWithHashIndex } from '@aztec/circuits.js/barretenberg'; import { PortalERC20Abi, PortalERC20Bytecode, TokenPortalAbi, TokenPortalBytecode } from '@aztec/l1-artifacts'; -import { NonNativeTokenContract } from '@aztec/noir-contracts/types'; +import { TokenBridgeContract, TokenContract } from '@aztec/noir-contracts/types'; import type { Abi, Narrow } from 'abitype'; import { Account, Chain, Hex, HttpTransport, PublicClient, WalletClient, getContract } from 'viem'; /** - * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract and attach is to the portal. - * @param wallet - A wallet instance. + * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract, its L2 bridge contract and attach is to the portal. + * @param wallet - the wallet instance * @param walletClient - A viem WalletClient. * @param publicClient - A viem PublicClient. * @param rollupRegistryAddress - address of rollup registry to pass to initialize the token portal - * @param initialBalance - initial balance of the owner of the L2 contract * @param owner - owner of the L2 contract * @param underlyingERC20Address - address of the underlying ERC20 contract to use (if none supplied, it deploys one) - * @returns l2 contract instance, token portal instance, token portal address and the underlying ERC20 instance + * @returns l2 contract instance, bridge contract instance, token portal instance, token portal address and the underlying ERC20 instance */ -export async function deployAndInitializeNonNativeL2TokenContracts( +export async function deployAndInitializeTokenAndBridgeContracts( wallet: Wallet, walletClient: WalletClient, publicClient: PublicClient, rollupRegistryAddress: EthAddress, - initialBalance = 0n, - owner = AztecAddress.ZERO, + owner: AztecAddress, underlyingERC20Address?: EthAddress, -) { - // deploy underlying contract if no address supplied +): Promise<{ + /** + * The L2 token contract instance. + */ + token: TokenContract; + /** + * The L2 bridge contract instance. + */ + bridge: TokenBridgeContract; + /** + * The token portal contract address. + */ + tokenPortalAddress: EthAddress; + /** + * The token portal contract instance + */ + tokenPortal: any; + /** + * The underlying ERC20 contract instance. + */ + underlyingERC20: any; +}> { if (!underlyingERC20Address) { underlyingERC20Address = await deployL1Contract(walletClient, publicClient, PortalERC20Abi, PortalERC20Bytecode); } - const underlyingERC20: any = getContract({ + const underlyingERC20 = getContract({ address: underlyingERC20Address.toString(), abi: PortalERC20Abi, walletClient, @@ -38,29 +58,65 @@ export async function deployAndInitializeNonNativeL2TokenContracts( // deploy the token portal const tokenPortalAddress = await deployL1Contract(walletClient, publicClient, TokenPortalAbi, TokenPortalBytecode); - const tokenPortal: any = getContract({ + const tokenPortal = getContract({ address: tokenPortalAddress.toString(), abi: TokenPortalAbi, walletClient, publicClient, }); - // deploy l2 contract and attach to portal - const tx = NonNativeTokenContract.deploy(wallet, initialBalance, owner).send({ + // deploy l2 token + const deployTx = TokenContract.deploy(wallet).send(); + + // deploy l2 token bridge and attach to the portal + const bridgeTx = TokenBridgeContract.deploy(wallet).send({ portalContract: tokenPortalAddress, + contractAddressSalt: Fr.random(), }); - await tx.isMined(); - const receipt = await tx.getReceipt(); - const l2Contract = await NonNativeTokenContract.at(receipt.contractAddress!, wallet); - await l2Contract.attach(tokenPortalAddress); - const l2TokenAddress = l2Contract.address.toString() as `0x${string}`; + + // now wait for the deploy txs to be mined. This way we send all tx in the same rollup. + const deployReceipt = await deployTx.wait(); + if (deployReceipt.status !== TxStatus.MINED) throw new Error(`Deploy token tx status is ${deployReceipt.status}`); + const token = await TokenContract.at(deployReceipt.contractAddress!, wallet); + + const bridgeReceipt = await bridgeTx.wait(); + if (bridgeReceipt.status !== TxStatus.MINED) throw new Error(`Deploy bridge tx status is ${bridgeReceipt.status}`); + const bridge = await TokenBridgeContract.at(bridgeReceipt.contractAddress!, wallet); + await bridge.attach(tokenPortalAddress); + const bridgeAddress = bridge.address.toString() as `0x${string}`; + + // initialize l2 token + const initializeTx = token.methods._initialize(owner).send(); + + // initialize bridge + const initializeBridgeTx = bridge.methods._initialize(token.address).send(); + + // now we wait for the txs to be mined. This way we send all tx in the same rollup. + const initializeReceipt = await initializeTx.wait(); + if (initializeReceipt.status !== TxStatus.MINED) + throw new Error(`Initialize token tx status is ${initializeReceipt.status}`); + if ((await token.methods.admin().view()) !== owner.toBigInt()) throw new Error(`Token admin is not ${owner}`); + + const initializeBridgeReceipt = await initializeBridgeTx.wait(); + if (initializeBridgeReceipt.status !== TxStatus.MINED) + throw new Error(`Initialize token bridge tx status is ${initializeBridgeReceipt.status}`); + if ((await bridge.methods.token().view()) !== token.address.toBigInt()) + throw new Error(`Bridge token is not ${token.address}`); + + // make the bridge a minter on the token: + const makeMinterTx = token.methods.set_minter(bridge.address, true).send(); + const makeMinterReceipt = await makeMinterTx.wait(); + if (makeMinterReceipt.status !== TxStatus.MINED) + throw new Error(`Make bridge a minter tx status is ${makeMinterReceipt.status}`); + if ((await token.methods.is_minter(bridge.address).view()) === 1n) throw new Error(`Bridge is not a minter`); // initialize portal await tokenPortal.write.initialize( - [rollupRegistryAddress.toString(), underlyingERC20Address.toString(), l2TokenAddress], + [rollupRegistryAddress.toString(), underlyingERC20Address.toString(), bridgeAddress], {} as any, ); - return { l2Contract, tokenPortalAddress, tokenPortal, underlyingERC20 }; + + return { token, bridge, tokenPortalAddress, tokenPortal, underlyingERC20 }; } /** @@ -93,3 +149,16 @@ export async function deployL1Contract( return EthAddress.fromString(receipt.contractAddress!); } + +/** + * Hash a payload to generate a signature on an account contract + * @param payload - payload to hash + * @returns the hashed message + */ +export const hashPayload = async (payload: Fr[]) => { + return pedersenPlookupCompressWithHashIndex( + await CircuitsWasm.get(), + payload.map(fr => fr.toBuffer()), + GeneratorIndex.SIGNATURE_PAYLOAD, + ); +}; diff --git a/yarn-project/canary/tsconfig.json b/yarn-project/canary/tsconfig.json index 36f2fd7fa21..837b3f136a5 100644 --- a/yarn-project/canary/tsconfig.json +++ b/yarn-project/canary/tsconfig.json @@ -19,6 +19,9 @@ "skipLibCheck": true }, "references": [ + { + "path": "../circuits.js" + }, { "path": "../aztec.js" }, diff --git a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts index 22ef739d9bb..4eba012fa93 100644 --- a/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/fixtures/cross_chain_test_harness.ts @@ -12,7 +12,7 @@ import { PXE, TxStatus } from '@aztec/types'; import { Chain, HttpTransport, PublicClient, getContract } from 'viem'; -import { deployAndInitializeStandardizedTokenAndBridgeContracts } from './utils.js'; +import { deployAndInitializeTokenAndBridgeContracts } from './utils.js'; /** * A Class for testing cross chain interactions, contains common interactions @@ -43,7 +43,7 @@ export class CrossChainTestHarness { // Deploy and initialize all required contracts logger('Deploying and initializing token, portal and its bridge...'); - const contracts = await deployAndInitializeStandardizedTokenAndBridgeContracts( + const contracts = await deployAndInitializeTokenAndBridgeContracts( wallet, walletClient, publicClient, @@ -154,11 +154,11 @@ export class CrossChainTestHarness { this.logger('Sending messages to L1 portal to be consumed publicly'); const args = [ - this.ownerAddress.toString(), bridgeAmount, + this.ownerAddress.toString(), + this.ethAccount.toString(), deadline, secretHash.toString(true), - this.ethAccount.toString(), ] as const; const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPublic(args, { account: this.ethAccount.toString(), @@ -181,10 +181,10 @@ export class CrossChainTestHarness { this.logger('Sending messages to L1 portal to be consumed privately'); const args = [ bridgeAmount, - deadline, - secretHashForL2MessageConsumption.toString(true), secretHashForRedeemingMintedNotes.toString(true), this.ethAccount.toString(), + deadline, + secretHashForL2MessageConsumption.toString(true), ] as const; const { result: messageKeyHex } = await this.tokenPortal.simulate.depositToAztecPrivate(args, { account: this.ethAccount.toString(), @@ -264,8 +264,12 @@ export class CrossChainTestHarness { expect(balance).toBe(expectedBalance); } + async getL2PublicBalanceOf(owner: AztecAddress) { + return await this.l2Token.methods.balance_of_public(owner).view(); + } + async expectPublicBalanceOnL2(owner: AztecAddress, expectedBalance: bigint) { - const balance = await this.l2Token.methods.balance_of_public(owner).view({ from: owner }); + const balance = await this.getL2PublicBalanceOf(owner); expect(balance).toBe(expectedBalance); } diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 43aae8bc26f..7b8e3c18e07 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -351,7 +351,7 @@ export function getLogger() { * @param underlyingERC20Address - address of the underlying ERC20 contract to use (if none supplied, it deploys one) * @returns l2 contract instance, bridge contract instance, token portal instance, token portal address and the underlying ERC20 instance */ -export async function deployAndInitializeStandardizedTokenAndBridgeContracts( +export async function deployAndInitializeTokenAndBridgeContracts( wallet: Wallet, walletClient: WalletClient, publicClient: PublicClient, diff --git a/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts b/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts index 2c021dba910..7c0878ae32c 100644 --- a/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts +++ b/yarn-project/end-to-end/src/integration_archiver_l1_to_l2.test.ts @@ -94,7 +94,7 @@ describe('archiver integration with l1 to l2 messages', () => { const mintAmount = 100n; logger('Sending messages to L1 portal'); - const args = [owner.toString(), mintAmount, deadline, secretString, ethAccount.toString()] as const; + const args = [mintAmount, owner.toString(), ethAccount.toString(), deadline, secretString] as const; await tokenPortal.write.depositToAztecPublic(args, {} as any); expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(1000000n - mintAmount); @@ -106,7 +106,7 @@ describe('archiver integration with l1 to l2 messages', () => { // cancel the message logger('cancelling the l1 to l2 message'); - const argsCancel = [owner.toString(), 100n, deadline, secretString, 0n] as const; + const argsCancel = [mintAmount, owner.toString(), deadline, secretString, 0n] as const; await tokenPortal.write.cancelL1ToAztecMessagePublic(argsCancel, { gas: 1_000_000n } as any); expect(await underlyingERC20.read.balanceOf([ethAccount.toString()])).toBe(1000000n); // let archiver sync up diff --git a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts index d3d30eee095..4f25b6bbbc9 100644 --- a/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts +++ b/yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts @@ -1,16 +1,17 @@ import { AztecNodeService } from '@aztec/aztec-node'; -import { AztecAddress, CheatCodes, Fr, Wallet } from '@aztec/aztec.js'; -import { DeployL1Contracts, deployL1Contract } from '@aztec/ethereum'; +import { AccountWallet, AztecAddress } from '@aztec/aztec.js'; +import { Fr, FunctionSelector } from '@aztec/circuits.js'; +import { deployL1Contract } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; import { DebugLogger } from '@aztec/foundation/log'; import { UniswapPortalAbi, UniswapPortalBytecode } from '@aztec/l1-artifacts'; import { UniswapContract } from '@aztec/noir-contracts/types'; -import { CompleteAddress, PXE, TxStatus } from '@aztec/types'; +import { PXE, TxStatus } from '@aztec/types'; import { getContract, parseEther } from 'viem'; import { CrossChainTestHarness } from './fixtures/cross_chain_test_harness.js'; -import { delay, setup } from './fixtures/utils.js'; +import { delay, hashPayload, setup } from './fixtures/utils.js'; // PSA: This tests works on forked mainnet. There is a dump of the data in `dumpedState` such that we // don't need to burn through RPC requests. @@ -26,23 +27,18 @@ const EXPECTED_FORKED_BLOCK = 0; //17514288; process.env.SEARCH_START_BLOCK = EXPECTED_FORKED_BLOCK.toString(); // Should mint WETH on L2, swap to DAI using L1 Uniswap and mint this DAI back on L2 -// TODO(2167) - Fix this! Adapt to new portal standard and new cross chain harness. -describe.skip('uniswap_trade_on_l1_from_l2', () => { +describe('uniswap_trade_on_l1_from_l2', () => { const WETH9_ADDRESS: EthAddress = EthAddress.fromString('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); const DAI_ADDRESS: EthAddress = EthAddress.fromString('0x6B175474E89094C44Da98b954EedeAC495271d0F'); let aztecNode: AztecNodeService | undefined; let pxe: PXE; - let wallet: Wallet; - let accounts: CompleteAddress[]; let logger: DebugLogger; - let cheatCodes: CheatCodes; let teardown: () => Promise; - let ethAccount: EthAddress; - let owner: AztecAddress; - const initialBalance = 10n; - const wethAmountToBridge = parseEther('1'); + let ownerWallet: AccountWallet; + let ownerAddress: AztecAddress; + let ownerEthAddress: EthAddress; let daiCrossChainHarness: CrossChainTestHarness; let wethCrossChainHarness: CrossChainTestHarness; @@ -51,13 +47,21 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { let uniswapPortalAddress: EthAddress; let uniswapL2Contract: UniswapContract; - beforeEach(async () => { - let deployL1ContractsValues: DeployL1Contracts; - ({ teardown, aztecNode, pxe, deployL1ContractsValues, accounts, logger, wallet, cheatCodes } = await setup( - 2, - dumpedState, - )); + const wethAmountToBridge = parseEther('1'); + const uniswapFeeTier = 3000n; + const minimumOutputAmount = 0n; + beforeEach(async () => { + const { + teardown: teardown_, + aztecNode: aztecNode_, + pxe: pxe_, + deployL1ContractsValues, + accounts, + logger: logger_, + wallet, + cheatCodes, + } = await setup(2, dumpedState); const walletClient = deployL1ContractsValues.walletClient; const publicClient = deployL1ContractsValues.publicClient; @@ -65,8 +69,13 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { throw new Error('This test must be run on a fork of mainnet with the expected fork block'); } - ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); - owner = accounts[0].address; + aztecNode = aztecNode_; + pxe = pxe_; + logger = logger_; + teardown = teardown_; + ownerWallet = wallet; + ownerAddress = accounts[0].address; + ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]); logger('Deploying DAI Portal, initializing and deploying l2 contract...'); daiCrossChainHarness = await CrossChainTestHarness.new( @@ -78,7 +87,6 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { logger, cheatCodes, DAI_ADDRESS, - initialBalance, ); logger('Deploying WETH Portal, initializing and deploying l2 contract...'); @@ -91,7 +99,6 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { logger, cheatCodes, WETH9_ADDRESS, - initialBalance, ); logger('Deploy Uniswap portal on L1 and L2...'); @@ -103,11 +110,7 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { publicClient, }); // deploy l2 uniswap contract and attach to portal - const tx = UniswapContract.deploy(pxe).send({ portalContract: uniswapPortalAddress }); - await tx.isMined({ interval: 0.1 }); - const receipt = await tx.getReceipt(); - expect(receipt.status).toEqual(TxStatus.MINED); - uniswapL2Contract = await UniswapContract.at(receipt.contractAddress!, wallet); + uniswapL2Contract = await UniswapContract.deploy(wallet).send({ portalContract: uniswapPortalAddress }).deployed(); await uniswapL2Contract.attach(uniswapPortalAddress); await uniswapPortal.write.initialize( @@ -127,116 +130,145 @@ describe.skip('uniswap_trade_on_l1_from_l2', () => { }); it('should uniswap trade on L1 from L2 funds privately (swaps WETH -> DAI)', async () => { - const meBeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ethAccount); + const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress); // 1. Approve and deposit weth to the portal and move to L2 - const [secretForL2MessageConsumption, secretHashForL2MessageConsumption] = - await wethCrossChainHarness.generateClaimSecret(); - const [, secretHashForRedeemingMintedNotes] = await wethCrossChainHarness.generateClaimSecret(); + const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret(); + const [secretForRedeemingWeth, secretHashForRedeemingWeth] = await wethCrossChainHarness.generateClaimSecret(); const messageKey = await wethCrossChainHarness.sendTokensToPortalPrivate( wethAmountToBridge, - secretHashForL2MessageConsumption, - secretHashForRedeemingMintedNotes, + secretHashForMintingWeth, + secretHashForRedeemingWeth, + ); + // funds transferred from owner to token portal + expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe(wethL1BeforeBalance - wethAmountToBridge); + expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe( + wethAmountToBridge, ); - expect(await wethCrossChainHarness.getL1BalanceOf(ethAccount)).toBe(meBeforeBalance - wethAmountToBridge); // Wait for the archiver to process the message await delay(5000); - // send a transfer tx to force through rollup with the message included - const transferAmount = 1n; - await wethCrossChainHarness.performL2Transfer(transferAmount); + // Perform an unrelated transaction on L2 to progress the rollup. Here we mint public tokens. + const unrelatedMintAmount = 1n; + await wethCrossChainHarness.mintTokensPublicOnL2(unrelatedMintAmount); + await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, unrelatedMintAmount); - // 3. Claim WETH on L2 + // 2. Claim WETH on L2 logger('Minting weth on L2'); await wethCrossChainHarness.consumeMessageOnAztecAndMintSecretly( wethAmountToBridge, + secretHashForRedeemingWeth, messageKey, - secretForL2MessageConsumption, - secretHashForRedeemingMintedNotes, + secretForMintingWeth, ); - await wethCrossChainHarness.expectPrivateBalanceOnL2(owner, wethAmountToBridge + initialBalance - transferAmount); + await wethCrossChainHarness.redeemShieldPrivatelyOnL2(wethAmountToBridge, secretForRedeemingWeth); + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethAmountToBridge); // Store balances - const wethBalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(owner); - const daiBalanceBeforeSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(owner); - - // 4. Send L2 to L1 message to withdraw funds and another message to swap assets. - logger('Send L2 tx to withdraw WETH to uniswap portal and send message to swap assets on L1'); - const minimumOutputAmount = 0; - - const withdrawTx = uniswapL2Contract.methods + const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + + // 3. Owner gives uniswap approval to unshield funds to self on its behalf + logger('Approving uniswap to unshield funds to self on my behalf'); + const nonceForWETHUnshieldApproval = new Fr(2n); + const unshieldToUniswapMessageHash = await hashPayload([ + uniswapL2Contract.address.toField(), + wethCrossChainHarness.l2Token.address.toField(), + FunctionSelector.fromSignature('unshield((Field),(Field),Field,Field)').toField(), + ownerAddress.toField(), + uniswapL2Contract.address.toField(), + new Fr(wethAmountToBridge), + nonceForWETHUnshieldApproval, + ]); + await ownerWallet.createAuthWitness(Fr.fromBuffer(unshieldToUniswapMessageHash)); + + // 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets. + logger('Withdrawing weth to L1 and sending message to swap to dai'); + const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1 + const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] = + await daiCrossChainHarness.generateClaimSecret(); + const [secretForRedeemingDai, secretHashForRedeemingDai] = await daiCrossChainHarness.generateClaimSecret(); + + const withdrawReceipt = await uniswapL2Contract.methods .swap( - wethCrossChainHarness.l2Token.address.toField(), + wethCrossChainHarness.l2Token.address, + wethCrossChainHarness.l2Bridge.address, wethAmountToBridge, - new Fr(3000), - daiCrossChainHarness.l2Token.address.toField(), - new Fr(minimumOutputAmount), - owner, - owner, - secretHashForL2MessageConsumption, - new Fr(2 ** 32 - 1), - ethAccount.toField(), - ethAccount.toField(), + daiCrossChainHarness.l2Bridge.address, + nonceForWETHUnshieldApproval, + uniswapFeeTier, + minimumOutputAmount, + secretHashForRedeemingDai, + secretHashForDepositingSwappedDai, + deadlineForDepositingSwappedDai, + ownerEthAddress, + ownerEthAddress, ) - .send(); - await withdrawTx.isMined({ interval: 0.1 }); - const withdrawReceipt = await withdrawTx.getReceipt(); + .send() + .wait(); expect(withdrawReceipt.status).toBe(TxStatus.MINED); - - // check weth balance of owner on L2 (we first bridged `wethAmountToBridge` into L2 and now withdrew it!) - await wethCrossChainHarness.expectPrivateBalanceOnL2(owner, initialBalance - transferAmount); + // ensure that user's funds were burnt + await wethCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge); + // ensure that uniswap contract didn't eat the funds. + await wethCrossChainHarness.expectPublicBalanceOnL2(uniswapL2Contract.address, 0n); // 5. Consume L2 to L1 message by calling uniswapPortal.swap() logger('Execute withdraw and swap on the uniswapPortal!'); - const daiBalanceOfPortalBefore = await daiCrossChainHarness.getL1BalanceOf(daiCrossChainHarness.tokenPortalAddress); - const deadline = 2 ** 32 - 1; // max uint32 - 1 + const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); const swapArgs = [ wethCrossChainHarness.tokenPortalAddress.toString(), wethAmountToBridge, - 3000, + uniswapFeeTier, daiCrossChainHarness.tokenPortalAddress.toString(), minimumOutputAmount, - owner.toString(), - secretHashForL2MessageConsumption.toString(true), - deadline, - ethAccount.toString(), + secretHashForRedeemingDai.toString(true), + secretHashForDepositingSwappedDai.toString(true), + deadlineForDepositingSwappedDai, + ownerEthAddress.toString(), true, ] as const; - const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swap(swapArgs, { - account: ethAccount.toString(), + const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPrivate(swapArgs, { + account: ownerEthAddress.toString(), } as any); + // this should also insert a message into the inbox. - await uniswapPortal.write.swap(swapArgs, {} as any); + await uniswapPortal.write.swapPrivate(swapArgs, {} as any); const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex); + // weth was swapped to dai and send to portal - const daiBalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf(daiCrossChainHarness.tokenPortalAddress); - expect(daiBalanceOfPortalAfter).toBeGreaterThan(daiBalanceOfPortalBefore); - const daiAmountToBridge = BigInt(daiBalanceOfPortalAfter - daiBalanceOfPortalBefore); + const daiL1BalanceOfPortalAfter = await daiCrossChainHarness.getL1BalanceOf( + daiCrossChainHarness.tokenPortalAddress, + ); + expect(daiL1BalanceOfPortalAfter).toBeGreaterThan(daiL1BalanceOfPortalBeforeSwap); + const daiAmountToBridge = BigInt(daiL1BalanceOfPortalAfter - daiL1BalanceOfPortalBeforeSwap); // Wait for the archiver to process the message await delay(5000); // send a transfer tx to force through rollup with the message included - await wethCrossChainHarness.performL2Transfer(transferAmount); + await wethCrossChainHarness.performL2Transfer(0n); // 6. claim dai on L2 logger('Consuming messages to mint dai on L2'); await daiCrossChainHarness.consumeMessageOnAztecAndMintSecretly( daiAmountToBridge, + secretHashForRedeemingDai, depositDaiMessageKey, - secretForL2MessageConsumption, - secretHashForRedeemingMintedNotes, + secretForDepositingSwappedDai, ); - await daiCrossChainHarness.expectPrivateBalanceOnL2(owner, initialBalance + daiAmountToBridge); + await daiCrossChainHarness.redeemShieldPrivatelyOnL2(daiAmountToBridge, secretForRedeemingDai); + await daiCrossChainHarness.expectPrivateBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge); - const wethBalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(owner); - const daiBalanceAfterSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(owner); + const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); + const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PrivateBalanceOf(ownerAddress); - logger('WETH balance before swap: ', wethBalanceBeforeSwap.toString()); - logger('DAI balance before swap : ', daiBalanceBeforeSwap.toString()); + logger('WETH balance before swap: ', wethL2BalanceBeforeSwap.toString()); + logger('DAI balance before swap : ', daiL2BalanceBeforeSwap.toString()); logger('***** 🧚‍♀️ SWAP L2 assets on L1 Uniswap 🧚‍♀️ *****'); - logger('WETH balance after swap : ', wethBalanceAfterSwap.toString()); - logger('DAI balance after swap : ', daiBalanceAfterSwap.toString()); - }, 240_000); + logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString()); + logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString()); + }, 140_000); }); diff --git a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr index a6c1c6d1553..88b854ed1e3 100644 --- a/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/token_bridge_contract/src/main.nr @@ -138,6 +138,13 @@ contract TokenBridge { 1 } /// docs:end:exit_to_l1_private + + // View function that is callable by other contracts. + // Unconstrained can't be called by others since it isn't safe. + #[aztec(public)] + fn get_token() -> Field { + storage.token.read() + } // /// Unconstrained /// diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr new file mode 100644 index 00000000000..d77c68eca83 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/interfaces.nr @@ -0,0 +1,62 @@ +use dep::aztec::{ + context::{ PrivateContext, PublicContext, Context }, + oracle::compute_selector::compute_selector, + types::address::AztecAddress, +}; + +struct Token { + address: Field, +} + +impl Token { + fn at(address: Field) -> Self { + Self { address } + } + + fn transfer_public(self: Self, context: PublicContext, from: Field, to: Field, amount: Field, nonce: Field) { + let _transfer_return_values = context.call_public_function( + self.address, + compute_selector("transfer_public((Field),(Field),Field,Field)"), + [from, to, amount, nonce] + ); + } + + fn unshield(self: Self, context: &mut PrivateContext, from: Field, to: Field, amount: Field, nonce: Field) { + let _return_values = context.call_private_function( + self.address, + compute_selector("unshield((Field),(Field),Field,Field)"), + [from, to, amount, nonce] + ); + } +} + +struct TokenBridge { + address: Field, +} + +impl TokenBridge { + fn at(address: Field) -> Self { + Self { address } + } + + fn token(self: Self, context: PublicContext) -> Field { + let return_values = context.call_public_function(self.address, compute_selector("get_token()"), []); + return_values[0] + } + + fn claim_public(self: Self, context: PublicContext, to: Field, amount: Field, canceller: Field, msg_key: Field, secret: Field) { + let _return_values = context.call_public_function( + self.address, + compute_selector("claim_public((Field),Field,Field,Field,Field)"), + [to, amount, canceller, msg_key, secret] + ); + } + + fn exit_to_l1_public(self: Self, context: PublicContext, recipient: Field, amount: Field, callerOnL1: Field, nonce: Field) { + let _return_values = context.call_public_function( + self.address, + compute_selector("exit_to_l1_public((Field),Field,(Field),Field)"), + [recipient, amount, callerOnL1, nonce] + ); + } +} diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr index 475181bcb63..ccca6825078 100644 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr @@ -1,133 +1,155 @@ -mod non_native_token_interface; +mod interfaces; +mod util; -// Demonstrates how to send a message to a portal contract on L1. We use Uniswap here as it's the most typical example. +// Demonstrates how to use portal contracts to swap on L1 Uniswap with funds on L2 +// Has two separate flows for private and public respectively +// Uses the token bridge contract, which tells which input token we need to talk to and handles the exit funds to L1 contract Uniswap { - use dep::aztec::oracle::context::get_portal_address; + use dep::aztec::{ + auth::IS_VALID_SELECTOR, + context::{PrivateContext, PublicContext, Context}, + oracle::compute_selector::compute_selector, + oracle::context::get_portal_address, + state_vars::{map::Map, public_state::PublicState}, + types::address::{AztecAddress, EthereumAddress}, + types::type_serialization::bool_serialization::{ + BoolSerializationMethods, BOOL_SERIALIZED_LEN, + }, + types::type_serialization::field_serialization::{ + FieldSerializationMethods, FIELD_SERIALIZED_LEN, + }, + }; - use crate::non_native_token_interface::NonNativeTokenPrivateContextInterface; + use crate::interfaces::{Token, TokenBridge}; + use crate::util::{compute_message_hash, compute_swap_private_content_hash}; + + struct Storage { + // like with account contracts, stores the approval message on a slot and tracks if they are active + approved_action: Map>, + // tracks the nonce used to create the approval message for burning funds + // gets incremented each time after use to prevent replay attacks + nonce_for_burn_approval: PublicState, + } + + impl Storage { + fn init(context: Context) -> pub Self { + Storage { + approved_action: Map::new( + context, + 1, + |context, slot| { + PublicState::new(context, slot, BoolSerializationMethods) + }, + ), + nonce_for_burn_approval: PublicState::new(context, 2, FieldSerializationMethods), + } + } + } #[aztec(private)] fn constructor() {} - // What we need to make this nicer: - // 1. A way to access other (and my own) contract's portal addresses (we need many additional args for no good reason now) - // 2. don't pass all args manually to initial context - // 3. Variable length arrays for input arguments (8 not enough) - // 4. Should have storage for fixed values. - // 5. account abstraction using its own "custom" constants instead of using shared constants... - // 6. currently have to manually set the args array for calling other contracts which is inconvenient when using structs etc. - #[aztec(private)] fn swap( - inputAsset: Field, - inputAmount: Field, - uniswapFeeTier: Field, // which uniswap tier to use (eg 3000 for 0.3% fee) - outputAsset: Field, - minimumOutputAmount: Field, // minimum output amount to receive (slippage protection for the swap) - sender: Field, - recipient: Field, // receiver address of output asset after the swap - secretHash: Field, // for when l1 uniswap portal inserts the message to consume output assets on L2 - deadlineForL1ToL2Message: Field, // for when l1 uniswap portal inserts the message to consume output assets on L2 - cancellerForL1ToL2Message: Field, // L1 address of who can cancel the message to consume assets on L2. - callerOnL1: Field, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) + input_asset: AztecAddress, // since private, we pass here and later assert that this is as expected by input_bridge + input_asset_bridge: AztecAddress, + input_amount: Field, + output_asset_bridge: AztecAddress, + // params for using the unshield approval + nonce_for_unshield_approval: Field, + // params for the swap + uniswap_fee_tier: Field, // which uniswap tier to use (eg 3000 for 0.3% fee) + minimum_output_amount: Field, // minimum output amount to receive (slippage protection for the swap) + // params for the depositing output_asset back to Aztec + secret_hash_for_redeeming_minted_notes: Field, // secret hash used to redeem minted notes at a later time. This enables anyone to call this function and mint tokens to a user on their behalf + secret_hash_for_L1_to_l2_message: Field, // for when l1 uniswap portal inserts the message to consume output assets on L2 + deadline_for_L1_to_l2_message: Field, // for when l1 uniswap portal inserts the message to consume output assets on L2 + canceller_for_L1_to_L2_message: EthereumAddress, // L1 address of who can cancel the message to consume assets on L2. + caller_on_L1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) ) -> Field { - // Get portal addresses - let l1UniswapPortal = context.this_portal_address(); - let inputAssetPortalAddress = get_portal_address(inputAsset); - let outputAssetPortalAddress = get_portal_address(outputAsset); - // inputAsset.withdraw(inputAmount, sender, recipient=l1UniswapPortal, callerOnL1=l1UniswapPortal) - // only uniswap portal can call this (done to safeguard ordering of message consumption) - // ref: https://docs.aztec.network/aztec/how-it-works/l1-l2-messaging#designated-caller - let inputAssetInterface = NonNativeTokenPrivateContextInterface::at(inputAsset); - let return_value = inputAssetInterface.withdraw( + // Transfer funds to this contract + Token::at(input_asset.address).unshield( &mut context, - inputAmount, - sender, - l1UniswapPortal, - l1UniswapPortal, - )[0]; - - // Send the swap message to L1 portal - let content_hash = _compute_swap_content_hash( - inputAssetPortalAddress, - inputAmount, - uniswapFeeTier, - outputAssetPortalAddress, - minimumOutputAmount, - recipient, - secretHash, - deadlineForL1ToL2Message, - cancellerForL1ToL2Message, - callerOnL1, + context.msg_sender(), + context.this_address(), + input_amount, + nonce_for_unshield_approval, ); - context.message_portal(content_hash); - - return_value - } - // refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected - fn _compute_swap_content_hash( - inputTokenPortal: Field, - inAmount: Field, - uniswapFeeTier: Field, - outputTokenPortal: Field, - amountOutMin: Field, - aztecRecipientAddress: Field, - secretHash: Field, - deadlineForL1ToL2Message: Field, - canceller: Field, - callerOnL1: Field, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call) - ) -> pub Field { + // Approve bridge to burn this contract's funds and exit to L1 Uniswap Portal + context.call_public_function( + context.this_address(), + compute_selector("_approve_bridge_and_exit_input_asset_to_L1((Field),(Field),Field)"), + [input_asset.address, input_asset_bridge.address, input_amount], + ); - let mut hash_bytes: [u8; 324] = [0; 324]; // 10 fields of 32 bytes each + 4 bytes fn selector + // Create swap message and send to Outbox for Uniswap Portal + // this ensures the integrity of what the user originally intends to do on L1. + let input_asset_bridge_portal_address = get_portal_address(input_asset_bridge.address); + let output_asset_bridge_portal_address = get_portal_address(output_asset_bridge.address); + assert(input_asset_bridge_portal_address != 0, "L1 portal address of input_asset's bridge is 0"); + assert(output_asset_bridge_portal_address != 0, "L1 portal address of output_asset's bridge is 0"); - let inputTokenPortal_bytes = inputTokenPortal.to_be_bytes(32); - let inAmount_bytes = inAmount.to_be_bytes(32); - let uniswapFeeTier_bytes = uniswapFeeTier.to_be_bytes(32); - let outputTokenPortal_bytes = outputTokenPortal.to_be_bytes(32); - let amountOutMin_bytes = amountOutMin.to_be_bytes(32); - let aztecRecipientAddress_bytes = aztecRecipientAddress.to_be_bytes(32); - let secretHash_bytes = secretHash.to_be_bytes(32); - let deadlineForL1ToL2Message_bytes = deadlineForL1ToL2Message.to_be_bytes(32); - let canceller_bytes = canceller.to_be_bytes(32); - let callerOnL1_bytes = callerOnL1.to_be_bytes(32); + let content_hash = compute_swap_private_content_hash( + input_asset_bridge_portal_address, + input_amount, + uniswap_fee_tier, + output_asset_bridge_portal_address, + minimum_output_amount, + secret_hash_for_redeeming_minted_notes, + secret_hash_for_L1_to_l2_message, + deadline_for_L1_to_l2_message, + canceller_for_L1_to_L2_message.address, + caller_on_L1.address, + ); + context.message_portal(content_hash); - // function selector: 0x9c073c81 keccak256("swap(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") - hash_bytes[0] = 0x9c; - hash_bytes[1] = 0x07; - hash_bytes[2] = 0x3c; - hash_bytes[3] = 0x81; + 1 + } - for i in 0..32 { - hash_bytes[i + 4] = inputTokenPortal_bytes[i]; - hash_bytes[i + 36] = inAmount_bytes[i]; - hash_bytes[i + 68] = uniswapFeeTier_bytes[i]; - hash_bytes[i + 100] = outputTokenPortal_bytes[i]; - hash_bytes[i + 132] = amountOutMin_bytes[i]; - hash_bytes[i + 164] = aztecRecipientAddress_bytes[i]; - hash_bytes[i + 196] = secretHash_bytes[i]; - hash_bytes[i + 228] = deadlineForL1ToL2Message_bytes[i]; - hash_bytes[i + 260] = canceller_bytes[i]; - hash_bytes[i + 292] = callerOnL1_bytes[i]; + // Since the token bridge burns funds on behalf of this contract, this contract has to tell the token contract if the signature is valid + // implementation is similar to how account contracts validate public approvals. + // if valid, it returns the IS_VALID selector which is expected by token contract + #[aztec(public)] + fn is_valid_public(message_hash: Field) -> Field { + let value = storage.approved_action.at(message_hash).read(); + if (value){ + IS_VALID_SELECTOR + } else { + 0 } + } - let content_sha256 = dep::std::hash::sha256(hash_bytes); - - // // Convert the content_sha256 to a field element - let mut v = 1; - let mut high = 0 as Field; - let mut low = 0 as Field; - - for i in 0..16 { - high = high + (content_sha256[15 - i] as Field) * v; - low = low + (content_sha256[16 + 15 - i] as Field) * v; - v = v * 256; - } + // This helper method approves the bridge to burn this contract's funds and exits the input asset to L1 + // Assumes contract already has funds + // Note that private can't read public return values so created an internal public that handles everything + // this method is used for both private and public swaps. + #[aztec(public)] + internal fn _approve_bridge_and_exit_input_asset_to_L1( + token: AztecAddress, + token_bridge: AztecAddress, + amount: Field, + ) { + // Assert that user provided token address is same as expected by token bridge. + assert(token.address == (TokenBridge::at(token_bridge.address).token(context)), "input_asset address is not the same as seen in the bridge contract"); + + // approve bridge to burn this contract's funds (required when exiting on L1, as it burns funds on L2): + let nonce_for_burn_approval = storage.nonce_for_burn_approval.read(); + let selector = compute_selector("burn_public((Field),Field,Field)"); + let message_hash = compute_message_hash([token_bridge.address, token.address, selector, context.this_address(), amount, nonce_for_burn_approval]); + storage.approved_action.at(message_hash).write(true); - // Abuse that a % p + b % p = (a + b) % p and that low < p - let content_hash = low + high * v; + // increment nonce_for_burn_approval so it won't be used again + storage.nonce_for_burn_approval.write(nonce_for_burn_approval + 1); - content_hash + // Exit to L1 Uniswap Portal ! + TokenBridge::at(token_bridge.address).exit_to_l1_public( + context, + context.this_portal_address(), + amount, + context.this_portal_address(), + nonce_for_burn_approval, + ); } } \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/non_native_token_interface.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/non_native_token_interface.nr deleted file mode 120000 index 52c95853c13..00000000000 --- a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/non_native_token_interface.nr +++ /dev/null @@ -1 +0,0 @@ -../../non_native_token_contract/src/interface.nr \ No newline at end of file diff --git a/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr new file mode 100644 index 00000000000..da1361ab8e3 --- /dev/null +++ b/yarn-project/noir-contracts/src/contracts/uniswap_contract/src/util.nr @@ -0,0 +1,73 @@ +use dep::std::hash::{pedersen_with_separator, sha256}; +use dep::aztec::constants_gen::GENERATOR_INDEX__SIGNATURE_PAYLOAD; + +fn compute_message_hash(args: [Field; N]) -> Field { + // @todo @lherskind We should probably use a separate generator for this, + // to avoid any potential collisions with payloads. + pedersen_with_separator(args, GENERATOR_INDEX__SIGNATURE_PAYLOAD)[0] +} + +// This method computes the L2 to L1 message content hash for the private +// refer `l1-contracts/test/portals/UniswapPortal.sol` on how L2 to L1 message is expected +fn compute_swap_private_content_hash( + input_asset_bridge_portal_address: Field, + input_amount: Field, + uniswap_fee_tier: Field, + output_asset_bridge_portal_address: Field, + minimum_output_amount: Field, + secret_hash_for_redeeming_minted_notes: Field, + secret_hash_for_L1_to_l2_message: Field, + deadline_for_L1_to_l2_message: Field, + canceller_for_L1_to_L2_message: Field, + caller_on_L1: Field, +) -> Field { + let mut hash_bytes: [u8; 324] = [0; 324]; // 10 fields of 32 bytes each + 4 bytes fn selector + + let input_token_portal_bytes = input_asset_bridge_portal_address.to_be_bytes(32); + let in_amount_bytes = input_amount.to_be_bytes(32); + let uniswap_fee_tier_bytes = uniswap_fee_tier.to_be_bytes(32); + let output_token_portal_bytes = output_asset_bridge_portal_address.to_be_bytes(32); + let amount_out_min_bytes = minimum_output_amount.to_be_bytes(32); + let secret_hash_for_redeeming_minted_notes_bytes = secret_hash_for_redeeming_minted_notes.to_be_bytes(32); + let secret_hash_for_L1_to_l2_message_bytes = secret_hash_for_L1_to_l2_message.to_be_bytes(32); + let deadline_for_L1_to_l2_message_bytes = deadline_for_L1_to_l2_message.to_be_bytes(32); + let canceller_bytes = canceller_for_L1_to_L2_message.to_be_bytes(32); + let caller_on_L1_bytes = caller_on_L1.to_be_bytes(32); + + // function selector: 0xbd87d14b keccak256("swap_private(address,uint256,uint24,address,uint256,bytes32,bytes32,uint32,address,address)") + hash_bytes[0] = 0xbd; + hash_bytes[1] = 0x87; + hash_bytes[2] = 0xd1; + hash_bytes[3] = 0x4b; + + for i in 0..32 { + hash_bytes[i + 4] = input_token_portal_bytes[i]; + hash_bytes[i + 36] = in_amount_bytes[i]; + hash_bytes[i + 68] = uniswap_fee_tier_bytes[i]; + hash_bytes[i + 100] = output_token_portal_bytes[i]; + hash_bytes[i + 132] = amount_out_min_bytes[i]; + hash_bytes[i + 164] = secret_hash_for_redeeming_minted_notes_bytes[i]; + hash_bytes[i + 196] = secret_hash_for_L1_to_l2_message_bytes[i]; + hash_bytes[i + 228] = deadline_for_L1_to_l2_message_bytes[i]; + hash_bytes[i + 260] = canceller_bytes[i]; + hash_bytes[i + 292] = caller_on_L1_bytes[i]; + } + + let content_sha256 = sha256(hash_bytes); + + // Convert the content_sha256 to a field element + let mut v = 1; + let mut high = 0 as Field; + let mut low = 0 as Field; + + for i in 0..16 { + high = high + (content_sha256[15 - i] as Field) * v; + low = low + (content_sha256[16 + 15 - i] as Field) * v; + v = v * 256; + } + + // Abuse that a % p + b % p = (a + b) % p and that low < p + let content_hash = low + high * v; + + content_hash +} diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index ab78dc201d1..422105898af 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -259,6 +259,7 @@ __metadata: resolution: "@aztec/canary@workspace:canary" dependencies: "@aztec/aztec.js": "workspace:^" + "@aztec/circuits.js": "workspace:^" "@aztec/cli": "workspace:^" "@aztec/end-to-end": "workspace:^" "@aztec/l1-artifacts": "workspace:^"