This codebase has been audited by three teams, whose reports are available in the repository: Nethermind, Peckshield and Certora.
- Table of contents
- Introduction
- Architecture
- Contracts
- Deployed Contracts
- How it works
- How to bridge without coding
- Installation
This bridge project is the first step of Aave in StarkNet ecosystem. The bridge allows users to deposit or withdraw their aTokens, and only aTokens
, on Ethereum side, then mints or burns them wrapped aTokens named static_a_tokens
on StarkNet side. static_a_tokens
are equivalent to aTokens
except that the former grow in value when the latter grow in balance.
Holding L1 aTokens lets you earn more tokens via two different mechanisms: (i) the amount of aTokens you hold increases over time and (ii) holding aTokens allows you to claim an accruing amount of Aave reward tokens. This bridge offers both mechanisms thanks to static_a_tokens
on L2, and the equivalent of L1 Aave reward token on L2.
The bridge is also shaped for liquidity providers who are able to assume the Ethereum gas cost of deposits and withdrawals as they transact large enough amounts. They will deposit on Aave Ethereum, bridge the static_a_tokens
to StarkNet and make them available for users there to buy and hold, thus accruing yield from L1.
We assume that L1 tokens approved by the bridge are pre-validated tokens, and that they are not deflationary.
L1
Bridge
- handles deposit ofaTokens
on L1, withdrawal ofstatic_a_tokens
from L2, and update of L2 rewards index. L1 deposits and withdrawals can be done withaToken
or with their underlying asset.
L2
static_a_token
- exchange-rate-increasing wrapper ofaTokens
on L2.incentivized_erc20
- ERC20-compliant token, tracks claimable rewards and stores the last updated rewards index for eachstatic_a_token
holder.rewAAVE
- ERC20 representing Aave reward token on L2.bridge
- bridge responsible for:- minting and burning
static_a_tokens
on message from L1. - bridging
rewAAVE
tokens back to L1. - updating
rewards_index
for eachstatic_a_token
on message from L1.
- minting and burning
proxy
- generic implementation of a proxy in cairo.
These static_a_tokens
are a starting point for almost any cross-chain liquidity development to minimize “active” communication between chains. By design, a holder of those tokens - on Ethereum or after bridging somewhere else - will be passively accumulating yield from Aave on Ethereum.
More precisely, static_a_tokens
are wrapped aTokens
that grow in value while aTokens
grow in balance. Such behavior is possible because static_a_tokens
are backed by increasing amounts of aTokens
locked in the L1 part of the bridge. static_a_tokens
living on L2 can be bridged back to aTokens
.
Each of the following contracts is deployed behind a proxy:
bridge
on L2static_a_token
on L2rewAAVE
token on L2Bridge
on L1
static_a_token
deployed contracts are controlled by L2bridge
.rewAAVE
token is controlled by L2bridge
.
- We rely on L1 -> L2 governance relayers to execute on L2 actions that have been decided on L1. In practice, we use two L1 contracts from Aave and one L2 contract from StarkNet DAI Bridge:
contracts/l1/governance/Executor.sol
: It corresponds to Aave Short Executor whose goal is to execute payload that have been previously accepted by the DAO after a vote. One first need to queue the transaction to execute, and execute it after waiting enough time. Its code has been taken from Etherscan: link.contracts/l1/governance/CrosschainForwarderStarknet.sol
: It contains a single function namedexecute
that sends a message to execute a functionrelay
of the contractl2_governance_relay
with an input address. It has been adapted from the one used for Polygon.contracts/l2/governance/l2_governance_relay.cairo
: It contains a single L1 handler namedrelay
as well that takes an address as argument, checks the origin of the call and executes the functiondelegate_execute
of the contract that correspond to the input address.
Ethereum
-
Bridge: proxy and implementation
-
CrosschainForwarderStarknet: implementation
-
AIP payload: implementation
StarkNet
-
bridge: proxy and implementation class
-
l2_governance_relay: proxy and implementation class
-
static Aave v2 Ethereum aDAI: proxy and implementation class
-
static Aave v2 Ethereum aUSDC: proxy and implementation class
-
static Aave v2 Ethereum aUSDT: proxy and implementation class
-
rewAAVE: proxy and implementation class
-
activate_bridge spell: implementation class
Ethereum
- Bridge: proxy and implementation
StarkNet
-
bridge: proxy and implementation class
-
static Aave v2 Ethereum aDAI: proxy and implementation class
-
static Aave v2 Ethereum aUSDC: proxy and implementation class
-
static Aave v2 Ethereum aUSDT: proxy and implementation class
-
rewAAVE: proxy and implementation class
L1 aTokens and their corresponding L2 static_a_tokens are approved on L1 bridge in initialize
function. The function _approveBridgeTokens
is called internally to approve an array of aTokens with their corresponding static_a_tokens on L2.
Users can either deposit their aTokens
(let's say aDai) or deposit the corresponding underlying asset (i.e Dai). Users first have to approve the bridge to spend the tokens - aTokens
or the underlying asset
. Calling deposit
function, the following actions happen:
-
If the user deposits underlying
asset
:asset
tokens will be transferred from the user account to L1 bridge.- The bridge will convert
asset
tokens to aTokens - by depositing in Aave's lending pool. - A message will be sent to L2 bridge with the amount of
static_a_token
to be minted, L1 token address, L2 recipient address, L1 block number and L1 rewards index. - L2 bridge will mint to L2 recipient the given amount of corresponding
static_a_tokens
.
-
If the user deposits
aToken
:aTokens
will be transferred from the user account to L1 bridge.- A message will be sent to L2 bridge with the amount of
static_a_token
to be minted, L1 token address, L2 recipient address, L1 block number and L1 rewards index. - L2 bridge will mint to L2 recipient the given amount of corresponding
static_a_tokens
.
To bridge their static_a_tokens
back to L1, users should first initiate a withdrawal on the L2 bridge. Calling initiate_withdraw
results in the following:
- The amount of
static_a_tokens
to withdraw will be burned by L2 bridge. - A message will be sent to L1 with L1 aToken address, L1 recipient, L2 rewards index and the amount.
Once the withdrawal is initiated on the L2 bridge, one can call the function withdraw
on L1 bridge. Calling this function results in the following:
- The message previously sent will be consumed: if function parameters and parameters sent in the message are not the same, the withdrawal fails, otherwise the rest follows.
- L1 bridge will then transfer
aTokens
to the L1 recipient. - L1 bridge will also check for any difference in the L1/L2 rewards index and transfer any unclaimed rewards to L1 recipient.
StarkNet users will keep enjoying the same rewards as on L1 after bridging their assets. To do so, L1 rewards index is stored in the state of static_a_tokens
. The index is updated every time a user deposits or withdraw the corresponding aToken
, and can also be updated in a permissionless manner by calling the function updateL2State
in L1 bridge. Rewards on L1 are sent to L1 recipient either when withdrawing static_a_tokens
from L2 or when calling and then bridging rewards on L2 as described below.
To claim rewards, an L2 user should call claim_rewards
on static_a_token
contract which calls L2 bridge in return. L2 bridge then mints due rewAAVE
tokens to the L2 user.
Calling bridge_rewards
on L2 token bridge results in:
- The bridged amount of
rewAAVE
tokens will be burned. - L1 bridge receives the bridging message and claims the rewards amount to
self by calling
claimRewards
on AaveIncentivesController
contract. - Rewards are then transferred to L1 recipient.
If L1 -> L2 message consumption is unsuccessful, the user would lose custody over his aTokens forever.
That's why we have added support for the L1->L2 message cancellation on our L1 bridge contract, where users can cancel deposits of their aTokens by following the steps below:
-
The user calls
startDepositCancellation
on L1 bridge by providing themessage payload
andnonce
of thedeposit
message. -
After the
messageCancellationDelay
period has passed (defined on StarknetMessaging contract), the user can finalize the aTokens deposit cancellation by callingcancelDeposit
on L1 bridge.
The amount of bridged aTokens is restricted to a certain amount set at the moment of deployment. We provide an array ceilings
with a ceiling for each aToken to be approved on the L1 bridge, and we make sure that the bridge will only hold a scaled balance (without taking into account the interest growth) inferior or equal to the decided ceiling for each aToken.
This section explains how to bridge Ethereum aTokens to StarkNet staticATokens using Etherscan UI and wallets on both networks.
The first step is to have an Ethereum-compatible wallet funded with one of the six following tokens: DAI, aDAI, USDC, aUSDC, USDT or aUSDT.
You should now select the amount you would like to bridge. Note that the tokens above do not have the same number of decimals. For this, go on the Etherscan page corresponding to your tokens, say DAI for instance, click on the tab "Read Contract" or "Read as Proxy" and call the function balanceOf
with your wallet address.
If you hold $50 of DAI / aDAI, the output of balanceOf
should be approximately 50000000000000000000
, and if you hold $50 of USDC / aUSDC / USDT / aUSDT, it should be about 50000000
.
Before depositing your tokens to the bridge, you should allow the bridge to spend your tokens. To do so, go on the token's Etherscan page, click on the tab "Write Contract" or "Write as Proxy", and then on "Connect to Web3" to connect your Ethereum wallet to Etherscan. Now, click on the function approve
, and fill in the bridge's address (0x25c0667E46a704AfCF5305B0A586CC24c171E94D
) as spender
and the amount you would like to bridge as amount
. Finally, click "Write" and accept the transaction on your wallet.
To deposit tokens to the bridge, go on the bridge contract Etherscan page here. If your wallet is disconnected, click on "Connect to Web3" again, and click on the deposit
function to display its arguments. You should then enter the following inputs:
l1AToken
: Fill in the address of the token you would like to bridge. For DAI, write0x6B175474E89094C44Da98b954EedeAC495271d0F
.l2Recipient
: Fill in your StarkNet wallet address, converted to decimal. For that, you can use this website, or useBigInt
function in JavaScript. For instance, if the StarkNet wallet address is0x01270059Ea5843794F1130830800EcEF60B7D1AFd195f1847a884223a5B94f4A
, you should fill in521222308224262530654458833061745344984501837223744122628617462097842360138
.amount
: Fill in the amount you would like to bridge. This amount should be lower or equal to the amount you have approved in the previous step.referralCode
: Fill in0
. This argument is proper to identify future integrators.fromUnderlyingAsset
: Fill infalse
if the token you bridge is an aToken (aDAI, aUSDC, aUSDT); otherwise, fill intrue
.
Finally, click on "Write", accept the transaction and wait for Ethereum and StarkNet transactions to finish.
On your StarkNet wallet, click on "+ New token" for Argent X or "+ Add token" for Braavos, and fill in staticAToken's address that corresponds to tokens you have deposited on the Ethereum side - see this section for deployed contracts' addresses.
Install Node 16
Our codebase relies on Node 16. To install it, you can first install nvm and then run the following commands:
nvm install 16
nvm use 16
Install Python 3.9.0
Our codebase relies on Python 3.9.0. To install it, you can first install pyenv and then run the following commands:
pyenv install 3.9.0
pyenv local 3.9.0
Install GMP (needed for Cairo)
Before installing Cairo you need to install GMP. Run one of the following command depending on your OS.
sudo apt install -y libgmp3-dev # linux
brew install gmp # mac
Install Node dependencies
Let's install all our project dependencies:
yarn install
Install Python dependencies
Let’s create a virtual environment to isolate your project’s requirements from your global Python environment.
python -m venv .venv
source .venv/bin/activate
Install poetry for dependencies management
python -m pip install --upgrade pip
pip install poetry
poetry install
Solidity files are automatically compiled before running the tests, but Cairo files are not. To compile them, run:
yarn compile:l2
We recommend to run L1 and L2 testnets in different terminals.
Start L2 testnet
In a terminal where venv
is activated, run:
yarn testnet:l2
Start L1 testnet
Create a .env
file from the sample (cp .env.sample .env
), and fill a value for the variable ALCHEMY KEY
- you can get one here. Then, load all the environment variables.
source .env
And start L1 testnet in the same terminal by running:
yarn testnet:l1
The project is tested using hardhat, the starknet hardhat plugin and starknet-devnet. We created a Docker Compose file to run tests easily: we start L1 and L2 test networks in two separate containers and run the tests from a third one. To run all tests, simply run the following commands:
docker compose up --build
docker exec -ti $(docker ps -f name=test-runner -q) bash
yarn test
First make sure to set the aTokens addresses to be approved on the bridge as well as the metadata related to the staticATokens
to be deployed on l2 in ./scripts/allowlistedTokens.ts
.
yarn deploy-bridge:testnet #deploys bridge on l1 & l2 testnets
Contributors