How ScopeLift Built a Flex Voting aToken on Aave

February 2, 2023 / David Laprade

Aave is a popular DeFi protocol that allows users to trustlessly earn interest, borrow assets, take out flash loans, etc.

ScopeLift is very grateful to have been recently awarded a grant by the Aave Grants DAO to add flexible voting to that list.

In this post, we're going to explain how Aave calculates interest on deposits, and how their approach made it possible for us to implement a flex voting aToken.

Flexible Voting

Flexible voting (or flex voting for short) is a novel extension to Compound-style governance systems originally developed with funding from Uniswap Grants. It allows delegates to split their voting weight across For/Against/Abstain options, rather than having to put all of it behind one.

Enabling flex voting for Aave would unlock a large amount of value for users.

To see why, imagine you hold UNI. UNI is the voting token for Uniswap governance proposals. Anyone who holds UNI can delegate and vote on these proposals and help set the course of one of the biggest projects in crypto.

It would be really nice if you could also deposit your UNI to earn yield at the same time.

The trouble is that if you were to deposit UNI into a DeFi protocol (like Aave) you would lose your UNI voting power.0 This is because the address that holds the tokens controls their voting weight. And if you deposit your UNI, the receiving contract -- not you -- would hold the tokens.

So governance token holders are largely forced to choose: either participate in governance or earn yield. They cannot currently have both.

If flex voting were added to Aave, however, they could.

In such a world, Aave depositors would express their voting preference to an Aave contract, which would then cast its vote in the appropriate ratios to the governance contract.

For example, if 35% of UNI depositors expressed a "No" voting preference on a given proposal, 60% expressed "Yes", and 5% "Abstain", then the Aave contract could cast its vote to the UNI governance system with 35% of its weight as Against, 60% of its weight as For, and 5% as Abstain.

Aave users would still get to vote while their UNI balance was deposited and earning yield!

flexible-voting-diagram

While flex voting makes this dream scenario possible, it's not without technical hurdles.

Here is a quick TLDR of the problem and how we solved it:

  1. Flex voting requires being able to compare past balances for users at arbitrary blocks.
  2. Aave balances are constantly changing (aka "rebasing") at a rate that potentially changes with each transaction.
  3. Rebasing makes it very difficult to accurately compare past balances, and thus implement flex voting for Aave.
  4. Aave's implementation of rebasing involves scaling balances down by the amount of interest acccrued globally.
  5. ScopeLift was able to implement flex voting on Aave by checkpointing these scaled-down balances.

Let's dig in and explain what all of that means.

aToken Rebasing ELI5

When users deposit tokens into Aave, they receive ERC20 tokens in return. Aave's ERC20s are called aTokens. If you deposited DAI on Polygon, you would receive aPolDAI back. Here is a list of other commonly-issued aTokens.

aTokens are receipt tokens: they represent a claim on deposited assets.1 When you want to withdraw your deposit and claim its accumulated interest, your aTokens are burned.

One thing that makes Aave's aTokens interesting is that their balance programmatically increases over time. If you have (say) 100 aPolWETH today, at an interest rate of 2% APY, you will have 102 aPolWETH in a year from now. That's without making any new deposits or interacting with the protocol in any way. It's built into the aToken contract.

This is called rebasing. Said another way, aTokens are rebase tokens.

Rebasing is what makes it technically challenging to add flex voting to Aave. If user balances are constantly changing, at an ever-changing rate, how do you know what their voting weight should be at any given time?

Aave has a really interesting implementation of rebasing -- one which is not well understood. In this post we'll explain how it works, then show how it's actually nicely compatible with flex voting.

Liquidity Indexes

A key concept in understanding aToken rebasing is the notion of a liquidity index. It's defined in Aave's v2 whitepaper as follows:

Interest cumulated by the reserve during the time interval ∆T, updated whenever a borrow, deposit, repay, redeem, swap, liquidation event occurs.

In other words, the liquidity index for a certain asset is the total amount of interest earned by deposits of that asset into Aave -- expressed as a percentage scaled up by a ray.

Let's unpack that for a second, because it's a mouthful and this is really important.

aPolDAI currently has a liquidity index of 1010832786421347125844242759, or 1.083%. This means that in aggregate, over the entire lifetime of Aave, DAI deposits have earned 1.083%. So if you were the very first person to deposit DAI into Aave and you haven't touched it since, your deposit would today be worth 1.083% more.

Each asset has an independent liquidity index associated with it. And indexes are constantly increasing. As noted in the whitepaper, an index increases any time a fee-generating action is taken by a user.

Take borrowing, for instance. Suppose you've supplied DAI to Aave and someone else borrows DAI. The borrower will pay a certain amount of interest into Aave for the privilege, and that interest will accrue to you and other suppliers of DAI.

How it does so is by increasing the DAI liquidity index. It happens here in the code:

// Calculate the amount of interest that accrued since the last update.
// The amount will be a ray-based percentage.
uint256 cumulatedLiquidityInterest = MathUtils.calculateLinearInterest(
  reserveCache.currLiquidityRate,
  reserveCache.reserveLastUpdateTimestamp
);

// Add that interest to the current index by multiplying them together.
reserveCache.nextLiquidityIndex = cumulatedLiquidityInterest.rayMul(
reserveCache.currLiquidityIndex
);

For example, if the current liquidity index is 1050000000000000000000000000 (i.e. 5%) and another 2.5% has accrued since the last update, the new index will be 1076250000000000000000000000 (because 1.05 * 1.025 = 1.07625).

It might not be obvious, but an increase in the liquidity index is the accrual of interest within Aave. The former constitutes the latter. This is because any increase to the liquidity index results in a corresponding increase in aToken balance, as we'll see in the next section.

Scaled Balances

The second key thing to understand about Aave is that it scales user balances down by the liquidity index before writing them to storage.

When you deposit (say) $100 DAI into Aave, your balance is not incremented by 100e18 in storage. Rather, your balance is incremented by your deposit divided by the current liquidity index. So if the current aPolDAI liquidity index is 1.083%, then your balance would be incremented by 100e18/1.01083, or ~98.93e18. This scaling down of balances can also be seen in the aToken's burn and transfer functions.

Does this mean that all deposits immediately lose value -- in this case 1.083%?

No.

Because on the way out Aave scales balances _up_ by the current index. This happens in two places in the aToken: totalSupply and balanceOf. For now, let's focus on the latter. It looks like this (indented to make it easier to read):

super.balanceOf(user).rayMul(
  POOL.getReserveNormalizedIncome(_underlyingAsset)
);

rayMul just means multiply these values together then divide by a ray.

So this balanceOf function is simply multiplying two values together. The first should be obvious: it's just the user's balance in storage.

The second is less obvious. The logic for the getReserveNormalizedIncome function looks like this (again, with indentation):

MathUtils.calculateLinearInterest(
  reserve.currentLiquidityRate,
  reserve.lastUpdateTimestamp
).rayMul(
  reserve.liquidityIndex
);

So, the aToken balanceOf function is simply multiplying the stored balance by the current liquidity index (incremented by whatever interest has not yet been factored in).

Initially, this cancels out the effect of scaling down the balance, so that if someone went to withdraw immediately after depositing they would get all of their money back. E.g. a stored balance of 98.93e18 aPolDAI would be multiplied by the liquidity index of 1.01083, resulting a withdrawable balance of 100 DAI.

But over time, as the liquidity index increases, it results in a net positive aToken balance, since the stored balance is being multiplied by a larger number than the deposit was initially divided by.

Implementing a Flex Voting aToken

If something like the dream scenario were to exist, it would have to take account of the fact that aToken balances are constantly increasing, and doing so at an ever-changing rate.

After all, if I could have burned my aTokens and withdrawn a certain weight of voting tokens at the time of a proposal, any vote cast on my behalf by the aToken should reflect that weight.

Let's look at a concrete example:

  • Alice deposits 100 UNI into Aave
  • a year passes and aUNI yields 50% interest (it was a very good year)
  • Bob deposits 100 UNI
  • Alice and Bob are the only UNI depositors
  • a UNI governance proposal is issued
  • both Alice and Bob express their votes on the proposal to the aUNI contract
  • Alice expresses a "For" preference
  • Bob expresses an "Against" preference

When aUNI casts its vote to the UNI governance system, what should the vote ratios be?

If you were only looking at their deposits you might think Alice and Bob should have equal voting weight -- since they both deposited 100 UNI. But that's wrong. Alice should have more voting weight than Bob because of all of the interest she earned over the last year.

Alice's balance at the time of proposal was 150 aUNI. This would have entitled her to have withdrawn (and voted with) 150 UNI at that time. Bob, by contrast, could have only withdrawn and voted with the 100 UNI he had just deposited.

So, it seems what we need to look at are rebased balances, not deposits. The rebased balances would have led us to the correct conclusion: Alice's voting preference should have 50% more voting weight than Bob's. If the aUNI contract holds 250 UNI and Alice is entitled to 150 of that, Alice should be able to determine 60% (150/250) of the votes aUNI casts, and Bob the remaining 40% (100/250).

Thus it seems the dream scenario requires us to be able to calculate the rebased aToken balance for any given user at any given block.

In case it isn't obvious, this is very challenging.

As already mentioned, aToken interest rates are constantly changing based on supply and demand within the protocol. If some transaction increases supply, the interest rate goes down. If some transaction increases demand, the rate goes up. The rate is adjusted on a per-transaction basis.

And each time the interest rate changes, the interest accrued since the last update is added to the liquidity index (as shown above). This means that the aToken liquidity index is constantly changing too.

Recall that the aToken's rebased balance is just the balance in storage multiplied by the liquidity index.

If both values (balance-in-storage and liquidity index) are constantly changing, how can we hope to reliably compute a value based on them?

One approach would be to checkpoint these variables, i.e. to save timestamped copies of them. This would allow us to pull the historical values of the variables and compute past balances.

But, short of checkpointing both values each time they change—which would be unfeasibly expensive to write and later search for a popular asset with a lot of usage on Aave—there seems to be no simple way to accurately compute an arbitrary user's rebased balance at a block in the past.

Fortunately, we don't have to.

A Solution!

Eventually we realized that if the aToken simply checkpoints each user's scaled down balance (i.e. their balance in storage) it has all of the information it needs to precisely calculate voting weight proportions.

This is because the rebased balance is scaled-down-balance _ liquidity index. Since every aToken holder's balance is being scaled up by the same liquidity index, the liquidity index I can be ignored when computing ratios. For example, if the rebased balance for Alice is A * I, where A is Alice's scaled-down balance in storage, and the rebased balance for Bob is B * I, then the ratio of Alice's rebased balance to Bob's is equal to A / B -- the index I cancels out.

But if we only compare the stored values, wouldn't Alice and Bob end up with the same voting weight since they deposited the same amount?

No.

This is because Aave divides incoming balances by the current liquidity index before storing them.

We can see how this works if we reframe the example above with stored balances:

  • suppose UNI has earned in total 5% over its lifetime on Aave (liquidity index = 1.05e27)
  • Alice deposits 100 UNI into Aave
  • Alice's balance is stored as ~95.24 (100/1.05)
  • a year passes and aUNI yields 50% interest (it was a very good year)
  • the new liquidity index is 1.575e27 (1.05 _ 1.5)
  • Bob deposits 100 UNI
  • Bob's stored balance is ~63.49 (100 / 1.575)
  • if Alice withdrew now, she could claim 150 UNI (95.24 _ 1.575), which is expected because she earned 50% APY this past year
  • if Bob withdrew now, Bob could claim 100 UNI (63.49 * 1.575), which is expected because he just deposited
  • a governance proposal is issued for UNI
  • both Alice and Bob express our votes on the proposal to the aUNI contract
  • Alice expresses a "For" preference
  • Bob expresses an "Against" preference
  • the aUNI contract can use the checkpointed raw balances at the proposal block to determine their relative voting weights: Alice has 50% more voting weight than Bob (95.24 stored balance vs 63.49)

Aave's clever interest implementation neatly accommodates the exact information needed to support flex voting aTokens.

Conclusion

We hope that this has been a useful introduction to Aave's interest rate implementation. You can view ScopeLift's flex voting aToken extension here.


0 In case it isn't clear: Uniswap governance is unrelated to Aave's governance system. If flex voting aTokens are added to Aave, holding aUNI will only confer the right to participate in Uniswap governance -- not Aave governance.

1 Strictly speaking, aTokens are a claim on available liquidity. If someone has borrowed the funds you've deposited, you will be unable to withdraw them.