Manipulation-Resilient Price Oracle for CoW AMM LP Tokens
Author
The implementation of this project will be carried out by: @bh2smith & @lumoswiz
About the authors
bh2smith is an experienced blockchain engineer and former contributor to CoW Protocol.
lumoswiz is a smart contract developer with experience working with DeFi protocols, including lending and p2p lending protocols.
Grant Category
Integrations and protocol order flow
Grant Description
In response to this RFG, we aim to develop the manipulation-resistant price oracle for CoW AMM LP tokens. This will require incorporating the following ideas:
Computing the rebalancing trade that a zero-fee constant function AMM would accept based on its current balance and external price feeds. This will require utilisation of the CoW helper contract and Chainlink price feeds for the underlying tokens.
The effect on the price of LP tokens is counteracted since their price is a function of the simulated pool state post-rebalancing.
Grant Goals and Impact
The use of AMM LP tokens as collateral is a challenge due to their susceptibility to manipulation within price oracles.
The objective of this work is to produce an oracle for LP tokens that guards against an attackerâs ability to capitalise from short-term under-reporting of value or short-term over-reporting of value.
The use of an oracle that guards against these attacks should boost the adoption of LP tokens as collateral in some lending markets.
To demonstrate real-world application, we will create a proof-of-concept integration with Aave V3, showcasing how LP tokens can be used as collateral while utilising our oracle solution.
Milestones
Milestone 1
This milestone has three deliverables, including:
A smart contract implementing the oracle. The oracle must:
Adhere to the Chainlink oracle interface.
Utilise existing Chainlink-compatible oracles for the underlying tokens.
Support two token pools with arbitrary weights.
Comprehensive test suite demonstrating the oracleâs manipulation resistance.
Documentation detailing the oracleâs functionality. The documentation will be supplied in markdown format within the projectâs Github repository.
Milestone 2
An integration example with Aave V3 will be provided where
LP tokens can be used as collateral.
The oracle source for these tokens will point to the oracle developed herein.
This will involve:
Setting up an Anvil mainnet fork.
Setup scripts to initialise and configure the LP tokens as a reserve.
Demonstration of user actions against this state, such as supplying LP tokens as collateral.
Demonstration of oracle resilience to manipulation.
By submitting this grant application, I acknowledge and agree to be bound by the CoW DAO Participation Agreement and the CoW Grant Terms and Conditions.
Having a robust oracle for CoW AMM LP tokens will enable using them as collateral in lending protocols, which can unlock further use cases.
Signalling my support
Hi, thanks for the inclusion of Foundry in the specification of the grant. Tooling and language selection is a large factor when it comes to burden that may be absorbed by the core team if there is some need to provide out-of-grant changes / support later on, significantly easing maintenance.
I signal my support as well for this grant, and think it would be fine to move this to snapshot.
Just as a heads up, votes can be changed on snapshot before the closing of the poll. Not that I think this will be an issue, just an FYI for your own risk mitigation
Development is well under way. We have established what we believe to be a complete and solid first draft of the LPOracle contract as well as an OracleFactory (for convenience).
Unit tests are in place and we have begun setting up fork tests.
A couple of points we would like to highlight for discussion/confirmation are:
I like how the discussion items are presented, they are all reasonable technical questions.
An overall point is that we should start with âwell-knownâ tokens and extend the oracle use to less reliable tokens at a later stage.
Token Decimals stored immutably as gas savings. I see that the gas impact can be significant, for example a call to decimals() on USDC, somewhat of a worst case, is about ~10k gas. Iâve never seen a token that can change decimals in the wild. So overall it sounds reasonable to have them immutable. Still, it would be good if it were possible to deploy a new oracle for the same two tokens in the case that the decimals change. Then, the occasional meme coin that could change decimals once in a while can still be supported, provided that the new oracle is used.
Underlying Pool Assets must have â¤18 decimals. I read itâs possible but what I donât see mentioned is the cost of supporting tokens with >18 decimals, I suppose itâs significant extra smart-contract risk and complexity. Iâve seen a few tokens with 21 decimals, but they are exceedingly rare and it should be ok to not implement support for them at this point if too costly.
Cannot rebalance pool reliably. Thatâs indeed concerning and I suppose itâs a consequence of the fact that the helper contract isnât really the right tool here (itâs more or less a mistake that the grant singles out the helper so much, it should focus more on the rebalancing act). Reverting when the imbalance is large would be a fairly big limitation for an oracle, especially at times of high volatility, which is when the oracle is the most critical. A better solution would be to use the oracle code itself to compute the rebalanced price, which shouldnât add too much complexity. This would also make the overall gas cost smaller.
Choice of oracleâs updatedAt. Overall, it feels wrong to hijack the oracle semantics to include extra data. I see itâs not clear what updatedAt should be, technically weâre updating the prices once the most recently updated oracle has been updated, but the least recently updated oracle is a better indication for price trustworthiness, which is usually the point of oracle timestamps. However, the latter might be an issue if for example some oracles rely on updatedAt for refreshing some price cache with a price update by backtracking to an earlier block and simulating the transaction at this point.
A possible idea to have all data included in the oracle and being semantically consistent could be the following:
updatedAt is the timestamp of the most recently updated oracle
startedAt is the timestamp of the least recently updated oracle
roundId is the same as startedAt, meaning that a new round starts once the least recently updated oracle gets updated (and quite a few roundIds are skipped).
Not sure how consistent is with the Chainlink oracle specs. One thing that might be against expectation is that old rounds can be current and updated after a new round started.
Thereâs no clear solution here and Iâm happy to discuss things further. If itâs just about the extra data, we could also use the deprecated field answeredInRound and keep the semantics more consistent.
Thanks for the insights and comments. I wanted to add a few things to this discussion on re-balancing the pool, however, please correct me if my understanding is off on anything.
I donât think the expectation that the helper contract should produce an order that perfectly balances the pool every time is correct, since the user inputs a prices vector. Its purpose seems to be to produce an order that satisfies the pool invariants given input prices. It is actually possible to get the order function to produce an order that balances the pool by varying the prices. However, this is not the intention for this work.
In this work, we are looking at pricing the LP tokens via simulating an order that:
satisfies pool invariants
zero fees
prices vector constructed using external chainlink price feeds
To achieve the above, the helper contract is great. The order function does a good job of producing simulated reserves that move toward pool rebalancing. Just based on some results I am seeing in unit tests from today, the LP token price is either at, or within 0.1%, of the balanced pool LP token price.
you are correct about the helper contract. And in light of the discussion, it may be better to forget about it altogether. The oracle should look at the pool invariant k (calculated by multiplying the two liquidity reserves) and use that to calculate the TVL of a hypothetical AMM with the same invariant k but prices equal to those from Chainlink. If you go through the math, the TVL computed this way should be 2* sqrt(k p1 *p2) (but please verify). Then, to get the LP token prices, you just divide that by the number of LP tokens.
The point we were tying to make (and which ended up generating confusion) is that the oracle logic follows more or less that of the helper contract: it looks at the on-chain state and uses some provided prices to compute a different state. But in this case, there is no need to have a separate helper contract.
I have been looking at this and, as far as I can tell, the math described above works for 2 token pools with equal weights (50/50). Just for reference, here is the 2 token weight pool equations for determining the reserves:
I get a different result. Let me show you my derivations:
first the invariant Q1^a * Q2^(1-a)=k (for a between 0 and 1), which can be equivalently written as (Q1/Q2)^a * Q2=k
Then the equation for the AMM marginal prices: if p1 and p2 are the chainlink prices, the AMM has the same prices as chainlink if and only if p1 * Q1/a =p2 * Q2/(1-a), which can be rewritten as Q1/Q2=(p2/p1)*(a/1-a)
Now you can compute the reserves as: Q1=k * [(p2/p1)*(a/(1-a))]^(1-a) ; Q2=k * [(p1/p2) * ((1-a)/a)]^a
and then TVL is k *p1^a * p2^(1-a) * [ (a/(1-a))^(1-a) + ((1-a)/a)^a ]
Again, you should double check my calculations, but as you can see, I donât get a sqrt anywhere (unless of course you set a=1/2)
Milestone 1 is estimated to be completed on 1/1/25, wanted to get an update and make sure we pay it out if it is was indeed completed.
(Otherwise, no problem to postpone the payment for when the milestone has been completed)
All the core logic has been implemented, and we had already moved on to testing. However, recent discussions have led us to a significant refactor: removing the helper contract dependency and implementing the math directly within the main contract.
We estimate that it will take an additional 2â3 weeks to finalize these changes.
We believe that this project is in shape for Milestone 1 Review. Note that, since the last discussion, we have removed the BCoWHelper in favour of intrinsic TVL calculation (as a gas optimization).
We highlight the tests demonstrating viability of this mechanism for âShort-term over-reporting of valueâ are captured by the assertions here. Unfortunately, we have not yet come up with an example resulting in âShort-term under-reporting of valueâ.
There are fork tests for all mainnet pools with ChainLink price oracles. These are all 50:50 pools since none of the all existing X:Y pools with X != Y have at least one token which doesnât have a ChainLink price feed.
At this stage we consider the project âready for reviewâ and, during this time, will proceed with milestone 2.