-
Notifications
You must be signed in to change notification settings - Fork 609
SIP-120: TWAP Exchange Function #1127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5bfa855
1cc81a3
a636257
dc2a596
ea912b0
5006195
7d673da
727f7fa
8c00986
0d13f92
3f2d02e
0ce3aab
6c599d1
eb434bc
4d44367
9dcf119
c4f7017
efcb2ab
12b6ff8
cd9b4d0
ee74c03
a5830ff
d9b3557
9cf8aa5
3a00545
4f1bff0
1396504
f26e027
9297499
8ee8699
4f49344
dc6b5b0
c524d8b
9b5eab4
29af20e
b93921f
9df8a6c
0fadca6
f9e4315
a2763b7
a064609
2977e5b
62dcb10
89c061a
5e0978c
1249c66
fedb980
41b5cb9
18c3116
0e9b1f0
61cbf04
10fe6c4
2f61305
72e6ed3
8b15ea7
76492c2
cf4ac49
1150c87
f9cd833
ef3af68
35726dc
517782e
69889c0
54e02b3
f733290
82cfb21
5ded13b
90baf12
2dfeff9
df1728d
ab8c221
e53d0fd
a2cde55
3881309
5ebadb8
d864225
62030ca
c814d64
df5803c
6c2bd7d
97bdd78
c1fb131
c0271f2
6c3c960
6bf88f2
5ba2cca
42a004a
5f80eed
4280ff6
431b9c0
c38afc4
0039dc8
07074cb
9f1c537
cdcbc24
72f682d
195c27e
43d820f
ae49dbb
1aa47be
5368328
7d4e868
47f4ac0
bcdbdcf
8892289
270b84c
05e236e
359ccdb
7a265d9
b49923b
02c1ba8
e06e164
7516d46
2c5dde0
558e666
bdf0e04
43a8c8b
7065e4a
344b4ce
cf972d5
15d0669
51ac00c
44418b6
84c7658
f2e98e2
29246ad
8ea4f3a
5694a08
6e0ada8
7c8ec13
34f8f59
ebae82d
7d422be
c5322d9
ab2f76d
4d7c03d
6446537
e50be51
5f9bc95
dac7715
faeeb4e
33dfbb1
9913039
17d3601
736bdfc
7ab9e95
06c4e79
9d3121d
63d830e
9e5ecfd
b921538
aa92214
e86093e
c14b980
6f0c673
ec86069
cfa3b9c
bb80fa6
b35186c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| pragma solidity ^0.5.16; | ||
|
|
||
| // Inheritance | ||
| import "./ExchangeRates.sol"; | ||
| import "./interfaces/IDexPriceAggregator.sol"; | ||
|
|
||
| // https://docs.synthetix.io/contracts/source/contracts/exchangerateswithdexpricing | ||
| contract ExchangeRatesWithDexPricing is ExchangeRates { | ||
| bytes32 public constant CONTRACT_NAME = "ExchangeRatesWithDexPricing"; | ||
|
|
||
| bytes32 internal constant SETTING_DEX_PRICE_AGGREGATOR = "dexPriceAggregator"; | ||
|
|
||
| constructor( | ||
| address _owner, | ||
| address _oracle, | ||
| address _resolver, | ||
| bytes32[] memory _currencyKeys, | ||
| uint[] memory _newRates | ||
| ) public ExchangeRates(_owner, _oracle, _resolver, _currencyKeys, _newRates) {} | ||
|
|
||
| /* ========== SETTERS ========== */ | ||
|
|
||
| function setDexPriceAggregator(IDexPriceAggregator _dexPriceAggregator) external onlyOwner { | ||
| flexibleStorage().setAddressValue( | ||
| ExchangeRates.CONTRACT_NAME, | ||
| SETTING_DEX_PRICE_AGGREGATOR, | ||
| address(_dexPriceAggregator) | ||
| ); | ||
| emit DexPriceAggregatorUpdated(address(_dexPriceAggregator)); | ||
| } | ||
|
|
||
| /* ========== VIEWS ========== */ | ||
|
|
||
| function dexPriceAggregator() public view returns (IDexPriceAggregator) { | ||
| return | ||
| IDexPriceAggregator( | ||
| flexibleStorage().getAddressValue(ExchangeRates.CONTRACT_NAME, SETTING_DEX_PRICE_AGGREGATOR) | ||
| ); | ||
| } | ||
|
|
||
| function atomicTwapWindow() external view returns (uint) { | ||
| return getAtomicTwapWindow(); | ||
| } | ||
|
|
||
| function atomicEquivalentForDexPricing(bytes32 currencyKey) external view returns (address) { | ||
| return getAtomicEquivalentForDexPricing(currencyKey); | ||
| } | ||
|
|
||
| function atomicPriceBuffer(bytes32 currencyKey) external view returns (uint) { | ||
| return getAtomicPriceBuffer(currencyKey); | ||
| } | ||
|
|
||
| function atomicVolatilityConsiderationWindow(bytes32 currencyKey) external view returns (uint) { | ||
| return getAtomicVolatilityConsiderationWindow(currencyKey); | ||
| } | ||
|
|
||
| function atomicVolatilityUpdateThreshold(bytes32 currencyKey) external view returns (uint) { | ||
| return getAtomicVolatilityUpdateThreshold(currencyKey); | ||
| } | ||
|
|
||
| // SIP-120 Atomic exchanges | ||
| // Note that the returned systemValue, systemSourceRate, and systemDestinationRate are based on | ||
| // the current system rate, which may not be the atomic rate derived from value / sourceAmount | ||
| function effectiveAtomicValueAndRates( | ||
| bytes32 sourceCurrencyKey, | ||
| uint sourceAmount, | ||
| bytes32 destinationCurrencyKey | ||
| ) | ||
| external | ||
| view | ||
| returns ( | ||
| uint value, | ||
| uint systemValue, | ||
| uint systemSourceRate, | ||
| uint systemDestinationRate | ||
| ) | ||
| { | ||
| IERC20 sourceEquivalent = IERC20(getAtomicEquivalentForDexPricing(sourceCurrencyKey)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. np consider it would be kind of slick to split majority this function into 2 internal functions, leaving the function body like: |
||
| require(address(sourceEquivalent) != address(0), "No atomic equivalent for src"); | ||
|
|
||
| IERC20 destEquivalent = IERC20(getAtomicEquivalentForDexPricing(destinationCurrencyKey)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. equivalent seems like misleading terminology here, maybe better to just say
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree that |
||
| require(address(destEquivalent) != address(0), "No atomic equivalent for dest"); | ||
|
|
||
| (systemValue, systemSourceRate, systemDestinationRate) = _effectiveValueAndRates( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the rate be incorrect if one of the currencies in Perhaps some extra checks / price normalisations are needed for |
||
| sourceCurrencyKey, | ||
| sourceAmount, | ||
| destinationCurrencyKey | ||
| ); | ||
| // Derive P_CLBUF from highest configured buffer between source and destination synth | ||
| uint sourceBuffer = getAtomicPriceBuffer(sourceCurrencyKey); | ||
| uint destBuffer = getAtomicPriceBuffer(destinationCurrencyKey); | ||
| uint priceBuffer = sourceBuffer > destBuffer ? sourceBuffer : destBuffer; // max | ||
| uint pClbufValue = systemValue.multiplyDecimal(SafeDecimalMath.unit().sub(priceBuffer)); | ||
|
|
||
| // refactired due to stack too deep | ||
| uint pDexValue = _dexPriceDestinationValue(sourceEquivalent, destEquivalent, sourceAmount); | ||
|
|
||
| // Final value is minimum output between P_CLBUF and P_TWAP | ||
| value = pClbufValue < pDexValue ? pClbufValue : pDexValue; // min | ||
| } | ||
|
|
||
| function _dexPriceDestinationValue( | ||
| IERC20 sourceEquivalent, | ||
| IERC20 destEquivalent, | ||
| uint sourceAmount | ||
| ) internal view returns (uint) { | ||
| // Normalize decimals in case equivalent asset uses different decimals from internal unit | ||
| uint sourceAmountInEquivalent = | ||
| (sourceAmount.mul(10**uint(sourceEquivalent.decimals()))).div(SafeDecimalMath.unit()); | ||
|
|
||
| uint twapWindow = getAtomicTwapWindow(); | ||
| require(twapWindow != 0, "Uninitialized atomic twap window"); | ||
|
|
||
| uint twapValueInEquivalent = | ||
| dexPriceAggregator().assetToAsset( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need to sanity check the return value here? |
||
| address(sourceEquivalent), | ||
| sourceAmountInEquivalent, | ||
| address(destEquivalent), | ||
| twapWindow | ||
| ); | ||
| require(twapValueInEquivalent > 0, "dex price returned 0"); | ||
|
|
||
| // Similar to source amount, normalize decimals back to internal unit for output amount | ||
| return (twapValueInEquivalent.mul(SafeDecimalMath.unit())).div(10**uint(destEquivalent.decimals())); | ||
| } | ||
|
|
||
| function synthTooVolatileForAtomicExchange(bytes32 currencyKey) external view returns (bool) { | ||
| // sUSD is a special case and is never volatile | ||
| if (currencyKey == "sUSD") return false; | ||
|
|
||
| uint considerationWindow = getAtomicVolatilityConsiderationWindow(currencyKey); | ||
| uint updateThreshold = getAtomicVolatilityUpdateThreshold(currencyKey); | ||
|
|
||
| if (considerationWindow == 0 || updateThreshold == 0) { | ||
| // If either volatility setting is not set, never judge an asset to be volatile | ||
| return false; | ||
| } | ||
|
|
||
| // Go back through the historical oracle update rounds to see if there have been more | ||
| // updates in the consideration window than the allowed threshold. | ||
| // If there have, consider the asset volatile--by assumption that many close-by oracle | ||
| // updates is a good proxy for price volatility. | ||
| uint considerationWindowStart = block.timestamp.sub(considerationWindow); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this heuristic too coupled to current CL implementation details? Would be easily broken improvements or differences between L1 & L2, requiring extra configuration overhead. It might be better to have a volatility heuristic that doesn't rely on an implementation quirk, but on the price values themselves. Volatility itself can be aggregated quite cheaply by aggregating the average price and the deviation from average price (https://www.investopedia.com/terms/v/volatility.asp) as EMA with L1 metrics (to avoid square and sqrt). This would also require just one call per round / per tx on average, and not Something like: // system settings
uint decayParam; // moving average decay
uint volatilityTreshold; // threshold for checking volatility
...
// persistent state variables
mapping(bytes32 => uint) lastRoundsSeen;
mapping(bytes32 => uint) priceEMAs; // mean aggregator
mapping(bytes32 => uint) deviationEMAs; // herustic volatility aggregator
...
// in the main method, check the volatility value vs. some threshold after updateing the aggregators
return _updatedVolatilityEMA(currencyKey) > volatilityTreshold;
...
function _updatedVolatilityEMA(currencyKey) internal returns (uint) {
uint roundId = _getCurrentRoundId(currencyKey);
uint lastRoundSeen = lastRoundsSeen[currencyKey];
if (roundId > lastRoundSeen) { // if round seen we can just use the up to date value
uint priceEMA = priceEMAs[currencyKey];
uint deviationEMA = deviationEMAs[currencyKey];
// for each round that was not see yet
for (lastRoundSeen; lastRoundSeen <= roundId; lastRoundSeen++) {
// the simple EMAs below don't use time, but can use it for more robustness
(uint price, uint time) = _getRateAndTimestampAtRound(currencyKey, roundId);
// update mean: priceEMA = priceEMA * decayParam + (1 - decayParam) * price
priceEMA = priceEMA.multiplyDecimal(decayParam).add(SafeDecimal.unit().sub(decayParam).multiplyDecimal(price));
// current deviation: curDeviation = abs(price - priceEMA)
// we may use abs (L1) difference to avoid squaring and squarerooting because
// we just need a value that can be used as threshold for heuristic and
// we don't need precise definition of volatility
uint curDeviation = price > priceEMA ? price - priceEMA : priceEMA - price;
deviationEMA = deviationEMA.multiplyDecimal(decayParam).add(SafeDecimal.unit().sub(decayParam).multiplyDecimal(curDeviation));
}
lastRoundsSeen[currencyKey] = lastRoundSeen;
priceEMAs[currencyKey] = priceEMA;
deviationEMAs[currencyKey] = deviationEMA;
}
return deviationEMAs[currencyKey];
}
|
||
| uint roundId = _getCurrentRoundId(currencyKey); | ||
| for (updateThreshold; updateThreshold > 0; updateThreshold--) { | ||
| (uint rate, uint time) = _getRateAndTimestampAtRound(currencyKey, roundId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this loop looks expensive in terms of external calls to oracle, are we sure that this volatility check is the best we can do here? perhaps some options that will only perform one external call:
|
||
| if (time != 0 && time < considerationWindowStart) { | ||
| // Round was outside consideration window so we can stop querying further rounds | ||
| return false; | ||
| } else if (rate == 0 || time == 0) { | ||
| // Either entire round or a rate inside consideration window was not available | ||
| // Consider the asset volatile | ||
| break; | ||
| } | ||
|
|
||
| if (roundId == 0) { | ||
| // Not enough historical data to continue further | ||
| // Consider the asset volatile | ||
| break; | ||
| } | ||
| roundId--; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /* ========== EVENTS ========== */ | ||
|
|
||
| event DexPriceAggregatorUpdated(address newDexPriceAggregator); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: perhaps worth adding detailed (natspec?) comments at least to all public / external methods. e.g. something
atomicEquivalentForDexPricing/atomicPriceBufferwithout a comment assumes detailed understanding of a lot of context that not every future reader may reasonably be expected to have.edit: I see there are some comments for those in
SystemSettings. I think it's worth to either duplicate them here as well, or at least add comments that mention where the actual detailed comments are. Also it's worth to expand the comments a bit more wherever they are.