-
Notifications
You must be signed in to change notification settings - Fork 599
feat(docs): add aave tutorial #21048
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
Open
sklppy88
wants to merge
1
commit into
next
Choose a base branch
from
ek/feat/aave-tutorial
base: next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
466 changes: 466 additions & 0 deletions
466
docs/docs-developers/docs/tutorials/js_tutorials/aave_bridge.md
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
| 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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