├── .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 |
--------------------------------------------------------------------------------