Skip to main content

Executing Private Swap on L1

To execute the swaps on L1, go back to the UniswapPortal.sol we created earlier in l1-contracts.

* @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 _withCaller - When true, using `msg.sender` as the caller, otherwise address(0)
* @return A hash of the L1 to L2 message inserted in the Inbox
function swapPrivate(
address _inputTokenPortal,
uint256 _inAmount,
uint24 _uniswapFeeTier,
address _outputTokenPortal,
uint256 _amountOutMinimum,
bytes32 _secretHashForRedeemingMintedNotes,
bytes32 _secretHashForL1ToL2Message,
bool _withCaller,
// Avoiding stack too deep
PortalDataStructures.OutboxMessageMetadata[2] calldata _outboxMessageMetadata
) public returns (bytes32) {
LocalSwapVars memory vars;

vars.inputAsset = TokenPortal(_inputTokenPortal).underlying();
vars.outputAsset = TokenPortal(_outputTokenPortal).underlying();


// prevent stack too deep errors
vars.contentHash = Hash.sha256ToField(
_withCaller ? msg.sender : address(0)

// Consume the message from the outbox
IOutbox outbox = registry.getOutbox();

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(
_secretHashForRedeemingMintedNotes, amountOut, _secretHashForL1ToL2Message
Source code: l1-contracts/test/portals/UniswapPortal.sol#L149-L254

This works very similarly to the public flow.