Skip to content

Commit 35f54de

Browse files
authored
feat(docs): add aave tutorial (#21048)
title
2 parents 67baaf3 + d46d672 commit 35f54de

15 files changed

Lines changed: 1316 additions & 2 deletions

File tree

docs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ test-results
3333
!CLAUDE.md
3434

3535
target
36+
foundry.lock
3637
static/aztec-nr-api/next

docs/Nargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ members = [
66
"examples/contracts/nft",
77
"examples/contracts/nft_bridge",
88
"examples/contracts/recursive_verification_contract",
9+
"examples/contracts/aave_bridge",
910
"examples/contracts/example_uniswap"
1011
]

docs/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md

Lines changed: 518 additions & 0 deletions
Large diffs are not rendered by default.

docs/docs-words.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ additonal
99
addressnote
1010
airgapped
1111
analysed
12+
Aave
1213
Anoncast
1314
arithmetisation
1415
asymptotics
@@ -403,6 +404,9 @@ critesjosh
403404
mcpServers
404405
NethermindEth
405406
Windsurf
407+
remappings
408+
atoken
409+
wagmi
406410
Uniswap
407411
uniswap
408412
WETH
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "aave_bridge"
3+
type = "contract"
4+
5+
[dependencies]
6+
aztec = { path = "../../../../noir-projects/aztec-nr/aztec" }
7+
token_portal_content_hash_lib = { path = "../../../../noir-projects/noir-contracts/contracts/libs/token_portal_content_hash_lib" }
8+
token = { path = "../../../../noir-projects/noir-contracts/contracts/app/token_contract" }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// docs:start:config
2+
use aztec::protocol::{
3+
address::{AztecAddress, EthAddress},
4+
traits::{Deserialize, Packable, Serialize},
5+
};
6+
use std::meta::derive;
7+
8+
#[derive(Deserialize, Eq, Packable, Serialize)]
9+
pub struct Config {
10+
pub token: AztecAddress,
11+
pub portal: EthAddress,
12+
}
13+
// docs:end:config
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// docs:start:bridge_setup
2+
mod config;
3+
4+
// A bridge contract that allows users to deposit tokens into Aave on L1 from Aztec L2,
5+
// and claim yield-bearing tokens back on L2. The bridge mirrors TokenBridge's pattern:
6+
// all Aave-specific logic lives on L1, while L2 simply burns/mints tokens and passes messages.
7+
8+
use aztec::macros::aztec;
9+
#[aztec]
10+
pub contract AaveBridge {
11+
use crate::config::Config;
12+
13+
use aztec::{protocol::address::{AztecAddress, EthAddress}, state_vars::PublicImmutable};
14+
15+
use token_portal_content_hash_lib::{
16+
get_mint_to_private_content_hash, get_mint_to_public_content_hash,
17+
get_withdraw_content_hash,
18+
};
19+
20+
use token::Token;
21+
22+
use aztec::macros::{functions::{external, initializer, view}, storage::storage};
23+
24+
#[storage]
25+
struct Storage<Context> {
26+
config: PublicImmutable<Config, Context>,
27+
}
28+
29+
#[external("public")]
30+
#[initializer]
31+
fn constructor(token: AztecAddress, portal: EthAddress) {
32+
self.storage.config.initialize(Config { token, portal });
33+
}
34+
35+
#[external("private")]
36+
#[view]
37+
fn get_config() -> Config {
38+
self.storage.config.read()
39+
}
40+
// docs:end:bridge_setup
41+
42+
// docs:start:claim_public
43+
/// Consume an L1->L2 message and mint tokens publicly.
44+
/// Called after the L1 AavePortal withdraws from Aave and sends a message.
45+
#[external("public")]
46+
fn claim_public(to: AztecAddress, amount: u128, secret: Field, message_leaf_index: Field) {
47+
let content_hash = get_mint_to_public_content_hash(to, amount);
48+
let config = self.storage.config.read();
49+
50+
// Consume message and emit nullifier
51+
self.context.consume_l1_to_l2_message(
52+
content_hash,
53+
secret,
54+
config.portal,
55+
message_leaf_index,
56+
);
57+
58+
// Mint tokens (including any yield from Aave)
59+
self.call(Token::at(config.token).mint_to_public(to, amount));
60+
}
61+
// docs:end:claim_public
62+
63+
// docs:start:claim_private
64+
/// Consume an L1->L2 message and mint tokens privately.
65+
/// The recipient's address is not revealed, but the amount is.
66+
#[external("private")]
67+
fn claim_private(
68+
recipient: AztecAddress,
69+
amount: u128,
70+
secret_for_L1_to_L2_message_consumption: Field,
71+
message_leaf_index: Field,
72+
) {
73+
let config = self.storage.config.read();
74+
75+
// Consume L1 to L2 message and emit nullifier
76+
let content_hash = get_mint_to_private_content_hash(amount);
77+
self.context.consume_l1_to_l2_message(
78+
content_hash,
79+
secret_for_L1_to_L2_message_consumption,
80+
config.portal,
81+
message_leaf_index,
82+
);
83+
84+
// Mint tokens privately
85+
self.call(Token::at(config.token).mint_to_private(recipient, amount));
86+
}
87+
// docs:end:claim_private
88+
89+
// docs:start:exit_to_l1_public
90+
/// Burn tokens publicly and create an L2->L1 message.
91+
/// The L1 AavePortal will consume this message and deposit into Aave.
92+
#[external("public")]
93+
fn exit_to_l1_public(
94+
recipient: EthAddress,
95+
amount: u128,
96+
caller_on_l1: EthAddress,
97+
authwit_nonce: Field,
98+
) {
99+
let config = self.storage.config.read();
100+
101+
// Send an L2 to L1 message
102+
let content = get_withdraw_content_hash(recipient, amount, caller_on_l1);
103+
self.context.message_portal(config.portal, content);
104+
105+
// Burn tokens
106+
self.call(Token::at(config.token).burn_public(self.msg_sender(), amount, authwit_nonce));
107+
}
108+
// docs:end:exit_to_l1_public
109+
110+
// docs:start:exit_to_l1_private
111+
/// Burn tokens privately and create an L2->L1 message.
112+
/// The L1 AavePortal will consume this message and deposit into Aave.
113+
#[external("private")]
114+
fn exit_to_l1_private(
115+
token: AztecAddress,
116+
recipient: EthAddress,
117+
amount: u128,
118+
caller_on_l1: EthAddress,
119+
authwit_nonce: Field,
120+
) {
121+
let config = self.storage.config.read();
122+
123+
// Assert that user provided token address is same as seen in storage
124+
assert_eq(config.token, token, "Token address is not the same as seen in storage");
125+
126+
// Send an L2 to L1 message
127+
let content = get_withdraw_content_hash(recipient, amount, caller_on_l1);
128+
self.context.message_portal(config.portal, content);
129+
130+
// Burn tokens privately
131+
self.call(Token::at(token).burn_private(self.msg_sender(), amount, authwit_nonce));
132+
}
133+
// docs:end:exit_to_l1_private
134+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.27;
3+
4+
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
5+
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";
6+
7+
import {IRegistry} from "@aztec/governance/interfaces/IRegistry.sol";
8+
import {IInbox} from "@aztec/core/interfaces/messagebridge/IInbox.sol";
9+
import {IOutbox} from "@aztec/core/interfaces/messagebridge/IOutbox.sol";
10+
import {IRollup} from "@aztec/core/interfaces/IRollup.sol";
11+
import {DataStructures} from "@aztec/core/libraries/DataStructures.sol";
12+
import {Hash} from "@aztec/core/libraries/crypto/Hash.sol";
13+
import {Epoch} from "@aztec/core/libraries/TimeLib.sol";
14+
15+
// docs:start:portal_setup
16+
interface IAavePool {
17+
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
18+
function withdraw(address asset, uint256 amount, address to) external returns (uint256);
19+
}
20+
21+
contract AavePortal {
22+
using SafeERC20 for IERC20;
23+
24+
IRegistry public registry;
25+
IERC20 public underlying;
26+
IERC20 public aToken;
27+
IAavePool public aavePool;
28+
bytes32 public l2Bridge;
29+
30+
IRollup public rollup;
31+
IOutbox public outbox;
32+
IInbox public inbox;
33+
uint256 public rollupVersion;
34+
35+
bool private _initialized;
36+
37+
function initialize(
38+
address _registry,
39+
address _underlying,
40+
address _aToken,
41+
address _aavePool,
42+
bytes32 _l2Bridge
43+
) external {
44+
require(!_initialized, "Already initialized");
45+
_initialized = true;
46+
47+
registry = IRegistry(_registry);
48+
underlying = IERC20(_underlying);
49+
aToken = IERC20(_aToken);
50+
aavePool = IAavePool(_aavePool);
51+
l2Bridge = _l2Bridge;
52+
53+
rollup = IRollup(address(registry.getCanonicalRollup()));
54+
outbox = rollup.getOutbox();
55+
inbox = rollup.getInbox();
56+
rollupVersion = rollup.getVersion();
57+
}
58+
// docs:end:portal_setup
59+
60+
// docs:start:portal_deposit_to_aave
61+
/// @notice Consume an L2->L1 withdraw message and deposit the underlying tokens into Aave
62+
/// @dev The content hash must match what the L2 bridge emits via get_withdraw_content_hash
63+
function depositToAave(
64+
address _recipient,
65+
uint256 _amount,
66+
bool _withCaller,
67+
Epoch _epoch,
68+
uint256 _leafIndex,
69+
bytes32[] calldata _path
70+
) external {
71+
// Reconstruct the L2->L1 message (must match the L2 bridge's exit_to_l1_public/private)
72+
DataStructures.L2ToL1Msg memory message = DataStructures.L2ToL1Msg({
73+
sender: DataStructures.L2Actor(l2Bridge, rollupVersion),
74+
recipient: DataStructures.L1Actor(address(this), block.chainid),
75+
content: Hash.sha256ToField(
76+
abi.encodeWithSignature(
77+
"withdraw(address,uint256,address)",
78+
_recipient,
79+
_amount,
80+
_withCaller ? msg.sender : address(0)
81+
)
82+
)
83+
});
84+
85+
// Consume the message from the outbox (verifies merkle proof)
86+
outbox.consume(message, _epoch, _leafIndex, _path);
87+
88+
// Deposit into Aave instead of sending tokens to the recipient.
89+
// The portal must already hold the underlying tokens (pre-funded or bridged separately).
90+
underlying.approve(address(aavePool), _amount);
91+
aavePool.supply(address(underlying), _amount, address(this), 0);
92+
}
93+
// docs:end:portal_deposit_to_aave
94+
95+
// docs:start:portal_claim_public
96+
/// @notice Withdraw from Aave and send an L1->L2 message to mint tokens publicly on L2
97+
function claimFromAavePublic(
98+
uint256 _aTokenAmount,
99+
bytes32 _to,
100+
bytes32 _secretHash
101+
) external returns (bytes32, uint256) {
102+
// Withdraw from Aave (returns underlying + yield)
103+
aToken.approve(address(aavePool), _aTokenAmount);
104+
uint256 withdrawn = aavePool.withdraw(address(underlying), _aTokenAmount, address(this));
105+
106+
// Send L1->L2 message with the total withdrawn amount (including yield)
107+
DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion);
108+
bytes32 contentHash =
109+
Hash.sha256ToField(abi.encodeWithSignature("mint_to_public(bytes32,uint256)", _to, withdrawn));
110+
111+
(bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, _secretHash);
112+
return (key, index);
113+
}
114+
// docs:end:portal_claim_public
115+
116+
// docs:start:portal_claim_private
117+
/// @notice Withdraw from Aave and send an L1->L2 message to mint tokens privately on L2
118+
function claimFromAavePrivate(
119+
uint256 _aTokenAmount,
120+
bytes32 _secretHash
121+
) external returns (bytes32, uint256) {
122+
// Withdraw from Aave (returns underlying + yield)
123+
aToken.approve(address(aavePool), _aTokenAmount);
124+
uint256 withdrawn = aavePool.withdraw(address(underlying), _aTokenAmount, address(this));
125+
126+
// Send L1->L2 message for private minting
127+
DataStructures.L2Actor memory actor = DataStructures.L2Actor(l2Bridge, rollupVersion);
128+
bytes32 contentHash =
129+
Hash.sha256ToField(abi.encodeWithSignature("mint_to_private(uint256)", withdrawn));
130+
131+
(bytes32 key, uint256 index) = inbox.sendL2Message(actor, contentHash, _secretHash);
132+
return (key, index);
133+
}
134+
// docs:end:portal_claim_private
135+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.27;
3+
4+
import {ERC20} from "@oz/token/ERC20/ERC20.sol";
5+
6+
// docs:start:mock_atoken
7+
contract MockAToken is ERC20 {
8+
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
9+
10+
function mint(address to, uint256 amount) external {
11+
_mint(to, amount);
12+
}
13+
14+
function burn(address from, uint256 amount) external {
15+
_burn(from, amount);
16+
}
17+
}
18+
// docs:end:mock_atoken
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.27;
3+
4+
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
5+
import {MockERC20} from "./MockERC20.sol";
6+
import {MockAToken} from "./MockAToken.sol";
7+
8+
// docs:start:mock_aave_pool
9+
/// @notice A simplified mock of Aave V3's lending pool for tutorial purposes.
10+
/// Supports supply and withdraw with a configurable yield in basis points.
11+
contract MockAavePool {
12+
MockERC20 public underlyingToken;
13+
MockAToken public aToken;
14+
uint256 public yieldBps; // e.g. 1000 = 10%
15+
16+
constructor(address _underlyingToken, address _aToken, uint256 _yieldBps) {
17+
underlyingToken = MockERC20(_underlyingToken);
18+
aToken = MockAToken(_aToken);
19+
yieldBps = _yieldBps;
20+
}
21+
22+
/// @notice Deposit underlying tokens and receive aTokens (mimics Aave V3 IPool.supply)
23+
function supply(
24+
address asset,
25+
uint256 amount,
26+
address onBehalfOf,
27+
uint16 /* referralCode */
28+
) external {
29+
require(asset == address(underlyingToken), "Wrong asset");
30+
IERC20(asset).transferFrom(msg.sender, address(this), amount);
31+
aToken.mint(onBehalfOf, amount);
32+
}
33+
34+
/// @notice Withdraw underlying tokens by burning aTokens (mimics Aave V3 IPool.withdraw)
35+
/// Returns the original amount plus simulated yield
36+
function withdraw(address asset, uint256 amount, address to) external returns (uint256) {
37+
require(asset == address(underlyingToken), "Wrong asset");
38+
39+
// Burn caller's aTokens
40+
aToken.burn(msg.sender, amount);
41+
42+
// Simulate yield: return amount + yield
43+
uint256 yieldAmount = (amount * yieldBps) / 10000;
44+
uint256 totalReturn = amount + yieldAmount;
45+
46+
// Mint extra underlying to cover yield (mock-only behavior)
47+
underlyingToken.mint(address(this), yieldAmount);
48+
49+
// Transfer underlying + yield to recipient
50+
underlyingToken.transfer(to, totalReturn);
51+
return totalReturn;
52+
}
53+
}
54+
// docs:end:mock_aave_pool

0 commit comments

Comments
 (0)