├── .dapprc ├── .env.example ├── .gas-snapshot ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .solhint.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── foundry.toml ├── goerli_deploy.json ├── package.json ├── remappings.txt ├── scripts ├── common.js ├── contract-size.sh ├── deploy.js └── run.sh ├── src ├── deploy │ └── ChainlinkMockProvider.sol ├── factory │ ├── ChainlinkFactory.sol │ ├── ChainlinkFactory.t.sol │ ├── IRelayerFactory.sol │ ├── LUSD3CRVFactory.sol │ ├── LUSD3CRVFactory.t.sol │ ├── NotionalFinanceFactory.sol │ ├── NotionalFinanceFactory.t.sol │ ├── RelayerFactory.sol │ ├── RelayerFactory.t.sol │ ├── YieldFactory.sol │ └── YieldFactory.t.sol ├── guarded │ ├── Guarded.sol │ └── Guarded.t.sol ├── oracle │ ├── IOracle.sol │ ├── Oracle.sol │ └── Oracle.t.sol ├── oracle_implementations │ ├── discount_rate │ │ ├── NotionalFinance │ │ │ ├── INotionalView.sol │ │ │ ├── NotionalFinanceValueProvider.sol │ │ │ └── NotionalFinanceValueProvider.t.sol │ │ ├── Yield │ │ │ ├── IYieldPool.sol │ │ │ ├── YieldValueProvider.sol │ │ │ └── YieldValueProvider.t.sol │ │ └── utils │ │ │ └── Convert.sol │ └── spot_price │ │ └── Chainlink │ │ ├── ChainlinkAggregatorV3Interface.sol │ │ ├── ChainlinkValueProvider.sol │ │ ├── ChainlinkValueProvider.t.sol │ │ └── LUSD3CRV │ │ ├── ICurvePool.sol │ │ ├── LUSD3CRVValueProvider.sol │ │ └── LUSD3CRVValueProvider.t.sol ├── pausable │ ├── Pausable.sol │ └── Pausable.t.sol ├── relayer │ ├── ICollybus.sol │ ├── IRelayer.sol │ ├── Relayer.sol │ ├── Relayer.t.sol │ ├── StaticRelayer.sol │ └── StaticRelayer.t.sol └── test │ └── utils │ ├── Caller.sol │ ├── CheatCodes.sol │ ├── Hevm.sol │ └── tokens │ ├── TokenERC20.sol │ └── TokenERC721.sol └── yarn.lock /.dapprc: -------------------------------------------------------------------------------- 1 | # Make dependencies available 2 | export DAPP_REMAPPINGS=$(cat remappings.txt) 3 | 4 | export DAPP_SOLC_VERSION=0.8.13 5 | export DAPP_BUILD_OPTIMIZE=1 6 | export DAPP_BUILD_OPTIMIZE_RUNS=1000000 7 | export DAPP_LINK_TEST_LIBRARIES=0 8 | export DAPP_TEST_VERBOSITY=1 9 | export HEVM_RPC=$RPC_ON 10 | export ETH_RPC_URL=$ETH_NODE 11 | 12 | # set so that we can deploy to local node w/o hosted private keys 13 | export ETH_RPC_ACCOUNTS=true 14 | 15 | if [ "$DEEP_FUZZ" == "true" ] 16 | then 17 | export DAPP_TEST_FUZZ_RUNS=1000 # Run multiple fuzz passes 18 | export DAPP_TEST_SMTTIMEOUT=100000 # SMT for a long time 19 | else 20 | export DAPP_TEST_FUZZ_RUNS=100 # Run shorter fuzz passes 21 | export DAPP_TEST_SMTTIMEOUT=50000 # SMT for a short time 22 | fi 23 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | export ALCHEMY_API_KEY=YOUR_API_KEY 2 | export ETH_FROM=YOUR_DEFAULT_SENDER_ACCOUNT 3 | export RPC_ON=no 4 | export ETH_NODE=http://eth-node:8545 5 | -------------------------------------------------------------------------------- /.gas-snapshot: -------------------------------------------------------------------------------- 1 | test_removeOracle_deletesOracle() (gas: 9670) 2 | test_checkExistenceOfOracle() (gas: 819998) 3 | test_update_retrurnsFalse_WhenOracleUpdateReturnsFalse() (gas: 133025) 4 | test_update_ignoresInvalidValues() (gas: 1382965) 5 | test_oracleAt_returnsCorrectAddress() (gas: 963474) 6 | test_update_withoutOracles_returnsZeroAndInvalid() (gas: 21455) 7 | test_deploy() (gas: 417) 8 | test_returnsNumberOfOracles() (gas: 1129) 9 | test_removeOracle_onlyAuthorizedUserShouldBeAbleToRemove() (gas: 156772) 10 | test_nonAuthorizedUser_shouldNotBeAbleTo_setRequiredValidValues() (gas: 156849) 11 | testFail_addOracle_shouldNotAllowDuplicates() (gas: 272747603190571) 12 | test_addOracle_onlyAuthorizedUserShouldBeAbleToAdd() (gas: 1023559) 13 | test_update_doesNotFail_ifOracleFails() (gas: 1311027) 14 | testFail_shouldNotBeAbleToSet_invalidParam() (gas: 3629) 15 | test_paused_stops_returnValue() (gas: 176279) 16 | test_aggregator_returnsInvalid_ifMinimumNumberOfValidValuesIsNotMet() (gas: 1408449) 17 | test_setParam_requiredValidValues_changesTheParameter() (gas: 28095) 18 | testFail_addOracle_shouldNotAllowNonPreAuthorizedOracles() (gas: 895367) 19 | testFail_removeOracle_possibleIf_minimumRequiredNumberOfValidValues_canStillBeMet() (gas: 32086) 20 | testFail_oracleAt_shouldFailWithInvalidIndex() (gas: 1957) 21 | test_aggregatorOracle_canUseAnother_aggregatorOracle_asAnOracle() (gas: 2754553) 22 | test_getAggregatedValue_willReturnAverage() (gas: 2618767) 23 | test_paused_doesNotStop_update() (gas: 328617) 24 | test_addOracle() (gas: 961196) 25 | test_update_NonAuthorizedUserCanNotCall_update() (gas: 156362) 26 | test_authorizedUser_shouldBeAbleTo_setRequiredValidValues() (gas: 198278) 27 | test_update_AuthorizedUserCanCall_update() (gas: 299315) 28 | testFail_shouldNot_setRequiredValidValues_higherThanOracleCount() (gas: 4786) 29 | test_canSetParam_requiredValidValues() (gas: 28116) 30 | testFail_removeOracle_shouldFailIfOracleDoesNotExist() (gas: 823466) 31 | test_update_returnsTrue_WhenSuccessful() (gas: 128409) 32 | test_triggerUpdate_shouldCallOracle() (gas: 132227) 33 | test_create() (gas: 1159071) 34 | test_deploy() (gas: 361) 35 | test_create_AddsPermission_OnSender() (gas: 1160699) 36 | test_create() (gas: 2070177) 37 | test_deploy() (gas: 350) 38 | test_create_validateProperties() (gas: 2070921) 39 | test_create() (gas: 4795905) 40 | test_deploy() (gas: 350) 41 | test_create_validateProperties() (gas: 4803833) 42 | test_deploy_NotionalFinanceValueProvider() (gas: 1710300) 43 | test_deploy_collybusSpotPriceArchitecture_onlyAuthorizedUsers() (gas: 4797172) 44 | test_deploy_Relayer_DiscountRate_createsContract() (gas: 1299082) 45 | test_deploy_Aggregator_forEveryCompatibleValueProvider() (gas: 22616068) 46 | test_deploy_YieldValueProvider_onlyAuthorizedUsers() (gas: 1127147) 47 | test_deploy() (gas: 6834) 48 | test_deploy_Relayer_SpotPrice_createsContract() (gas: 1299081) 49 | test_deploy_ElementFiValueProvider() (gas: 4797969) 50 | test_deploy_collybusSpotPriceArchitecture() (gas: 18712239) 51 | test_deploy_collybusDiscountRateArchitecture() (gas: 18691849) 52 | test_deploy_AggregatorOracle_onlyAuthorizedUsers() (gas: 4045979) 53 | test_deploy_AggregatorOracle_forEveryValueProviderType() (gas: 16576444) 54 | test_deploy_NotionalFinanceValueProvider_onlyAuthorizedUsers() (gas: 159508) 55 | test_deploy_ChainlinkValueProvider() (gas: 2071306) 56 | test_deploy_Aggregator_onlyAuthorizedUsers() (gas: 4182068) 57 | test_deploy_Aggregator_CheckExistenceOfOracles() (gas: 17331669) 58 | test_deploy_ChainlinkValueProvider_onlyAuthorizedUsers() (gas: 1067750) 59 | test_deploy_collybusDiscountRateArchitecture_onlyAuthorizedUsers() (gas: 4796658) 60 | test_addAggregator() (gas: 21866402) 61 | test_deploy_Aggregator_CheckValidValues() (gas: 7576917) 62 | test_addOracle() (gas: 20484652) 63 | test_deploy_YieldValueProvider() (gas: 2952599) 64 | test_deploy_ElementFiValueProvider_OnlyAuthrorizedUsers() (gas: 2886669) 65 | test_deploy_Relayer_onlyAuthorizedUsers() (gas: 156920) 66 | test_create() (gas: 1717006) 67 | test_deploy() (gas: 350) 68 | test_create_validateProperties() (gas: 1722794) 69 | test_create() (gas: 2955427) 70 | test_deploy() (gas: 350) 71 | test_create_validateProperties() (gas: 2960130) 72 | test_root_role() (gas: 201790) 73 | test_custom_role() (gas: 201550) 74 | test_root_has_god_mode_access() (gas: 186706) 75 | test_Paused_Stops_ReturnValue() (gas: 176201) 76 | test_Paused_DoesNotStop_Update() (gas: 293263) 77 | test_deploy() (gas: 418) 78 | test_update_Recalculates_MovingAverage() (gas: 116979) 79 | test_ValueReturned_ShouldNotBeValid_IfNotUpdatedForTooLong() (gas: 99677) 80 | test_check_timeUpdateWindow() (gas: 1133) 81 | test_check_alpha() (gas: 1187) 82 | test_update_ShouldNotFailWhenValueProviderFails() (gas: 8741) 83 | test_Reset_ResetsContract() (gas: 134154) 84 | testFail_alphaHasToBe_greaterThanZero() (gas: 101665) 85 | test_ValueReturned_ShouldNotBeValid_IfNeverUpdated() (gas: 5555) 86 | test_update_ShouldNotUpdatePreviousValues_IfNotEnoughTimePassed() (gas: 97061) 87 | test_value_ShouldBecomeValidAfterSuccesfullUpdate() (gas: 123130) 88 | test_check_maxValidTime() (gas: 1174) 89 | test_value_ShouldBeInvalidAfterValueProviderFails() (gas: 102048) 90 | test_Reset_ShouldBePossible_IfPaused() (gas: 37655) 91 | testFail_alphaHasToBe_lowerOrEqualToOne() (gas: 101654) 92 | testFail_update_cannotBeReentered() (gas: 869747) 93 | test_update_retrurnsFalse_WhenUpdateDoesNotChangeAnything() (gas: 94925) 94 | test_ValueReturned_ShouldBeValid_IfJustUpdated() (gas: 96376) 95 | testFail_Reset_ShouldNotBePossible_IfNotPaused() (gas: 4019) 96 | test_AuthorizedUser_ShouldBeAble_ToReset() (gas: 208537) 97 | test_update_NonAuthorizedUserCanNotCall_update() (gas: 156326) 98 | test_update_AuthorizedUserCanCall_update() (gas: 263875) 99 | test_NonAuthorizedUser_ShouldNotBeAble_ToReset() (gas: 182988) 100 | test_update_Updates_timestamp() (gas: 93786) 101 | test_update_returnsTrue_WhenSuccessful() (gas: 92987) 102 | test_update_ShouldUpdatePreviousValues_IfEnoughTimePassed() (gas: 94372) 103 | test_update_UpdateDoesNotChangeTheValue_InTheSameWindow() (gas: 103537) 104 | test_getValue() (gas: 21292) 105 | test_deploy() (gas: 373) 106 | test_check_ePTokenBond() (gas: 1193) 107 | test_check_maturity() (gas: 1055) 108 | test_check_underlierDecimals() (gas: 1136) 109 | test_check_poolId() (gas: 1076) 110 | test_check_underlier() (gas: 1169) 111 | test_check_poolToken() (gas: 1103) 112 | test_check_poolTokenDecimals() (gas: 1092) 113 | test_check_balancerVault() (gas: 1170) 114 | test_check_timeScale() (gas: 1676) 115 | test_check_ePTokenBondDecimals() (gas: 1156) 116 | testFail_getValue_revertsOnOrAfterMaturityDate() (gas: 1841) 117 | test_check_settlementDate() (gas: 1086) 118 | test_getValue() (gas: 8516) 119 | test_deploy() (gas: 350) 120 | test_check_notionalView() (gas: 1138) 121 | test_check_maturityDate() (gas: 1042) 122 | testFail_getValue_revertsOnOrAfterMaturityDate() (gas: 1828) 123 | test_check_currencyId() (gas: 1128) 124 | test_getValue() (gas: 27474) 125 | test_deploy() (gas: 408) 126 | test_check_blockTimestampLast() (gas: 1350) 127 | test_check_maturity() (gas: 1145) 128 | test_check_poolId() (gas: 1069) 129 | test_check_cumulativeBalanceRatioLast() (gas: 1770) 130 | test_check_timeScale() (gas: 1120) 131 | testFail_getValue_revertsOnOrAfterMaturityDate() (gas: 1854) 132 | test_getValue() (gas: 5340) 133 | test_description() (gas: 5974) 134 | test_deploy() (gas: 372) 135 | test_check_underlierDecimals() (gas: 1014) 136 | testFail_deploy_ShouldFailWithUnsupportedDecimals() (gas: 2041606) 137 | test_check_chainlinkAggregatorAddress() (gas: 1211) 138 | test_AuthorizedUser_CanPauseUnpause() (gas: 227726) 139 | test_WhenNotPaused_Success_When_NotPaused() (gas: 24136) 140 | testFail_WhenPaused_Fails_When_NotPaused() (gas: 3062) 141 | test_WhenPaused_Success_When_Paused() (gas: 26213) 142 | testFail_WhenNotPaused_Fails_When_Paused() (gas: 25089) 143 | test_Pause_Unpause() (gas: 28494) 144 | test_addOracle_tokenIdMarkedAsUsed() (gas: 1460) 145 | test_removeOracle_deletesOracle() (gas: 8621) 146 | test_checkExistenceOfOracle() (gas: 1591) 147 | test_deploy() (gas: 438) 148 | testFail_addOracle_shouldNotAllowDuplicateOracles() (gas: 5357) 149 | test_executeWithRevert_ShouldBeSuccessful_WhenAtLeastOneOracleIsUpdated() (gas: 125472) 150 | test_removeOracle_resetsTokenIdUsedFlag() (gas: 8535) 151 | testFail_executeWithRevert_ShouldNotBeSuccessful_WhenNoOracleIsUpdated() (gas: 60731) 152 | test_executeWithRevert() (gas: 125450) 153 | test_check_relayerType() (gas: 1359) 154 | test_execute_NonAuthorizedUserCanNotCall_executeWithRevert() (gas: 156340) 155 | test_removeOracle_onlyAuthorizedUserShouldBeAbleToRemove() (gas: 156771) 156 | test_returnNumberOfOracles() (gas: 1202) 157 | testFail_addOracle_shouldNotAllowDuplicateTokenIds() (gas: 5274) 158 | test_execute_updatesRatesInCollybusWhenDeltaIsAboveThreshold() (gas: 2626654) 159 | test_addOracle_onlyAuthorizedUserShouldBeAbleToAdd() (gas: 157184) 160 | test_check_oracleData() (gas: 2758) 161 | test_check_collybus() (gas: 1226) 162 | test_execute_ReturnsTrue_WhenAtLeastOneOracleIsUpdated() (gas: 125054) 163 | test_execute_doesNotUpdatesRatesInCollybusWhenDeltaIsBelowThreshold() (gas: 2623944) 164 | test_execute_updateSpotPriceInCollybus() (gas: 2609045) 165 | test_execute_ReturnsFalse_WhenNoOracleIsUpdated() (gas: 60296) 166 | test_execute_AuthorizedUserCanCall_executeWithRevert() (gas: 296076) 167 | testFail_addOracle_shouldNotAllowNonPreAuthorizedOracles() (gas: 895748) 168 | test_execute_updateDiscountRateInCollybus() (gas: 126250) 169 | test_addOracle() (gas: 1055063) 170 | test_executeCalls_updateOnAllOracles() (gas: 1536485) 171 | testFail_removeOracle_shouldFailIfOracleDoesNotExist() (gas: 6016) 172 | test_execute_OnlyAuthorizedUsers() (gas: 683148) 173 | test_deploy() (gas: 527398) 174 | test_execute_updates_DiscountRateInCollybus() (gas: 563426) 175 | test_execute_updates_SpotPriceInCollybus() (gas: 563663) 176 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .dapprc linguist-language=Shell 2 | *.sol linguist-language=Solidity 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐜 Bug report 3 | about: If something isn't working 🔧 4 | title: "[BUG NAME]" 5 | labels: bug, help wanted 6 | --- 7 | 8 | ### What is the expected behavior? 9 | 10 | ### What is the actual behavior? 11 | 12 | ### Please provide a proof of concept that demonstrates the bug. 13 | 14 | ### Other notes on how to reproduce the issue? 15 | 16 | ### Any possible solutions? 17 | 18 | ### Can you identify the location in the source code where the problem exists? 19 | 20 | ### If the bug is confirmed, would you be willing to submit a PR? 21 | 22 | Yes / No _(Help can be provided if you need assistance submitting a PR)_ 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: If you have a feature request 💡 4 | title: "[FEATURE NAME]" 5 | labels: enhancement, help wanted 6 | --- 7 | 8 | **Context** 9 | 10 | What are you trying to do and how would you want to do it differently? Is it something you currently you cannot do? Is this related to an issue/problem? 11 | 12 | **Details** 13 | 14 | Add as many details to this issue to help anyone implementing the feature by themselves. 15 | 16 | **Alternatives** 17 | 18 | Can you achieve the same result doing it in an alternative way? Is the alternative considerable? 19 | 20 | **Security concerns** 21 | 22 | How does this affect the security of the system? Are there any other risks associated with the feature? 23 | 24 | **Has the feature been requested before?** 25 | 26 | Please provide a link to the issue. 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 10 | 11 | ### Issues 12 | 13 | 16 | 17 | - Closes #ISSUE 18 | 19 | ### Todo 20 | 21 | - [ ] Link issues 22 | - [ ] Link projects 23 | - [ ] Update tests 24 | - [ ] Update code 25 | - [ ] Comment code 26 | - [ ] Test locally 27 | - [ ] Update changelog -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Install Foundry 13 | uses: onbjerg/foundry-toolchain@v1.0.6 14 | with: 15 | version: nightly 16 | 17 | - name: Install node 18 | uses: actions/setup-node@v2 19 | 20 | - name: Clone repo with submodules 21 | uses: actions/checkout@v2 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install yarn 26 | run: npm i -g yarn 27 | 28 | - name: Install dependencies 29 | run: make 30 | 31 | - name: Show Foundry config 32 | run: forge config 33 | 34 | - name: Check contracts are linted 35 | run: yarn lint:check 36 | 37 | - name: Run tests 38 | run: make test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # MacOS 5 | .DS_Store 6 | 7 | # Node 8 | node_modules 9 | 10 | # Dapptools 11 | out/ 12 | 13 | # Foundry 14 | cache/ 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/openzeppelin-contracts"] 2 | path = lib/openzeppelin-contracts 3 | url = https://github.com/OpenZeppelin/openzeppelin-contracts 4 | ignore = dirty 5 | [submodule "lib/ds-test"] 6 | path = lib/ds-test 7 | url = https://github.com/dapphub/ds-test 8 | [submodule "lib/prb-math"] 9 | path = lib/prb-math 10 | url = https://github.com/hifi-finance/prb-math 11 | [submodule "lib/abdk-libraries-solidity"] 12 | path = lib/abdk-libraries-solidity 13 | url = https://github.com/abdk-consulting/abdk-libraries-solidity 14 | [submodule "lib/mockprovider"] 15 | path = lib/mockprovider 16 | url = https://github.com/cleanunicorn/mockprovider 17 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 8], 6 | "compiler-version": ["error", ">=0.5.8"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "func-name-mixedcase": "off", 11 | "max-line-length": ["error", 150], 12 | "not-rely-on-time": "off", 13 | "prettier/prettier": [ 14 | "error", 15 | { 16 | "endOfLine": "auto" 17 | } 18 | ], 19 | "reason-string": ["warn", { "maxLength": 64 }] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "solidity.packageDefaultDependenciesContractsDirectory": "src", 3 | "solidity.packageDefaultDependenciesDirectory": "lib" 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | - Added StaticRelayer contract which is used to update a pre-determined rate for a predefined token id to Collybus once and only once 11 | 12 | ### Changed 13 | 14 | - Added bounds validation for the Oracle's alpha parameter. Issue [#66](https://github.com/fiatdao/delphi/issues/66) 15 | - Changed how the minimum threshold value is used when deciding when to push new values into Collybus; previously an absolute values was used, now a percentage change is used. Issue [#69](https://github.com/fiatdao/delphi/issues/69) 16 | - Merged the CollybusDiscountRateRelayer and CollybusSpotPriceRelayer into a more generic Relayer contract. Issue [#68](https://github.com/fiatdao/delphi/issues/68) 17 | - Added reentrancy guard for `Oracle.update()` 18 | - Updated AggregatorOracle and Oracle Contracts tests for full coverage. Fix for issue[#74](https://github.com/fiatdao/delphi/issues/74) 19 | - Added guard checks to the Oracle update flow. Fix for issue[#81](https://github.com/fiatdao/delphi/issues/81) 20 | - Redesigned some events in `AggregatorOracle` to also emit the oracle's address. Issue [#79](https://github.com/fiatdao/delphi/issues/79) 21 | - Upgrade Solidity version from 0.8.7 to 0.8.12. Issue [#73](https://github.com/fiatdao/delphi/issues/73) 22 | - Changed execution flow for `executeWithRevert` to allow for oracle updates even if no update to Collybus was performed. Issue [#83](https://github.com/fiatdao/delphi/issues/83) 23 | - Updated `Relayer.execute()` to return whether a keeper should execute or not the current transaction instead of if Collybus was updated. Fix for issue [#125](https://github.com/fiatdao/delphi/issues/125) 24 | - Added Market sanity checks for the NotionalFinanceValueProvider Oracle. Fix for issue [#127](https://github.com/fiatdao/delphi/issues/127) 25 | - Computed settlementDate internally instead of having it as a required parameter when deploying the NotionalFinanceValueProvider Oracle. 26 | 27 | ### Removed -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # include .env file and export its env vars 2 | # (-include to ignore error if it does not exist) 3 | -include .env 4 | 5 | # Update dependencies 6 | setup :; make update-libs ; make install-deps 7 | update-libs :; git submodule update --init --recursive 8 | install-deps :; yarn install 9 | 10 | # Build & test & deploy 11 | build :; forge clean; forge build 12 | xclean :; forge clean 13 | lint :; yarn run lint 14 | test :; forge test 15 | # test-fork :; forge test --gas-report --fork-url ${ETH_NODE} 16 | watch :; forge test --watch src/ 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

Delphi 🔮

2 | 3 | **Repository containing the core smart contracts of Delphi** 4 | 5 | ## Requirements 6 | 7 | Having installed [Foundry](https://github.com/gakonst/foundry) and [Node.js](https://nodejs.org/) is the minimum requirement to get started. 8 | 9 | Run `make` to install dependencies. 10 | 11 | ## Tests 12 | 13 | After installing dependencies with `make`, run `make test` to run the tests. 14 | 15 | ## Building and testing 16 | 17 | ```sh 18 | git clone https://github.com/fiatdao/delphi 19 | cd delphi 20 | make # This installs the project's dependencies. 21 | make test 22 | ``` 23 | 24 | ## Deploying contracts 25 | 26 | This is done typically in a different repo that imports this repo. 27 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | # Full reference 2 | # https://onbjerg.github.io/foundry-book/reference/config.html 3 | 4 | [default] 5 | solc_version = "0.8.13" 6 | optimizer = true 7 | optimizer_runs = 1000000 8 | gas_reports = ["*"] 9 | 10 | [ci] 11 | verbosity = 4 -------------------------------------------------------------------------------- /goerli_deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "0x9a4b0fe8bda4b212b1a9978c116bad49ef5c5d37": { 3 | "collybus": "0x4B1002aB18D7Ea45125D6F908B3A13f947C3FBF9", 4 | "type": "Discount Rate Relayer", 5 | "aggregatorOracles": { 6 | "0x437663Db402d49c414e6C61d46a4316D1fd195f7": { 7 | "tokenId": 1, 8 | "minimumThresholdValue": 1, 9 | "oracles": { 10 | "0x053f48B0114A6dcb03101A08659298953A7904f9": { 11 | "timeUpdateWindow": 60, 12 | "maxValidTime": 600, 13 | "alpha": "200000000000000000" 14 | }, 15 | "0x70a324E44E6E04b9F6F2E1c33B2953eA351372E7": { 16 | "timeUpdateWindow": 60, 17 | "maxValidTime": 600, 18 | "alpha": "200000000000000000" 19 | }, 20 | "0xAe15Fe3B87cE13C00A275D16F79c363c3eAbf572": { 21 | "timeUpdateWindow": 60, 22 | "maxValidTime": 600, 23 | "alpha": "200000000000000000" 24 | } 25 | } 26 | } 27 | } 28 | }, 29 | "0x7ef09dc5c50ebee3882fb8af57659531c60aae70": { 30 | "collybus": "0x4B1002aB18D7Ea45125D6F908B3A13f947C3FBF9", 31 | "type": "Spot Price Relayer", 32 | "aggregatorOracles": { 33 | "0x382Ad073D64FAbc94Ca8fB1085b99fd75C8E66d0": { 34 | "tokenAddress": "0x78dEca24CBa286C0f8d56370f5406B48cFCE2f86", 35 | "minimumThresholdValue": 1, 36 | "oracles": { 37 | "0xF4D6bC1E7C6937Ab1922D05A8ab7900e120D832E": { 38 | "timeUpdateWindow": 60, 39 | "maxValidTime": 600, 40 | "alpha": "200000000000000000" 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magnus", 3 | "author": "Daniel Luca (@CleanUnicorn)", 4 | "license": "Apache-2.0", 5 | "version": "1.0.0", 6 | "description": "Sweeper contracts", 7 | "files": [ 8 | "*.sol" 9 | ], 10 | "devDependencies": { 11 | "copyfiles": "^2.4.1", 12 | "prettier": "^2.4.1", 13 | "prettier-plugin-solidity": "^1.0.0-beta.18", 14 | "rimraf": "^3.0.2", 15 | "solhint": "^3.3.6", 16 | "solhint-plugin-prettier": "^0.0.5" 17 | }, 18 | "scripts": { 19 | "lint": "yarn prettier && yarn solhint", 20 | "lint:check": "yarn prettier:check && yarn solhint:check", 21 | "prettier": "yarn prettier:check --write", 22 | "prettier:check": "prettier --check \"src/**/*.sol\"", 23 | "solhint": "yarn solhint:check --fix", 24 | "solhint:check": "solhint --config ./.solhint.json \"src/**/*.sol\"" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=lib/openzeppelin-contracts/ 2 | @cleanunicorn/mockprovider=lib/mockprovider/ 3 | ds-auth/=lib/ds-auth/src/ 4 | ds-math/=lib/ds-math/src/ 5 | ds-note/=lib/ds-note/src/ 6 | ds-test/=lib/ds-test/src/ 7 | prb-math/=lib/prb-math/ -------------------------------------------------------------------------------- /scripts/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const ethers = require('ethers'); 3 | const Artifacts = require('../out/dapp.sol.json'); 4 | 5 | ethers.utils.Logger.setLogLevel(ethers.utils.Logger.levels.ERROR); 6 | 7 | function getContractFactory(path, name, deployer) { 8 | console.log(path + " " + name ); 9 | const artifact = Artifacts.contracts[path][name]; 10 | 11 | return new ethers.ContractFactory(artifact.abi, artifact.evm.bytecode, deployer); 12 | } 13 | 14 | function getContractAt(address, path, name, deployer) 15 | { 16 | console.log(path + " " + name ); 17 | const artifact = Artifacts.contracts[path][name]; 18 | return new ethers.Contract(address, artifact.abi, deployer); 19 | } 20 | 21 | async function deployContract(name, factory, ...args) { 22 | const contract = await factory.deploy(...args); 23 | console.log(`${name}: ${contract.address}`); 24 | console.log(` address: ${contract.address}`); 25 | console.log(` txHash: ${contract.deployTransaction.hash}`); 26 | const file = `${(await factory.signer.provider.getNetwork()).chainId}.json`; 27 | const addr = (fs.existsSync(file)) ? JSON.parse(fs.readFileSync(file)) : {}; 28 | fs.writeFileSync(file, JSON.stringify({ ...addr, [name]: contract.address }, null, 2)); 29 | return contract; 30 | } 31 | 32 | module.exports = { getContractFactory, deployContract, getContractAt }; -------------------------------------------------------------------------------- /scripts/contract-size.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | contract_size() { 6 | NAME=$1 7 | ARGS=${@:2} 8 | # select the filename and the contract in it 9 | PATTERN=".contracts[\"src/$NAME.sol\"].$NAME" 10 | 11 | dapp build # first, build the contract 12 | 13 | # get the bytecode from the compiled file 14 | BYTECODE=0x$(jq -r "$PATTERN.evm.bytecode.object" out/dapp.sol.json) 15 | length=$(echo "$BYTECODE" | wc -m) 16 | echo $(($length / 2)) 17 | } 18 | 19 | if [[ -z $contract ]]; then 20 | if [[ -z ${1} ]];then 21 | echo '"$contract" env variable is not set. Set it to the name of the contract you want to estimate size for.' 22 | exit 1 23 | else 24 | contract=${1} 25 | fi 26 | fi 27 | 28 | contract_size=$(contract_size ${contract}) 29 | echo "Contract Name: ${contract}" 30 | echo "Contract Size: ${contract_size} bytes" 31 | echo 32 | echo "$(( 24576 - ${contract_size} )) bytes left to reach the smart contract size limit of 24576 bytes." 33 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const ethers = require('ethers'); 2 | const { getContractFactory, deployContract, getContractAt } = require('./common.js'); 3 | 4 | ethers.utils.Logger.setLogLevel(ethers.utils.Logger.levels.ERROR); 5 | const { parseEther: toWei, formatBytes32String: toBytes32 } = ethers.utils; 6 | 7 | // requires setting ETH_FROM_KEY and ETH_RPC_URL and a running instance of a network 8 | (async function () { 9 | const deployer = new ethers.Wallet(process.env.ETH_FROM_KEY, new ethers.providers.JsonRpcProvider(process.env.ETH_RPC_URL)); 10 | const me = await deployer.getAddress(); 11 | 12 | console.log("START"); 13 | // Add contracts to be deployed here :) 14 | console.log("END"); 15 | })(); -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | set -o pipefail 4 | set -o nounset 5 | 6 | if [ -d .env ]; then 7 | set -o allexport; source .env; set +o allexport 8 | fi 9 | 10 | if [ -z "$ALCHEMY_API_KEY" ]; then 11 | echo "ALCHEMY_API_KEY is undefined in .env"; 12 | exit 1; 13 | fi 14 | 15 | networks=(local mainnet goerli) 16 | if [[ ! " ${networks[*]} " =~ " $1 " ]]; then 17 | echo "Unsupported network '$1'"; 18 | exit 1; 19 | fi 20 | 21 | if [ "$1" == "local" ]; then 22 | export ETH_RPC_URL=http://localhost:8545 23 | elif [ "$1" == "mainnet" ]; then 24 | export DAPP_TEST_NUMBER=13627845 25 | export DAPP_TEST_CACHE=.cache/mainnet-${DAPP_TEST_NUMBER} 26 | export ETH_RPC_URL=https://eth-$1.alchemyapi.io/v2/${ALCHEMY_API_KEY} 27 | else 28 | export ETH_RPC_URL=https://eth-$1.alchemyapi.io/v2/${ALCHEMY_API_KEY} 29 | fi 30 | 31 | set +o nounset 32 | if [ -n "$DAPP_TEST_CACHE" ]; then 33 | if [ ! -d ${DAPP_TEST_CACHE} ]; then 34 | dapp --make-cache ${DAPP_TEST_CACHE}; 35 | fi 36 | fi 37 | set -o nounset 38 | 39 | eval $2 40 | -------------------------------------------------------------------------------- /src/deploy/ChainlinkMockProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | import {IChainlinkAggregatorV3Interface} from "../oracle_implementations/spot_price/Chainlink/ChainlinkAggregatorV3Interface.sol"; 4 | 5 | contract ChainlinkMockProvider is IChainlinkAggregatorV3Interface { 6 | int256 public answer; 7 | 8 | function setAnswer(int256 answer_) external { 9 | answer = answer_; 10 | } 11 | 12 | function decimals() 13 | external 14 | pure 15 | override(IChainlinkAggregatorV3Interface) 16 | returns (uint8) 17 | { 18 | return 18; 19 | } 20 | 21 | function description() 22 | external 23 | pure 24 | override(IChainlinkAggregatorV3Interface) 25 | returns (string memory) 26 | { 27 | return "MOCK / USD "; 28 | } 29 | 30 | function version() 31 | external 32 | pure 33 | override(IChainlinkAggregatorV3Interface) 34 | returns (uint256) 35 | { 36 | return 0; 37 | } 38 | 39 | function getRoundData( 40 | uint80 /*_roundId*/ 41 | ) 42 | external 43 | view 44 | override(IChainlinkAggregatorV3Interface) 45 | returns ( 46 | uint80 roundId_, 47 | int256 answer_, 48 | uint256 startedAt_, 49 | uint256 updatedAt_, 50 | uint80 answeredInRound_ 51 | ) 52 | { 53 | roundId_ = 92233720368547764552; 54 | answer_ = answer; 55 | startedAt_ = 1644474147; 56 | updatedAt_ = 1644474147; 57 | answeredInRound_ = 92233720368547764552; 58 | } 59 | 60 | function latestRoundData() 61 | external 62 | view 63 | override(IChainlinkAggregatorV3Interface) 64 | returns ( 65 | uint80 roundId_, 66 | int256 answer_, 67 | uint256 startedAt_, 68 | uint256 updatedAt_, 69 | uint80 answeredInRound_ 70 | ) 71 | { 72 | roundId_ = 92233720368547764552; 73 | answer_ = answer; 74 | startedAt_ = 1644474147; 75 | updatedAt_ = 1644474147; 76 | answeredInRound_ = 92233720368547764552; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/factory/ChainlinkFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {ChainlinkValueProvider} from "../oracle_implementations/spot_price/Chainlink/ChainlinkValueProvider.sol"; 5 | import {Relayer} from "../relayer/Relayer.sol"; 6 | import {IRelayer} from "../relayer/IRelayer.sol"; 7 | 8 | contract ChainlinkFactory { 9 | event ChainlinkDeployed(address relayerAddress, address oracleAddress); 10 | 11 | /// @param collybus_ Address of the collybus 12 | /// @param tokenAddress_ Token address that will be used to push values to Collybus 13 | /// @param minimumPercentageDeltaValue_ Minimum delta value used to determine when to 14 | /// push data to Collybus 15 | /// @param timeUpdateWindow_ Minimum time between updates of the value 16 | /// @param chainlinkAggregatorAddress_ Address of the deployed chainlink aggregator contract. 17 | /// @return The address of the Relayer 18 | function create( 19 | // Relayer parameters 20 | address collybus_, 21 | address tokenAddress_, 22 | uint256 minimumPercentageDeltaValue_, 23 | // Oracle parameters 24 | uint256 timeUpdateWindow_, 25 | // Chainlink specific parameters, see ChainlinkValueProvider for more info 26 | address chainlinkAggregatorAddress_ 27 | ) public returns (address) { 28 | ChainlinkValueProvider chainlinkValueProvider = new ChainlinkValueProvider( 29 | timeUpdateWindow_, 30 | chainlinkAggregatorAddress_ 31 | ); 32 | 33 | // Create the relayer that manages the oracle and pushes data to Collybus 34 | Relayer relayer = new Relayer( 35 | collybus_, 36 | IRelayer.RelayerType.SpotPrice, 37 | address(chainlinkValueProvider), 38 | bytes32(uint256(uint160(tokenAddress_))), 39 | minimumPercentageDeltaValue_ 40 | ); 41 | 42 | // Whitelist the Relayer in the Oracle so it can trigger updates 43 | chainlinkValueProvider.allowCaller( 44 | chainlinkValueProvider.ANY_SIG(), 45 | address(relayer) 46 | ); 47 | 48 | // Whitelist the deployer 49 | chainlinkValueProvider.allowCaller( 50 | chainlinkValueProvider.ANY_SIG(), 51 | msg.sender 52 | ); 53 | relayer.allowCaller(relayer.ANY_SIG(), msg.sender); 54 | 55 | // Renounce permissions 56 | chainlinkValueProvider.blockCaller( 57 | chainlinkValueProvider.ANY_SIG(), 58 | address(this) 59 | ); 60 | relayer.blockCaller(relayer.ANY_SIG(), address(this)); 61 | 62 | emit ChainlinkDeployed( 63 | address(relayer), 64 | address(chainlinkValueProvider) 65 | ); 66 | return address(relayer); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/factory/ChainlinkFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 7 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | 9 | import {ChainlinkFactory} from "./ChainlinkFactory.sol"; 10 | import {Relayer} from "../relayer/Relayer.sol"; 11 | import {IRelayer} from "../relayer/IRelayer.sol"; 12 | 13 | import {ChainlinkValueProvider} from "../oracle_implementations/spot_price/Chainlink/ChainlinkValueProvider.sol"; 14 | import {IChainlinkAggregatorV3Interface} from "../oracle_implementations/spot_price/Chainlink/ChainlinkAggregatorV3Interface.sol"; 15 | 16 | contract ChainlinkFactoryTest is DSTest { 17 | address private _collybusAddress = address(0xC011b005); 18 | uint256 private _oracleUpdateWindow = 1 * 3600; 19 | address private _tokenAddress = address(0x105311); 20 | uint256 private _minimumPercentageDeltaValue = 25; 21 | 22 | ChainlinkFactory private _factory; 23 | 24 | function setUp() public { 25 | _factory = new ChainlinkFactory(); 26 | } 27 | 28 | function test_deploy() public { 29 | assertTrue(address(_factory) != address(0)); 30 | } 31 | 32 | function test_create_relayerIsDeployed() public { 33 | // Create a chainlink mock aggregator 34 | MockProvider chainlinkMock = new MockProvider(); 35 | chainlinkMock.givenQueryReturnResponse( 36 | abi.encodeWithSelector( 37 | IChainlinkAggregatorV3Interface.decimals.selector 38 | ), 39 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 40 | false 41 | ); 42 | 43 | // Create the chainlink Relayer 44 | address chainlinkRelayerAddress = _factory.create( 45 | _collybusAddress, 46 | _tokenAddress, 47 | _minimumPercentageDeltaValue, 48 | _oracleUpdateWindow, 49 | address(chainlinkMock) 50 | ); 51 | 52 | assertTrue( 53 | chainlinkRelayerAddress != address(0), 54 | "Factory Chainlink Relayer create failed" 55 | ); 56 | } 57 | 58 | function test_create_oracleIsDeployed() public { 59 | // Create a chainlink mock aggregator 60 | MockProvider chainlinkMock = new MockProvider(); 61 | chainlinkMock.givenQueryReturnResponse( 62 | abi.encodeWithSelector( 63 | IChainlinkAggregatorV3Interface.decimals.selector 64 | ), 65 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 66 | false 67 | ); 68 | 69 | // Create the chainlink Relayer 70 | address chainlinkRelayerAddress = _factory.create( 71 | _collybusAddress, 72 | _tokenAddress, 73 | _minimumPercentageDeltaValue, 74 | _oracleUpdateWindow, 75 | address(chainlinkMock) 76 | ); 77 | 78 | assertTrue( 79 | Relayer(chainlinkRelayerAddress).oracle() != address(0), 80 | "Invalid oracle address" 81 | ); 82 | } 83 | 84 | function test_create_validateRelayerProperties() public { 85 | // Create a chainlink mock aggregator 86 | MockProvider chainlinkMock = new MockProvider(); 87 | chainlinkMock.givenQueryReturnResponse( 88 | abi.encodeWithSelector( 89 | IChainlinkAggregatorV3Interface.decimals.selector 90 | ), 91 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 92 | false 93 | ); 94 | 95 | // Create the chainlink Relayer 96 | Relayer chainlinkRelayer = Relayer( 97 | _factory.create( 98 | _collybusAddress, 99 | _tokenAddress, 100 | _minimumPercentageDeltaValue, 101 | _oracleUpdateWindow, 102 | address(chainlinkMock) 103 | ) 104 | ); 105 | 106 | assertEq( 107 | Relayer(chainlinkRelayer).collybus(), 108 | _collybusAddress, 109 | "Chainlink Relayer incorrect Collybus address" 110 | ); 111 | 112 | assertTrue( 113 | Relayer(chainlinkRelayer).relayerType() == 114 | IRelayer.RelayerType.SpotPrice, 115 | "Chainlink Relayer incorrect RelayerType" 116 | ); 117 | 118 | assertEq( 119 | Relayer(chainlinkRelayer).encodedTokenId(), 120 | bytes32(uint256(uint160(_tokenAddress))), 121 | "Chainlink Relayer incorrect tokenId" 122 | ); 123 | 124 | assertEq( 125 | Relayer(chainlinkRelayer).minimumPercentageDeltaValue(), 126 | _minimumPercentageDeltaValue, 127 | "Chainlink Relayer incorrect minimumPercentageDeltaValue" 128 | ); 129 | } 130 | 131 | function test_create_validateOracleProperties() public { 132 | // Create a chainlink mock aggregator 133 | MockProvider chainlinkMock = new MockProvider(); 134 | chainlinkMock.givenQueryReturnResponse( 135 | abi.encodeWithSelector( 136 | IChainlinkAggregatorV3Interface.decimals.selector 137 | ), 138 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 139 | false 140 | ); 141 | 142 | // Create the chainlink Relayer 143 | address chainlinkRelayerAddress = _factory.create( 144 | _collybusAddress, 145 | _tokenAddress, 146 | _minimumPercentageDeltaValue, 147 | _oracleUpdateWindow, 148 | address(chainlinkMock) 149 | ); 150 | 151 | address chainlinkOracleAddress = Relayer(chainlinkRelayerAddress) 152 | .oracle(); 153 | 154 | // Test that properties are correctly set 155 | assertEq( 156 | ChainlinkValueProvider(chainlinkOracleAddress).timeUpdateWindow(), 157 | _oracleUpdateWindow, 158 | "Chainlink Value Provider incorrect oracleUpdateWindow" 159 | ); 160 | 161 | assertEq( 162 | ChainlinkValueProvider(chainlinkOracleAddress) 163 | .chainlinkAggregatorAddress(), 164 | address(chainlinkMock), 165 | "Chainlink Value Provider incorrect chainlinkAggregatorAddress" 166 | ); 167 | } 168 | 169 | function test_create_factoryPassesOraclePermissions() public { 170 | // Create a chainlink mock aggregator 171 | MockProvider chainlinkMock = new MockProvider(); 172 | chainlinkMock.givenQueryReturnResponse( 173 | abi.encodeWithSelector( 174 | IChainlinkAggregatorV3Interface.decimals.selector 175 | ), 176 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 177 | false 178 | ); 179 | 180 | // Create chainlink Relayer 181 | address chainlinkRelayerAddress = _factory.create( 182 | _collybusAddress, 183 | _tokenAddress, 184 | _minimumPercentageDeltaValue, 185 | _oracleUpdateWindow, 186 | address(chainlinkMock) 187 | ); 188 | 189 | ChainlinkValueProvider chainlinkValueProvider = ChainlinkValueProvider( 190 | Relayer(chainlinkRelayerAddress).oracle() 191 | ); 192 | 193 | bool factoryIsAuthorized = chainlinkValueProvider.canCall( 194 | chainlinkValueProvider.ANY_SIG(), 195 | address(_factory) 196 | ); 197 | assertTrue( 198 | factoryIsAuthorized == false, 199 | "The Factory should not have rights over the created contract" 200 | ); 201 | 202 | bool callerIsAuthorized = chainlinkValueProvider.canCall( 203 | chainlinkValueProvider.ANY_SIG(), 204 | address(this) 205 | ); 206 | assertTrue( 207 | callerIsAuthorized, 208 | "Caller should have rights over the created contract" 209 | ); 210 | } 211 | 212 | function test_create_factoryPassesRelayerPermissions() public { 213 | // Create a chainlink mock aggregator 214 | MockProvider chainlinkMock = new MockProvider(); 215 | chainlinkMock.givenQueryReturnResponse( 216 | abi.encodeWithSelector( 217 | IChainlinkAggregatorV3Interface.decimals.selector 218 | ), 219 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 220 | false 221 | ); 222 | 223 | // Create chainlink Relayer 224 | Relayer chainlinkRelayer = Relayer( 225 | _factory.create( 226 | _collybusAddress, 227 | _tokenAddress, 228 | _minimumPercentageDeltaValue, 229 | _oracleUpdateWindow, 230 | address(chainlinkMock) 231 | ) 232 | ); 233 | 234 | bool factoryIsAuthorized = chainlinkRelayer.canCall( 235 | chainlinkRelayer.ANY_SIG(), 236 | address(_factory) 237 | ); 238 | assertTrue( 239 | factoryIsAuthorized == false, 240 | "The Factory should not have rights over the created contract" 241 | ); 242 | 243 | bool callerIsAuthorized = chainlinkRelayer.canCall( 244 | chainlinkRelayer.ANY_SIG(), 245 | address(this) 246 | ); 247 | assertTrue( 248 | callerIsAuthorized, 249 | "Caller should have rights over the created contract" 250 | ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/factory/IRelayerFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {IRelayer} from "../relayer/IRelayer.sol"; 5 | 6 | interface IRelayerFactory { 7 | function create( 8 | address collybus_, 9 | IRelayer.RelayerType relayerType_, 10 | address oracleAddress, 11 | bytes32 encodedTokenId, 12 | uint256 minimumPercentageDeltaValue 13 | ) external returns (address); 14 | 15 | function createStatic( 16 | address collybus_, 17 | IRelayer.RelayerType relayerType_, 18 | bytes32 encodedTokenId_, 19 | uint256 value_ 20 | ) external returns (address); 21 | } 22 | -------------------------------------------------------------------------------- /src/factory/LUSD3CRVFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {LUSD3CRVValueProvider} from "../oracle_implementations/spot_price/Chainlink/LUSD3CRV/LUSD3CRVValueProvider.sol"; 5 | import {Relayer} from "../relayer/Relayer.sol"; 6 | import {IRelayer} from "../relayer/IRelayer.sol"; 7 | 8 | contract LUSD3CRVFactory { 9 | event LUSD3CRVDeployed(address relayerAddress, address oracleAddress); 10 | 11 | /// @param collybus_ Address of the collybus 12 | /// @param tokenAddress_ Token address that will be used to push values to Collybus 13 | /// @param minimumPercentageDeltaValue_ Minimum delta value used to determine when to 14 | /// push data to Collybus 15 | /// @param timeUpdateWindow_ Minimum time between updates of the value 16 | /// @param curve3Pool_ Address of the Curve 3pool 17 | /// @param curve3PoolLpToken_ Address of the lp token for the Curve 3pool 18 | /// @param chainlinkLUSD_ Address of the LUSD chainlink data feed 19 | /// @param chainlinkUSDC_ Address of the USDC chainlink data feed 20 | /// @param chainlinkDAI_ Address of the DAI chainlink data feed 21 | /// @param chainlinkUSDT_ Address of the USDT chainlink data feed 22 | function create( 23 | // Relayer parameters 24 | address collybus_, 25 | address tokenAddress_, 26 | uint256 minimumPercentageDeltaValue_, 27 | // Oracle parameters 28 | uint256 timeUpdateWindow_, 29 | // LUSD3CRVValueProvider specific parameters 30 | address curve3Pool_, 31 | address curve3PoolLpToken_, 32 | address chainlinkLUSD_, 33 | address chainlinkUSDC_, 34 | address chainlinkDAI_, 35 | address chainlinkUSDT_ 36 | ) public returns (address) { 37 | // The tokenAddress is the address of the LUSD3CRV Curve Pool so we can pass it to the Oracle 38 | LUSD3CRVValueProvider lusd3crvValueProvider = new LUSD3CRVValueProvider( 39 | timeUpdateWindow_, 40 | curve3Pool_, 41 | curve3PoolLpToken_, 42 | tokenAddress_, 43 | chainlinkLUSD_, 44 | chainlinkUSDC_, 45 | chainlinkDAI_, 46 | chainlinkUSDT_ 47 | ); 48 | 49 | // Create the relayer that manages the oracle and pushes data to Collybus 50 | Relayer relayer = new Relayer( 51 | collybus_, 52 | IRelayer.RelayerType.SpotPrice, 53 | address(lusd3crvValueProvider), 54 | bytes32(uint256(uint160(tokenAddress_))), 55 | minimumPercentageDeltaValue_ 56 | ); 57 | 58 | // Whitelist the Relayer in the Oracle so it can trigger updates 59 | lusd3crvValueProvider.allowCaller( 60 | lusd3crvValueProvider.ANY_SIG(), 61 | address(relayer) 62 | ); 63 | 64 | // Whitelist the deployer 65 | lusd3crvValueProvider.allowCaller( 66 | lusd3crvValueProvider.ANY_SIG(), 67 | msg.sender 68 | ); 69 | relayer.allowCaller(relayer.ANY_SIG(), msg.sender); 70 | 71 | // Renounce permissions 72 | lusd3crvValueProvider.blockCaller( 73 | lusd3crvValueProvider.ANY_SIG(), 74 | address(this) 75 | ); 76 | relayer.blockCaller(relayer.ANY_SIG(), address(this)); 77 | 78 | emit LUSD3CRVDeployed(address(relayer), address(lusd3crvValueProvider)); 79 | 80 | return address(relayer); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/factory/LUSD3CRVFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 7 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | 9 | import {ICurvePool} from "../oracle_implementations/spot_price/Chainlink/LUSD3CRV/ICurvePool.sol"; 10 | import {LUSD3CRVFactory} from "./LUSD3CRVFactory.sol"; 11 | import {Relayer} from "../relayer/Relayer.sol"; 12 | import {IRelayer} from "../relayer/IRelayer.sol"; 13 | 14 | import {LUSD3CRVValueProvider} from "../oracle_implementations/spot_price/Chainlink/LUSD3CRV/LUSD3CRVValueProvider.sol"; 15 | import {IChainlinkAggregatorV3Interface} from "../oracle_implementations/spot_price/Chainlink/ChainlinkAggregatorV3Interface.sol"; 16 | 17 | contract LUSD3CRVFactoryTest is DSTest { 18 | address private _collybusAddress = address(0xC011b005); 19 | uint256 private _oracleUpdateWindow = 1 * 3600; 20 | uint256 private _minimumPercentageDeltaValue = 25; 21 | 22 | address private _lusd3crvRelayerAddress; 23 | 24 | LUSD3CRVFactory private _factory; 25 | 26 | function setUp() public { 27 | _factory = new LUSD3CRVFactory(); 28 | 29 | // Create a chainlink mock aggregator 30 | MockProvider chainlinkMock = new MockProvider(); 31 | chainlinkMock.givenQueryReturnResponse( 32 | abi.encodeWithSelector( 33 | IChainlinkAggregatorV3Interface.decimals.selector 34 | ), 35 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 36 | false 37 | ); 38 | 39 | MockProvider curveTokenMock = new MockProvider(); 40 | curveTokenMock.givenQueryReturnResponse( 41 | abi.encodeWithSelector(ERC20.decimals.selector), 42 | MockProvider.ReturnData({success: true, data: abi.encode(18)}), 43 | false 44 | ); 45 | 46 | // We can use a random curve 3pool address 47 | address curve3PoolAddress = address(0xC1133); 48 | // Create the Relayer 49 | _lusd3crvRelayerAddress = _factory.create( 50 | _collybusAddress, 51 | address(curveTokenMock), 52 | _minimumPercentageDeltaValue, 53 | _oracleUpdateWindow, 54 | curve3PoolAddress, 55 | address(curveTokenMock), 56 | address(chainlinkMock), 57 | address(chainlinkMock), 58 | address(chainlinkMock), 59 | address(chainlinkMock) 60 | ); 61 | } 62 | 63 | function test_deploy() public { 64 | assertTrue(address(_factory) != address(0)); 65 | } 66 | 67 | function test_create_relayerIsDeployed() public { 68 | assertTrue( 69 | _lusd3crvRelayerAddress != address(0), 70 | "Factory LUSD3CRV Relayer create failed" 71 | ); 72 | } 73 | 74 | function test_create_oracleIsDeployed() public { 75 | assertTrue( 76 | Relayer(_lusd3crvRelayerAddress).oracle() != address(0), 77 | "Invalid oracle address" 78 | ); 79 | } 80 | 81 | function test_create_validateRelayerProperties() public { 82 | assertEq( 83 | Relayer(_lusd3crvRelayerAddress).collybus(), 84 | _collybusAddress, 85 | "LUSD3CRV Relayer incorrect Collybus address" 86 | ); 87 | 88 | assertTrue( 89 | Relayer(_lusd3crvRelayerAddress).relayerType() == 90 | IRelayer.RelayerType.SpotPrice, 91 | "LUSD3CRV Relayer incorrect RelayerType" 92 | ); 93 | 94 | assertTrue( 95 | Relayer(_lusd3crvRelayerAddress).encodedTokenId() != bytes32(0), 96 | "LUSD3CRV Relayer incorrect tokenId" 97 | ); 98 | 99 | assertEq( 100 | Relayer(_lusd3crvRelayerAddress).minimumPercentageDeltaValue(), 101 | _minimumPercentageDeltaValue, 102 | "LUSD3CRV Relayer incorrect minimumPercentageDeltaValue" 103 | ); 104 | } 105 | 106 | function test_create_validateOracleProperties() public { 107 | address oracleAddress = Relayer(_lusd3crvRelayerAddress).oracle(); 108 | 109 | // Test that properties are correctly set 110 | assertEq( 111 | LUSD3CRVValueProvider(oracleAddress).timeUpdateWindow(), 112 | _oracleUpdateWindow, 113 | "LUSD3CRV Value Provider incorrect oracleUpdateWindow" 114 | ); 115 | 116 | assertTrue( 117 | LUSD3CRVValueProvider(oracleAddress).curve3Pool() != address(0), 118 | "LUSD3CRV Value Provider incorrect curve3Pool" 119 | ); 120 | 121 | assertTrue( 122 | LUSD3CRVValueProvider(oracleAddress).curveLUSD3Pool() != address(0), 123 | "LUSD3CRV Value Provider incorrect curveLUSD3Pool" 124 | ); 125 | 126 | assertTrue( 127 | LUSD3CRVValueProvider(oracleAddress).chainlinkLUSD() != address(0), 128 | "LUSD3CRV Value Provider incorrect chainlinkLUSD" 129 | ); 130 | 131 | assertTrue( 132 | LUSD3CRVValueProvider(oracleAddress).chainlinkUSDC() != address(0), 133 | "LUSD3CRV Value Provider incorrect chainlinkUSDC" 134 | ); 135 | 136 | assertTrue( 137 | LUSD3CRVValueProvider(oracleAddress).chainlinkDAI() != address(0), 138 | "LUSD3CRV Value Provider incorrect chainlinkDAI" 139 | ); 140 | 141 | assertTrue( 142 | LUSD3CRVValueProvider(oracleAddress).chainlinkUSDT() != address(0), 143 | "LUSD3CRV Value Provider incorrect chainlinkUSDT" 144 | ); 145 | } 146 | 147 | function test_create_factoryPassesOraclePermissions() public { 148 | LUSD3CRVValueProvider lusd3crvValueProvider = LUSD3CRVValueProvider( 149 | Relayer(_lusd3crvRelayerAddress).oracle() 150 | ); 151 | 152 | bool factoryIsAuthorized = lusd3crvValueProvider.canCall( 153 | lusd3crvValueProvider.ANY_SIG(), 154 | address(_factory) 155 | ); 156 | assertTrue( 157 | factoryIsAuthorized == false, 158 | "The Factory should not have rights over the created contract" 159 | ); 160 | 161 | bool callerIsAuthorized = lusd3crvValueProvider.canCall( 162 | lusd3crvValueProvider.ANY_SIG(), 163 | address(this) 164 | ); 165 | assertTrue( 166 | callerIsAuthorized, 167 | "Caller should have rights over the created contract" 168 | ); 169 | } 170 | 171 | function test_create_factoryPassesRelayerPermissions() public { 172 | Relayer relayer = Relayer(_lusd3crvRelayerAddress); 173 | bool factoryIsAuthorized = relayer.canCall( 174 | relayer.ANY_SIG(), 175 | address(_factory) 176 | ); 177 | assertTrue( 178 | factoryIsAuthorized == false, 179 | "The Factory should not have rights over the created contract" 180 | ); 181 | 182 | bool callerIsAuthorized = relayer.canCall( 183 | relayer.ANY_SIG(), 184 | address(this) 185 | ); 186 | assertTrue( 187 | callerIsAuthorized, 188 | "Caller should have rights over the created contract" 189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/factory/NotionalFinanceFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {NotionalFinanceValueProvider} from "../oracle_implementations/discount_rate/NotionalFinance/NotionalFinanceValueProvider.sol"; 5 | import {Relayer} from "../relayer/Relayer.sol"; 6 | import {IRelayer} from "../relayer/IRelayer.sol"; 7 | 8 | contract NotionalFinanceFactory { 9 | event NotionalFinanceDeployed( 10 | address relayerAddress, 11 | address oracleAddress 12 | ); 13 | 14 | /// @param collybus_ Address of the collybus 15 | /// @param tokenId_ Token Id that will be used to push values to Collybus 16 | /// @param minimumPercentageDeltaValue_ Minimum delta value used to determine when to 17 | /// push data to Collybus 18 | /// @param timeUpdateWindow_ Minimum time between updates of the value 19 | /// @param notional_ The address of the deployed notional contract 20 | /// @param currencyId_ Currency ID(eth = 1, dai = 2, usdc = 3, wbtc = 4) 21 | /// @param oracleRateDecimals_ Precision of the Notional oracle rate 22 | /// @param maturity_ Maturity date. 23 | /// @return The address of the Relayer 24 | function create( 25 | // Relayer parameters 26 | address collybus_, 27 | uint256 tokenId_, 28 | uint256 minimumPercentageDeltaValue_, 29 | // Oracle parameters 30 | uint256 timeUpdateWindow_, 31 | // Notional specific parameters, see NotionalFinanceValueProvider for more info 32 | address notional_, 33 | uint256 currencyId_, 34 | uint256 oracleRateDecimals_, 35 | uint256 maturity_ 36 | ) external returns (address) { 37 | // Create the oracle that will fetch data from the NotionalFinance contract 38 | NotionalFinanceValueProvider notionalFinanceValueProvider = new NotionalFinanceValueProvider( 39 | timeUpdateWindow_, 40 | notional_, 41 | currencyId_, 42 | oracleRateDecimals_, 43 | maturity_ 44 | ); 45 | 46 | // Create the relayer that manages the oracle and pushes data to Collybus 47 | Relayer relayer = new Relayer( 48 | collybus_, 49 | IRelayer.RelayerType.DiscountRate, 50 | address(notionalFinanceValueProvider), 51 | bytes32(tokenId_), 52 | minimumPercentageDeltaValue_ 53 | ); 54 | 55 | // Whitelist the Relayer in the Oracle so it can trigger updates 56 | notionalFinanceValueProvider.allowCaller( 57 | notionalFinanceValueProvider.ANY_SIG(), 58 | address(relayer) 59 | ); 60 | // Whitelist the deployer 61 | notionalFinanceValueProvider.allowCaller( 62 | notionalFinanceValueProvider.ANY_SIG(), 63 | msg.sender 64 | ); 65 | // Whitelist the deployer 66 | relayer.allowCaller(relayer.ANY_SIG(), msg.sender); 67 | 68 | // Renounce permissions 69 | relayer.blockCaller(relayer.ANY_SIG(), address(this)); 70 | notionalFinanceValueProvider.blockCaller( 71 | notionalFinanceValueProvider.ANY_SIG(), 72 | address(this) 73 | ); 74 | 75 | emit NotionalFinanceDeployed( 76 | address(relayer), 77 | address(notionalFinanceValueProvider) 78 | ); 79 | 80 | // Return the address of the relayer 81 | return address(relayer); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/factory/NotionalFinanceFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 7 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 8 | import {NotionalFinanceFactory} from "./NotionalFinanceFactory.sol"; 9 | import {NotionalFinanceValueProvider} from "../oracle_implementations/discount_rate/NotionalFinance/NotionalFinanceValueProvider.sol"; 10 | import {Relayer} from "../relayer/Relayer.sol"; 11 | import {IRelayer} from "../relayer/IRelayer.sol"; 12 | 13 | contract NotionalFinanceFactoryTest is DSTest { 14 | address private _collybusAddress = address(0xC011b005); 15 | uint256 private _oracleUpdateWindow = 1 * 3600; 16 | uint256 private _tokenId = 1; 17 | uint256 private _minimumPercentageDeltaValue = 25; 18 | 19 | address private _notional; 20 | uint256 private _currencyId = 2; 21 | uint256 private _oracleRateDecimals = 9; 22 | uint256 private _maturityDate = 1671840000; 23 | 24 | NotionalFinanceFactory private _factory; 25 | 26 | function setUp() public { 27 | _factory = new NotionalFinanceFactory(); 28 | } 29 | 30 | function test_deploy() public { 31 | assertTrue(address(_factory) != address(0)); 32 | } 33 | 34 | function test_create_relayerIsDeployed() public { 35 | // Create Notional Relayer 36 | address notionalRelayerAddress = _factory.create( 37 | _collybusAddress, 38 | _tokenId, 39 | _minimumPercentageDeltaValue, 40 | _oracleUpdateWindow, 41 | _notional, 42 | _currencyId, 43 | _oracleRateDecimals, 44 | _maturityDate 45 | ); 46 | 47 | assertTrue( 48 | notionalRelayerAddress != address(0), 49 | "Factory Notional Relayer create failed" 50 | ); 51 | } 52 | 53 | function test_create_oracleIsDeployed() public { 54 | // Create Notional Relayer 55 | address notionalRelayerAddress = _factory.create( 56 | _collybusAddress, 57 | _tokenId, 58 | _minimumPercentageDeltaValue, 59 | _oracleUpdateWindow, 60 | _notional, 61 | _currencyId, 62 | _oracleRateDecimals, 63 | _maturityDate 64 | ); 65 | 66 | Relayer notionalRelayer = Relayer(notionalRelayerAddress); 67 | 68 | assertTrue( 69 | notionalRelayer.oracle() != address(0), 70 | "Invalid oracle address" 71 | ); 72 | } 73 | 74 | function test_create_validateRelayerProperties() public { 75 | // Create Notional Relayer 76 | address notionalRelayerAddress = _factory.create( 77 | _collybusAddress, 78 | _tokenId, 79 | _minimumPercentageDeltaValue, 80 | _oracleUpdateWindow, 81 | _notional, 82 | _currencyId, 83 | _oracleRateDecimals, 84 | _maturityDate 85 | ); 86 | 87 | assertEq( 88 | Relayer(notionalRelayerAddress).collybus(), 89 | _collybusAddress, 90 | "Notional Relayer incorrect Collybus address" 91 | ); 92 | 93 | assertTrue( 94 | Relayer(notionalRelayerAddress).relayerType() == 95 | IRelayer.RelayerType.DiscountRate, 96 | "Notional Relayer incorrect RelayerType" 97 | ); 98 | 99 | assertEq( 100 | Relayer(notionalRelayerAddress).encodedTokenId(), 101 | bytes32(_tokenId), 102 | "Notional Relayer incorrect tokenId" 103 | ); 104 | 105 | assertEq( 106 | Relayer(notionalRelayerAddress).minimumPercentageDeltaValue(), 107 | _minimumPercentageDeltaValue, 108 | "Notional Relayer incorrect minimumPercentageDeltaValue" 109 | ); 110 | } 111 | 112 | function test_create_validateOracleProperties() public { 113 | // Create Notional Relayer 114 | address notionalRelayerAddress = _factory.create( 115 | _collybusAddress, 116 | _tokenId, 117 | _minimumPercentageDeltaValue, 118 | _oracleUpdateWindow, 119 | _notional, 120 | _currencyId, 121 | _oracleRateDecimals, 122 | _maturityDate 123 | ); 124 | 125 | address oracleAddress = Relayer(notionalRelayerAddress).oracle(); 126 | // Test that properties are correctly set 127 | assertEq( 128 | NotionalFinanceValueProvider(oracleAddress).timeUpdateWindow(), 129 | _oracleUpdateWindow, 130 | "Notional Value Provider incorrect timeUpdateWindow" 131 | ); 132 | 133 | assertEq( 134 | NotionalFinanceValueProvider(oracleAddress).notional(), 135 | _notional, 136 | "Notional Value Provider incorrect notionalView" 137 | ); 138 | 139 | assertEq( 140 | NotionalFinanceValueProvider(oracleAddress).currencyId(), 141 | _currencyId, 142 | "Notional Value Provider incorrect currencyId" 143 | ); 144 | 145 | assertEq( 146 | NotionalFinanceValueProvider(oracleAddress).maturityDate(), 147 | _maturityDate, 148 | "Notional Value Provider incorrect maturityDate" 149 | ); 150 | } 151 | 152 | function test_create_factoryPassesRelayerPermissions() public { 153 | // Create Notional Value Provider 154 | address notionalRelayerAddress = _factory.create( 155 | _collybusAddress, 156 | _tokenId, 157 | _minimumPercentageDeltaValue, 158 | _oracleUpdateWindow, 159 | _notional, 160 | _currencyId, 161 | _oracleRateDecimals, 162 | _maturityDate 163 | ); 164 | 165 | Relayer notionalRelayer = Relayer(notionalRelayerAddress); 166 | 167 | bool factoryIsAuthorized = notionalRelayer.canCall( 168 | notionalRelayer.ANY_SIG(), 169 | address(_factory) 170 | ); 171 | assertTrue( 172 | factoryIsAuthorized == false, 173 | "The Factory should not have rights over the created contract" 174 | ); 175 | 176 | bool callerIsAuthorized = notionalRelayer.canCall( 177 | notionalRelayer.ANY_SIG(), 178 | address(this) 179 | ); 180 | assertTrue( 181 | callerIsAuthorized, 182 | "Caller should have rights over the created contract" 183 | ); 184 | } 185 | 186 | function test_create_factoryPassesOraclePermissions() public { 187 | // Create Notional Value Provider 188 | address notionalRelayerAddress = _factory.create( 189 | _collybusAddress, 190 | _tokenId, 191 | _minimumPercentageDeltaValue, 192 | _oracleUpdateWindow, 193 | _notional, 194 | _currencyId, 195 | _oracleRateDecimals, 196 | _maturityDate 197 | ); 198 | 199 | NotionalFinanceValueProvider oracle = NotionalFinanceValueProvider( 200 | Relayer(notionalRelayerAddress).oracle() 201 | ); 202 | bool factoryIsAuthorized = oracle.canCall( 203 | oracle.ANY_SIG(), 204 | address(_factory) 205 | ); 206 | assertTrue( 207 | factoryIsAuthorized == false, 208 | "The Factory should not have rights over the created contract" 209 | ); 210 | 211 | bool callerIsAuthorized = oracle.canCall( 212 | oracle.ANY_SIG(), 213 | address(this) 214 | ); 215 | assertTrue( 216 | callerIsAuthorized, 217 | "Caller should have rights over the created contract" 218 | ); 219 | } 220 | 221 | function testFail_create_failsWithInvalidCurrencyId() public { 222 | uint256 invalidCurrencyId = type(uint16).max + 1; 223 | // Create Notional Value Provider 224 | _factory.create( 225 | _collybusAddress, 226 | _tokenId, 227 | _minimumPercentageDeltaValue, 228 | _oracleUpdateWindow, 229 | _notional, 230 | invalidCurrencyId, 231 | _oracleRateDecimals, 232 | _maturityDate 233 | ); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/factory/RelayerFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | import {Relayer} from "../relayer/Relayer.sol"; 4 | import {StaticRelayer} from "../relayer/StaticRelayer.sol"; 5 | import {IRelayer} from "../relayer/IRelayer.sol"; 6 | import {IRelayerFactory} from "./IRelayerFactory.sol"; 7 | 8 | contract RelayerFactory is IRelayerFactory { 9 | // Emitted when a Relayer is created 10 | event RelayerDeployed( 11 | address relayerAddress, 12 | IRelayer.RelayerType relayerType, 13 | address oracleAddress, 14 | bytes32 encodedTokenId, 15 | uint256 minimumPercentageDeltaValue 16 | ); 17 | // Emitted when a Static Relayer is created 18 | event StaticRelayerDeployed( 19 | address relayerAddress, 20 | IRelayer.RelayerType relayerType, 21 | bytes32 encodedTokenId, 22 | uint256 value 23 | ); 24 | 25 | /// @notice Creates a Relayer contract that manages an Oracle in order to push data to Collybus 26 | /// @param collybus_ The address of the Collybus where the Relayer will push data 27 | /// @param relayerType_ Relayer type, can be DiscountRate or SpotPrice 28 | /// @param oracleAddress_ The address of the oracle that will provide data 29 | /// @param encodedTokenId_ Encoded tokenId that will be used to push data to Collybus 30 | /// @param minimumPercentageDeltaValue_ Minimum delta value used to decide when to push data to Collybus 31 | function create( 32 | address collybus_, 33 | Relayer.RelayerType relayerType_, 34 | address oracleAddress_, 35 | bytes32 encodedTokenId_, 36 | uint256 minimumPercentageDeltaValue_ 37 | ) public override(IRelayerFactory) returns (address) { 38 | Relayer relayer = new Relayer( 39 | collybus_, 40 | relayerType_, 41 | oracleAddress_, 42 | encodedTokenId_, 43 | minimumPercentageDeltaValue_ 44 | ); 45 | 46 | // Pass permissions to the intended contract owner 47 | relayer.allowCaller(relayer.ANY_SIG(), msg.sender); 48 | relayer.blockCaller(relayer.ANY_SIG(), address(this)); 49 | 50 | emit RelayerDeployed( 51 | address(relayer), 52 | relayerType_, 53 | oracleAddress_, 54 | encodedTokenId_, 55 | minimumPercentageDeltaValue_ 56 | ); 57 | return address(relayer); 58 | } 59 | 60 | /// @notice Creates a Static Relayer contract that acts as a one time data provider. 61 | /// @param collybus_ The address of the Collybus where the StaticRelayer will push data 62 | /// @param relayerType_ Relayer type, can be DiscountRate or SpotPrice 63 | /// @param encodedTokenId_ Encoded tokenId that will be used to push data to Collybus 64 | /// @param value_ The value that will be pushed. 65 | /// @dev The contract will self-destruct after the rate is successfully pushed to Collybus 66 | function createStatic( 67 | address collybus_, 68 | Relayer.RelayerType relayerType_, 69 | bytes32 encodedTokenId_, 70 | uint256 value_ 71 | ) public override(IRelayerFactory) returns (address) { 72 | // Create the Static Relayer contract 73 | StaticRelayer staticRelayer = new StaticRelayer( 74 | collybus_, 75 | relayerType_, 76 | encodedTokenId_, 77 | value_ 78 | ); 79 | 80 | emit StaticRelayerDeployed( 81 | address(staticRelayer), 82 | relayerType_, 83 | encodedTokenId_, 84 | value_ 85 | ); 86 | return address(staticRelayer); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/factory/RelayerFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import {Caller} from "../test/utils/Caller.sol"; 6 | import {Guarded} from "../guarded/Guarded.sol"; 7 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 8 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 9 | import {RelayerFactory} from "./RelayerFactory.sol"; 10 | import {Relayer} from "../relayer/Relayer.sol"; 11 | import {StaticRelayer} from "../relayer/StaticRelayer.sol"; 12 | import {IRelayer} from "../relayer/IRelayer.sol"; 13 | 14 | contract RelayerFactoryTest is DSTest { 15 | address private _collybusAddress = address(0x1); 16 | address private _oracleAddress = address(0x2); 17 | IRelayer.RelayerType private _relayerType = 18 | IRelayer.RelayerType.DiscountRate; 19 | bytes32 private _encodedTokenId = "encodedTokeId"; 20 | uint256 private _minimumPercentageDeltaValue = 100_00; 21 | 22 | RelayerFactory private _factory; 23 | 24 | function setUp() public { 25 | _factory = new RelayerFactory(); 26 | } 27 | 28 | function test_deploy() public { 29 | assertTrue(address(_factory) != address(0)); 30 | } 31 | 32 | function test_create() public { 33 | // Create Relayer 34 | address relayerAddress = _factory.create( 35 | _collybusAddress, 36 | _relayerType, 37 | _oracleAddress, 38 | _encodedTokenId, 39 | _minimumPercentageDeltaValue 40 | ); 41 | 42 | assertTrue( 43 | relayerAddress != address(0), 44 | "Factory Relayer create failed" 45 | ); 46 | } 47 | 48 | function test_createStatic() public { 49 | // Create a static Relayer 50 | uint256 value = 1; 51 | address staticRelayerAddress = _factory.createStatic( 52 | _collybusAddress, 53 | _relayerType, 54 | _encodedTokenId, 55 | value 56 | ); 57 | 58 | assertTrue( 59 | staticRelayerAddress != address(0), 60 | "Factory StaticRelayer create failed" 61 | ); 62 | } 63 | 64 | function test_create_validateProperties() public { 65 | Relayer relayer = Relayer( 66 | _factory.create( 67 | _collybusAddress, 68 | _relayerType, 69 | _oracleAddress, 70 | _encodedTokenId, 71 | _minimumPercentageDeltaValue 72 | ) 73 | ); 74 | 75 | assertEq( 76 | relayer.collybus(), 77 | _collybusAddress, 78 | "Relayer incorrect collybus" 79 | ); 80 | 81 | assertTrue( 82 | relayer.relayerType() == _relayerType, 83 | "Relayer incorrect relayerType" 84 | ); 85 | 86 | assertEq( 87 | relayer.oracle(), 88 | _oracleAddress, 89 | "Relayer incorrect oracle address" 90 | ); 91 | 92 | assertEq( 93 | relayer.encodedTokenId(), 94 | _encodedTokenId, 95 | "Relayer incorrect encoded token id" 96 | ); 97 | 98 | assertEq( 99 | relayer.minimumPercentageDeltaValue(), 100 | _minimumPercentageDeltaValue, 101 | "Relayer incorrect minimumPercentageDeltaValue" 102 | ); 103 | } 104 | 105 | function test_createStatic_validateProperties() public { 106 | uint256 value = 1; 107 | StaticRelayer staticRelayer = StaticRelayer( 108 | _factory.createStatic( 109 | _collybusAddress, 110 | _relayerType, 111 | _encodedTokenId, 112 | value 113 | ) 114 | ); 115 | 116 | assertEq( 117 | staticRelayer.collybus(), 118 | _collybusAddress, 119 | "StaticRelayer incorrect collybus" 120 | ); 121 | 122 | assertTrue( 123 | staticRelayer.relayerType() == _relayerType, 124 | "StaticRelayer incorrect relayerType" 125 | ); 126 | 127 | assertEq( 128 | staticRelayer.encodedTokenId(), 129 | _encodedTokenId, 130 | "StaticRelayer incorrect encoded token id" 131 | ); 132 | 133 | assertEq(staticRelayer.value(), value, "StaticRelayer incorrect value"); 134 | } 135 | 136 | function test_create_factoryPassesPermissions() public { 137 | Relayer relayer = Relayer( 138 | _factory.create( 139 | _collybusAddress, 140 | _relayerType, 141 | _oracleAddress, 142 | _encodedTokenId, 143 | _minimumPercentageDeltaValue 144 | ) 145 | ); 146 | 147 | bool factoryIsAuthorized = relayer.canCall( 148 | relayer.ANY_SIG(), 149 | address(_factory) 150 | ); 151 | assertTrue( 152 | factoryIsAuthorized == false, 153 | "The Factory should not have rights over the created contract" 154 | ); 155 | 156 | bool callerIsAuthorized = relayer.canCall( 157 | relayer.ANY_SIG(), 158 | address(this) 159 | ); 160 | assertTrue( 161 | callerIsAuthorized, 162 | "Caller should have rights over the created contract" 163 | ); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/factory/YieldFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {YieldValueProvider} from "../oracle_implementations/discount_rate/Yield/YieldValueProvider.sol"; 5 | import {Relayer} from "../relayer/Relayer.sol"; 6 | import {IRelayer} from "../relayer/IRelayer.sol"; 7 | 8 | contract YieldFactory { 9 | event YieldDeployed(address relayerAddress, address oracleAddress); 10 | 11 | /// @param collybus_ Address of the collybus 12 | /// @param tokenId_ Token id that will be used to push values to Collybus 13 | /// @param minimumPercentageDeltaValue_ Minimum delta value used to determine when to 14 | /// push data to Collybus 15 | /// @param timeUpdateWindow_ Minimum time between updates of the value 16 | /// @param poolAddress_ Address of the pool 17 | /// @param maturity_ Expiration of the pool 18 | /// @param timeScale_ Time scale used on this pool (i.e. 1/(timeStretch*secondsPerYear)) in 59x18 fixed point 19 | /// @return The address of the Relayer 20 | function create( 21 | // Relayer parameters 22 | address collybus_, 23 | uint256 tokenId_, 24 | uint256 minimumPercentageDeltaValue_, 25 | // Oracle parameters 26 | uint256 timeUpdateWindow_, 27 | // Yield specific parameters, see YieldValueProvider for more info 28 | address poolAddress_, 29 | uint256 maturity_, 30 | int256 timeScale_ 31 | ) public returns (address) { 32 | YieldValueProvider yieldValueProvider = new YieldValueProvider( 33 | timeUpdateWindow_, 34 | poolAddress_, 35 | maturity_, 36 | timeScale_ 37 | ); 38 | 39 | // Create the relayer that manages the oracle and pushes data to Collybus 40 | Relayer relayer = new Relayer( 41 | collybus_, 42 | IRelayer.RelayerType.DiscountRate, 43 | address(yieldValueProvider), 44 | bytes32(tokenId_), 45 | minimumPercentageDeltaValue_ 46 | ); 47 | 48 | // Whitelist the Relayer in the Oracle so it can trigger updates 49 | yieldValueProvider.allowCaller( 50 | yieldValueProvider.ANY_SIG(), 51 | address(relayer) 52 | ); 53 | 54 | // Whitelist the deployer 55 | yieldValueProvider.allowCaller( 56 | yieldValueProvider.ANY_SIG(), 57 | msg.sender 58 | ); 59 | relayer.allowCaller(relayer.ANY_SIG(), msg.sender); 60 | 61 | // Renounce permissions 62 | yieldValueProvider.blockCaller( 63 | yieldValueProvider.ANY_SIG(), 64 | address(this) 65 | ); 66 | relayer.blockCaller(relayer.ANY_SIG(), address(this)); 67 | 68 | emit YieldDeployed(address(relayer), address(yieldValueProvider)); 69 | return address(relayer); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/factory/YieldFactory.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import {YieldFactory} from "./YieldFactory.sol"; 8 | import {IYieldPool} from "../oracle_implementations/discount_rate/Yield/IYieldPool.sol"; 9 | import {YieldValueProvider} from "../oracle_implementations/discount_rate/Yield/YieldValueProvider.sol"; 10 | 11 | import {Relayer} from "../relayer/Relayer.sol"; 12 | import {IRelayer} from "../relayer/IRelayer.sol"; 13 | 14 | contract YieldFactoryTest is DSTest { 15 | address private _collybusAddress = address(0xC011b005); 16 | uint256 private _oracleUpdateWindow = 1 * 3600; 17 | uint256 private _tokenId = 1; 18 | uint256 private _minimumPercentageDeltaValue = 25; 19 | 20 | uint256 private _maturity = 1648177200; 21 | int256 private _timeScale = 3168808781; // computed from 58454204609 which is in 64.64 format 22 | 23 | YieldFactory private _factory; 24 | 25 | function setUp() public { 26 | _factory = new YieldFactory(); 27 | } 28 | 29 | function test_deploy() public { 30 | assertTrue(address(_factory) != address(0)); 31 | } 32 | 33 | function test_create_relayerIsDeployed() public { 34 | // Mock the yield pool that is needed when the value provider contract is created 35 | MockProvider yieldPool = new MockProvider(); 36 | yieldPool.givenQueryReturnResponse( 37 | abi.encodeWithSelector(IYieldPool.getCache.selector), 38 | MockProvider.ReturnData({success: true, data: abi.encode(0, 0, 0)}), 39 | false 40 | ); 41 | 42 | yieldPool.givenQueryReturnResponse( 43 | abi.encodeWithSelector(IYieldPool.cumulativeBalancesRatio.selector), 44 | MockProvider.ReturnData({success: true, data: abi.encode(0)}), 45 | false 46 | ); 47 | 48 | // Create Yield Relayer 49 | address yieldRelayerAddress = _factory.create( 50 | _collybusAddress, 51 | _tokenId, 52 | _minimumPercentageDeltaValue, 53 | _oracleUpdateWindow, 54 | address(yieldPool), 55 | _maturity, 56 | _timeScale 57 | ); 58 | 59 | assertTrue( 60 | yieldRelayerAddress != address(0), 61 | "Yield Relayer create failed" 62 | ); 63 | } 64 | 65 | function test_create_oracleIsDeployed() public { 66 | // Mock the yield pool that is needed when the value provider contract is created 67 | MockProvider yieldPool = new MockProvider(); 68 | yieldPool.givenQueryReturnResponse( 69 | abi.encodeWithSelector(IYieldPool.getCache.selector), 70 | MockProvider.ReturnData({success: true, data: abi.encode(0, 0, 0)}), 71 | false 72 | ); 73 | 74 | yieldPool.givenQueryReturnResponse( 75 | abi.encodeWithSelector(IYieldPool.cumulativeBalancesRatio.selector), 76 | MockProvider.ReturnData({success: true, data: abi.encode(0)}), 77 | false 78 | ); 79 | 80 | // Create Yield Relayer 81 | address yieldRelayerAddress = _factory.create( 82 | _collybusAddress, 83 | _tokenId, 84 | _minimumPercentageDeltaValue, 85 | _oracleUpdateWindow, 86 | address(yieldPool), 87 | _maturity, 88 | _timeScale 89 | ); 90 | 91 | assertTrue( 92 | Relayer(yieldRelayerAddress).oracle() != address(0), 93 | "Invalid oracle address" 94 | ); 95 | } 96 | 97 | function test_create_validateOracleProperties() public { 98 | // Mock the yield pool that is needed when the value provider contract is created 99 | MockProvider yieldPool = new MockProvider(); 100 | yieldPool.givenQueryReturnResponse( 101 | abi.encodeWithSelector(IYieldPool.getCache.selector), 102 | MockProvider.ReturnData({success: true, data: abi.encode(0, 0, 0)}), 103 | false 104 | ); 105 | 106 | yieldPool.givenQueryReturnResponse( 107 | abi.encodeWithSelector(IYieldPool.cumulativeBalancesRatio.selector), 108 | MockProvider.ReturnData({success: true, data: abi.encode(0)}), 109 | false 110 | ); 111 | 112 | // Create Yield Relayer 113 | address yieldRelayerAddress = _factory.create( 114 | _collybusAddress, 115 | _tokenId, 116 | _minimumPercentageDeltaValue, 117 | _oracleUpdateWindow, 118 | address(yieldPool), 119 | _maturity, 120 | _timeScale 121 | ); 122 | 123 | address yieldOracleAddress = Relayer(yieldRelayerAddress).oracle(); 124 | 125 | // Test that properties are correctly set 126 | assertEq( 127 | YieldValueProvider(yieldOracleAddress).timeUpdateWindow(), 128 | _oracleUpdateWindow, 129 | "Yield Value Provider incorrect timeUpdateWindow" 130 | ); 131 | 132 | assertEq( 133 | YieldValueProvider(yieldOracleAddress).poolAddress(), 134 | address(yieldPool), 135 | "Yield Value Provider incorrect poolAddress" 136 | ); 137 | 138 | assertEq( 139 | YieldValueProvider(yieldOracleAddress).maturity(), 140 | _maturity, 141 | "Yield Value Provider incorrect maturity" 142 | ); 143 | 144 | assertEq( 145 | YieldValueProvider(yieldOracleAddress).timeScale(), 146 | _timeScale, 147 | "Yield Value Provider incorrect timeScale" 148 | ); 149 | } 150 | 151 | function test_create_validateRelayerProperties() public { 152 | // Mock the yield pool that is needed when the value provider contract is created 153 | MockProvider yieldPool = new MockProvider(); 154 | yieldPool.givenQueryReturnResponse( 155 | abi.encodeWithSelector(IYieldPool.getCache.selector), 156 | MockProvider.ReturnData({success: true, data: abi.encode(0, 0, 0)}), 157 | false 158 | ); 159 | 160 | yieldPool.givenQueryReturnResponse( 161 | abi.encodeWithSelector(IYieldPool.cumulativeBalancesRatio.selector), 162 | MockProvider.ReturnData({success: true, data: abi.encode(0)}), 163 | false 164 | ); 165 | 166 | // Create Yield Relayer 167 | address yieldRelayerAddress = _factory.create( 168 | _collybusAddress, 169 | _tokenId, 170 | _minimumPercentageDeltaValue, 171 | _oracleUpdateWindow, 172 | address(yieldPool), 173 | _maturity, 174 | _timeScale 175 | ); 176 | 177 | // Test that properties are correctly set 178 | assertEq( 179 | Relayer(yieldRelayerAddress).collybus(), 180 | _collybusAddress, 181 | "Yield Relayer incorrect Collybus address" 182 | ); 183 | 184 | assertTrue( 185 | Relayer(yieldRelayerAddress).relayerType() == 186 | IRelayer.RelayerType.DiscountRate, 187 | "Yield Relayer incorrect RelayerType" 188 | ); 189 | 190 | assertEq( 191 | Relayer(yieldRelayerAddress).encodedTokenId(), 192 | bytes32(_tokenId), 193 | "Yield Relayer incorrect tokenId" 194 | ); 195 | 196 | assertEq( 197 | Relayer(yieldRelayerAddress).minimumPercentageDeltaValue(), 198 | _minimumPercentageDeltaValue, 199 | "Yield Relayer incorrect minimumPercentageDeltaValue" 200 | ); 201 | } 202 | 203 | function test_create_factoryPassesPermissions() public { 204 | // Mock the yield pool that is needed when the value provider contract is created 205 | MockProvider yieldPool = new MockProvider(); 206 | yieldPool.givenQueryReturnResponse( 207 | abi.encodeWithSelector(IYieldPool.getCache.selector), 208 | MockProvider.ReturnData({success: true, data: abi.encode(0, 0, 0)}), 209 | false 210 | ); 211 | 212 | yieldPool.givenQueryReturnResponse( 213 | abi.encodeWithSelector(IYieldPool.cumulativeBalancesRatio.selector), 214 | MockProvider.ReturnData({success: true, data: abi.encode(0)}), 215 | false 216 | ); 217 | 218 | // Create Yield Relayer 219 | address yieldRelayerAddress = _factory.create( 220 | _collybusAddress, 221 | _tokenId, 222 | _minimumPercentageDeltaValue, 223 | _oracleUpdateWindow, 224 | address(yieldPool), 225 | _maturity, 226 | _timeScale 227 | ); 228 | 229 | YieldValueProvider yieldValueProvider = YieldValueProvider( 230 | Relayer(yieldRelayerAddress).oracle() 231 | ); 232 | bool factoryIsAuthorized = yieldValueProvider.canCall( 233 | yieldValueProvider.ANY_SIG(), 234 | address(_factory) 235 | ); 236 | assertTrue( 237 | factoryIsAuthorized == false, 238 | "The Factory should not have rights over the created contract" 239 | ); 240 | 241 | bool callerIsAuthorized = yieldValueProvider.canCall( 242 | yieldValueProvider.ANY_SIG(), 243 | address(this) 244 | ); 245 | assertTrue( 246 | callerIsAuthorized, 247 | "Caller should have rights over the created contract" 248 | ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/guarded/Guarded.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicensed 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title Guarded 5 | /// @notice Mixin implementing an authentication scheme on a method level 6 | abstract contract Guarded { 7 | /// ======== Custom Errors ======== /// 8 | 9 | error Guarded__notRoot(); 10 | error Guarded__notGranted(); 11 | 12 | /// ======== Storage ======== /// 13 | 14 | /// @notice Wildcard for granting a caller to call every guarded method 15 | bytes32 public constant ANY_SIG = keccak256("ANY_SIG"); 16 | /// @notice Wildcard for granting a caller to call every guarded method 17 | address public constant ANY_CALLER = 18 | address(uint160(uint256(bytes32(keccak256("ANY_CALLER"))))); 19 | 20 | /// @notice Mapping storing who is granted to which method 21 | /// @dev Method Signature => Caller => Bool 22 | mapping(bytes32 => mapping(address => bool)) private _canCall; 23 | 24 | /// ======== Events ======== /// 25 | 26 | event AllowCaller(bytes32 sig, address who); 27 | event BlockCaller(bytes32 sig, address who); 28 | 29 | constructor() { 30 | // set root 31 | _setRoot(msg.sender); 32 | } 33 | 34 | /// ======== Auth ======== /// 35 | 36 | modifier callerIsRoot() { 37 | if (_canCall[ANY_SIG][msg.sender]) { 38 | _; 39 | } else revert Guarded__notRoot(); 40 | } 41 | 42 | modifier checkCaller() { 43 | if (canCall(msg.sig, msg.sender)) { 44 | _; 45 | } else revert Guarded__notGranted(); 46 | } 47 | 48 | /// @notice Grant the right to call method `sig` to `who` 49 | /// @dev Only the root user (granted `ANY_SIG`) is able to call this method 50 | /// @param sig_ Method signature (4Byte) 51 | /// @param who_ Address of who should be able to call `sig` 52 | function allowCaller(bytes32 sig_, address who_) public callerIsRoot { 53 | _canCall[sig_][who_] = true; 54 | emit AllowCaller(sig_, who_); 55 | } 56 | 57 | /// @notice Revoke the right to call method `sig` from `who` 58 | /// @dev Only the root user (granted `ANY_SIG`) is able to call this method 59 | /// @param sig_ Method signature (4Byte) 60 | /// @param who_ Address of who should not be able to call `sig` anymore 61 | function blockCaller(bytes32 sig_, address who_) public callerIsRoot { 62 | _canCall[sig_][who_] = false; 63 | emit BlockCaller(sig_, who_); 64 | } 65 | 66 | /// @notice Returns if `who` can call `sig` 67 | /// @param sig_ Method signature (4Byte) 68 | /// @param who_ Address of who should be able to call `sig` 69 | function canCall(bytes32 sig_, address who_) public view returns (bool) { 70 | return (_canCall[sig_][who_] || 71 | _canCall[ANY_SIG][who_] || 72 | _canCall[sig_][ANY_CALLER]); 73 | } 74 | 75 | /// @notice Sets the root user (granted `ANY_SIG`) 76 | /// @param root_ Address of who should be set as root 77 | function _setRoot(address root_) internal { 78 | _canCall[ANY_SIG][root_] = true; 79 | emit AllowCaller(ANY_SIG, root_); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/guarded/Guarded.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import {DSTest} from "ds-test/test.sol"; 5 | 6 | import {Caller} from "../test/utils/Caller.sol"; 7 | 8 | import {Guarded} from "./Guarded.sol"; 9 | 10 | contract GuardedInstance is Guarded { 11 | constructor() Guarded() { 12 | this; 13 | } 14 | 15 | function guardedMethod() external view checkCaller { 16 | this; 17 | } 18 | 19 | function guardedMethodRoot() external view checkCaller { 20 | this; 21 | } 22 | } 23 | 24 | contract GuardedTest is DSTest { 25 | GuardedInstance internal guarded; 26 | 27 | function setUp() public { 28 | guarded = new GuardedInstance(); 29 | } 30 | 31 | function test_custom_role() public { 32 | Caller user = new Caller(); 33 | bool ok; 34 | bool canCall; 35 | 36 | // Should not be able to call method 37 | (ok, ) = user.externalCall( 38 | address(guarded), 39 | abi.encodeWithSelector(guarded.guardedMethod.selector) 40 | ); 41 | assertTrue( 42 | ok == false, 43 | "Cannot call guarded method before adding permissions" 44 | ); 45 | 46 | // Adding permission should allow user to call method 47 | guarded.allowCaller(guarded.guardedMethod.selector, address(user)); 48 | (ok, ) = user.externalCall( 49 | address(guarded), 50 | abi.encodeWithSelector(guarded.guardedMethod.selector) 51 | ); 52 | assertTrue(ok, "Can call method after adding permissions"); 53 | 54 | // User has custom permission to call method 55 | canCall = guarded.canCall( 56 | guarded.guardedMethod.selector, 57 | address(user) 58 | ); 59 | assertTrue(canCall, "User has permission"); 60 | 61 | // Removing role disables permission 62 | guarded.blockCaller(guarded.guardedMethod.selector, address(user)); 63 | (ok, ) = user.externalCall( 64 | address(guarded), 65 | abi.encodeWithSelector(guarded.guardedMethod.selector) 66 | ); 67 | assertTrue( 68 | ok == false, 69 | "Cannot call method after removing permissions" 70 | ); 71 | 72 | // User does not have custom role 73 | canCall = guarded.canCall( 74 | guarded.guardedMethod.selector, 75 | address(user) 76 | ); 77 | assertTrue(canCall == false, "User does not have permission"); 78 | } 79 | 80 | function test_root_role() public { 81 | Caller user = new Caller(); 82 | bool ok; 83 | 84 | // Should not be able to call method 85 | (ok, ) = user.externalCall( 86 | address(guarded), 87 | abi.encodeWithSelector(guarded.guardedMethodRoot.selector) 88 | ); 89 | assertTrue(ok == false, "Root can call method"); 90 | 91 | // Adding ANY_SIG should allow user to call method 92 | guarded.allowCaller(guarded.ANY_SIG(), address(user)); 93 | (ok, ) = user.externalCall( 94 | address(guarded), 95 | abi.encodeWithSelector(guarded.guardedMethodRoot.selector) 96 | ); 97 | assertTrue(ok, "User can call method after adding root permissions"); 98 | 99 | // User has senatus role 100 | bool canCall = guarded.canCall(guarded.ANY_SIG(), address(user)); 101 | assertTrue(canCall, "User has permission"); 102 | 103 | // Removing senatus role disables permission 104 | guarded.blockCaller(guarded.ANY_SIG(), address(user)); 105 | (ok, ) = user.externalCall( 106 | address(guarded), 107 | abi.encodeWithSelector(guarded.guardedMethodRoot.selector) 108 | ); 109 | assertTrue( 110 | ok == false, 111 | "Senatus cannot call method after removing permissions" 112 | ); 113 | 114 | // User does not have senatus role 115 | canCall = guarded.canCall(guarded.ANY_SIG(), address(user)); 116 | assertTrue(canCall == false, "User does not have role"); 117 | } 118 | 119 | function test_root_has_god_mode_access() public { 120 | Caller user = new Caller(); 121 | bool ok; 122 | 123 | // Should not be able to call method 124 | (ok, ) = user.externalCall( 125 | address(guarded), 126 | abi.encodeWithSelector(guarded.guardedMethod.selector) 127 | ); 128 | assertTrue( 129 | ok == false, 130 | "Cannot call guarded method before adding permissions" 131 | ); 132 | 133 | // Adding senatus role should allow user to call method 134 | guarded.allowCaller(guarded.ANY_SIG(), address(user)); 135 | (ok, ) = user.externalCall( 136 | address(guarded), 137 | abi.encodeWithSelector(guarded.guardedMethod.selector) 138 | ); 139 | assertTrue(ok, "Can call method after adding senatus role"); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/oracle/IOracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | interface IOracle { 5 | function value() external view returns (int256, bool); 6 | 7 | function nextValue() external view returns (int256); 8 | 9 | function update() external returns (bool); 10 | } 11 | -------------------------------------------------------------------------------- /src/oracle/Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {IOracle} from "./IOracle.sol"; 5 | 6 | import {Pausable} from "../pausable/Pausable.sol"; 7 | 8 | abstract contract Oracle is Pausable, IOracle { 9 | /// @notice Emitted when a method is reentered 10 | error Oracle__nonReentrant(); 11 | 12 | /// ======== Events ======== /// 13 | 14 | event ValueInvalid(); 15 | event ValueUpdated(int256 currentValue, int256 nextValue); 16 | event OracleReset(); 17 | 18 | /// ======== Storage ======== /// 19 | // Time interval between the value updates 20 | uint256 public immutable timeUpdateWindow; 21 | 22 | // Timestamp of the current value 23 | uint256 public lastTimestamp; 24 | 25 | // The next value that will replace the current value once the timeUpdateWindow has passed 26 | int256 public override(IOracle) nextValue; 27 | 28 | // Current value that will be returned by the Oracle 29 | int256 private _currentValue; 30 | 31 | // Flag that tells if the value provider returned successfully 32 | bool private _validReturnedValue; 33 | 34 | // Reentrancy constants 35 | uint256 private constant _NOT_ENTERED = 1; 36 | uint256 private constant _ENTERED = 2; 37 | 38 | // Reentrancy guard flag 39 | uint256 private _reentrantGuard = _NOT_ENTERED; 40 | 41 | /// ======== Modifiers ======== /// 42 | 43 | modifier nonReentrant() { 44 | // Check if the guard is set 45 | if (_reentrantGuard != _NOT_ENTERED) { 46 | revert Oracle__nonReentrant(); 47 | } 48 | 49 | // Set the guard 50 | _reentrantGuard = _ENTERED; 51 | 52 | // Allow execution 53 | _; 54 | 55 | // Reset the guard 56 | _reentrantGuard = _NOT_ENTERED; 57 | } 58 | 59 | constructor(uint256 timeUpdateWindow_) { 60 | timeUpdateWindow = timeUpdateWindow_; 61 | _validReturnedValue = false; 62 | } 63 | 64 | /// @notice Get the current value of the oracle 65 | /// @return The current value of the oracle 66 | /// @return Whether the value is valid 67 | function value() 68 | public 69 | view 70 | override(IOracle) 71 | whenNotPaused 72 | returns (int256, bool) 73 | { 74 | // Value is considered valid if the value provider successfully returned a value 75 | return (_currentValue, _validReturnedValue); 76 | } 77 | 78 | function getValue() external virtual returns (int256); 79 | 80 | function update() 81 | public 82 | override(IOracle) 83 | checkCaller 84 | nonReentrant 85 | returns (bool) 86 | { 87 | // Not enough time has passed since the last update 88 | if (lastTimestamp + timeUpdateWindow > block.timestamp) { 89 | // Exit early if no update is needed 90 | return false; 91 | } 92 | 93 | // Oracle update should not fail even if the value provider fails to return a value 94 | try this.getValue() returns (int256 returnedValue) { 95 | // Update the value using an exponential moving average 96 | if (_currentValue == 0) { 97 | // First update takes the current value 98 | nextValue = returnedValue; 99 | _currentValue = nextValue; 100 | } else { 101 | // Update the current value with the next value 102 | _currentValue = nextValue; 103 | // Set the returnedValue as the next value 104 | nextValue = returnedValue; 105 | } 106 | 107 | // Save when the value was last updated 108 | lastTimestamp = block.timestamp; 109 | _validReturnedValue = true; 110 | 111 | emit ValueUpdated(_currentValue, nextValue); 112 | 113 | return true; 114 | } catch { 115 | // When a value provider fails, we update the valid flag which will 116 | // invalidate the value instantly 117 | _validReturnedValue = false; 118 | emit ValueInvalid(); 119 | } 120 | 121 | return false; 122 | } 123 | 124 | function pause() public checkCaller { 125 | _pause(); 126 | } 127 | 128 | function unpause() public checkCaller { 129 | _unpause(); 130 | } 131 | 132 | function reset() public whenPaused checkCaller { 133 | _currentValue = 0; 134 | nextValue = 0; 135 | lastTimestamp = 0; 136 | _validReturnedValue = false; 137 | 138 | emit OracleReset(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/oracle/Oracle.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "../test/utils/Caller.sol"; 6 | import {Hevm} from "../test/utils/Hevm.sol"; 7 | import {Oracle} from "./Oracle.sol"; 8 | 9 | contract OracleImplementation is Oracle { 10 | constructor(uint256 timeUpdateWindow_) Oracle(timeUpdateWindow_) { 11 | this; 12 | } 13 | 14 | // Allow the test to change the return value 15 | // This will not be used in the actual implementation 16 | int256 internal _returnValue; 17 | 18 | function setValue(int256 value_) public { 19 | _returnValue = value_; 20 | } 21 | 22 | // We want to handle cases where the oracle fails to obtain values 23 | // Ideally the `getValue()` will never fail, but the current implementation will also handle fails 24 | bool internal _success = true; 25 | 26 | function setSuccess(bool success_) public { 27 | _success = success_; 28 | } 29 | 30 | // We mock this value provider to return the expected previously set value 31 | function getValue() external view override(Oracle) returns (int256) { 32 | if (_success) { 33 | return _returnValue; 34 | } else { 35 | revert("Oracle failed"); 36 | } 37 | } 38 | } 39 | 40 | contract OracleReenter is Oracle { 41 | constructor(uint256 timeUpdateWindow_) Oracle(timeUpdateWindow_) { 42 | this; 43 | } 44 | 45 | bool public reentered = false; 46 | 47 | function getValue() external override(Oracle) returns (int256) { 48 | if (reentered) { 49 | return 42; 50 | } else { 51 | reentered = true; 52 | super.update(); 53 | } 54 | 55 | return 0; 56 | } 57 | } 58 | 59 | contract OracleTest is DSTest { 60 | Hevm internal hevm = Hevm(DSTest.HEVM_ADDRESS); 61 | 62 | OracleImplementation internal oracle; 63 | uint256 internal timeUpdateWindow = 100; // seconds 64 | 65 | function setUp() public { 66 | oracle = new OracleImplementation(timeUpdateWindow); 67 | 68 | // Set the value returned to 100 69 | oracle.setValue(int256(100 * 10**18)); 70 | 71 | hevm.warp(timeUpdateWindow * 10); 72 | } 73 | 74 | function test_deploy() public { 75 | assertTrue(address(oracle) != address(0)); 76 | } 77 | 78 | function test_check_timeUpdateWindow() public { 79 | // Check that the property was properly set 80 | assertTrue( 81 | oracle.timeUpdateWindow() == timeUpdateWindow, 82 | "Invalid oracle timeUpdateWindow" 83 | ); 84 | } 85 | 86 | function test_update_updates_timestamp() public { 87 | oracle.update(); 88 | 89 | // Check if the timestamp was updated 90 | assertEq(oracle.lastTimestamp(), block.timestamp); 91 | } 92 | 93 | function test_update_shouldNotUpdatePreviousValues_ifNotEnoughTimePassed() 94 | public 95 | { 96 | // Get current timestamp 97 | uint256 blockTimestamp = block.timestamp; 98 | 99 | // Update the oracle 100 | oracle.update(); 101 | 102 | // Check if the timestamp was updated 103 | assertEq(oracle.lastTimestamp(), blockTimestamp); 104 | 105 | // Advance time 106 | hevm.warp(blockTimestamp + timeUpdateWindow - 1); 107 | 108 | // Calling update should not update the values 109 | // because not enough time has passed 110 | oracle.update(); 111 | 112 | // Check if the values are still the same 113 | assertEq(oracle.lastTimestamp(), blockTimestamp); 114 | } 115 | 116 | function test_update_shouldUpdatePreviousValues_ifEnoughTimePassed() 117 | public 118 | { 119 | // Get current timestamp 120 | uint256 blockTimestamp = block.timestamp; 121 | 122 | // Advance time 123 | hevm.warp(blockTimestamp + timeUpdateWindow); 124 | 125 | // Calling update should update the values 126 | // because enough time has passed 127 | oracle.update(); 128 | 129 | // Last timestamp should be updated 130 | assertEq(oracle.lastTimestamp(), blockTimestamp + timeUpdateWindow); 131 | } 132 | 133 | function test_update_updateDoesNotChangeTheValue_inTheSameWindow() public { 134 | oracle.setValue(int256(100 * 10**18)); 135 | 136 | // Update the oracle 137 | oracle.update(); 138 | 139 | (int256 value1, ) = oracle.value(); 140 | assertEq(value1, 100 * 10**18); 141 | 142 | int256 nextValue1 = oracle.nextValue(); 143 | assertEq(nextValue1, 100 * 10**18); 144 | 145 | oracle.setValue(int256(150 * 10**18)); 146 | 147 | // Advance time but stay in the same time update window 148 | hevm.warp(block.timestamp + 1); 149 | 150 | // Update the oracle 151 | oracle.update(); 152 | 153 | // Values are not modified 154 | (int256 value2, ) = oracle.value(); 155 | assertEq(value2, 100 * 10**18); 156 | 157 | int256 nextValue2 = oracle.nextValue(); 158 | assertEq(nextValue2, 100 * 10**18); 159 | } 160 | 161 | function test_update_shouldNotFail_whenValueProviderFails() public { 162 | oracle.setSuccess(false); 163 | 164 | // Update the oracle 165 | oracle.update(); 166 | } 167 | 168 | function test_value_shouldBeInvalid_afterValueProviderFails() public { 169 | // We first successfully update the value to make sure the lastTimestamp is updated 170 | // After that, we wait for the required amount of time and try update the value again 171 | // The second update will fail and the value should be invalid because of the flag only. 172 | // (time check is still correct because maxValidTime >= timeUpdateWindow) 173 | 174 | oracle.setValue(10**18); 175 | 176 | // Update the oracle 177 | oracle.update(); 178 | 179 | // Advance time 180 | hevm.warp(block.timestamp + timeUpdateWindow); 181 | 182 | oracle.setSuccess(false); 183 | 184 | // Update the oracle 185 | oracle.update(); 186 | 187 | (, bool isValid) = oracle.value(); 188 | assertTrue(isValid == false); 189 | } 190 | 191 | function test_value_shouldBecomeValid_afterSuccessfulUpdate() public { 192 | oracle.setSuccess(false); 193 | 194 | oracle.update(); 195 | 196 | (, bool isValid1) = oracle.value(); 197 | assertTrue(isValid1 == false); 198 | 199 | oracle.setSuccess(true); 200 | 201 | oracle.update(); 202 | 203 | (, bool isValid2) = oracle.value(); 204 | assertTrue(isValid2 == true); 205 | } 206 | 207 | function test_valueReturned_shouldNotBeValid_ifNeverUpdated() public { 208 | // Initially the value should be considered stale 209 | (, bool valid) = oracle.value(); 210 | assertTrue(valid == false); 211 | } 212 | 213 | function test_paused_stops_returnValue() public { 214 | // Pause oracle 215 | oracle.pause(); 216 | 217 | // Create user 218 | Caller user = new Caller(); 219 | 220 | // Should fail trying to get value 221 | bool success; 222 | (success, ) = user.externalCall( 223 | address(oracle), 224 | abi.encodeWithSelector(oracle.value.selector) 225 | ); 226 | 227 | assertTrue(success == false, "value() should fail when paused"); 228 | } 229 | 230 | function test_paused_doesNotStop_update() public { 231 | // Pause oracle 232 | oracle.pause(); 233 | 234 | // Create user 235 | Caller user = new Caller(); 236 | 237 | // Allow user to call update on the oracle 238 | oracle.allowCaller(oracle.ANY_SIG(), address(user)); 239 | 240 | // Should not fail trying to get update 241 | bool success; 242 | (success, ) = user.externalCall( 243 | address(oracle), 244 | abi.encodeWithSelector(oracle.update.selector) 245 | ); 246 | 247 | assertTrue(success, "update() should not fail when paused"); 248 | } 249 | 250 | function test_reset_resetsContract() public { 251 | // Make sure there are some values in there 252 | oracle.update(); 253 | 254 | // Last updated timestamp is this block 255 | assertEq(oracle.lastTimestamp(), block.timestamp); 256 | 257 | // Value should be 100 and valid 258 | int256 value; 259 | bool valid; 260 | (value, valid) = oracle.value(); 261 | assertEq(value, 100 * 10**18); 262 | assertTrue(valid == true); 263 | 264 | // Oracle should be paused when resetting 265 | oracle.pause(); 266 | 267 | // Reset contract 268 | oracle.reset(); 269 | 270 | // Unpause contract 271 | oracle.unpause(); 272 | 273 | // Last updated timestamp should be 0 274 | assertEq(oracle.lastTimestamp(), 0); 275 | 276 | // Value should be 0 and not valid 277 | (value, valid) = oracle.value(); 278 | assertEq(value, 0); 279 | assertTrue(valid == false); 280 | 281 | // Next value should be 0 282 | assertEq(oracle.nextValue(), 0); 283 | } 284 | 285 | function test_reset_shouldBePossible_ifPaused() public { 286 | // Pause oracle 287 | oracle.pause(); 288 | 289 | // Reset contract 290 | oracle.reset(); 291 | } 292 | 293 | function testFail_reset_shouldNotBePossible_ifNotPaused() public { 294 | // Oracle is not paused 295 | assertTrue(oracle.paused() == false); 296 | 297 | // Reset contract should fail 298 | oracle.reset(); 299 | } 300 | 301 | function test_authorizedUser_shouldBeAble_toReset() public { 302 | // Create user 303 | Caller user = new Caller(); 304 | 305 | // Grant ability to reset 306 | oracle.allowCaller(oracle.reset.selector, address(user)); 307 | 308 | // Oracle should be paused when calling reset 309 | oracle.pause(); 310 | 311 | // Should not fail trying to reset 312 | bool success; 313 | (success, ) = user.externalCall( 314 | address(oracle), 315 | abi.encodeWithSelector(oracle.reset.selector) 316 | ); 317 | 318 | assertTrue( 319 | success, 320 | "Only authorized user should be able to call reset()" 321 | ); 322 | } 323 | 324 | function test_nonAuthorizedUser_shouldNotBeAble_toReset() public { 325 | // Create user 326 | // Do not authorize user 327 | Caller user = new Caller(); 328 | 329 | // Oracle should be paused when calling reset 330 | oracle.pause(); 331 | 332 | // Should fail trying to reset 333 | bool success; 334 | (success, ) = user.externalCall( 335 | address(oracle), 336 | abi.encodeWithSelector(oracle.reset.selector) 337 | ); 338 | 339 | assertTrue( 340 | success == false, 341 | "Non-authorized user should not be able to call reset()" 342 | ); 343 | } 344 | 345 | function testFail_update_cannotBeReentered() public { 346 | OracleReenter oracleReenter = new OracleReenter(timeUpdateWindow); 347 | 348 | oracleReenter.update(); 349 | 350 | assertTrue(oracleReenter.reentered()); 351 | } 352 | 353 | function test_update_returnsTrue_whenSuccessful() public { 354 | bool updated; 355 | updated = oracle.update(); 356 | 357 | assertTrue(updated, "Should return `true` no successful update"); 358 | } 359 | 360 | function test_update_returnsFalse_whenUpdateDoesNotChangeAnything() public { 361 | bool updated; 362 | updated = oracle.update(); 363 | 364 | // Second update should return false since it doesn't change anything 365 | updated = oracle.update(); 366 | 367 | assertTrue( 368 | updated == false, 369 | "Should return `true` no successful update" 370 | ); 371 | } 372 | 373 | function test_update_nonAuthorizedUserCanNotCall_update() public { 374 | Caller user = new Caller(); 375 | 376 | // A non permissioned user should not be able to call 377 | bool ok; 378 | (ok, ) = user.externalCall( 379 | address(oracle), 380 | abi.encodeWithSelector(oracle.update.selector) 381 | ); 382 | assertTrue( 383 | ok == false, 384 | "Non permissioned user should not be able to call update" 385 | ); 386 | } 387 | 388 | function test_update_authorizedUserCanCall_update() public { 389 | Caller user = new Caller(); 390 | 391 | // Give permission to the user 392 | oracle.allowCaller(oracle.update.selector, address(user)); 393 | 394 | // A non permissioned user should not be able to call 395 | bool ok; 396 | (ok, ) = user.externalCall( 397 | address(oracle), 398 | abi.encodeWithSelector(oracle.update.selector) 399 | ); 400 | assertTrue(ok, "Permissioned user should be able to call update"); 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/NotionalFinance/INotionalView.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /// Imported from: 5 | /// https://github.com/notional-finance/contracts-v2/blob/23a3d5fcdba8a2e2ae6b0730f73eed810484e4cc/contracts/global/Types.sol 6 | /// @dev Holds information about a market, total storage is 42 bytes so this spans 7 | /// two storage words 8 | struct MarketStorage { 9 | // Total fCash in the market 10 | uint80 totalfCash; 11 | // Total asset cash in the market 12 | uint80 totalAssetCash; 13 | // Last annualized interest rate the market traded at 14 | uint32 lastImpliedRate; 15 | // Last recorded oracle rate for the market 16 | uint32 oracleRate; 17 | // Last time a trade was made 18 | uint32 previousTradeTime; 19 | // This is stored in slot + 1 20 | uint80 totalLiquidity; 21 | } 22 | 23 | /// Imported from: 24 | /// https://github.com/notional-finance/contracts-v2/blob/23a3d5fcdba8a2e2ae6b0730f73eed810484e4cc/contracts/global/Types.sol 25 | /// @dev Market object as represented in memory 26 | struct MarketParameters { 27 | bytes32 storageSlot; 28 | uint256 maturity; 29 | // Total amount of fCash available for purchase in the market. 30 | int256 totalfCash; 31 | // Total amount of cash available for purchase in the market. 32 | int256 totalAssetCash; 33 | // Total amount of liquidity tokens (representing a claim on liquidity) in the market. 34 | int256 totalLiquidity; 35 | // This is the previous annualized interest rate in RATE_PRECISION that the market traded 36 | // at. This is used to calculate the rate anchor to smooth interest rates over time. 37 | // RATE_PRECISION is defined as 1e9 in the constants contract deployed here: 38 | // https://github.com/notional-finance/contracts-v2/blob/23a3d5fcdba8a2e2ae6b0730f73eed810484e4cc/contracts/global/Constants.sol 39 | uint256 lastImpliedRate; 40 | // Time lagged version of lastImpliedRate, used to value fCash assets at market rates while 41 | // remaining resistent to flash loan attacks. 42 | uint256 oracleRate; 43 | // This is the timestamp of the previous trade 44 | uint256 previousTradeTime; 45 | } 46 | 47 | interface INotionalView { 48 | /// @notice Returns a single market 49 | function getMarket( 50 | uint16 currencyId_, 51 | uint256 maturity_, 52 | uint256 settlementDate_ 53 | ) external view returns (MarketParameters memory); 54 | 55 | function getActiveMarkets(uint16 currencyId) 56 | external 57 | view 58 | returns (MarketParameters[] memory); 59 | } 60 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/NotionalFinance/NotionalFinanceValueProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {Convert} from "../utils/Convert.sol"; 5 | import {Oracle} from "../../../oracle/Oracle.sol"; 6 | import "prb-math/contracts/PRBMathSD59x18.sol"; 7 | import {INotionalView} from "./INotionalView.sol"; 8 | import {MarketParameters} from "./INotionalView.sol"; 9 | 10 | contract NotionalFinanceValueProvider is Oracle, Convert { 11 | /// @notice Emitted when trying to add pull a value for an expired pool 12 | error NotionalFinanceValueProvider__getValue_maturityLessThanBlocktime( 13 | uint256 maturity 14 | ); 15 | 16 | /// @notice Emitted when an invalid currencyId is used to deploy the contract 17 | error NotionalFinanceValueProvider__constructor_invalidCurrencyId( 18 | uint256 currencyId 19 | ); 20 | 21 | /// @notice Emitted when the parameters do not map to an active / initialized Notional Market 22 | error NotionalFinanceValueProvider__getValue_invalidMarketParameters( 23 | uint256 currencyId, 24 | uint256 maturityDate, 25 | uint256 settlementDate 26 | ); 27 | 28 | // Seconds in a 360 days year as used by Notional in 18 digits precision 29 | int256 internal constant SECONDS_PER_YEAR = 31104000 * 1e18; 30 | 31 | // Quarter computed as it's defined in the Notional protocol 32 | // 3 months , 5 weeks per month, 6 days per week computed in seconds 33 | // Reference: https://github.com/notional-finance/contracts-v2/blob/master/contracts/global/Constants.sol#L56 34 | uint256 internal constant QUARTER = 3 * 5 * 6 * 86400; 35 | 36 | address public immutable notional; 37 | uint256 public immutable currencyId; 38 | uint256 public immutable maturityDate; 39 | uint256 public immutable oracleRateDecimals; 40 | 41 | /// @notice Constructs the Value provider contracts with the needed Notional contract data in order to 42 | /// calculate the per-second rate. 43 | /// @param timeUpdateWindow_ Minimum time between updates of the value 44 | /// @param notional_ The address of the deployed notional contract 45 | /// @param currencyId_ Currency ID(eth = 1, dai = 2, usdc = 3, wbtc = 4) 46 | /// @param oracleRateDecimals_ Precision of the Notional oracle rate 47 | /// @param maturityDate_ Maturity date. 48 | /// @dev reverts if the CurrencyId is bigger than uint16 max value 49 | constructor( 50 | // Oracle parameters 51 | uint256 timeUpdateWindow_, 52 | // Notional specific parameters 53 | address notional_, 54 | uint256 currencyId_, 55 | uint256 oracleRateDecimals_, 56 | uint256 maturityDate_ 57 | ) Oracle(timeUpdateWindow_) { 58 | if (currencyId_ > type(uint16).max) { 59 | revert NotionalFinanceValueProvider__constructor_invalidCurrencyId( 60 | currencyId_ 61 | ); 62 | } 63 | 64 | oracleRateDecimals = oracleRateDecimals_; 65 | notional = notional_; 66 | currencyId = currencyId_; 67 | maturityDate = maturityDate_; 68 | } 69 | 70 | /// @notice Calculates the annual rate used by the FIAT DAO contracts 71 | /// the rate is precomputed by the notional contract and scaled to 1e18 precision 72 | /// @dev For more details regarding the computed rate in the Notional contracts: 73 | /// https://github.com/notional-finance/contracts-v2/blob/b8e3792e39486b2719c6153acc270199377cc6b9/contracts/internal/markets/Market.sol#L495 74 | /// @return result The result as an signed 59.18-decimal fixed-point number 75 | function getValue() external view override(Oracle) returns (int256) { 76 | // No values for matured pools 77 | if (block.timestamp >= maturityDate) { 78 | revert NotionalFinanceValueProvider__getValue_maturityLessThanBlocktime( 79 | maturityDate 80 | ); 81 | } 82 | 83 | // Retrieve the oracle rate from Notional 84 | uint256 oracleRate = getOracleRate(); 85 | 86 | // Convert rate per annum to 18 digits precision. 87 | uint256 ratePerAnnum = uconvert(oracleRate, oracleRateDecimals, 18); 88 | 89 | // Convert per annum to per second rate 90 | int256 ratePerSecondD59x18 = PRBMathSD59x18.div( 91 | int256(ratePerAnnum), 92 | SECONDS_PER_YEAR 93 | ); 94 | 95 | // Convert continuous compounding to discrete compounding rate 96 | int256 discreteRateD59x18 = PRBMathSD59x18.exp(ratePerSecondD59x18) - 97 | PRBMathSD59x18.SCALE; 98 | 99 | // The result is a 59.18 fixed-point number. 100 | return discreteRateD59x18; 101 | } 102 | 103 | /// @notice Retrieve the oracle rate from the NotionalFinance Market 104 | function getOracleRate() internal view returns (uint256) { 105 | uint256 settlementDate = getSettlementDate(); 106 | // Attempt to retrieve the oracle rate directly by using the maturity and settlement date 107 | MarketParameters memory marketParams = INotionalView(notional) 108 | .getMarket(uint16(currencyId), maturityDate, settlementDate); 109 | 110 | // If we find an active market we can return the oracle rate otherwise we revert 111 | if (marketParams.oracleRate <= 0) { 112 | revert NotionalFinanceValueProvider__getValue_invalidMarketParameters( 113 | currencyId, 114 | maturityDate, 115 | settlementDate 116 | ); 117 | } 118 | 119 | return marketParams.oracleRate; 120 | } 121 | 122 | /// @notice Computes the settlement date as is done in the NotionalFinance contracts 123 | /// Reference 1: 124 | /// https://github.com/notional-finance/contracts-v2/blob/d89be9474e181b322480830501728ea625e853d0/contracts/internal/markets/DateTime.sol#L14 125 | /// Reference 2: 126 | /// https://github.com/notional-finance/contracts-v2/blob/d89be9474e181b322480830501728ea625e853d0/contracts/internal/markets/Market.sol#L536 127 | function getSettlementDate() public view returns (uint256) { 128 | return block.timestamp - (block.timestamp % QUARTER) + QUARTER; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/NotionalFinance/NotionalFinanceValueProvider.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "../../../test/utils/Caller.sol"; 6 | import {Hevm} from "../../../test/utils/Hevm.sol"; 7 | import {CheatCodes} from "../../../test/utils/CheatCodes.sol"; 8 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 9 | import {INotionalView} from "./INotionalView.sol"; 10 | import {MarketParameters} from "./INotionalView.sol"; 11 | import {NotionalFinanceValueProvider} from "./NotionalFinanceValueProvider.sol"; 12 | 13 | contract NotionalFinanceValueProviderTest is DSTest { 14 | CheatCodes internal cheatCodes = CheatCodes(HEVM_ADDRESS); 15 | 16 | MockProvider internal mockNotional; 17 | 18 | NotionalFinanceValueProvider internal notionalVP; 19 | 20 | uint16 internal _currencyId = 2; 21 | uint256 internal _maturityDate = 1671840000; 22 | uint256 internal _timeUpdateWindow = 100; // seconds 23 | 24 | function setUp() public { 25 | // Values taken from interrogating the active markets via the Notional View Contract deployed at 26 | // 0x1344A36A1B56144C3Bc62E7757377D288fDE0369 27 | // block: 13979660 28 | mockNotional = new MockProvider(); 29 | 30 | notionalVP = new NotionalFinanceValueProvider( 31 | // Oracle arguments 32 | // Time update window 33 | _timeUpdateWindow, 34 | // Notional Finance arguments 35 | address(mockNotional), 36 | _currencyId, 37 | 9, 38 | _maturityDate 39 | ); 40 | } 41 | 42 | function test_deploy() public { 43 | assertTrue(address(notionalVP) != address(0)); 44 | } 45 | 46 | function test_check_notional() public { 47 | // Check the address of the notional view contract 48 | assertEq(notionalVP.notional(), address(mockNotional)); 49 | } 50 | 51 | function test_check_currencyId() public { 52 | // Check the currency Id is correctly set 53 | assertEq(notionalVP.currencyId(), _currencyId); 54 | } 55 | 56 | function test_check_maturityDate() public { 57 | // Check the maturity date 58 | assertEq(notionalVP.maturityDate(), _maturityDate); 59 | } 60 | 61 | function test_getValue() public { 62 | mockNotional.givenQueryReturnResponse( 63 | // Used Parameters are: currency ID, maturity date and settlement date. 64 | abi.encodeWithSelector( 65 | INotionalView.getMarket.selector, 66 | _currencyId, 67 | _maturityDate, 68 | notionalVP.getSettlementDate() 69 | ), 70 | MockProvider.ReturnData({ 71 | success: true, 72 | data: abi.encode( 73 | MarketParameters({ 74 | storageSlot: bytes32( 75 | 0xc0ddee3e85a71c2541e1bd9f87cf75833c3860ea32afc5fab9589fd51748147b 76 | ), 77 | maturity: _maturityDate, 78 | totalfCash: int256(7134342186012091), 79 | totalAssetCash: int256(222912257923357058), 80 | totalLiquidity: int256(221856382336730813), 81 | lastImpliedRate: uint256(88688026), 82 | oracleRate: uint256(88688026), 83 | previousTradeTime: uint256(1641600791) 84 | }) 85 | ) 86 | }), 87 | false 88 | ); 89 | 90 | // Expected value is the lastImpliedRate(1e9 precision) in 1e18 precision 91 | int256 expectedValue = 2851338287; 92 | 93 | // Computed value based on the parameters that are sent via the mock provider 94 | int256 value = notionalVP.getValue(); 95 | assertTrue(value == expectedValue); 96 | } 97 | 98 | function testFail_getValue_revertsOnOrAfterMaturityDate() public { 99 | cheatCodes.warp(notionalVP.maturityDate()); 100 | notionalVP.getValue(); 101 | } 102 | 103 | function test_getValue_failsWithInvalidMarketParameters() public { 104 | // Update the mock to return an un-initialized market 105 | mockNotional.givenQueryReturnResponse( 106 | abi.encodeWithSelector( 107 | INotionalView.getMarket.selector, 108 | _currencyId, 109 | _maturityDate, 110 | notionalVP.getSettlementDate() 111 | ), 112 | MockProvider.ReturnData({ 113 | success: true, 114 | data: abi.encode( 115 | MarketParameters({ 116 | storageSlot: bytes32( 117 | 0xc0ddee3e85a71c2541e1bd9f87cf75833c3860ea32afc5fab9589fd51748147b 118 | ), 119 | maturity: _maturityDate, 120 | totalfCash: int256(0), 121 | totalAssetCash: int256(0), 122 | totalLiquidity: int256(0), 123 | lastImpliedRate: uint256(0), 124 | oracleRate: uint256(0), 125 | previousTradeTime: uint256(0) 126 | }) 127 | ) 128 | }), 129 | false 130 | ); 131 | 132 | // Call should revert because of the invalid market 133 | cheatCodes.expectRevert( 134 | abi.encodeWithSelector( 135 | NotionalFinanceValueProvider 136 | .NotionalFinanceValueProvider__getValue_invalidMarketParameters 137 | .selector, 138 | _currencyId, 139 | _maturityDate, 140 | notionalVP.getSettlementDate() 141 | ) 142 | ); 143 | notionalVP.getValue(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/Yield/IYieldPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /// @notice The Yield pool contract interface 5 | /// Only the useful functionality is defined in the interface. 6 | /// For the full contract interface: 7 | /// https://github.com/yieldprotocol/yieldspace-interfaces/blob/0266fbfd0117ff821cb2f43010a004cc44d1bfc1/IPool.sol 8 | /// deployed contract example : https://etherscan.io/address/0x3771c99c087a81df4633b50d8b149afaa83e3c9e 9 | interface IYieldPool { 10 | function ts() external view returns (int128); 11 | 12 | function getCache() 13 | external 14 | view 15 | returns ( 16 | uint112, 17 | uint112, 18 | uint32 19 | ); 20 | 21 | function getBaseBalance() external view returns (uint112); 22 | 23 | function getFYTokenBalance() external view returns (uint112); 24 | 25 | // Fixed point factor with 27 decimals (ray) 26 | function cumulativeBalancesRatio() external view returns (uint256); 27 | } 28 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/Yield/YieldValueProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {Convert} from "../utils/Convert.sol"; 5 | import {IYieldPool} from "./IYieldPool.sol"; 6 | import {Oracle} from "../../../oracle/Oracle.sol"; 7 | import "prb-math/contracts/PRBMathSD59x18.sol"; 8 | 9 | contract YieldValueProvider is Oracle, Convert { 10 | // @notice Emitted when trying to add pull a value for an expired pool 11 | error YieldProtocolValueProvider__getValue_maturityLessThanBlocktime( 12 | uint256 maturity 13 | ); 14 | 15 | // The cumulative Balance Ratio in 18 digit precision 16 | uint256 public cumulativeBalanceRatioLast; 17 | uint32 public blockTimestampLast; 18 | 19 | address public immutable poolAddress; 20 | uint256 public immutable maturity; 21 | int256 public immutable timeScale; 22 | 23 | /// @notice Constructs the Value provider contracts with the needed data in order to 24 | /// calculate the annual rate. 25 | /// @param timeUpdateWindow_ Minimum time between updates of the value 26 | /// @param poolAddress_ Address of the pool 27 | /// @param maturity_ Expiration of the pool 28 | /// @param timeScale_ Time scale used on this pool (i.e. 1/(timeStretch*secondsPerYear)) in 59x18 fixed point 29 | constructor( 30 | // Oracle parameters 31 | uint256 timeUpdateWindow_, 32 | // Yield specific parameters 33 | address poolAddress_, 34 | uint256 maturity_, 35 | int256 timeScale_ 36 | ) Oracle(timeUpdateWindow_) { 37 | poolAddress = poolAddress_; 38 | maturity = maturity_; 39 | timeScale = timeScale_; 40 | 41 | // Load the initial values from the pool 42 | (, , blockTimestampLast) = IYieldPool(poolAddress_).getCache(); 43 | cumulativeBalanceRatioLast = uconvert( 44 | IYieldPool(poolAddress_).cumulativeBalancesRatio(), 45 | 27, 46 | 18 47 | ); 48 | } 49 | 50 | /// @notice Calculates the implied interest rate based on reserves in the pool 51 | /// @dev Documentation: 52 | /// https://www.notion.so/fiatdao/Delphi-Interest-Rate-Oracle-System-01092c10abf14e5fb0f1353b3b24a804 53 | /// @dev Reverts if the block time exceeds or is equal to pool maturity. 54 | /// @return result The result as an signed 59.18-decimal fixed-point number. 55 | function getValue() external override(Oracle) returns (int256) { 56 | // No values for matured pools 57 | if (block.timestamp >= maturity) { 58 | revert YieldProtocolValueProvider__getValue_maturityLessThanBlocktime( 59 | maturity 60 | ); 61 | } 62 | 63 | // Get the current block timestamp for the Cumulative Balance Ratio 64 | (, , uint32 blockTimestamp) = IYieldPool(poolAddress).getCache(); 65 | 66 | // Compute the elapsed time 67 | uint32 timeElapsed = blockTimestamp - blockTimestampLast; 68 | 69 | // Get the current cumulative balance ratio and scale it to 18 digit precision 70 | uint256 cumulativeBalanceRatio = uconvert( 71 | IYieldPool(poolAddress).cumulativeBalancesRatio(), 72 | 27, 73 | 18 74 | ); 75 | 76 | // Compute the scaled cumulative balance delta 77 | // Reverting here if timeElapsed is 0 is wanted 78 | int256 cumulativeScaledBalanceDelta59x18 = PRBMathSD59x18.div( 79 | int256(cumulativeBalanceRatio - cumulativeBalanceRatioLast), 80 | PRBMathSD59x18.fromInt(int256(uint256(timeElapsed))) 81 | ); 82 | 83 | // Save the last used values 84 | blockTimestampLast = blockTimestamp; 85 | cumulativeBalanceRatioLast = cumulativeBalanceRatio; 86 | 87 | // Compute the per-second rate in signed 59.18 format 88 | int256 ratePerSecond59x18 = (PRBMathSD59x18.pow( 89 | cumulativeScaledBalanceDelta59x18, 90 | timeScale 91 | ) - PRBMathSD59x18.SCALE); 92 | 93 | // The result is a 59.18 fixed-point number. 94 | return ratePerSecond59x18; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/Yield/YieldValueProvider.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | import "../../../test/utils/Caller.sol"; 6 | import {Hevm} from "../../../test/utils/Hevm.sol"; 7 | import {Convert} from "../utils/Convert.sol"; 8 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 9 | import {YieldValueProvider} from "./YieldValueProvider.sol"; 10 | import {IYieldPool} from "./IYieldPool.sol"; 11 | 12 | contract YieldValueProviderTest is DSTest, Convert { 13 | Hevm internal hevm = Hevm(DSTest.HEVM_ADDRESS); 14 | 15 | MockProvider internal mockValueProvider; 16 | 17 | YieldValueProvider internal yieldVP; 18 | 19 | // Values taken from contract 20 | // https://etherscan.io/token/0x3771c99c087a81df4633b50d8b149afaa83e3c9e 21 | // at block 13911954 22 | uint256 private _maturity = 1648177200; 23 | int256 private _timeScale = 3168808781; // computed from 58454204609 which is in 64.64 format 24 | uint112 private _cumulativeBalancesRatio = 25 | 5141501570599198210548627855691773; 26 | uint32 private _blockTime = 1639432842; 27 | 28 | // Default oracle parameters 29 | uint256 private _timeUpdateWindow = 100; // seconds 30 | 31 | function setUp() public { 32 | mockValueProvider = new MockProvider(); 33 | 34 | setMockValues(mockValueProvider, _cumulativeBalancesRatio, _blockTime); 35 | 36 | yieldVP = new YieldValueProvider( 37 | // Oracle arguments 38 | // Time update window 39 | _timeUpdateWindow, 40 | // Yield arguments 41 | address(mockValueProvider), 42 | uint256(_maturity), 43 | int256(_timeScale) 44 | ); 45 | } 46 | 47 | function setMockValues( 48 | MockProvider mockValueProvider_, 49 | uint256 cumulativeBalancesRatio_, 50 | uint32 blocktime_ 51 | ) internal { 52 | // Set the getCache values returned by the pool contract 53 | mockValueProvider_.givenQueryReturnResponse( 54 | abi.encodePacked(IYieldPool.getCache.selector), 55 | MockProvider.ReturnData({ 56 | success: true, 57 | data: abi.encode(0, 0, blocktime_) 58 | }), 59 | false 60 | ); 61 | 62 | // Set the cumulativeBalancesRatio returned by the pool contract 63 | mockValueProvider_.givenQueryReturnResponse( 64 | abi.encodePacked(IYieldPool.cumulativeBalancesRatio.selector), 65 | MockProvider.ReturnData({ 66 | success: true, 67 | data: abi.encode(cumulativeBalancesRatio_) 68 | }), 69 | false 70 | ); 71 | } 72 | 73 | function test_deploy() public { 74 | assertTrue( 75 | address(yieldVP) != address(0), 76 | "Yield value provider should be deployed" 77 | ); 78 | } 79 | 80 | function test_check_poolId() public { 81 | assertTrue( 82 | yieldVP.poolAddress() != address(0), 83 | "Yield pool address should be valid" 84 | ); 85 | } 86 | 87 | function test_check_maturity() public { 88 | assertEq(yieldVP.maturity(), _maturity, "Invalid maturity date"); 89 | } 90 | 91 | function test_check_timeScale() public { 92 | assertEq(yieldVP.timeScale(), _timeScale, "Invalid time scale value"); 93 | } 94 | 95 | function test_check_cumulativeBalanceRatioLast() public { 96 | assertEq( 97 | yieldVP.cumulativeBalanceRatioLast(), 98 | uconvert(_cumulativeBalancesRatio, 27, 18), 99 | "Invalid cumulativeBalanceRatioLast" 100 | ); 101 | } 102 | 103 | function test_check_blockTimestampLast() public { 104 | assertEq( 105 | yieldVP.blockTimestampLast(), 106 | _blockTime, 107 | "Invalid blockTimestampLast" 108 | ); 109 | } 110 | 111 | function test_getValue() public { 112 | // Values take from contract 113 | // https://etherscan.io/token/0x3771c99c087a81df4633b50d8b149afaa83e3c9e 114 | // at block 13800244 115 | int256 expectedValue = 1204540138; 116 | setMockValues( 117 | mockValueProvider, 118 | 7342183639948751441026554744281105, 119 | 1640937617 120 | ); 121 | int256 value = yieldVP.getValue(); 122 | assertTrue(value == expectedValue); 123 | } 124 | 125 | function testFail_getValue_revertsOnOrAfterMaturityDate() public { 126 | hevm.warp(yieldVP.maturity()); 127 | yieldVP.getValue(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/oracle_implementations/discount_rate/utils/Convert.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | contract Convert { 5 | function convert( 6 | int256 x_, 7 | uint256 currentPrecision_, 8 | uint256 targetPrecision_ 9 | ) internal pure returns (int256) { 10 | if (targetPrecision_ > currentPrecision_) 11 | return x_ * int256(10**(targetPrecision_ - currentPrecision_)); 12 | 13 | return x_ / int256(10**(currentPrecision_ - targetPrecision_)); 14 | } 15 | 16 | function uconvert( 17 | uint256 x_, 18 | uint256 currentPrecision_, 19 | uint256 targetPrecision_ 20 | ) internal pure returns (uint256) { 21 | if (targetPrecision_ > currentPrecision_) 22 | return x_ * 10**(targetPrecision_ - currentPrecision_); 23 | 24 | return x_ / 10**(currentPrecision_ - targetPrecision_); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/oracle_implementations/spot_price/Chainlink/ChainlinkAggregatorV3Interface.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | // Chainlink Aggregator v3 interface 5 | // https://github.com/smartcontractkit/chainlink/blob/6fea3ccd275466e082a22be690dbaf1609f19dce/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol 6 | interface IChainlinkAggregatorV3Interface { 7 | function decimals() external view returns (uint8); 8 | 9 | function description() external view returns (string memory); 10 | 11 | function version() external view returns (uint256); 12 | 13 | // getRoundData and latestRoundData should both raise "No data present" 14 | // if they do not have data to report, instead of returning unset values 15 | // which could be misinterpreted as actual reported values. 16 | function getRoundData(uint80 _roundId) 17 | external 18 | view 19 | returns ( 20 | uint80 roundId, 21 | int256 answer, 22 | uint256 startedAt, 23 | uint256 updatedAt, 24 | uint80 answeredInRound 25 | ); 26 | 27 | function latestRoundData() 28 | external 29 | view 30 | returns ( 31 | uint80 roundId, 32 | int256 answer, 33 | uint256 startedAt, 34 | uint256 updatedAt, 35 | uint80 answeredInRound 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/oracle_implementations/spot_price/Chainlink/ChainlinkValueProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {Oracle} from "../../../oracle/Oracle.sol"; 5 | import {Convert} from "../../discount_rate/utils/Convert.sol"; 6 | import {IChainlinkAggregatorV3Interface} from "./ChainlinkAggregatorV3Interface.sol"; 7 | 8 | contract ChainlinkValueProvider is Oracle, Convert { 9 | uint8 public immutable underlierDecimals; 10 | address public chainlinkAggregatorAddress; 11 | 12 | /// @notice Constructs the Value provider contracts with the needed Chainlink. 13 | /// @param timeUpdateWindow_ Minimum time between updates of the value 14 | /// @param chainlinkAggregatorAddress_ Address of the deployed chainlink aggregator contract. 15 | constructor( 16 | // Oracle parameters 17 | uint256 timeUpdateWindow_, 18 | // Chainlink specific parameter 19 | address chainlinkAggregatorAddress_ 20 | ) Oracle(timeUpdateWindow_) { 21 | chainlinkAggregatorAddress = chainlinkAggregatorAddress_; 22 | underlierDecimals = IChainlinkAggregatorV3Interface( 23 | chainlinkAggregatorAddress_ 24 | ).decimals(); 25 | } 26 | 27 | /// @notice Retrieves the price from the chainlink aggregator 28 | /// @return result The result as an signed 59.18-decimal fixed-point number. 29 | function getValue() external view override(Oracle) returns (int256) { 30 | // Convert the annual rate to 1e18 precision. 31 | (, int256 answer, , , ) = IChainlinkAggregatorV3Interface( 32 | chainlinkAggregatorAddress 33 | ).latestRoundData(); 34 | 35 | return convert(answer, underlierDecimals, 18); 36 | } 37 | 38 | /// @notice returns the description of the chainlink aggregator the proxy points to. 39 | function description() external view returns (string memory) { 40 | return 41 | IChainlinkAggregatorV3Interface(chainlinkAggregatorAddress) 42 | .description(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/oracle_implementations/spot_price/Chainlink/ChainlinkValueProvider.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import "ds-test/test.sol"; 5 | 6 | import "../../../test/utils/Caller.sol"; 7 | import {Hevm} from "../../../test/utils/Hevm.sol"; 8 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 9 | 10 | import {IChainlinkAggregatorV3Interface} from "./ChainlinkAggregatorV3Interface.sol"; 11 | import {ChainlinkValueProvider} from "./ChainlinkValueProvider.sol"; 12 | 13 | contract ChainlinkValueProviderTest is DSTest { 14 | Hevm internal hevm = Hevm(DSTest.HEVM_ADDRESS); 15 | 16 | MockProvider internal mockChainlinkAggregator; 17 | 18 | ChainlinkValueProvider internal chainlinkVP; 19 | 20 | uint256 private _timeUpdateWindow = 100; // seconds 21 | 22 | function setUp() public { 23 | mockChainlinkAggregator = new MockProvider(); 24 | 25 | // Values taken from https://etherscan.io/address/0x8fffffd4afb6115b954bd326cbe7b4ba576818f6 26 | // At block: 14041337 27 | mockChainlinkAggregator.givenQueryReturnResponse( 28 | abi.encodeWithSelector( 29 | IChainlinkAggregatorV3Interface.latestRoundData.selector 30 | ), 31 | MockProvider.ReturnData({ 32 | success: true, 33 | data: abi.encode( 34 | uint80(36893488147419103548), // roundId 35 | int256(100016965), // answer 36 | uint256(1642615905), // startedAt 37 | uint256(1642615905), // updatedAt 38 | uint80(36893488147419103548) // answeredInRound 39 | ) 40 | }), 41 | false 42 | ); 43 | 44 | mockChainlinkAggregator.givenQueryReturnResponse( 45 | abi.encodeWithSelector( 46 | IChainlinkAggregatorV3Interface.decimals.selector 47 | ), 48 | MockProvider.ReturnData({success: true, data: abi.encode(8)}), 49 | false 50 | ); 51 | 52 | mockChainlinkAggregator.givenQueryReturnResponse( 53 | abi.encodeWithSelector( 54 | IChainlinkAggregatorV3Interface.description.selector 55 | ), 56 | MockProvider.ReturnData({ 57 | success: true, 58 | data: abi.encode("USDC / USD") 59 | }), 60 | false 61 | ); 62 | 63 | chainlinkVP = new ChainlinkValueProvider( 64 | // Oracle arguments 65 | // Time update window 66 | _timeUpdateWindow, 67 | // Chainlink arguments 68 | address(mockChainlinkAggregator) 69 | ); 70 | } 71 | 72 | function test_deploy() public { 73 | assertTrue(address(chainlinkVP) != address(0)); 74 | } 75 | 76 | function test_getValue() public { 77 | // Expected value is the value sent by the mock provider in 10**18 precision 78 | int256 expectedValue = 100016965 * 1e10; 79 | // Computed value based on the parameters that are sent via the mock provider 80 | int256 value = chainlinkVP.getValue(); 81 | 82 | assertTrue(value == expectedValue); 83 | } 84 | 85 | function test_description() public { 86 | string memory expectedDescription = "USDC / USD"; 87 | string memory desc = chainlinkVP.description(); 88 | assertTrue( 89 | keccak256(abi.encodePacked(desc)) == 90 | keccak256(abi.encodePacked(expectedDescription)) 91 | ); 92 | } 93 | 94 | function test_check_underlierDecimals() public { 95 | assertEq(chainlinkVP.underlierDecimals(), uint256(8)); 96 | } 97 | 98 | function test_check_chainlinkAggregatorAddress() public { 99 | assertEq( 100 | chainlinkVP.chainlinkAggregatorAddress(), 101 | address(mockChainlinkAggregator) 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/oracle_implementations/spot_price/Chainlink/LUSD3CRV/ICurvePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /// @notice Lightweight interface used to interrogate Curve pools 5 | interface ICurvePool { 6 | function get_virtual_price() external view returns (uint256); 7 | } 8 | -------------------------------------------------------------------------------- /src/oracle_implementations/spot_price/Chainlink/LUSD3CRV/LUSD3CRVValueProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | import {Oracle} from "../../../../oracle/Oracle.sol"; 5 | import {Convert} from "../../../discount_rate/utils/Convert.sol"; 6 | import {IChainlinkAggregatorV3Interface} from "../ChainlinkAggregatorV3Interface.sol"; 7 | import {ICurvePool} from "./ICurvePool.sol"; 8 | import "prb-math/contracts/PRBMathSD59x18.sol"; 9 | 10 | /// @notice Oracle implementation for Curve LP tokens via Chainlink Oracles 11 | /// as described in this guide: https://news.curve.fi/chainlink-oracles-and-curve-pools/ 12 | contract LUSD3CRVValueProvider is Oracle, Convert { 13 | /// @notice Emitted when a pool with unsupported decimals is used 14 | error LUSD3CRVValueProvider__constructor_InvalidPoolDecimals(address pool); 15 | 16 | uint256 public immutable decimalsUSDC; 17 | uint256 public immutable decimalsDAI; 18 | uint256 public immutable decimalsUSDT; 19 | uint256 public immutable decimalsLUSD; 20 | 21 | // DAI/USDC/USDT pool 22 | address public immutable curve3Pool; 23 | address public immutable curveLUSD3Pool; 24 | 25 | address public immutable chainlinkLUSD; 26 | address public immutable chainlinkUSDC; 27 | address public immutable chainlinkDAI; 28 | address public immutable chainlinkUSDT; 29 | 30 | /// @notice Constructs the Value provider contracts with the needed Chainlink data feeds 31 | /// @param timeUpdateWindow_ Minimum time between updates of the value 32 | /// @param curve3Pool_ Address of the Curve 3pool 33 | /// @param curve3PoolLpToken_ Address of the lp token for the Curve 3pool 34 | /// @param curveLUSD3Pool_ Address of the Curve LUSD-3pool pool 35 | /// @param chainlinkLUSD_ Address of the LUSD chainlink data feed 36 | /// @param chainlinkUSDC_ Address of the USDC chainlink data feed 37 | /// @param chainlinkDAI_ Address of the DAI chainlink data feed 38 | /// @param chainlinkUSDT_ Address of the USDT chainlink data feed 39 | constructor( 40 | // Oracle parameters 41 | uint256 timeUpdateWindow_, 42 | // Chainlink specific parameters 43 | address curve3Pool_, 44 | address curve3PoolLpToken_, 45 | address curveLUSD3Pool_, 46 | address chainlinkLUSD_, 47 | address chainlinkUSDC_, 48 | address chainlinkDAI_, 49 | address chainlinkUSDT_ 50 | ) Oracle(timeUpdateWindow_) { 51 | if (ERC20(curve3PoolLpToken_).decimals() != 18) { 52 | revert LUSD3CRVValueProvider__constructor_InvalidPoolDecimals( 53 | curve3Pool_ 54 | ); 55 | } 56 | // Init the Curve 3pool 57 | curve3Pool = curve3Pool_; 58 | 59 | if (ERC20(curveLUSD3Pool_).decimals() != 18) { 60 | revert LUSD3CRVValueProvider__constructor_InvalidPoolDecimals( 61 | curveLUSD3Pool_ 62 | ); 63 | } 64 | // Init the Curve LUSD-3pool 65 | curveLUSD3Pool = curveLUSD3Pool_; 66 | 67 | // Init USDC chainlink properties 68 | chainlinkUSDC = chainlinkUSDC_; 69 | decimalsUSDC = IChainlinkAggregatorV3Interface(chainlinkUSDC_) 70 | .decimals(); 71 | 72 | // Init LUSD chainlink properties 73 | chainlinkLUSD = chainlinkLUSD_; 74 | decimalsLUSD = IChainlinkAggregatorV3Interface(chainlinkLUSD_) 75 | .decimals(); 76 | 77 | // Init DAI chainlink properties 78 | chainlinkDAI = chainlinkDAI_; 79 | decimalsDAI = IChainlinkAggregatorV3Interface(chainlinkDAI_).decimals(); 80 | 81 | // Init USDT chainlink properties 82 | chainlinkUSDT = chainlinkUSDT_; 83 | decimalsUSDT = IChainlinkAggregatorV3Interface(chainlinkUSDT_) 84 | .decimals(); 85 | } 86 | 87 | /// @notice The price is calculated following the steps described in this document 88 | /// https://news.curve.fi/chainlink-oracles-and-curve-pools/ 89 | /// @return result The result as an signed 59.18-decimal fixed-point number 90 | function getValue() external view override(Oracle) returns (int256) { 91 | // Get the USDC price and convert it to 59.18-decimal fixed-point format 92 | (, int256 usdcPrice, , , ) = IChainlinkAggregatorV3Interface( 93 | chainlinkUSDC 94 | ).latestRoundData(); 95 | 96 | // Minimum token prices needed in the formula 97 | int256 minTokenPrice59x18; 98 | 99 | // Init min price with the first token price 100 | minTokenPrice59x18 = convert(usdcPrice, decimalsUSDC, 18); 101 | 102 | // Get the DAI price and convert it to 59.18-decimal fixed-point format 103 | (, int256 daiPrice, , , ) = IChainlinkAggregatorV3Interface( 104 | chainlinkDAI 105 | ).latestRoundData(); 106 | // Update the min price as we fetch data 107 | minTokenPrice59x18 = min( 108 | minTokenPrice59x18, 109 | convert(daiPrice, decimalsDAI, 18) 110 | ); 111 | 112 | // Get the USDT price and convert it to 59.18-decimal fixed-point format 113 | (, int256 usdtPrice, , , ) = IChainlinkAggregatorV3Interface( 114 | chainlinkUSDT 115 | ).latestRoundData(); 116 | // Update the min price as we fetch data 117 | minTokenPrice59x18 = min( 118 | minTokenPrice59x18, 119 | convert(usdtPrice, decimalsUSDT, 18) 120 | ); 121 | 122 | // Calculate the price the Curve 3pool lpToken 123 | int256 curve3lpTokenPrice59x18 = PRBMathSD59x18.mul( 124 | int256(ICurvePool(curve3Pool).get_virtual_price()), 125 | minTokenPrice59x18 126 | ); 127 | 128 | // Get the LUSD price and convert it to 59.18-decimal fixed-point format 129 | (, int256 lusdPrice, , , ) = IChainlinkAggregatorV3Interface( 130 | chainlinkLUSD 131 | ).latestRoundData(); 132 | int256 lusd59x18 = convert(lusdPrice, decimalsLUSD, 18); 133 | 134 | // Compute the final price for the Curve LUSD-3pool 135 | return 136 | PRBMathSD59x18.mul( 137 | int256(ICurvePool(curveLUSD3Pool).get_virtual_price()), 138 | min(curve3lpTokenPrice59x18, lusd59x18) 139 | ); 140 | } 141 | 142 | /// @notice Returns the description of the value provider. 143 | function description() external pure returns (string memory) { 144 | return "LUSD3CRV"; 145 | } 146 | 147 | /// @notice Helper math min function 148 | function min(int256 a, int256 b) internal pure returns (int256) { 149 | return a < b ? a : b; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/oracle_implementations/spot_price/Chainlink/LUSD3CRV/LUSD3CRVValueProvider.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "ds-test/test.sol"; 6 | import "../../../../test/utils/Caller.sol"; 7 | import {CheatCodes} from "../../../../test/utils/CheatCodes.sol"; 8 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 9 | 10 | import {IChainlinkAggregatorV3Interface} from "../ChainlinkAggregatorV3Interface.sol"; 11 | import {LUSD3CRVValueProvider} from "./LUSD3CRVValueProvider.sol"; 12 | import {ICurvePool} from "./ICurvePool.sol"; 13 | 14 | contract LUSD3CRVValueProviderTest is DSTest { 15 | CheatCodes internal cheatCodes = CheatCodes(HEVM_ADDRESS); 16 | 17 | MockProvider internal curve3PoolMock; 18 | MockProvider internal curve3PoolTokenMock; 19 | MockProvider internal curveLUSD3CRVPoolMock; 20 | 21 | MockProvider internal mockChainlinkUSDC; 22 | MockProvider internal mockChainlinkDAI; 23 | MockProvider internal mockChainlinkUSDT; 24 | MockProvider internal mockChainlinkLUSD; 25 | 26 | int256 private _curve3poolVirtualPrice = 1020628557740573240; 27 | int256 private _lusd3PoolVirtualPrice = 1012508837937838125; 28 | 29 | int256 private _lusdPrice = 100102662; 30 | int256 private _usdcPrice = 100000298; 31 | int256 private _daiPrice = 100100000; 32 | int256 private _usdtPrice = 100030972; 33 | 34 | LUSD3CRVValueProvider internal lusd3ValueProvider; 35 | 36 | function initChainlinkMockProvider( 37 | MockProvider chainlinkMock_, 38 | int256 value_, 39 | uint256 decimals_ 40 | ) private { 41 | chainlinkMock_.givenQueryReturnResponse( 42 | abi.encodeWithSelector( 43 | IChainlinkAggregatorV3Interface.latestRoundData.selector 44 | ), 45 | MockProvider.ReturnData({ 46 | success: true, 47 | data: abi.encode( 48 | uint80(36893488147419103548), // roundId 49 | value_, 50 | uint256(1642615905), // startedAt 51 | uint256(1642615905), // updatedAt 52 | uint80(36893488147419103548) // answeredInRound 53 | ) 54 | }), 55 | false 56 | ); 57 | chainlinkMock_.givenQueryReturnResponse( 58 | abi.encodeWithSelector( 59 | IChainlinkAggregatorV3Interface.decimals.selector 60 | ), 61 | MockProvider.ReturnData({ 62 | success: true, 63 | data: abi.encode(decimals_) 64 | }), 65 | false 66 | ); 67 | } 68 | 69 | function setUp() public { 70 | curve3PoolMock = new MockProvider(); 71 | curve3PoolMock.givenQueryReturnResponse( 72 | abi.encodeWithSelector(ICurvePool.get_virtual_price.selector), 73 | MockProvider.ReturnData({ 74 | success: true, 75 | data: abi.encode(_curve3poolVirtualPrice) 76 | }), 77 | false 78 | ); 79 | curve3PoolMock.givenQueryReturnResponse( 80 | abi.encodeWithSelector(ERC20.decimals.selector), 81 | MockProvider.ReturnData({success: true, data: abi.encode(18)}), 82 | false 83 | ); 84 | 85 | curve3PoolTokenMock = new MockProvider(); 86 | curve3PoolTokenMock.givenQueryReturnResponse( 87 | abi.encodeWithSelector(ERC20.decimals.selector), 88 | MockProvider.ReturnData({success: true, data: abi.encode(18)}), 89 | false 90 | ); 91 | 92 | curveLUSD3CRVPoolMock = new MockProvider(); 93 | curveLUSD3CRVPoolMock.givenQueryReturnResponse( 94 | abi.encodeWithSelector(ICurvePool.get_virtual_price.selector), 95 | MockProvider.ReturnData({ 96 | success: true, 97 | data: abi.encode(_lusd3PoolVirtualPrice) 98 | }), 99 | false 100 | ); 101 | curveLUSD3CRVPoolMock.givenQueryReturnResponse( 102 | abi.encodeWithSelector(ERC20.decimals.selector), 103 | MockProvider.ReturnData({success: true, data: abi.encode(18)}), 104 | false 105 | ); 106 | 107 | mockChainlinkLUSD = new MockProvider(); 108 | initChainlinkMockProvider(mockChainlinkLUSD, _lusdPrice, 8); 109 | 110 | mockChainlinkUSDC = new MockProvider(); 111 | initChainlinkMockProvider(mockChainlinkUSDC, _usdcPrice, 8); 112 | 113 | mockChainlinkDAI = new MockProvider(); 114 | initChainlinkMockProvider(mockChainlinkDAI, _daiPrice, 8); 115 | 116 | mockChainlinkUSDT = new MockProvider(); 117 | initChainlinkMockProvider(mockChainlinkUSDT, _usdtPrice, 8); 118 | 119 | lusd3ValueProvider = new LUSD3CRVValueProvider( 120 | // Oracle arguments 121 | // Time update window 122 | 100, 123 | // Chainlink arguments 124 | address(curve3PoolMock), 125 | address(curve3PoolTokenMock), 126 | address(curveLUSD3CRVPoolMock), 127 | address(mockChainlinkLUSD), 128 | address(mockChainlinkUSDC), 129 | address(mockChainlinkDAI), 130 | address(mockChainlinkUSDT) 131 | ); 132 | } 133 | 134 | function test_deploy() public { 135 | assertTrue(address(lusd3ValueProvider) != address(0)); 136 | } 137 | 138 | function test_token_decimals() public { 139 | assertTrue( 140 | lusd3ValueProvider.decimalsLUSD() == 8, 141 | "Invalid LUSD Decimals" 142 | ); 143 | assertTrue( 144 | lusd3ValueProvider.decimalsUSDC() == 8, 145 | "Invalid USDC Decimals" 146 | ); 147 | assertTrue( 148 | lusd3ValueProvider.decimalsDAI() == 8, 149 | "Invalid DAI Decimals" 150 | ); 151 | assertTrue( 152 | lusd3ValueProvider.decimalsUSDT() == 8, 153 | "Invalid USDT Decimals" 154 | ); 155 | } 156 | 157 | function test_curve_pools() public { 158 | assertEq( 159 | lusd3ValueProvider.curve3Pool(), 160 | address(curve3PoolMock), 161 | "Invalid Curve3Pool" 162 | ); 163 | assertEq( 164 | lusd3ValueProvider.curveLUSD3Pool(), 165 | address(curveLUSD3CRVPoolMock), 166 | "Invalid LUSD3CRV Curve Pool" 167 | ); 168 | } 169 | 170 | function test_chainlink_feeds() public { 171 | assertEq( 172 | lusd3ValueProvider.chainlinkLUSD(), 173 | address(mockChainlinkLUSD), 174 | "Invalid LUSD Chainlink Feed" 175 | ); 176 | assertEq( 177 | lusd3ValueProvider.chainlinkUSDC(), 178 | address(mockChainlinkUSDC), 179 | "Invalid USDC Chainlink Feed" 180 | ); 181 | assertEq( 182 | lusd3ValueProvider.chainlinkDAI(), 183 | address(mockChainlinkDAI), 184 | "Invalid DAI Chainlink Feed" 185 | ); 186 | assertEq( 187 | lusd3ValueProvider.chainlinkUSDT(), 188 | address(mockChainlinkUSDT), 189 | "Invalid USDT Chainlink Feed" 190 | ); 191 | } 192 | 193 | function test_getValue() public { 194 | // Expected value is the value sent by the mock provider in 10**18 precision 195 | // The expect value was computed with this formula: 196 | // solhint-disable-next-line 197 | // https://www.wolframalpha.com/input?i2d=true&i=%5C%2840%29Divide%5B1012508837937838125%2CPower%5B10%2C18%5D%5D%5C%2841%29+*+minimum%5C%2840%29Divide%5B100102662%2CPower%5B10%2C8%5D%5D%5C%2844%29%5C%2840%29+Divide%5B1020628557740573240%2CPower%5B10%2C18%5D%5D+*+Divide%5Bminimum%5C%2840%29100030972%5C%2844%29100100000%5C%2844%29100000298%5C%2841%29%2CPower%5B10%2C8%5D%5D%5C%2841%29%5C%2841%29 198 | int256 expectedValue = 1013548299761041868; 199 | // Computed value based on the parameters that are sent via the mock provider 200 | int256 value = lusd3ValueProvider.getValue(); 201 | 202 | assertTrue(value == expectedValue); 203 | } 204 | 205 | function test_description() public { 206 | string memory expectedDescription = "LUSD3CRV"; 207 | string memory desc = lusd3ValueProvider.description(); 208 | assertTrue( 209 | keccak256(abi.encodePacked(desc)) == 210 | keccak256(abi.encodePacked(expectedDescription)) 211 | ); 212 | } 213 | 214 | function test_deploy_shouldRevertWithInvalidCurve3TokenDecimals() public { 215 | // Update the mock to return an unsupported decimal number 216 | curve3PoolTokenMock.givenQueryReturnResponse( 217 | abi.encodeWithSelector(ERC20.decimals.selector), 218 | MockProvider.ReturnData({success: true, data: abi.encode(1)}), 219 | false 220 | ); 221 | 222 | // Set the expected error for the revert 223 | cheatCodes.expectRevert( 224 | abi.encodeWithSelector( 225 | LUSD3CRVValueProvider 226 | .LUSD3CRVValueProvider__constructor_InvalidPoolDecimals 227 | .selector, 228 | address(curve3PoolMock) 229 | ) 230 | ); 231 | 232 | new LUSD3CRVValueProvider( 233 | // Oracle arguments 234 | // Time update window 235 | 100, 236 | // Chainlink arguments 237 | address(curve3PoolMock), 238 | address(curve3PoolTokenMock), 239 | address(curveLUSD3CRVPoolMock), 240 | address(mockChainlinkLUSD), 241 | address(mockChainlinkUSDC), 242 | address(mockChainlinkDAI), 243 | address(mockChainlinkUSDT) 244 | ); 245 | } 246 | 247 | function test_deploy_shouldRevertWithInvalidLUSDPoolDecimals() public { 248 | // Update the mock to return an unsupported decimal number 249 | curveLUSD3CRVPoolMock.givenQueryReturnResponse( 250 | abi.encodeWithSelector(ERC20.decimals.selector), 251 | MockProvider.ReturnData({success: true, data: abi.encode(1)}), 252 | false 253 | ); 254 | 255 | // Set the expected error for the revert 256 | cheatCodes.expectRevert( 257 | abi.encodeWithSelector( 258 | LUSD3CRVValueProvider 259 | .LUSD3CRVValueProvider__constructor_InvalidPoolDecimals 260 | .selector, 261 | address(curveLUSD3CRVPoolMock) 262 | ) 263 | ); 264 | 265 | new LUSD3CRVValueProvider( 266 | // Oracle arguments 267 | // Time update window 268 | 100, 269 | // Chainlink arguments 270 | address(curve3PoolMock), 271 | address(curve3PoolTokenMock), 272 | address(curveLUSD3CRVPoolMock), 273 | address(mockChainlinkLUSD), 274 | address(mockChainlinkUSDC), 275 | address(mockChainlinkDAI), 276 | address(mockChainlinkUSDT) 277 | ); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/pausable/Pausable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | /// @notice Emitted when paused 5 | error Pausable__whenNotPaused_paused(); 6 | 7 | /// @notice Emitted when not paused 8 | error Pausable__whenPaused_notPaused(); 9 | 10 | import {Guarded} from "../guarded/Guarded.sol"; 11 | 12 | contract Pausable is Guarded { 13 | event Paused(address who); 14 | event Unpaused(address who); 15 | 16 | bool private _paused; 17 | 18 | function paused() public view virtual returns (bool) { 19 | return _paused; 20 | } 21 | 22 | modifier whenNotPaused() { 23 | // If the contract is paused, throw an error 24 | if (_paused) { 25 | revert Pausable__whenNotPaused_paused(); 26 | } 27 | _; 28 | } 29 | 30 | modifier whenPaused() { 31 | // If the contract is not paused, throw an error 32 | if (_paused == false) { 33 | revert Pausable__whenPaused_notPaused(); 34 | } 35 | _; 36 | } 37 | 38 | function _pause() internal whenNotPaused { 39 | _paused = true; 40 | emit Paused(msg.sender); 41 | } 42 | 43 | function _unpause() internal whenPaused { 44 | _paused = false; 45 | emit Unpaused(msg.sender); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pausable/Pausable.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {DSTest} from "ds-test/test.sol"; 5 | import {Caller} from "../test/utils/Caller.sol"; 6 | import {Pausable} from "../pausable/Pausable.sol"; 7 | 8 | contract PausableInstance is Pausable { 9 | // This is set to true if execution was successful. 10 | // Use to check modifier execution. 11 | bool public executedSuccessfully; 12 | 13 | function pause() public { 14 | _pause(); 15 | } 16 | 17 | function unpause() public { 18 | _unpause(); 19 | } 20 | 21 | function check_whenNotPaused() public whenNotPaused { 22 | executedSuccessfully = true; 23 | } 24 | 25 | function check_whenPaused() public whenPaused { 26 | executedSuccessfully = true; 27 | } 28 | } 29 | 30 | contract PausableTest is DSTest { 31 | PausableInstance internal pausable; 32 | 33 | function setUp() public { 34 | pausable = new PausableInstance(); 35 | } 36 | 37 | function test_pause_unpause() public { 38 | pausable.pause(); 39 | assertTrue(pausable.paused() == true, "paused() should be true"); 40 | 41 | pausable.unpause(); 42 | assertTrue(pausable.paused() == false, "paused() should be false"); 43 | } 44 | 45 | function test_whenPaused_success_when_paused() public { 46 | pausable.pause(); 47 | pausable.check_whenPaused(); 48 | assertTrue(pausable.executedSuccessfully() == true); 49 | } 50 | 51 | function testFail_whenPaused_fails_when_notPaused() public { 52 | // Starts as unpaused 53 | pausable.check_whenPaused(); 54 | } 55 | 56 | function test_whenNotPaused_success_when_notPaused() public { 57 | // Starts as unpaused 58 | pausable.check_whenNotPaused(); 59 | assertTrue(pausable.executedSuccessfully() == true); 60 | } 61 | 62 | function testFail_whenNotPaused_fails_when_paused() public { 63 | pausable.pause(); 64 | pausable.check_whenNotPaused(); 65 | } 66 | 67 | function test_authorizedUser_canPauseUnpause() public { 68 | // Create a user 69 | Caller user = new Caller(); 70 | 71 | // Grant ability to pause/unpause to user 72 | pausable.allowCaller(pausable.pause.selector, address(user)); 73 | pausable.allowCaller(pausable.unpause.selector, address(user)); 74 | 75 | // Should be be able to pause 76 | user.externalCall( 77 | address(pausable), 78 | abi.encodeWithSelector(pausable.pause.selector) 79 | ); 80 | 81 | // Contract is paused 82 | assertTrue(pausable.paused() == true, "paused() should be true"); 83 | 84 | // Should be able to unpause 85 | user.externalCall( 86 | address(pausable), 87 | abi.encodeWithSelector(pausable.unpause.selector) 88 | ); 89 | 90 | // Contract is unpaused 91 | assertTrue(pausable.paused() == false, "paused() should be false"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/relayer/ICollybus.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | // Lightweight interface for Collybus 5 | // Source: https://github.com/fiatdao/fiat-lux/blob/f49a9457fbcbdac1969c35b4714722f00caa462c/src/interfaces/ICollybus.sol 6 | interface ICollybus { 7 | function updateDiscountRate(uint256 tokenId_, uint256 rate_) external; 8 | 9 | function updateSpot(address token_, uint256 spot_) external; 10 | } 11 | -------------------------------------------------------------------------------- /src/relayer/IRelayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | interface IRelayer { 5 | enum RelayerType { 6 | DiscountRate, 7 | SpotPrice, 8 | COUNT 9 | } 10 | 11 | function execute() external returns (bool); 12 | 13 | function executeWithRevert() external; 14 | } 15 | -------------------------------------------------------------------------------- /src/relayer/Relayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {IRelayer} from "./IRelayer.sol"; 5 | import {IOracle} from "../oracle/IOracle.sol"; 6 | import {ICollybus} from "./ICollybus.sol"; 7 | import {Guarded} from "../guarded/Guarded.sol"; 8 | 9 | /// @notice The Relayer contract manages the relationship between an oracle and Collybus. 10 | /// The Relayer manages an Oracle for which it controls the update flow and via execute() calls 11 | /// pushes data to Collybus when it's needed 12 | /// @dev The Relayer should be the single entity that updates the oracle so that the Relayer and the Oracle 13 | /// are value synched. The same is true for the Relayer-Collybus relationship as we do not interrogate the Collybus 14 | /// for the current value and use a storage cached last updated value. 15 | contract Relayer is Guarded, IRelayer { 16 | /// @notice Emitter during executeWithRevert() if the oracle is not updated successfully 17 | error Relayer__executeWithRevert_noUpdate(RelayerType relayerType); 18 | 19 | /// @notice Emitted when trying to set a parameter that does not exist 20 | error Relayer__setParam_unrecognizedParam(bytes32 param); 21 | 22 | event SetParam(bytes32 param, uint256 value); 23 | event UpdateOracle(address oracle, int256 value, bool valid); 24 | event UpdatedCollybus(bytes32 tokenId, uint256 rate, RelayerType); 25 | 26 | /// ======== Storage ======== /// 27 | 28 | address public immutable collybus; 29 | RelayerType public immutable relayerType; 30 | address public immutable oracle; 31 | bytes32 public immutable encodedTokenId; 32 | 33 | uint256 public minimumPercentageDeltaValue; 34 | int256 private _lastUpdateValue; 35 | 36 | /// @param collybusAddress_ Address of the collybus 37 | /// @param type_ Relayer type, DiscountRate or SpotPrice 38 | /// @param oracleAddress_ The address of the oracle used by the Relayer 39 | /// @param encodedTokenId_ Encoded token Id that will be used to push values to Collybus 40 | /// uint256 for discount rate, address for spot price 41 | /// @param minimumPercentageDeltaValue_ Minimum delta value used to determine when to 42 | /// push data to Collybus 43 | constructor( 44 | address collybusAddress_, 45 | RelayerType type_, 46 | address oracleAddress_, 47 | bytes32 encodedTokenId_, 48 | uint256 minimumPercentageDeltaValue_ 49 | ) { 50 | collybus = collybusAddress_; 51 | relayerType = type_; 52 | oracle = oracleAddress_; 53 | encodedTokenId = encodedTokenId_; 54 | minimumPercentageDeltaValue = minimumPercentageDeltaValue_; 55 | _lastUpdateValue = 0; 56 | } 57 | 58 | /// @notice Sets a Relayer parameter 59 | /// Supported parameters are: 60 | /// - minimumPercentageDeltaValue 61 | /// @param param_ The identifier of the parameter that should be updated 62 | /// @param value_ The new value 63 | /// @dev Reverts if parameter is not found 64 | function setParam(bytes32 param_, uint256 value_) public checkCaller { 65 | if (param_ == "minimumPercentageDeltaValue") { 66 | minimumPercentageDeltaValue = value_; 67 | } else revert Relayer__setParam_unrecognizedParam(param_); 68 | 69 | emit SetParam(param_, value_); 70 | } 71 | 72 | /// @notice Updates the oracle and pushes the updated data to Collybus if the 73 | /// delta change in value is larger than the minimum threshold value 74 | /// @return Whether the Collybus was or is about to be updated 75 | /// @dev Return value is mainly meant to be used by Keepers in order to optimize costs 76 | function execute() public override(IRelayer) returns (bool) { 77 | // We always update the oracles before retrieving the rates 78 | bool oracleUpdated = IOracle(oracle).update(); 79 | (int256 oracleValue, ) = IOracle(oracle).value(); 80 | 81 | // If the oracle was not updated we exit early because no value was updated 82 | if (!oracleUpdated) return false; 83 | 84 | // We calculate whether the returned value is over the minimumPercentageDeltaValue 85 | // If the deviation is large enough we push the value to Collybus and return true 86 | bool currentValueThresholdPassed = checkDeviation( 87 | _lastUpdateValue, 88 | oracleValue, 89 | minimumPercentageDeltaValue 90 | ); 91 | if (currentValueThresholdPassed) { 92 | updateCollybus(oracleValue); 93 | return true; 94 | } 95 | 96 | // If the oracle received a new value (nextValue != oracleValue), but wasn't updated yet, 97 | // we return true to indicate that the Collybus is about to be updated 98 | bool nextValueThresholdPassed = checkDeviation( 99 | _lastUpdateValue, 100 | IOracle(oracle).nextValue(), 101 | minimumPercentageDeltaValue 102 | ); 103 | 104 | return nextValueThresholdPassed; 105 | } 106 | 107 | /// @notice The function will call execute() and will revert if false 108 | /// @dev This method is needed for services that run on each block and only call the method if it doesn't fail 109 | /// For extra information on the revert conditions check the execute() function 110 | function executeWithRevert() public override(IRelayer) { 111 | if (!execute()) { 112 | revert Relayer__executeWithRevert_noUpdate(relayerType); 113 | } 114 | } 115 | 116 | /// @notice Updates Collybus with the new value 117 | /// @param oracleValue_ the new value pushed into Collybus 118 | function updateCollybus(int256 oracleValue_) internal { 119 | // Save the new value to be able to check if the next value is over the threshold 120 | _lastUpdateValue = oracleValue_; 121 | 122 | if (relayerType == RelayerType.DiscountRate) { 123 | ICollybus(collybus).updateDiscountRate( 124 | uint256(encodedTokenId), 125 | uint256(oracleValue_) 126 | ); 127 | } else if (relayerType == RelayerType.SpotPrice) { 128 | ICollybus(collybus).updateSpot( 129 | address(uint160(uint256(encodedTokenId))), 130 | uint256(oracleValue_) 131 | ); 132 | } 133 | 134 | emit UpdatedCollybus( 135 | encodedTokenId, 136 | uint256(oracleValue_), 137 | relayerType 138 | ); 139 | } 140 | 141 | /// @notice Returns true if the percentage difference between the two values is larger than the percentage 142 | /// @param baseValue_ The value that the percentage is based on 143 | /// @param newValue_ The new value 144 | /// @param percentage_ The percentage threshold value (100% = 100_00, 50% = 50_00, etc) 145 | function checkDeviation( 146 | int256 baseValue_, 147 | int256 newValue_, 148 | uint256 percentage_ 149 | ) public pure returns (bool) { 150 | int256 deviation = (baseValue_ * int256(percentage_)) / 100_00; 151 | 152 | if ( 153 | baseValue_ + deviation <= newValue_ || 154 | baseValue_ - deviation >= newValue_ 155 | ) return true; 156 | 157 | return false; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/relayer/Relayer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {Hevm} from "../test/utils/Hevm.sol"; 5 | import {DSTest} from "ds-test/test.sol"; 6 | import {MockProvider} from "@cleanunicorn/mockprovider/src/MockProvider.sol"; 7 | import {Caller} from "../test/utils/Caller.sol"; 8 | import {Guarded} from "../guarded/Guarded.sol"; 9 | import {ICollybus} from "./ICollybus.sol"; 10 | import {Relayer} from "./Relayer.sol"; 11 | import {IRelayer} from "./IRelayer.sol"; 12 | import {IOracle} from "../oracle/IOracle.sol"; 13 | import {ChainlinkValueProvider} from "../oracle_implementations/spot_price/Chainlink/ChainlinkValueProvider.sol"; 14 | import {IChainlinkAggregatorV3Interface} from "../oracle_implementations/spot_price/Chainlink/ChainlinkAggregatorV3Interface.sol"; 15 | import {CheatCodes} from "../test/utils/CheatCodes.sol"; 16 | 17 | contract TestCollybus is ICollybus { 18 | mapping(bytes32 => uint256) public valueForToken; 19 | 20 | function updateDiscountRate(uint256 tokenId_, uint256 rate_) 21 | external 22 | override(ICollybus) 23 | { 24 | valueForToken[bytes32(uint256(tokenId_))] = rate_; 25 | } 26 | 27 | function updateSpot(address tokenAddress_, uint256 spot_) 28 | external 29 | override(ICollybus) 30 | { 31 | valueForToken[bytes32(uint256(uint160(tokenAddress_)))] = spot_; 32 | } 33 | } 34 | 35 | contract RelayerTest is DSTest { 36 | CheatCodes internal cheatCodes = CheatCodes(HEVM_ADDRESS); 37 | Relayer internal relayer; 38 | TestCollybus internal collybus; 39 | IRelayer.RelayerType internal relayerType = 40 | IRelayer.RelayerType.DiscountRate; 41 | 42 | MockProvider internal oracle; 43 | 44 | bytes32 private _mockTokenId; 45 | uint256 private _mockMinThreshold = 1; 46 | int256 private _oracleValue = 100 * 10**18; 47 | 48 | function setUp() public { 49 | collybus = new TestCollybus(); 50 | oracle = new MockProvider(); 51 | 52 | _mockTokenId = bytes32(uint256(1)); 53 | 54 | // Set the value returned by the Oracle. 55 | oracle.givenQueryReturnResponse( 56 | abi.encodePacked(IOracle.value.selector), 57 | MockProvider.ReturnData({ 58 | success: true, 59 | data: abi.encode(_oracleValue, true) 60 | }), 61 | false 62 | ); 63 | 64 | // We can use the same value as the nextValue 65 | oracle.givenQueryReturnResponse( 66 | abi.encodePacked(IOracle.nextValue.selector), 67 | MockProvider.ReturnData({ 68 | success: true, 69 | data: abi.encode(_oracleValue, true) 70 | }), 71 | false 72 | ); 73 | oracle.givenSelectorReturnResponse( 74 | Guarded.canCall.selector, 75 | MockProvider.ReturnData({success: true, data: abi.encode(true)}), 76 | false 77 | ); 78 | 79 | // Set update to return a boolean 80 | oracle.givenQueryReturnResponse( 81 | abi.encodePacked(IOracle.update.selector), 82 | MockProvider.ReturnData({success: true, data: abi.encode(true)}), 83 | true 84 | ); 85 | 86 | relayer = new Relayer( 87 | address(collybus), 88 | relayerType, 89 | address(oracle), 90 | _mockTokenId, 91 | _mockMinThreshold 92 | ); 93 | } 94 | 95 | function test_deploy() public { 96 | assertTrue( 97 | address(relayer) != address(0), 98 | "Relayer should be deployed" 99 | ); 100 | } 101 | 102 | function test_check_collybus() public { 103 | assertEq(relayer.collybus(), address(collybus)); 104 | } 105 | 106 | function test_check_relayerType() public { 107 | assertTrue(relayer.relayerType() == relayerType, "Invalid relayerType"); 108 | } 109 | 110 | function test_check_oracle() public { 111 | assertTrue( 112 | relayer.oracle() == address(oracle), 113 | "Invalid oracle address" 114 | ); 115 | } 116 | 117 | function test_check_encodedTokenId() public { 118 | assertTrue( 119 | relayer.encodedTokenId() == _mockTokenId, 120 | "Invalid encoded token id" 121 | ); 122 | } 123 | 124 | function test_check_minimumPercentageDeltaValue() public { 125 | assertTrue( 126 | relayer.minimumPercentageDeltaValue() == _mockMinThreshold, 127 | "Invalid minimumPercentageDeltaValue" 128 | ); 129 | } 130 | 131 | function test_canSetParam_minimumPercentageDeltaValue() public { 132 | // Set the minimumPercentageDeltaValue 133 | relayer.setParam("minimumPercentageDeltaValue", 10_00); 134 | 135 | // Check the minimumPercentageDeltaValue 136 | assertEq(relayer.minimumPercentageDeltaValue(), 10_00); 137 | } 138 | 139 | function testFail_shouldNotBeAbleToSet_invalidParam() public { 140 | relayer.setParam("invalidParam", 100_00); 141 | } 142 | 143 | function test_executeCalls_updateOnOracle() public { 144 | // Execute must call update on all oracles before pushing the values to Collybus 145 | relayer.execute(); 146 | 147 | // Update was called for both oracles 148 | MockProvider.CallData memory cd = oracle.getCallData(0); 149 | assertTrue(cd.functionSelector == IOracle.update.selector); 150 | } 151 | 152 | function test_execute_updateDiscountRateInCollybus() public { 153 | relayer.execute(); 154 | 155 | assertTrue( 156 | collybus.valueForToken(_mockTokenId) == uint256(_oracleValue), 157 | "Invalid discount rate relayer rate value" 158 | ); 159 | } 160 | 161 | function test_execute_updateSpotPriceInCollybus() public { 162 | // Create a spot price relayer and check the spot prices in the Collybus 163 | 164 | bytes32 mockSpotTokenAddress = bytes32(uint256(1)); 165 | Relayer spotPriceRelayer = new Relayer( 166 | address(collybus), 167 | IRelayer.RelayerType.SpotPrice, 168 | address(oracle), 169 | mockSpotTokenAddress, 170 | _mockMinThreshold 171 | ); 172 | 173 | // Update the rates in collybus 174 | spotPriceRelayer.execute(); 175 | 176 | assertTrue( 177 | collybus.valueForToken(mockSpotTokenAddress) == 178 | uint256(_oracleValue), 179 | "Invalid spot price relayer spot value" 180 | ); 181 | } 182 | 183 | function test_execute_doesNotUpdatesRatesInCollybusWhenDeltaIsBelowThreshold() 184 | public 185 | { 186 | // Threshold percentage 187 | uint256 thresholdPercentage = 50_00; // 50% 188 | 189 | relayer.setParam("minimumPercentageDeltaValue", thresholdPercentage); 190 | 191 | // Set the value returned by the Oracle. 192 | int256 initialValue = 100; 193 | oracle.givenQueryReturnResponse( 194 | abi.encodePacked(IOracle.value.selector), 195 | MockProvider.ReturnData({ 196 | success: true, 197 | data: abi.encode(initialValue, true) 198 | }), 199 | false 200 | ); 201 | 202 | // Make sure the values are updated, start clean 203 | relayer.execute(); 204 | 205 | // The initial value is the value we just defined 206 | assertEq( 207 | collybus.valueForToken(_mockTokenId), 208 | uint256(initialValue), 209 | "We should have the initial value" 210 | ); 211 | 212 | // Update the oracle with a new value under the threshold limit 213 | int256 secondValue = initialValue + 214 | (initialValue * int256(thresholdPercentage)) / 215 | 100_00 - 216 | 1; 217 | oracle.givenQueryReturnResponse( 218 | abi.encodePacked(IOracle.value.selector), 219 | MockProvider.ReturnData({ 220 | success: true, 221 | data: abi.encode(secondValue, true) 222 | }), 223 | false 224 | ); 225 | 226 | // Execute the relayer 227 | relayer.execute(); 228 | 229 | // Make sure the new value was not pushed into Collybus 230 | assertEq( 231 | collybus.valueForToken(_mockTokenId), 232 | uint256(initialValue), 233 | "Collybus should not have been updated" 234 | ); 235 | } 236 | 237 | function test_execute_updatesRatesInCollybusWhenDeltaIsAboveThreshold() 238 | public 239 | { 240 | // Threshold percentage 241 | uint256 thresholdPercentage = 50_00; // 50% 242 | 243 | relayer.setParam("minimumPercentageDeltaValue", thresholdPercentage); 244 | 245 | // Set the value returned by the Oracle. 246 | int256 initialValue = 100; 247 | oracle.givenQueryReturnResponse( 248 | abi.encodePacked(IOracle.value.selector), 249 | MockProvider.ReturnData({ 250 | success: true, 251 | data: abi.encode(initialValue, true) 252 | }), 253 | false 254 | ); 255 | 256 | // Make sure the values are updated, start from `initialValue` 257 | relayer.execute(); 258 | 259 | // The initial value is the value we just defined 260 | assertEq( 261 | collybus.valueForToken(_mockTokenId), 262 | uint256(initialValue), 263 | "We should have the initial value" 264 | ); 265 | 266 | // Update the oracle with a new value above the threshold limit 267 | int256 secondValue = initialValue + 268 | (initialValue * int256(thresholdPercentage)) / 269 | 100_00 + 270 | 1; 271 | 272 | oracle.givenQueryReturnResponse( 273 | abi.encodePacked(IOracle.value.selector), 274 | MockProvider.ReturnData({ 275 | success: true, 276 | data: abi.encode(secondValue, true) 277 | }), 278 | false 279 | ); 280 | 281 | // Execute the relayer 282 | relayer.execute(); 283 | 284 | // Make sure the new value was pushed into Collybus 285 | assertEq( 286 | collybus.valueForToken(_mockTokenId), 287 | uint256(secondValue), 288 | "Collybus should have the new value" 289 | ); 290 | } 291 | 292 | function test_executeWithRevert() public { 293 | // Call should not revert 294 | relayer.executeWithRevert(); 295 | } 296 | 297 | function test_execute_returnsTrue_whenCollybusIsUpdated() public { 298 | bool executed; 299 | 300 | executed = relayer.execute(); 301 | 302 | assertTrue(executed, "The relayer should return true"); 303 | } 304 | 305 | function test_execute_returnsFalse_whenCollybusIsNotUpdated() public { 306 | bool executed; 307 | 308 | // The first execute should return true 309 | executed = relayer.execute(); 310 | assertTrue(executed, "The relayer's execute() should return true"); 311 | 312 | // The second execute should return false because the Collybus will not be updated 313 | executed = relayer.execute(); 314 | assertTrue( 315 | executed == false, 316 | "The relayer execute() should return false" 317 | ); 318 | } 319 | 320 | function test_execute_returnsFalse_whenOracleIsNotUpdated() public { 321 | // Set `update` to return `false` 322 | oracle.givenQueryReturnResponse( 323 | abi.encodePacked(IOracle.update.selector), 324 | MockProvider.ReturnData({success: true, data: abi.encode(false)}), 325 | true 326 | ); 327 | 328 | // When the oracle's `update` returns `false`, the relayer's `execute` should return `false` 329 | bool executeReturnedValue = relayer.execute(); 330 | assertTrue( 331 | executeReturnedValue == false, 332 | "The relayer execute() should return false" 333 | ); 334 | } 335 | 336 | function test_executeWithRevert_shouldBeSuccessful_onFirstExecution() 337 | public 338 | { 339 | // Call should not revert 340 | relayer.executeWithRevert(); 341 | } 342 | 343 | function testFail_executeWithRevert_shouldNotBeSuccessful_whenCollybusIsNotUpdated() 344 | public 345 | { 346 | relayer.execute(); 347 | 348 | // Call should revert 349 | relayer.executeWithRevert(); 350 | } 351 | 352 | function setChainlinkMockReturnedValue( 353 | MockProvider mockChainlinkAggregator, 354 | int256 value 355 | ) internal { 356 | mockChainlinkAggregator.givenQueryReturnResponse( 357 | abi.encodeWithSelector( 358 | IChainlinkAggregatorV3Interface.latestRoundData.selector 359 | ), 360 | MockProvider.ReturnData({ 361 | success: true, 362 | data: abi.encode( 363 | uint80(0), // roundId 364 | value, // answer 365 | uint256(0), // startedAt 366 | uint256(0), // updatedAt 367 | uint80(0) // answeredInRound 368 | ) 369 | }), 370 | false 371 | ); 372 | } 373 | 374 | function test_executeWithRevert_canBeUpdatedByKeepers() public { 375 | // For this test we will simulate an external actor that must be able to successfully update 376 | // the Relayer via multiple `executeWithRevert()` calls. We will do this by providing multiple 377 | // mocked Chainlink values and making sure that each value is properly validated and used. 378 | int256 firstValue = 1e18; 379 | int256 secondValue = 2e18; 380 | uint256 timeUpdateWindow = 100; 381 | 382 | // Test setup, create the mockChainlinkAggregator that will provide data for the Oracle 383 | MockProvider mockChainlinkAggregator = new MockProvider(); 384 | mockChainlinkAggregator.givenQueryReturnResponse( 385 | abi.encodeWithSelector( 386 | IChainlinkAggregatorV3Interface.decimals.selector 387 | ), 388 | MockProvider.ReturnData({success: true, data: abi.encode(18)}), 389 | false 390 | ); 391 | 392 | // Deploy the Chainlink Oracle with the mocked Chainlink datafeed contract 393 | ChainlinkValueProvider chainlinkVP = new ChainlinkValueProvider( 394 | timeUpdateWindow, 395 | address(mockChainlinkAggregator) 396 | ); 397 | 398 | // Deploy the Relayer 399 | Relayer testRelayer = new Relayer( 400 | address(collybus), 401 | relayerType, 402 | address(chainlinkVP), 403 | _mockTokenId, 404 | _mockMinThreshold 405 | ); 406 | 407 | // Whitelist the relayer in the oracle so it can trigger Oracle.update() 408 | chainlinkVP.allowCaller(chainlinkVP.ANY_SIG(), address(testRelayer)); 409 | 410 | // Simulate a small time offset as the Oracle.lastTimestamp will start at 0 and we should not be able to update values 411 | uint256 timeStart = timeUpdateWindow + 1; 412 | 413 | // Set the Chainlink mock to return the first value 414 | setChainlinkMockReturnedValue(mockChainlinkAggregator, firstValue); 415 | 416 | // Forward time to the start of this test scenario 417 | cheatCodes.warp(timeStart); 418 | 419 | // Run the first `executeWithRevert()`, `Oracle.currentValue` and `Oracle.nextValue` will be equal to `firstValue` 420 | testRelayer.executeWithRevert(); 421 | 422 | // Set the next value for the Chainlink mock datafeed 423 | setChainlinkMockReturnedValue(mockChainlinkAggregator, secondValue); 424 | 425 | // Forward time to the next window 426 | cheatCodes.warp(timeStart + timeUpdateWindow); 427 | 428 | // Call should not revert 429 | testRelayer.executeWithRevert(); 430 | 431 | // Verify that the oracle was properly updated 432 | // The `nextValue` should now be equal to `secondValue` because of the update 433 | assertTrue( 434 | chainlinkVP.nextValue() == secondValue, 435 | "Invalid Oracle nextValue" 436 | ); 437 | 438 | // The current oracle value should still be the first value because of the previous executeWithRevert() 439 | // `currentValue` and `nextValue` where initialized to `firstValue` 440 | (int256 value, bool isValid) = chainlinkVP.value(); 441 | assertTrue(isValid, "Invalid Oracle value"); 442 | assertTrue(value == firstValue, "Incorrect Oracle value"); 443 | 444 | // Forward time to the next window 445 | cheatCodes.warp(timeStart + timeUpdateWindow * 2); 446 | 447 | // Call should not revert 448 | testRelayer.executeWithRevert(); 449 | 450 | // Make sure the oracle value was updated and is now equal to `secondValue` 451 | (value, isValid) = chainlinkVP.value(); 452 | assertTrue(isValid, "Invalid Oracle value"); 453 | assertTrue(value == secondValue, "Incorrect Oracle value"); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/relayer/StaticRelayer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {ICollybus} from "./ICollybus.sol"; 5 | import {IRelayer} from "./IRelayer.sol"; 6 | 7 | contract StaticRelayer is IRelayer { 8 | /// @notice Emitted during executeWithRevert() if the Collybus was already updated 9 | error StaticRelayer__executeWithRevert_collybusAlreadyUpdated( 10 | IRelayer.RelayerType relayerType 11 | ); 12 | 13 | /// ======== Events ======== /// 14 | 15 | event UpdatedCollybus( 16 | bytes32 tokenId, 17 | uint256 rate, 18 | IRelayer.RelayerType relayerType 19 | ); 20 | 21 | /// ======== Storage ======== /// 22 | 23 | address public immutable collybus; 24 | IRelayer.RelayerType public immutable relayerType; 25 | bytes32 public immutable encodedTokenId; 26 | uint256 public immutable value; 27 | 28 | // Flag used to ensure that the value is pushed to Collybus only once 29 | bool private _updatedCollybus; 30 | 31 | /// @param collybusAddress_ Address of the collybus 32 | /// @param type_ Relayer type, DiscountRate or SpotPrice 33 | /// @param encodedTokenId_ Encoded token Id that will be used to push the value to Collybus 34 | /// uint256 for discount rate, address for spot price 35 | /// @param value_ The value that will be pushed to Collybus 36 | constructor( 37 | address collybusAddress_, 38 | IRelayer.RelayerType type_, 39 | bytes32 encodedTokenId_, 40 | uint256 value_ 41 | ) { 42 | collybus = collybusAddress_; 43 | relayerType = type_; 44 | encodedTokenId = encodedTokenId_; 45 | value = value_; 46 | _updatedCollybus = false; 47 | } 48 | 49 | /// @notice Pushes the hardcoded value to Collybus for the hardcoded token id 50 | /// @dev The execute will exit early after the first update 51 | function execute() public override(IRelayer) returns (bool) { 52 | if (_updatedCollybus) return false; 53 | 54 | _updatedCollybus = true; 55 | if (relayerType == IRelayer.RelayerType.DiscountRate) { 56 | ICollybus(collybus).updateDiscountRate( 57 | uint256(encodedTokenId), 58 | value 59 | ); 60 | } else if (relayerType == IRelayer.RelayerType.SpotPrice) { 61 | ICollybus(collybus).updateSpot( 62 | address(uint160(uint256(encodedTokenId))), 63 | value 64 | ); 65 | } 66 | 67 | emit UpdatedCollybus(encodedTokenId, value, relayerType); 68 | return true; 69 | } 70 | 71 | /// @notice The function will call `execute()` and will revert if _updatedCollybus is true 72 | function executeWithRevert() public override(IRelayer) { 73 | if (!execute()) { 74 | revert StaticRelayer__executeWithRevert_collybusAlreadyUpdated( 75 | relayerType 76 | ); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/relayer/StaticRelayer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {Hevm} from "../test/utils/Hevm.sol"; 5 | import {DSTest} from "ds-test/test.sol"; 6 | import {Caller} from "../test/utils/Caller.sol"; 7 | 8 | import {ICollybus} from "./ICollybus.sol"; 9 | import {IRelayer} from "./IRelayer.sol"; 10 | 11 | import {StaticRelayer} from "./StaticRelayer.sol"; 12 | 13 | contract TestCollybus is ICollybus { 14 | mapping(uint256 => uint256) public discountRateForToken; 15 | mapping(address => uint256) public spotPriceForToken; 16 | 17 | function updateDiscountRate(uint256 tokenId, uint256 rate) 18 | external 19 | override(ICollybus) 20 | { 21 | discountRateForToken[tokenId] = rate; 22 | } 23 | 24 | function updateSpot(address tokenAddress, uint256 spot) 25 | external 26 | override(ICollybus) 27 | { 28 | spotPriceForToken[tokenAddress] = spot; 29 | } 30 | } 31 | 32 | contract StaticRelayerTest is DSTest { 33 | Hevm internal hevm = Hevm(DSTest.HEVM_ADDRESS); 34 | 35 | TestCollybus internal collybus; 36 | 37 | function setUp() public { 38 | collybus = new TestCollybus(); 39 | } 40 | 41 | function test_deploy() public { 42 | StaticRelayer staticRelayer = new StaticRelayer( 43 | address(collybus), 44 | IRelayer.RelayerType.DiscountRate, 45 | bytes32(uint256(1)), 46 | 1e18 47 | ); 48 | 49 | assertTrue( 50 | address(staticRelayer) != address(0), 51 | "StaticRelayer should be deployed" 52 | ); 53 | } 54 | 55 | function test_execute_updates_discountRateInCollybus() public { 56 | // Create the static relayer with a tokenId and a value 57 | uint256 tokenId = 1; 58 | StaticRelayer staticRelayer = new StaticRelayer( 59 | address(collybus), 60 | IRelayer.RelayerType.DiscountRate, 61 | bytes32(tokenId), 62 | 1e18 63 | ); 64 | 65 | // Push the value to Collybus 66 | staticRelayer.execute(); 67 | 68 | assertTrue( 69 | collybus.discountRateForToken(tokenId) == 1e18, 70 | "Invalid discount rate in Collybus" 71 | ); 72 | } 73 | 74 | function test_execute_updates_spotPriceInCollybus() public { 75 | // Create the static relayer with a tokenId and a value 76 | address tokenAddress = address(0x1234); 77 | StaticRelayer staticRelayer = new StaticRelayer( 78 | address(collybus), 79 | IRelayer.RelayerType.SpotPrice, 80 | bytes32(uint256(uint160(tokenAddress))), 81 | 1e18 82 | ); 83 | 84 | // Push the value to Collybus 85 | staticRelayer.execute(); 86 | 87 | assertTrue( 88 | collybus.spotPriceForToken(tokenAddress) == 1e18, 89 | "Invalid spot price in Collybus" 90 | ); 91 | } 92 | 93 | function test_executeWithRevert() public { 94 | address tokenAddress = address(0x1234); 95 | // The Collybus type does not change the behavior so we can use either of them 96 | StaticRelayer staticRelayer = new StaticRelayer( 97 | address(collybus), 98 | IRelayer.RelayerType.SpotPrice, 99 | bytes32(uint256(uint160(tokenAddress))), 100 | 1e18 101 | ); 102 | 103 | // Call should not revert 104 | staticRelayer.executeWithRevert(); 105 | } 106 | 107 | function testFail_executeWithRevert_shouldRevertAfterCollybusIsUpdated() 108 | public 109 | { 110 | address tokenAddress = address(0x1234); 111 | // The Collybus type does not change the behavior so we can use either of them 112 | StaticRelayer staticRelayer = new StaticRelayer( 113 | address(collybus), 114 | IRelayer.RelayerType.SpotPrice, 115 | bytes32(uint256(uint160(tokenAddress))), 116 | 1e18 117 | ); 118 | 119 | // Call execute() so the StaticRelayer will update the Collybus 120 | staticRelayer.execute(); 121 | 122 | // Call to executeWithRevert() should revert because we already updated the Collybus 123 | staticRelayer.executeWithRevert(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/test/utils/Caller.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | contract Caller { 5 | /// @dev Can use this method to call any other contract's function 6 | /// @param _contractAddress Address of the contract to call 7 | /// @param _callData Call data 8 | /// @return ok is `true` if the call was successful 9 | /// @return data is the encoded result of the call 10 | function externalCall(address _contractAddress, bytes calldata _callData) 11 | external 12 | returns (bool, bytes memory) 13 | { 14 | // solhint-disable-next-line 15 | (bool ok, bytes memory data) = _contractAddress.call(_callData); 16 | return (ok, data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/utils/CheatCodes.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | interface CheatCodes { 5 | function warp(uint256) external; 6 | 7 | // Set block.timestamp 8 | 9 | function roll(uint256) external; 10 | 11 | // Set block.number 12 | 13 | function fee(uint256) external; 14 | 15 | // Set block.basefee 16 | 17 | function load(address account, bytes32 slot) external returns (bytes32); 18 | 19 | // Loads a storage slot from an address 20 | 21 | function store( 22 | address account, 23 | bytes32 slot, 24 | bytes32 value 25 | ) external; 26 | 27 | // Stores a value to an address' storage slot 28 | 29 | function sign(uint256 privateKey, bytes32 digest) 30 | external 31 | returns ( 32 | uint8 v, 33 | bytes32 r, 34 | bytes32 s 35 | ); 36 | 37 | // Signs data 38 | 39 | function addr(uint256 privateKey) external returns (address); 40 | 41 | // Computes address for a given private key 42 | 43 | function ffi(string[] calldata) external returns (bytes memory); 44 | 45 | // Performs a foreign function call via terminal 46 | 47 | function prank(address) external; 48 | 49 | // Sets the *next* call's msg.sender to be the input address 50 | 51 | function startPrank(address) external; 52 | 53 | // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called 54 | 55 | function prank(address, address) external; 56 | 57 | // Sets the *next* call's msg.sender to be the input address, and the tx.origin to be the second input 58 | 59 | function startPrank(address, address) external; 60 | 61 | // Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input 62 | 63 | function stopPrank() external; 64 | 65 | // Resets subsequent calls' msg.sender to be `address(this)` 66 | 67 | function deal(address who, uint256 newBalance) external; 68 | 69 | // Sets an address' balance 70 | 71 | function etch(address who, bytes calldata code) external; 72 | 73 | // Sets an address' code 74 | 75 | function expectRevert() external; 76 | 77 | function expectRevert(bytes calldata) external; 78 | 79 | function expectRevert(bytes4) external; 80 | 81 | // Expects an error on next call 82 | 83 | function record() external; 84 | 85 | // Record all storage reads and writes 86 | 87 | function accesses(address) 88 | external 89 | returns (bytes32[] memory reads, bytes32[] memory writes); 90 | 91 | // Gets all accessed reads and write slot from a recording session, for a given address 92 | 93 | function expectEmit( 94 | bool, 95 | bool, 96 | bool, 97 | bool 98 | ) external; 99 | 100 | // Prepare an expected log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData). 101 | // Call this function, then emit an event, then call a function. Internally after the call, we check if 102 | // logs were emitted in the expected order with the expected topics and data (as specified by the booleans) 103 | 104 | function mockCall( 105 | address, 106 | bytes calldata, 107 | bytes calldata 108 | ) external; 109 | 110 | // Mocks a call to an address, returning specified data. 111 | // Calldata can either be strict or a partial match, e.g. if you only 112 | // pass a Solidity selector to the expected calldata, then the entire Solidity 113 | // function will be mocked. 114 | 115 | function clearMockedCalls() external; 116 | 117 | // Clears all mocked calls 118 | 119 | function expectCall(address, bytes calldata) external; 120 | 121 | // Expect a call to an address with the specified calldata. 122 | // Calldata can either be strict or a partial match 123 | 124 | function getCode(string calldata) external returns (bytes memory); 125 | 126 | // Gets the bytecode for a contract in the project given the path to the contract. 127 | 128 | function label(address addr, string calldata label) external; 129 | 130 | // Label an address in test traces 131 | 132 | function assume(bool) external; 133 | // When fuzzing, generate new inputs if conditional not met 134 | } 135 | -------------------------------------------------------------------------------- /src/test/utils/Hevm.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | abstract contract Hevm { 5 | // sets the block timestamp to x 6 | function warp(uint256 x) public virtual; 7 | 8 | // sets the block number to x 9 | function roll(uint256 x) public virtual; 10 | 11 | // sets the slot loc of contract c to val 12 | function store( 13 | address c, 14 | bytes32 loc, 15 | bytes32 val 16 | ) public virtual; 17 | 18 | function ffi(string[] calldata) external virtual returns (bytes memory); 19 | } 20 | -------------------------------------------------------------------------------- /src/test/utils/tokens/TokenERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TokenERC20 is ERC20 { 7 | constructor(string memory _name, string memory _symbol) 8 | ERC20(_name, _symbol) 9 | { 10 | this; 11 | } 12 | 13 | function mint(address _to, uint256 _amount) public { 14 | _mint(_to, _amount); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/utils/tokens/TokenERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.0; 3 | 4 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | contract Token is ERC721 { 7 | constructor(string memory _name, string memory _symbol) 8 | ERC721(_name, _symbol) 9 | { 10 | this; 11 | } 12 | 13 | function mint(address _to, uint256 _id) public { 14 | _mint(_to, _id); 15 | } 16 | } 17 | --------------------------------------------------------------------------------