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)