Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ members = [
"examples/contracts/bob_token_contract",
"examples/contracts/nft",
"examples/contracts/nft_bridge",
"examples/contracts/recursive_verification_contract"
"examples/contracts/recursive_verification_contract",
"examples/contracts/aave_bridge"
]
466 changes: 466 additions & 0 deletions docs/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/docs-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ additonal
addressnote
airgapped
analysed
Aave
Anoncast
arithmetisation
asymptotics
Expand Down Expand Up @@ -400,6 +401,9 @@ critesjosh
mcpServers
NethermindEth
Windsurf
remappings
atoken
wagmi
callee
completer
interoperate
Expand Down
8 changes: 8 additions & 0 deletions docs/examples/contracts/aave_bridge/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "aave_bridge"
type = "contract"

[dependencies]
aztec = { path = "../../../../noir-projects/aztec-nr/aztec" }
token_portal_content_hash_lib = { path = "../../../../noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib" }
token = { path = "../../../../noir-projects/noir-contracts/contracts/app/token_contract" }
13 changes: 13 additions & 0 deletions docs/examples/contracts/aave_bridge/src/config.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// docs:start:config
use aztec::protocol::{
address::{AztecAddress, EthAddress},
traits::{Deserialize, Packable, Serialize},
};
use std::meta::derive;

#[derive(Deserialize, Eq, Packable, Serialize)]
pub struct Config {
pub token: AztecAddress,
pub portal: EthAddress,
}
// docs:end:config
134 changes: 134 additions & 0 deletions docs/examples/contracts/aave_bridge/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// docs:start:bridge_setup
mod config;

// A bridge contract that allows users to deposit tokens into Aave on L1 from Aztec L2,
// and claim yield-bearing tokens back on L2. The bridge mirrors TokenBridge's pattern:
// all Aave-specific logic lives on L1, while L2 simply burns/mints tokens and passes messages.

use aztec::macros::aztec;
#[aztec]
pub contract AaveBridge {
use crate::config::Config;

use aztec::{protocol::address::{AztecAddress, EthAddress}, state_vars::PublicImmutable};

use token_portal_content_hash_lib::{
get_mint_to_private_content_hash, get_mint_to_public_content_hash,
get_withdraw_content_hash,
};

use token::Token;

use aztec::macros::{functions::{external, initializer, view}, storage::storage};

#[storage]
struct Storage<Context> {
config: PublicImmutable<Config, Context>,
}

#[external("public")]
#[initializer]
fn constructor(token: AztecAddress, portal: EthAddress) {
self.storage.config.initialize(Config { token, portal });
}

#[external("private")]
#[view]
fn get_config() -> Config {
self.storage.config.read()
}
// docs:end:bridge_setup

// docs:start:claim_public
/// Consume an L1->L2 message and mint tokens publicly.
/// Called after the L1 AavePortal withdraws from Aave and sends a message.
#[external("public")]
fn claim_public(to: AztecAddress, amount: u128, secret: Field, message_leaf_index: Field) {
let content_hash = get_mint_to_public_content_hash(to, amount);
let config = self.storage.config.read();

// Consume message and emit nullifier
self.context.consume_l1_to_l2_message(
content_hash,
secret,
config.portal,
message_leaf_index,
);

// Mint tokens (including any yield from Aave)
self.call(Token::at(config.token).mint_to_public(to, amount));
}
// docs:end:claim_public

// docs:start:claim_private
/// Consume an L1->L2 message and mint tokens privately.
/// The recipient's address is not revealed, but the amount is.
#[external("private")]
fn claim_private(
recipient: AztecAddress,
amount: u128,
secret_for_L1_to_L2_message_consumption: Field,
message_leaf_index: Field,
) {
let config = self.storage.config.read();

// Consume L1 to L2 message and emit nullifier
let content_hash = get_mint_to_private_content_hash(amount);
self.context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
config.portal,
message_leaf_index,
);

// Mint tokens privately
self.call(Token::at(config.token).mint_to_private(recipient, amount));
}
// docs:end:claim_private

// docs:start:exit_to_l1_public
/// Burn tokens publicly and create an L2->L1 message.
/// The L1 AavePortal will consume this message and deposit into Aave.
#[external("public")]
fn exit_to_l1_public(
recipient: EthAddress,
amount: u128,
caller_on_l1: EthAddress,
authwit_nonce: Field,
) {
let config = self.storage.config.read();

// Send an L2 to L1 message
let content = get_withdraw_content_hash(recipient, amount, caller_on_l1);
self.context.message_portal(config.portal, content);

// Burn tokens
self.call(Token::at(config.token).burn_public(self.msg_sender(), amount, authwit_nonce));
}
// docs:end:exit_to_l1_public

// docs:start:exit_to_l1_private
/// Burn tokens privately and create an L2->L1 message.
/// The L1 AavePortal will consume this message and deposit into Aave.
#[external("private")]
fn exit_to_l1_private(
token: AztecAddress,
recipient: EthAddress,
amount: u128,
caller_on_l1: EthAddress,
authwit_nonce: Field,
) {
let config = self.storage.config.read();

// Assert that user provided token address is same as seen in storage
assert_eq(config.token, token, "Token address is not the same as seen in storage");

// Send an L2 to L1 message
let content = get_withdraw_content_hash(recipient, amount, caller_on_l1);
self.context.message_portal(config.portal, content);

// Burn tokens privately
self.call(Token::at(token).burn_private(self.msg_sender(), amount, authwit_nonce));
}
// docs:end:exit_to_l1_private
}
135 changes: 135 additions & 0 deletions docs/examples/solidity/aave_bridge/AavePortal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.27;

// docs:start:portal_setup
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";

import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

I was also struggling with the remappings here. Claude worked around it by importing directly from the npm packages

import {IRegistry} from "@aztec/l1-contracts/src/governance/interfaces/IRegistry.sol";
import {IInbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IInbox.sol";
import {IOutbox} from "@aztec/l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol";
import {IRollup} from "@aztec/l1-contracts/src/core/interfaces/IRollup.sol";
import {DataStructures} from "@aztec/l1-contracts/src/core/libraries/DataStructures.sol";
import {Hash} from "@aztec/l1-contracts/src/core/libraries/crypto/Hash.sol";
import {Epoch} from "@aztec/l1-contracts/src/core/libraries/TimeLib.sol";

import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol";
import {IRollup} from "@aztec/core/interfaces/IRollup.sol";
import {DataStructures} from "@aztec/core/libraries/DataStructures.sol";
import {Hash} from "@aztec/core/libraries/crypto/Hash.sol";
import {Epoch} from "@aztec/core/libraries/TimeLib.sol";

interface IAavePool {
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function withdraw(address asset, uint256 amount, address to) external returns (uint256);
}

contract AavePortal {
using SafeERC20 for IERC20;

IRegistry public registry;
IERC20 public underlying;
IERC20 public aToken;
IAavePool public aavePool;
bytes32 public l2Bridge;

IRollup public rollup;
IOutbox public outbox;
IInbox public inbox;
uint256 public rollupVersion;

bool private _initialized;

function initialize(
address _registry,
address _underlying,
address _aToken,
address _aavePool,
bytes32 _l2Bridge
) external {
require(!_initialized, "Already initialized");
_initialized = true;

registry = IRegistry(_registry);
underlying = IERC20(_underlying);
aToken = IERC20(_aToken);
aavePool = IAavePool(_aavePool);
l2Bridge = _l2Bridge;

rollup = IRollup(address(registry.getCanonicalRollup()));
outbox = rollup.getOutbox();
inbox = rollup.getInbox();
rollupVersion = rollup.getVersion();
}
// docs:end:portal_setup

// docs:start:portal_deposit_to_aave
/// @notice Consume an L2->L1 withdraw message and deposit the underlying tokens into Aave
/// @dev The content hash must match what the L2 bridge emits via get_withdraw_content_hash
function depositToAave(
address _recipient,
uint256 _amount,
bool _withCaller,
Epoch _epoch,
uint256 _leafIndex,
bytes32[] calldata _path
) external {
// Reconstruct the L2->L1 message (must match the L2 bridge's exit_to_l1_public/private)
DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({
sender: DataStructures.L2Actor(l2Bridge, rollupVersion),
recipient: DataStructures.L1Actor(address(this), block.chainid),
content: Hash.sha256ToField(
abi.encodeWithSignature(
"withdraw(address,uint256,address)",
_recipient,
_amount,
_withCaller ? msg.sender : address(0)
)
)
});

// Consume the message from the outbox (verifies merkle proof)
outbox.consume(message, _epoch, _leafIndex, _path);

// Deposit into Aave instead of sending tokens to the recipient.
// The portal must already hold the underlying tokens (pre-funded or bridged separately).
underlying.approve(address(aavePool), _amount);
aavePool.supply(address(underlying), _amount, address(this), 0);
}
// docs:end:portal_deposit_to_aave

// docs:start:portal_claim_public
/// @notice Withdraw from Aave and send an L1->L2 message to mint tokens publicly on L2
function claimFromAavePublic(
uint256 _aTokenAmount,
bytes32 _to,
bytes32 _secretHash
) external returns (bytes32, uint256) {
// Withdraw from Aave (returns underlying + yield)
aToken.approve(address(aavePool), _aTokenAmount);
uint256 withdrawn = aavePool.withdraw(address(underlying), _aTokenAmount, address(this));

// Send L1->L2 message with the total withdrawn amount (including yield)
DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion);
bytes32 contentHash =
Hash.sha256ToField(abi.encodeWithSignature("mint_to_public(bytes32,uint256)", _to, withdrawn));

(bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, _secretHash);
return (key, index);
}
// docs:end:portal_claim_public

// docs:start:portal_claim_private
/// @notice Withdraw from Aave and send an L1->L2 message to mint tokens privately on L2
function claimFromAavePrivate(
uint256 _aTokenAmount,
bytes32 _secretHash
) external returns (bytes32, uint256) {
// Withdraw from Aave (returns underlying + yield)
aToken.approve(address(aavePool), _aTokenAmount);
uint256 withdrawn = aavePool.withdraw(address(underlying), _aTokenAmount, address(this));

// Send L1->L2 message for private minting
DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion);
bytes32 contentHash =
Hash.sha256ToField(abi.encodeWithSignature("mint_to_private(uint256)", withdrawn));

(bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, _secretHash);
return (key, index);
}
// docs:end:portal_claim_private
}
18 changes: 18 additions & 0 deletions docs/examples/solidity/aave_bridge/MockAToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.27;

// docs:start:mock_atoken
import {ERC20} from "@oz/token/ERC20/ERC20.sol";

contract MockAToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}

function mint(address to, uint256 amount) external {
_mint(to, amount);
}

function burn(address from, uint256 amount) external {
_burn(from, amount);
}
}
// docs:end:mock_atoken
54 changes: 54 additions & 0 deletions docs/examples/solidity/aave_bridge/MockAavePool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.27;

// docs:start:mock_aave_pool
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {MockERC20} from "./MockERC20.sol";
import {MockAToken} from "./MockAToken.sol";

/// @notice A simplified mock of Aave V3's lending pool for tutorial purposes.
/// Supports supply and withdraw with a configurable yield in basis points.
contract MockAavePool {
MockERC20 public underlyingToken;
MockAToken public aToken;
uint256 public yieldBps; // e.g. 1000 = 10%

constructor(address _underlyingToken, address _aToken, uint256 _yieldBps) {
underlyingToken = MockERC20(_underlyingToken);
aToken = MockAToken(_aToken);
yieldBps = _yieldBps;
}

/// @notice Deposit underlying tokens and receive aTokens (mimics Aave V3 IPool.supply)
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 /* referralCode */
) external {
require(asset == address(underlyingToken), "Wrong asset");
IERC20(asset).transferFrom(msg.sender, address(this), amount);
aToken.mint(onBehalfOf, amount);
}

/// @notice Withdraw underlying tokens by burning aTokens (mimics Aave V3 IPool.withdraw)
/// Returns the original amount plus simulated yield
function withdraw(address asset, uint256 amount, address to) external returns (uint256) {
require(asset == address(underlyingToken), "Wrong asset");

// Burn caller's aTokens
aToken.burn(msg.sender, amount);

// Simulate yield: return amount + yield
uint256 yieldAmount = (amount * yieldBps) / 10000;
uint256 totalReturn = amount + yieldAmount;

// Mint extra underlying to cover yield (mock-only behavior)
underlyingToken.mint(address(this), yieldAmount);

// Transfer underlying + yield to recipient
underlyingToken.transfer(to, totalReturn);
return totalReturn;
}
}
// docs:end:mock_aave_pool
Loading