Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(e2e): public flow for uniswap #2596

Merged
merged 2 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kinda nicely shows why #2199 #2448 is desired.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just thinking, for public calls does nonce still need to be send around? Could the account contract itself maintain it in state? then it an be infered?

Bare with me here as i might be horribly wrong

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say you storage the nonce in the contract as a counter or whatever. What nonce would the uniswap contract read if you have done multiple approvals? If we expect only one approval to be active at a time you can do it quite easily, but in some cases, we might want multiple approvals within the same tx as well, then the ordering we approve them in suddenly becomes important as well and we also end up having to update more storage.

It can work more easily when every token have their own nonces, but with all nonces handled by the account contract, you might need quite a bit of business logic to handle races etc.

);

// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May aswell just return a bool if its being used for a check?

Copy link
Contributor

@LHerskind LHerskind Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably a leftover from doing similar to what I did in some of the other contracts when bools was not supported, don't think we changed it other places as well 🤔 Might be fine to go over all the contracts and alter those separately as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made #2615

}

#[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