How AAVE Hacked the ERC-20 Token in the Most Beautiful Way

Bcral
Coinmonks
5 min readDec 17, 2021

--

Source: Aave.com.

“How does a token balance increase every block with no transaction? “

This was the question that led me into a deep dissection of the AAVE smart contracts, and brought me to the point of...well...feeling kind of stupid. This is going to be a technical explanation of how AAVE works under the hood to give you live-updating LP token balances, and how this could just as easily be used in a malicious or insecure fashion. By the end, you should know how to safely implement the same functionality in your ERC-20 contracts.

I’m writing this, not as a step-by-step guide to developers that don’t enjoy reinventing the wheel(though it could also be that), but as a literary tip of my hat to the amazing engineering of the AAVE team. I am not in any way associated with them, though I have interacted with their contracts in my own work. Their live-updating LP token(aka: aToken) provides a smooth, convenient, and all-around pleasant user experience. Who doesn’t like watching the balance on their account go up before their eyes?

At this point I should pause to clarify: I’m not positive where this yield-calculating token structure originated. I first discovered it in AAVE’s contract collection, though the idea may have been borrowed from another. Please leave a comment if this is the case, so credit can be appropriately attributed.

Intro over. Commence dive-in:

Excerpt from AAVE’s documentation — ERC20 interface

We’ll begin with perhaps the two most called functions in the dApp developers’ dictionary: balanceOf() and totalSupply(). AAVE explains here that these functions are called to return “…the amount of tokens owned…” and “…the amount of tokens in existence…”, respectively. No shock to those of us accustomed to these, and the docs show no anomalies.

If AAVE’s aToken is returning a continually incrementing number every time balanceOf() is called, who is paying the gas? Something isn’t adding up(pun intended).

To get a better understanding, here is an excerpt from AAVE’s actual aToken contract(v2) via GitHub:

Source: https://github.com/aave/protocol-v2/blob/master/contracts/protocol/tokenization/AToken.sol

At first glance, we’re looking at the same balanceOf() function, with the same use as before. On closer inspection, there’s a lot more happening here. I will first emphasize the use of the “override()” method. This prevents the contract from using the standard ERC20 balanceOf() logic, and instead, executes this function when called.

“Ok, cool. The logic is different despite the function call being the same. How does it return a dynamic value without consuming gas?”

Maybe the earlier question was a lie. THIS is the question that really racked my brain: How can a function call, which is read-only, return a different value every block? To answer this, first we need to understand AAVE’s indexing system for calculating lending/borrowing interest rates(APY and API, accordingly).

This “Liquidity Index” is re-calculated every time a pool is updated, either with a new deposit of liquidity, a loan repayment, or a collateral liquidation. Functionally, this is actually calculated with time as a consideration. After all, APY and API are annual calculations. The details of this are outside the scope of this analysis, but to be thorough, just know that time is another factor that keeps the index growing in addition to transactions.

So the Liquidity Index is a living, breathing representation of AAVE lenders’, and borrowers’, relationship to their relative liquidity pools. To understand how the Liquidity Index is used to calculate aToken balances, we need to understand the scaledBalanceOf() function from the aToken contract, which AAVE explains here: https://docs.aave.com/developers/the-core-protocol/atokens#scaledbalanceof

In summery, when a deposit is made, it is stored as the user’s “Scaled Balance” — meaning as a representation of both the value deposited, and the AAVE pool market at the time of deposit. Since I’m not one for advanced algebra, I’ll break this down at my pace:

Deposit Amount / Current Liquidity Index = Scaled Balance

So now that we have the actual value that is stored in AAVE’s contracts for a user’s balance, how do we find the current balance at any given time? Simply reverse the formula:

Scaled Balance * Current Liquidity Index = aToken Balance

Since the Liquidity Index only increments, the account’s aToken balance can never go down without a withdrawal. This makes the whole indexing system unidirectional, and consequently, simpler and more secure.

“Gas-free calculation explanation, please!”

Now that all of the pieces are here, let’s pull them together. As you may have noticed in the snap of the aToken contract’s balanceOf() function, the scope of the function is restricted to “view”(as balance-checking functions should be). Sometimes I forget that just because a function is restricted to view-only, it can still calculate complex arithmetic, making cross-contract calls if required, as long as it is not used to update state. Well, that’s what’s happening.

To get the most recent balance the aToken contract must calculate the current Liquidity Index to multiply by “super.balanceOf”(referencing the underlying IncentivizedERC20 contract’s balanceOf()), which is also the Scaled Balance. Once the index is returned(again, via read-only calculation), without storing it, the account’s current balance can be retrieved.

And there it is; How AAVE hacked our understanding of ERC-20 tokens to make something amazing!

“What are the possible security risks of function overriding?”

Now that we know how it’s done, we can see how this can be used not only to make ERC20 interactions more user-friendly, but also the dangers in assuming the safety of any function named balanceOf(), transfer(), or any other standard. Always inspect the contract for integrity instead of blindly making a web3 call.

Another possible issue is algorithmic — What if AAVE’s contract generated more aTokens for a user than they were actually owed? Using this model, we need all factors effecting user balances to be included in the Liquidity Index calculation. This is why exhaustive tests should always be done before any contract is made public.

Join Coinmonks Telegram Channel and Youtube Channel learn about crypto trading and investing

Also Read

--

--

Bcral
Coinmonks
0 Followers

Blockchain Developer, focusing mostly on Solidity, DeFi, and whatever else is shiny.