Skip to content

Commit

Permalink
feat(e2e): public flow for uniswap (#2596)
Browse files Browse the repository at this point in the history
Also added a `sender` address in the public flow, so like AC, anyone can
swap on a user's behalf.

Separate PR for failure cases that will be combined for both public and
private! Trying to keep PRs small
  • Loading branch information
rahul-kothari authored and Maddiaa0 committed Oct 5, 2023
1 parent 7fc6d3f commit 9e56948
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 11 deletions.
174 changes: 169 additions & 5 deletions yarn-project/end-to-end/src/uniswap_trade_on_l1_from_l2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ describe('uniswap_trade_on_l1_from_l2', () => {
let ownerWallet: AccountWallet;
let ownerAddress: AztecAddress;
let ownerEthAddress: EthAddress;
// does transactions on behalf of owner on Aztec:
let sponsorWallet: AccountWallet;
let sponsorAddress: AztecAddress;

let daiCrossChainHarness: CrossChainTestHarness;
let wethCrossChainHarness: CrossChainTestHarness;
Expand All @@ -58,8 +61,8 @@ describe('uniswap_trade_on_l1_from_l2', () => {
pxe: pxe_,
deployL1ContractsValues,
accounts,
wallets,
logger: logger_,
wallet,
cheatCodes,
} = await setup(2, dumpedState);
const walletClient = deployL1ContractsValues.walletClient;
Expand All @@ -73,8 +76,10 @@ describe('uniswap_trade_on_l1_from_l2', () => {
pxe = pxe_;
logger = logger_;
teardown = teardown_;
ownerWallet = wallet;
ownerWallet = wallets[0];
sponsorWallet = wallets[1];
ownerAddress = accounts[0].address;
sponsorAddress = accounts[1].address;
ownerEthAddress = EthAddress.fromString((await walletClient.getAddresses())[0]);

logger('Deploying DAI Portal, initializing and deploying l2 contract...');
Expand All @@ -83,7 +88,7 @@ describe('uniswap_trade_on_l1_from_l2', () => {
pxe,
deployL1ContractsValues,
accounts,
wallet,
ownerWallet,
logger,
cheatCodes,
DAI_ADDRESS,
Expand All @@ -95,7 +100,7 @@ describe('uniswap_trade_on_l1_from_l2', () => {
pxe,
deployL1ContractsValues,
accounts,
wallet,
ownerWallet,
logger,
cheatCodes,
WETH9_ADDRESS,
Expand All @@ -110,7 +115,9 @@ describe('uniswap_trade_on_l1_from_l2', () => {
publicClient,
});
// deploy l2 uniswap contract and attach to portal
uniswapL2Contract = await UniswapContract.deploy(wallet).send({ portalContract: uniswapPortalAddress }).deployed();
uniswapL2Contract = await UniswapContract.deploy(ownerWallet)
.send({ portalContract: uniswapPortalAddress })
.deployed();
await uniswapL2Contract.attach(uniswapPortalAddress);

await uniswapPortal.write.initialize(
Expand Down Expand Up @@ -271,4 +278,161 @@ describe('uniswap_trade_on_l1_from_l2', () => {
logger('WETH balance after swap : ', wethL2BalanceAfterSwap.toString());
logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString());
}, 140_000);

it('should uniswap trade on L1 from L2 funds publicly (swaps WETH -> DAI)', async () => {
const wethL1BeforeBalance = await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress);

// 1. Approve and deposit weth to the portal and move to L2
const [secretForMintingWeth, secretHashForMintingWeth] = await wethCrossChainHarness.generateClaimSecret();

const messageKey = await wethCrossChainHarness.sendTokensToPortalPublic(
wethAmountToBridge,
secretHashForMintingWeth,
);
// funds transferred from owner to token portal
expect(await wethCrossChainHarness.getL1BalanceOf(ownerEthAddress)).toBe(wethL1BeforeBalance - wethAmountToBridge);
expect(await wethCrossChainHarness.getL1BalanceOf(wethCrossChainHarness.tokenPortalAddress)).toBe(
wethAmountToBridge,
);

// Wait for the archiver to process the message
await delay(5000);

// Perform an unrelated transaction on L2 to progress the rollup. Here we transfer 0 tokens
await wethCrossChainHarness.mintTokensPublicOnL2(0n);

// 2. Claim WETH on L2
logger('Minting weth on L2');
await wethCrossChainHarness.consumeMessageOnAztecAndMintPublicly(
wethAmountToBridge,
messageKey,
secretForMintingWeth,
);
await wethCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, wethAmountToBridge);

// Store balances
const wethL2BalanceBeforeSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress);
const daiL2BalanceBeforeSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress);

// 3. Owner gives uniswap approval to transfer funds on its behalf
const nonceForWETHTransferApproval = new Fr(2n);
const transferMessageHash = await hashPayload([
uniswapL2Contract.address.toField(),
wethCrossChainHarness.l2Token.address.toField(),
FunctionSelector.fromSignature('transfer_public((Field),(Field),Field,Field)').toField(),
ownerAddress.toField(),
uniswapL2Contract.address.toField(),
new Fr(wethAmountToBridge),
nonceForWETHTransferApproval,
]);
await ownerWallet.setPublicAuth(transferMessageHash, true).send().wait();

// 4. Swap on L1 - sends L2 to L1 message to withdraw WETH to L1 and another message to swap assets.
const deadlineForDepositingSwappedDai = BigInt(2 ** 32 - 1); // max uint32 - 1
const [secretForDepositingSwappedDai, secretHashForDepositingSwappedDai] =
await daiCrossChainHarness.generateClaimSecret();

// 4.1 Owner approves user to swap on their behalf:
const nonceForSwap = new Fr(3n);
const swapMessageHash = await hashPayload([
sponsorAddress.toField(),
uniswapL2Contract.address.toField(),
FunctionSelector.fromSignature(
'swap_public((Field),(Field),Field,(Field),Field,Field,Field,(Field),Field,Field,(Field),(Field),Field)',
).toField(),
ownerAddress.toField(),
wethCrossChainHarness.l2Bridge.address.toField(),
new Fr(wethAmountToBridge),
daiCrossChainHarness.l2Bridge.address.toField(),
nonceForWETHTransferApproval,
new Fr(uniswapFeeTier),
new Fr(minimumOutputAmount),
ownerAddress.toField(),
secretHashForDepositingSwappedDai,
new Fr(deadlineForDepositingSwappedDai),
ownerEthAddress.toField(),
ownerEthAddress.toField(),
nonceForSwap,
]);
await ownerWallet.setPublicAuth(swapMessageHash, true).send().wait();

// 4.2 Call swap_public from user2 on behalf of owner
const withdrawReceipt = await uniswapL2Contract
.withWallet(sponsorWallet)
.methods.swap_public(
ownerAddress,
wethCrossChainHarness.l2Bridge.address,
wethAmountToBridge,
daiCrossChainHarness.l2Bridge.address,
nonceForWETHTransferApproval,
uniswapFeeTier,
minimumOutputAmount,
ownerAddress,
secretHashForDepositingSwappedDai,
deadlineForDepositingSwappedDai,
ownerEthAddress,
ownerEthAddress,
nonceForSwap,
)
.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.expectPublicBalanceOnL2(ownerAddress, wethL2BalanceBeforeSwap - wethAmountToBridge);

// 5. Perform the swap on L1 with the `uniswapPortal.swap()` (consuming L2 to L1 messages)
logger('Execute withdraw and swap on the uniswapPortal!');
const daiL1BalanceOfPortalBeforeSwap = await daiCrossChainHarness.getL1BalanceOf(
daiCrossChainHarness.tokenPortalAddress,
);
const swapArgs = [
wethCrossChainHarness.tokenPortalAddress.toString(),
wethAmountToBridge,
uniswapFeeTier,
daiCrossChainHarness.tokenPortalAddress.toString(),
minimumOutputAmount,
ownerAddress.toString(),
secretHashForDepositingSwappedDai.toString(true),
deadlineForDepositingSwappedDai,
ownerEthAddress.toString(),
true,
] as const;
const { result: depositDaiMessageKeyHex } = await uniswapPortal.simulate.swapPublic(swapArgs, {
account: ownerEthAddress.toString(),
} as any);

// this should also insert a message into the inbox.
await uniswapPortal.write.swapPublic(swapArgs, {} as any);
const depositDaiMessageKey = Fr.fromString(depositDaiMessageKeyHex);
// weth was swapped to dai and send to portal
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(0n);

// 6. claim dai on L2
logger('Consuming messages to mint dai on L2');
await daiCrossChainHarness.consumeMessageOnAztecAndMintPublicly(
daiAmountToBridge,
depositDaiMessageKey,
secretForDepositingSwappedDai,
);
await daiCrossChainHarness.expectPublicBalanceOnL2(ownerAddress, daiL2BalanceBeforeSwap + daiAmountToBridge);

const wethL2BalanceAfterSwap = await wethCrossChainHarness.getL2PublicBalanceOf(ownerAddress);
const daiL2BalanceAfterSwap = await daiCrossChainHarness.getL2PublicBalanceOf(ownerAddress);

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 : ', wethL2BalanceAfterSwap.toString());
logger('DAI balance after swap : ', daiL2BalanceAfterSwap.toString());
}, 140_000);
});
109 changes: 103 additions & 6 deletions yarn-project/noir-contracts/src/contracts/uniswap_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod util;
// 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::{
auth::IS_VALID_SELECTOR,
auth::{IS_VALID_SELECTOR, assert_valid_public_message_for},
context::{PrivateContext, PublicContext, Context},
oracle::compute_selector::compute_selector,
oracle::context::get_portal_address,
Expand All @@ -21,7 +21,7 @@ contract Uniswap {
};

use crate::interfaces::{Token, TokenBridge};
use crate::util::{compute_message_hash, compute_swap_private_content_hash};
use crate::util::{compute_message_hash, compute_swap_private_content_hash, compute_swap_public_content_hash};

struct Storage {
// like with account contracts, stores the approval message on a slot and tracks if they are active
Expand Down Expand Up @@ -49,6 +49,96 @@ contract Uniswap {
#[aztec(private)]
fn constructor() {}

#[aztec(public)]
fn swap_public(
sender: AztecAddress,
input_asset_bridge: AztecAddress,
input_amount: Field,
output_asset_bridge: AztecAddress,
// params for using the transfer approval
nonce_for_transfer_approval: Field,
// params for the swap
uniswap_fee_tier: Field,
minimum_output_amount: Field,
// params for the depositing output_asset back to Aztec
recipient: AztecAddress,
secret_hash_for_L1_to_l2_message: Field,
deadline_for_L1_to_l2_message: Field,
canceller_for_L1_to_L2_message: EthereumAddress,
caller_on_L1: EthereumAddress,
// nonce for someone to call swap on sender's behalf
nonce_for_swap_approval: Field,
) -> Field {

if (sender.address != context.msg_sender()) {
// if someone else is calling on swap on sender's behalf, they need to have authorisation to do so:
let selector = compute_selector(
"swap_public((Field),(Field),Field,(Field),Field,Field,Field,(Field),Field,Field,(Field),(Field),Field)"
);
let message_field = compute_message_hash([
context.msg_sender(),
context.this_address(),
selector,
sender.address,
input_asset_bridge.address,
input_amount,
output_asset_bridge.address,
nonce_for_transfer_approval,
uniswap_fee_tier,
minimum_output_amount,
recipient.address,
secret_hash_for_L1_to_l2_message,
deadline_for_L1_to_l2_message,
canceller_for_L1_to_L2_message.address,
caller_on_L1.address,
nonce_for_swap_approval,
]);
// this also emits a nullifier for the message
assert_valid_public_message_for(&mut context,sender.address,message_field);
}

let input_asset = AztecAddress::new(TokenBridge::at(input_asset_bridge.address).token(context));

// Transfer funds to this contract
Token::at(input_asset.address).transfer_public(
context,
sender.address,
context.this_address(),
input_amount,
nonce_for_transfer_approval,
);

// Approve bridge to burn this contract's funds and exit to L1 Uniswap Portal
let _void = 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],
);

// 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 content_hash = compute_swap_public_content_hash(
input_asset_bridge_portal_address,
input_amount,
uniswap_fee_tier,
output_asset_bridge_portal_address,
minimum_output_amount,
recipient.address,
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);

1
}

#[aztec(private)]
fn swap(
input_asset: AztecAddress, // since private, we pass here and later assert that this is as expected by input_bridge
Expand All @@ -68,6 +158,10 @@ contract Uniswap {
caller_on_L1: EthereumAddress, // ethereum address that can call this function on the L1 portal (0x0 if anyone can call)
) -> Field {

// Assert that user provided token address is same as expected by token bridge.
// we can't directly use `input_asset_bridge.token` because that is a public method and public can't return data to private
context.call_public_function(context.this_address(), compute_selector("_assert_token_is_same(Field,Field)"), [input_asset.address, input_asset_bridge.address]);

// Transfer funds to this contract
Token::at(input_asset.address).unshield(
&mut context,
Expand Down Expand Up @@ -122,7 +216,8 @@ contract Uniswap {
}

// This helper method approves the bridge to burn this contract's funds and exits the input asset to L1
// Assumes contract already has funds
// Assumes contract already has funds.
// Assume `token` relates to `token_bridge` (ie token_bridge.token == token)
// 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)]
Expand All @@ -131,9 +226,6 @@ contract Uniswap {
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)");
Expand All @@ -152,4 +244,9 @@ contract Uniswap {
nonce_for_burn_approval,
);
}

#[aztec(public)]
internal fn _assert_token_is_same(token: Field, token_bridge: Field) {
assert(token == (TokenBridge::at(token_bridge).token(context)), "input_asset address is not the same as seen in the bridge contract");
}
}
Loading

0 comments on commit 9e56948

Please sign in to comment.