<Lend/> with Aave V2 đź‘»
So nice they named it twice (@AaveAave)! We built a minimal React component on top of the ghostly lending protocol’s V2, combining 🏗️ scaffold-eth, Aave’s subgraph and aave-js. Read on to see what we learned along the way, or check the code here!
🚨 This is experimental, use at your own risk! 🚨
Aave overview
The official V2 docs have the real deal, but super quick: users can deposit assets on Aave to receive corresponding “aTokens” which earn interest. Users borrow assets from Aave in one of two ways:
- Over-collateralised, borrowing up to a set loan-to-value amount, based on an amount they have deposited (receiving debt tokens, which accrue interest).
- Under-collateralised, via Flashloans, borrowing for the lifetime of a transaction.
We wanted to see what it took to provide the core Aave actions — deposit, borrow, repay and withdraw — in a plug and play component (and watch this space or this fork for more on Flashloans!)
This is a React.js integration — we aren’t writing solidity or deploying anything on-chain, this is just building on top of existing smart contracts
The local setup: a mainnet fork
As discussed previously, the Cambrian explosion on Ethereum has meant that you can no longer just spin up a fresh local blockchain if you want to plug into all that is good in Defi. Fortunately a local mainnet fork is included out of the box with 🏗️ scaffold-eth:
yarn fork
The meat of the component then lives in the <Lend/> component subdirectory of the react-app
: src/components/Lend
Fetching data: on-chain calls and subgraphs 🎣
The first challenge is getting information about the current state of the protocol. There are two levels of information we are interested in:
- Market data: information about the different reserve assets — availability & interest rates, as well as meta-information which we will need to interact with the protcol
- User data: understanding a user’s overall position on Aave, as well as their positions in specific reserve assets, so that we know how they can interact with the protocol.
If we were going straight to main-net or testing on Kovan (see below), both of the above would be best fetched using the Aave v2 subgraph. However we are running a local fork of mainnet, so our local transactions will soon be out of sync. Our initial version therefore involved fetching all the required data from the Aave contracts themselves. This worked, but required pretty heavy polling đź‘·, which was a lot of hard work if we want to go to mainnet in future.
🤔 Another approach would have been to deploy the Aave v2 subgraph on a local graph node looking at our local chain, however this would also have introduced quite a lot of overhead to our local setup.
The current branch therefore takes an expedient approach — we use the live mainnet subgraph for market data, while fetching user data from our local chain. This means that user state is correctly reflected, the component is pretty fast, and we are ready for mainnet (we also get the added learning benefit of digging into the Aave subgraph!)
⚠️ This does mean that our in-app reserve liquidity data will not precisely match our local chain’s liquidity. This is acceptable for our testing purposes, but it may not work if your build needs to be precisely matching, or if you hope to move the market & see the effects!
This data fetching happens in a custom React hook, useAaveData
which can be found in ./AaveData.jsx
GraphQL gets market data 🕸️
We are using Apollo to access Subgraph data. The AavesugraphUri
is set in the <ApolloProvider/>
at the top-level of the app ./index.jsx
. Fetching the data itself is as simple as defining the graphQL query and using useQuery
, receiving data
in response.
import { useQuery, gql } from '@apollo/client';const RESERVE_GRAPHQL = `
{
pool (id: "${POOL_ADDRESSES_PROVIDER_ADDRESS.toLowerCase()}"){
id
lendingPool
reserves {
id
underlyingAsset
symbol
name
...
}
}
}
`
const RESERVE_GQL = gql(RESERVE_GRAPHQL)
const { loading, data } = useQuery(RESERVE_GQL,{pollInterval: 6000});
Common gotcha, at least for me: make sure any addresses passed in as Subgraph filters are cast
toLowerCase()
!
We return the array of reserves
as assetData
. Our usage of the subgraph is pretty rudimentary here — we are just fetching current reserve state, and some meta-data.
Going on-chain for user data ⛓️
To know which actions are available to a user, we need to know their current standing on Aave — do they have any deposits or outstanding debts? This data is all available on-chain, but it is obviously slightly harder work than getting data back from a subgraph:
lendingPool.getUserAccountData(address)
— returns a summary of current deposits & debt, with totals stated in terms of ETHlendingPool.getUserConfiguration(address)
— returns a singleuint256
bitmask which captures the user’s current standing across all the assets on Aave.protocolDataProvider.getAllReservesTokens()
— returns a list of assets, the order of which we can use to decode the user’s bitmask Configuration (see more details on decoding configuration here).protocolDataProvider.getUserReserveData(asset, address)
— returns information about the user for a specific asset (we can selectively call this based on their decoded user configuration).
These on-chain calls are polling on an ongoing basis to stay up to date, courtesy of usePoller
frometh-hooks
. We return the latest user data, along with the overall assetData
, giving our app a full suite of data with which to build a component on Aave.
The <Lend/> Component đź’°
yarn start
We’re prototyping here, and as ever Ant Design has what we need to get started (and of course a tip of the hat to the original Aave app!)
Before we get into the table, we have the <AccountSummary/>
, showing the user’s collateral, debt and borrow allowance. <AccountSettings/>
is a “Settings” button, which slides out to show some key health statistics — crucially if a user’s Health factor dips below 1, they are at risk of Liquidation!
The table itself does three things. Firstly, it shows the key market summary statistics — the market size, the available liquidity, the rates for depositing and borrowing. Secondly it shows the current user’s position for each asset. And finally and most interestingly, it lets the user interact with Aave…
Interacting with Aave: aave-js txBuilder
In our initial implementation, we simply instantiated the relevant Aave contracts in ethers.js
in order to make contract calls, and we were happy enough. But then we discovered a hidden gem that we had somehow missed: TxBuilderV2
by aave-js
.
TxBuilderV2
can generate a lendingPool
object, which takes relevant method parameters to generate ethers.js transactionRequests
, including any approvals that might be required. These requests then just need to be sent by an ethers Signer.
Instantiating a txBuilder
lending pool (done once, in Lend.jsx
):
const httpProvider = new Web3.providers.HttpProvider(
process.env.REACT_APP_PROVIDER ||
"http://localhost:8545"
);
let customProvider = new ethers.providers.Web3Provider(httpProvider)let txBuilder
let lendingPoolif(customProvider) {
txBuilder = new TxBuilderV2(Network.main, customProvider);
lendingPool = txBuilder.getLendingPool(Market.Proto);
}
We initially tried to instantiate a
txBuilder
using theethers.providers.Web3Provider()
used elsewhere in the app, which did not work, so if you are having difficulties try the above!
Once you have a lendingPool
, you can then interact with it per the documentation, generating an array of transaction requests which you can iterate through as required (from AaveAction.jsx
, which we instantiate for each market for each applicable Aave action):
// will generate a "deposit" request plus an "approve" request if required in an array
_requests = await lendingPool.deposit({user: _address, reserve: assetData.underlyingAsset, amount: _amount})for (const t in _requests) {
// the await on the .tx() function is important!
let tx = await signer.sendTransaction(await _requests[t].tx())
// we then wait for one block confirmation before sending the next request
let receipt = await tx.wait(1)
}
tx.wait()
— this is a really helpful function from ethers.js that ensures that our transaction has been included on-chain before the next one is sent. This avoids the situation where the second transaction reverts because the first hasn’t been confirmed yet, which is more problematic on slower chains.
aave-js
handles a lot of complexity, including decimal calculations and argument validation — the improvement of the approve
UX is particularly close to my heart. The simple functions implemented as part of <Lend/>
are also the tip of the iceberg: watch out for more scaffold-eth
builds in this area!
So we’ve achieved our goal of supporting the core Aave actions — deposit, borrow, repay and withdraw — in a proof of concept React component (if a little rough around the edges, improving Pull Requests very welcome!)
Moving on up
Testing on Kovan
All of the Aave V2 contracts are also deployed on Kovan, so we can easily tweak the app to deploy it there:
- Update our .env file in
packages/react-app
to include aREACT_APP_PROVIDER
pointing at the Kovan Infura endpoint, and add aREACT_APP_NETWORK=kovan
- Add a switch to use the Kovan specific subgraph URL when
REACT_APP_NETWORK === 'kovan'
- Use the Kovan Infura endpoint for our Aave
TxBuilder
Web3 Provider, and switch the config to useNetwork.kovan
There are a few things to be aware of when interacting with Aave on Kovan…
- The ERC20s for Aave on Kovan are changeable, and aren’t necessarily interoperable with other deployed Defi protocols (e.g. Uniswap pools on Kovan might be way out of whack with Aave oracle prices)
- Not all markets are topped up equally, and there can be some pretty crazy interest rates on the testnet, but price oracles are updated to stay in line with mainnet prices, which means it can feel pretty close to reality!
But with those caveats, it’s a good stepping stone — see an example deployment here!
Catching the main train
One great thing about using a mainnet fork is that updating the app to work with mainnet is simple! Simply update the .env
file in ./packages/react-app
to include a REACT_APP_PROVIDER
pointing at a mainnet node, add REACT_APP_NETWORK === 'mainnet'
, and<Lend/>
is ready for the big leagues. Check out a deployed version here.
Conclusion
Aave is one of the foremost names in Defi, with ~$5B in assets stored at time of writing. Given a provider & the price of ETH, this 🏗️ scaffold-eth build returns a simple market component supporting all the key user actions, that works easily for local development as well as Kovan and mainnet deployment.
<Lend selectedProvider={userProvider} ethPrice={price} /
In part two, we run through the Aave Ape, a simple contract built on top of Aave and Uniswap.
We are excited to build on Aave & learn along the way — feedback, enhancements and comments very welcome!
References
- Aave docs
- Aave-js
- Aave subgraph information
- Hardhat mainnet forking
- ethers.js documentation
- Part two of this series
Hat tip as always to Austin Griffith and the rest of the Buidl Guidl!