From 7323a6135c3d46aa5964bcf160b1fc14f29145c9 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 4 Jul 2024 13:42:14 +0200 Subject: [PATCH 01/38] chore: reduce CI foundry test verbosity (#475) ### Description The verbosity setting for CI tests was set to `4`, which outputs full stack traces for all (even passed) tests. This leads to extremely verbose output that doesn't get fully displayed in GitHub UI (need to download a 52MB, 350k line log file). This makes it more time consuming to pinpoint what specific tests are causing the workflow to fail. [Example](https://github.com/mento-protocol/mento-core/actions/runs/9779856065/job/27000074200?pr=456). I propose we set verbosity to `3`, which should still output stack traces for failing tests. The one thing missing will be setup traces for failing tests, but this should still be a quality of life improvement. ### Other changes Nothing, this is a one byte change. ### Tested Will need to look at CI output for this PR. --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index e2ce9905..230d2233 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,7 +15,7 @@ via_ir = true [profile.ci] fuzz_runs = 1_000 -verbosity = 4 +verbosity = 3 [profile.fork-tests] no_match_contract = "_random" # in order to reset the no_match_contract From 3a53cd2d33f90b020585c5401fc14be2c73d982e Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:35:27 +0200 Subject: [PATCH 02/38] fix: add nativeUSDT to collateral assets (#434) --- test/fork-tests/BaseForkTest.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fork-tests/BaseForkTest.t.sol b/test/fork-tests/BaseForkTest.t.sol index ec6c48eb..ca1acdaa 100644 --- a/test/fork-tests/BaseForkTest.t.sol +++ b/test/fork-tests/BaseForkTest.t.sol @@ -126,8 +126,8 @@ contract BaseForkTest is Test, TokenHelpers, TestAsserts { } require(exchanges.length > 0, "No exchanges found"); - // The number of collateral assets 4 is hardcoded here [CELO, AxelarUSDC, EUROC, NativeUSDC] - for (uint256 i = 0; i < 4; i++) { + // The number of collateral assets 5 is hardcoded here [CELO, AxelarUSDC, EUROC, NativeUSDC, NativeUSDT] + for (uint256 i = 0; i < 5; i++) { address collateralAsset = reserve.collateralAssets(i); mint(collateralAsset, address(reserve), Utils.toSubunits(25_000_000, collateralAsset)); console.log("Minting 25mil %s to reserve", IERC20Metadata(collateralAsset).symbol()); From db790576a3c54ca2f85fd2a1f152325bb5a04f87 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 15 Jul 2024 12:33:19 +0200 Subject: [PATCH 03/38] feat: implement Chainlink relayer contract (#456) ### Description Adds a ChainlinkAdapter contract which can relay anwers from a Chainlink AggregatorV3 to SortedOracles. It assumes it is the only reporter for the given rate feed in SortedOracles (e.g. `greater`/`lesserKey` are not needed). Every rate feed that relies on this mechanism should have a separate ChainlinkAdapter deployed, with the appropriate parameters passed to the constructor. To save gas, the adapter is stateless, unupgradeable, and relies on immutable parameters set in the constructor. Next steps: * Integration test of the contract. * Off-chain component that can ping this adapter contract. * Deployment tooling/docs for setting up a rate feed with the ChainlinkAdapter. ### Tested Unit tests. ### Related issues - Fixes #458 --------- Co-authored-by: chapati --- .gitmodules | 3 + contracts/interfaces/IChainlinkRelayer.sol | 12 ++ contracts/oracles/ChainlinkRelayerV1.sol | 144 +++++++++++++++++++++ lib/foundry-chainlink-toolkit | 1 + test/mocks/MockAggregatorV3.sol | 30 +++++ test/oracles/ChainlinkRelayer.t.sol | 116 +++++++++++++++++ 6 files changed, 306 insertions(+) create mode 100644 contracts/interfaces/IChainlinkRelayer.sol create mode 100644 contracts/oracles/ChainlinkRelayerV1.sol create mode 160000 lib/foundry-chainlink-toolkit create mode 100644 test/mocks/MockAggregatorV3.sol create mode 100644 test/oracles/ChainlinkRelayer.t.sol diff --git a/.gitmodules b/.gitmodules index b70e25a1..498e8d0f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "lib/safe-contracts"] path = lib/safe-contracts url = https://github.com/safe-global/safe-contracts +[submodule "lib/foundry-chainlink-toolkit"] + path = lib/foundry-chainlink-toolkit + url = https://github.com/smartcontractkit/foundry-chainlink-toolkit diff --git a/contracts/interfaces/IChainlinkRelayer.sol b/contracts/interfaces/IChainlinkRelayer.sol new file mode 100644 index 00000000..b99b78d2 --- /dev/null +++ b/contracts/interfaces/IChainlinkRelayer.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.5.13 <0.8.19; + +interface IChainlinkRelayer { + function rateFeedId() external returns (address); + + function sortedOracles() external returns (address); + + function chainlinkAggregator() external returns (address); + + function relay() external; +} diff --git a/contracts/oracles/ChainlinkRelayerV1.sol b/contracts/oracles/ChainlinkRelayerV1.sol new file mode 100644 index 00000000..1230d7dd --- /dev/null +++ b/contracts/oracles/ChainlinkRelayerV1.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.18; + +import "../interfaces/IChainlinkRelayer.sol"; +import "foundry-chainlink-toolkit/src/interfaces/feeds/AggregatorV3Interface.sol"; + +/** + * @notice The minimal subset of the SortedOracles interface needed by the + * relayer. + * @dev SortedOracles is a Solidity 5.13 contract, thus we can't import the + * interface directly, so we use a minimal hand-copied one. + * See https://github.com/mento-protocol/mento-core/blob/develop/contracts/common/SortedOracles.sol + */ +interface ISortedOraclesMin { + function report( + address rateFeedId, + uint256 value, + address lesserKey, + address greaterKey + ) external; + + function medianTimestamp(address rateFeedId) external view returns (uint256); + + function getTokenReportExpirySeconds(address rateFeedId) external view returns (uint256); +} + +/** + * @title ChainlinkRelayer + * @notice The ChainlinkRelayer relays rate feed data from a Chainlink price feed to + * the SortedOracles contract. A separate instance should be deployed for each + * rate feed. + * @dev Assumes that it itself is the only reporter for the given SortedOracles + * feed. + */ +contract ChainlinkRelayerV1 is IChainlinkRelayer { + /** + * @notice The number of digits after the decimal point in FixidityLib + * values, as used by SortedOracles. + * @dev See contracts/common/FixidityLib.sol + */ + uint256 public constant FIXIDITY_DECIMALS = 24; + /// @notice The rateFeedId this relayer relays for. + address public immutable rateFeedId; + /// @notice The address of the SortedOracles contract to report to. + address public immutable sortedOracles; + /** + * @notice The address of the Chainlink aggregator this contract fetches + * data from. + */ + address public immutable chainlinkAggregator; + + /** + * @notice Used when a new price's timestamp is not newer than the most recent + * SortedOracles timestamp. + */ + error TimestampNotNew(); + /** + * @notice Used when a new price's timestamp would be considered expired by + * SortedOracles. + */ + error ExpiredTimestamp(); + /** + * @notice Used when a negative price is returned by the Chainlink + * aggregator. + */ + error NegativePrice(); + + /** + * @notice Initializes the contract and sets immutable parameters. + * @param _rateFeedId ID of the rate feed this relayer instance relays for. + * @param _sortedOracles Address of the SortedOracles contract to relay to. + * @param _chainlinkAggregator Address of the Chainlink price feed to fetch data from. + */ + constructor( + address _rateFeedId, + address _sortedOracles, + address _chainlinkAggregator + ) { + rateFeedId = _rateFeedId; + sortedOracles = _sortedOracles; + chainlinkAggregator = _chainlinkAggregator; + } + + /** + * @notice Relays data from the configured Chainlink aggregator to + * SortedOracles. + * @dev Checks the price is non-negative (Chainlink uses `int256` rather + * than `uint256`. + * @dev Converts the price to a Fixidity value, as expected by + * SortedOracles. + * @dev Performs checks on the timestamp, will revert if any fails: + * - The timestamp should be strictly newer than the most recent + * timestamp in SortedOracles. + * - The timestamp should not be considered expired by SortedOracles. + */ + function relay() external { + ISortedOraclesMin _sortedOracles = ISortedOraclesMin(sortedOracles); + (, int256 price, , uint256 timestamp, ) = AggregatorV3Interface(chainlinkAggregator).latestRoundData(); + + uint256 lastTimestamp = _sortedOracles.medianTimestamp(rateFeedId); + + if (lastTimestamp > 0) { + if (timestamp <= lastTimestamp) { + revert TimestampNotNew(); + } + } + + if (isTimestampExpired(timestamp)) { + revert ExpiredTimestamp(); + } + + if (price < 0) { + revert NegativePrice(); + } + + uint256 report = chainlinkToFixidity(price); + + // This contract is built for a setup where it is the only reporter for the + // given `rateFeedId`. As such, we don't need to compute and provide + // `lesserKey`/`greaterKey` each time, the "null pointer" `address(0)` will + // correctly place the report in SortedOracles' sorted linked list. + ISortedOraclesMin(sortedOracles).report(rateFeedId, report, address(0), address(0)); + } + + /** + * @notice Checks if a Chainlink price's timestamp would be expired in + * SortedOracles. + * @param timestamp The timestamp returned by the Chainlink aggregator. + * @return `true` if expired based on SortedOracles expiry parameter. + */ + function isTimestampExpired(uint256 timestamp) internal view returns (bool) { + return block.timestamp - timestamp >= ISortedOraclesMin(sortedOracles).getTokenReportExpirySeconds(rateFeedId); + } + + /** + * @notice Converts a Chainlink price to an unwrapped Fixidity value. + * @param price An price from the Chainlink aggregator. + * @return The converted Fixidity value (with 24 decimals). + */ + function chainlinkToFixidity(int256 price) internal view returns (uint256) { + uint256 chainlinkDecimals = uint256(AggregatorV3Interface(chainlinkAggregator).decimals()); + return uint256(price) * 10**(FIXIDITY_DECIMALS - chainlinkDecimals); + } +} diff --git a/lib/foundry-chainlink-toolkit b/lib/foundry-chainlink-toolkit new file mode 160000 index 00000000..d610ec9e --- /dev/null +++ b/lib/foundry-chainlink-toolkit @@ -0,0 +1 @@ +Subproject commit d610ec9ef54c325de7de1a5622c19933b2ae26cf diff --git a/test/mocks/MockAggregatorV3.sol b/test/mocks/MockAggregatorV3.sol new file mode 100644 index 00000000..cb636d4e --- /dev/null +++ b/test/mocks/MockAggregatorV3.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.13; + +contract MockAggregatorV3 { + int256 public _answer; + uint256 public _updatedAt; + + function setRoundData(int256 answer, uint256 updatedAt) external { + _answer = answer; + _updatedAt = updatedAt; + } + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + return (uint80(0), _answer, uint256(0), _updatedAt, uint80(0)); + } + + function decimals() external view returns (uint8) { + return 8; + } +} diff --git a/test/oracles/ChainlinkRelayer.t.sol b/test/oracles/ChainlinkRelayer.t.sol new file mode 100644 index 00000000..5abf292d --- /dev/null +++ b/test/oracles/ChainlinkRelayer.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import "../utils/BaseTest.t.sol"; +import "contracts/common/SortedOracles.sol"; +import "../mocks/MockAggregatorV3.sol"; +import "contracts/interfaces/IChainlinkRelayer.sol"; + +contract ChainlinkRelayerTest is BaseTest { + bytes constant TIMESTAMP_NOT_NEW_ERROR = abi.encodeWithSignature("TimestampNotNew()"); + bytes constant EXPIRED_TIMESTAMP_ERROR = abi.encodeWithSignature("ExpiredTimestamp()"); + bytes constant NEGATIVE_PRICE_ERROR = abi.encodeWithSignature("NegativePrice()"); + SortedOracles sortedOracles; + MockAggregatorV3 chainlinkAggregator; + IChainlinkRelayer relayer; + address rateFeedId = address(0xbeef); + int256 aPrice = 420000000; + uint256 expectedReport = 4200000000000000000000000; + uint256 aReport = 4100000000000000000000000; + uint256 expirySeconds = 600; + + function setUp() public { + sortedOracles = new SortedOracles(true); + chainlinkAggregator = new MockAggregatorV3(); + relayer = IChainlinkRelayer( + factory.createContract( + "ChainlinkRelayerV1", + abi.encode(rateFeedId, address(sortedOracles), address(chainlinkAggregator)) + ) + ); + sortedOracles.addOracle(rateFeedId, address(relayer)); + sortedOracles.setTokenReportExpiry(rateFeedId, expirySeconds); + } +} + +contract ChainlinkRelayerTest_constructor is ChainlinkRelayerTest { + function test_constructorSetsRateFeedId() public { + address _rateFeedId = relayer.rateFeedId(); + assertEq(_rateFeedId, rateFeedId); + } + + function test_constructorSetsSortedOracles() public { + address _sortedOracles = relayer.sortedOracles(); + assertEq(_sortedOracles, address(sortedOracles)); + } + + function test_constructorSetsAggregator() public { + address _chainlinkAggregator = relayer.chainlinkAggregator(); + assertEq(_chainlinkAggregator, address(chainlinkAggregator)); + } +} + +contract ChainlinkRelayerTest_relay is ChainlinkRelayerTest { + function setUp() public { + super.setUp(); + chainlinkAggregator.setRoundData(int256(aPrice), uint256(block.timestamp)); + } + + function test_relaysTheRate() public { + relayer.relay(); + (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); + assertEq(medianRate, expectedReport); + } + + function testFuzz_convertsChainlinkToFixidityCorrectly(int256 x) public { + vm.assume(x >= 0); + vm.assume(uint256(x) < uint256(2**256 - 1) / (10**(24 - 8))); + chainlinkAggregator.setRoundData(x, uint256(block.timestamp)); + relayer.relay(); + (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); + assertEq(medianRate, uint256(x) * 10**(24 - 8)); + } + + function test_revertsOnNegativePrice() public { + chainlinkAggregator.setRoundData(-1 * aPrice, block.timestamp); + vm.expectRevert(NEGATIVE_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnEarlierTimestamp() public { + vm.prank(address(relayer)); + sortedOracles.report(rateFeedId, aReport, address(0), address(0)); + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + chainlinkAggregator.setRoundData(aPrice, latestTimestamp - 1); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_revertsOnRepeatTimestamp() public { + vm.prank(address(relayer)); + sortedOracles.report(rateFeedId, aReport, address(0), address(0)); + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + chainlinkAggregator.setRoundData(aPrice, latestTimestamp); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_revertsOnExpiredTimestamp() public { + vm.prank(address(relayer)); + sortedOracles.report(rateFeedId, aReport, address(0), address(0)); + chainlinkAggregator.setRoundData(aPrice, block.timestamp + 1); + vm.warp(block.timestamp + expirySeconds + 1); + vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); + relayer.relay(); + } + + function test_revertsWhenFirstTimestampIsExpired() public { + chainlinkAggregator.setRoundData(aPrice, block.timestamp); + vm.warp(block.timestamp + expirySeconds); + vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); + relayer.relay(); + } +} From 0f12c99a97707ccd4e382d4e4cedea751dcd54c3 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 7 Aug 2024 11:04:05 +0200 Subject: [PATCH 04/38] feat: implement Chainlink relayer factory (#476) ### Description This implements a factory contract to deploy Chainlink relayers (see #456). TODO: - [x] Make the factory Ownable and set permissions for functions that affect state - [x] Add NatSpec documentation LATER (follow-up PR, see #481 ): - [x] Use a proxy for upgradability - [x] Add integration tests that test the Factory -> Relayer -> SortedOracles -> BreakerBox flow ### Other changes * export a `contractPath` helper function from `test/utils/Factory.sol`, it's useful for computing the `CREATE2` address in tests ### Tested Unit tests. ### Related issues - Fixes #459 --------- Co-authored-by: chapati Co-authored-by: bowd --- .prettierignore | 3 +- .prettierrc.yml | 25 +- .solhintignore | 1 + contracts/interfaces/IChainlinkRelayer.sol | 2 +- .../interfaces/IChainlinkRelayerFactory.sol | 37 ++ contracts/oracles/ChainlinkRelayerFactory.sol | 208 +++++++++ contracts/oracles/ChainlinkRelayerV1.sol | 53 +-- foundry.toml | 2 +- test/oracles/ChainlinkRelayer.t.sol | 17 +- test/oracles/ChainlinkRelayerFactory.t.sol | 403 ++++++++++++++++++ test/utils/Factory.sol | 6 +- 11 files changed, 700 insertions(+), 57 deletions(-) create mode 100644 contracts/interfaces/IChainlinkRelayerFactory.sol create mode 100644 contracts/oracles/ChainlinkRelayerFactory.sol create mode 100644 test/oracles/ChainlinkRelayerFactory.t.sol diff --git a/.prettierignore b/.prettierignore index 4671f5a9..c4ce4777 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ # directories .yarn/ +.trunk/ **/broadcast **/cache **/lib @@ -13,4 +14,4 @@ coverage.json yarn-debug.log* yarn-error.log* -slither.db.json +slither.db.json \ No newline at end of file diff --git a/.prettierrc.yml b/.prettierrc.yml index d3931011..a28a3534 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -1,26 +1,29 @@ -arrowParens: "avoid" +arrowParens: avoid bracketSpacing: true -endOfLine: "auto" +endOfLine: auto printWidth: 120 singleQuote: false tabWidth: 2 -trailingComma: "all" +trailingComma: all overrides: - files: ["*.sol"] options: - compiler: "0.5.17" + compiler: 0.5.17 tabWidth: 2 printWidth: 120 - - files: ["contracts/tokens/patched/*.sol"] + - files: [contracts/tokens/patched/*.sol] options: - compiler: "0.8.18" - - files: ["contracts/tokens/StableTokenV2.sol"] + compiler: 0.8.18 + - files: [contracts/tokens/StableTokenV2.sol] options: - compiler: "0.8.18" - - files: ["contracts/governance/**/*.sol"] + compiler: 0.8.18 + - files: [contracts/governance/**/*.sol] options: - compiler: "0.8.18" - - files: ["test/**/*.sol"] + compiler: 0.8.18 + - files: [test/**/*.sol] options: compiler: "" + - files: [contracts/oracles/ChainlinkRelayerFactory.sol, contracts/oracles/ChainlinkRelayerV1.sol] + options: + compiler: 0.8.18 diff --git a/.solhintignore b/.solhintignore index 10076a52..8ab70ec7 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1,3 +1,4 @@ # directories **/lib **/node_modules +ChainlinkRelayerFactory.sol \ No newline at end of file diff --git a/contracts/interfaces/IChainlinkRelayer.sol b/contracts/interfaces/IChainlinkRelayer.sol index b99b78d2..a360be25 100644 --- a/contracts/interfaces/IChainlinkRelayer.sol +++ b/contracts/interfaces/IChainlinkRelayer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.13 <0.8.19; +pragma solidity >=0.5.13 <0.9; interface IChainlinkRelayer { function rateFeedId() external returns (address); diff --git a/contracts/interfaces/IChainlinkRelayerFactory.sol b/contracts/interfaces/IChainlinkRelayerFactory.sol new file mode 100644 index 00000000..6b9c372f --- /dev/null +++ b/contracts/interfaces/IChainlinkRelayerFactory.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.5.13 <0.9; + +interface IChainlinkRelayerFactory { + /** + * @notice Emitted when a relayer is deployed. + * @param relayerAddress Address of the newly deployed relayer. + * @param rateFeedId Rate feed ID for which the relayer will report. + * @param chainlinkAggregator Address of the Chainlink aggregator the relayer will fetch prices from. + */ + event RelayerDeployed( + address indexed relayerAddress, + address indexed rateFeedId, + address indexed chainlinkAggregator + ); + + /** + * @notice Emitted when a relayer is removed. + * @param relayerAddress Address of the removed relayer. + * @param rateFeedId Rate feed ID for which the relayer reported. + */ + event RelayerRemoved(address indexed relayerAddress, address indexed rateFeedId); + + function initialize(address _sortedOracles) external; + + function sortedOracles() external returns (address); + + function deployRelayer(address rateFeedId, address chainlinkAggregator) external returns (address); + + function removeRelayer(address rateFeedId) external; + + function redeployRelayer(address rateFeedId, address chainlinkAggregator) external returns (address); + + function getRelayer(address rateFeedId) external view returns (address); + + function getRelayers() external view returns (address[] memory); +} diff --git a/contracts/oracles/ChainlinkRelayerFactory.sol b/contracts/oracles/ChainlinkRelayerFactory.sol new file mode 100644 index 00000000..a595ba59 --- /dev/null +++ b/contracts/oracles/ChainlinkRelayerFactory.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import { ChainlinkRelayerV1 } from "./ChainlinkRelayerV1.sol"; +import { IChainlinkRelayerFactory } from "../interfaces/IChainlinkRelayerFactory.sol"; + +/** + * @title ChainlinkRelayerFactory + * @notice The ChainlinkRelayerFactory creates and keeps track of ChainlinkRelayers. + * TODO: choose a proxy implementation and make this contract upgradeable + */ +contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable { + /// @notice Address of the SortedOracles contract. + address public sortedOracles; + + /// @notice Maps a rate feed ID to the relayer contract most recently deployed by this contract. + mapping(address rateFeedId => ChainlinkRelayerV1 relayer) public deployedRelayers; + + /** + * @notice List of rate feed IDs for which a relayer has been deployed. + * @dev Used to enumerate the `deployedRelayer` mapping. + */ + address[] public rateFeeds; + + /** + * @notice Thrown when trying to deploy a relayer to an address that already has code. + * @param contractAddress Address at which the relayer could not be deployed. + * @param rateFeedId Rate feed ID for which the relayer would have reported. + * @param chainlinkAggregator Address of the Chainlink aggregator the relayer would have fetched prices from. + */ + error ContractAlreadyExists(address contractAddress, address rateFeedId, address chainlinkAggregator); + + /** + * @notice Thrown when trying to deploy a relayer for a rate feed ID that already has a relayer. + * @param rateFeedId The rate feed ID for which a relayer already exists. + * @dev A new relayer *can* be deployed for the same rate feed ID, but only with a different + * chainlink aggregator or bytecode with `redeployRelayer`. + */ + error RelayerForFeedExists(address rateFeedId); + + /** + * @notice Thrown when the sanity check to verify the CREATE2 address computation fails. + * @param expectedAddress The address expected by local computation of the CREATE2 address. + * @param returnedAddress The address actually returned by CREATE2. + */ + error UnexpectedAddress(address expectedAddress, address returnedAddress); + + /** + * @notice Thrown when trying to remove a relayer for a rate feed ID that doesn't have a relayer. + * @param rateFeedId The rate feed ID. + */ + error NoRelayerForRateFeedId(address rateFeedId); + + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /** + * @notice Initializes the factory. + * @param _sortedOracles The SortedOracles instance deployed relayers should report to. + */ + function initialize(address _sortedOracles) external initializer { + __Ownable_init(); + sortedOracles = _sortedOracles; + } + + /** + * @notice Deploys a new relayer contract. + * @param rateFeedId The rate feed ID for which the relayer will report. + * @param chainlinkAggregator The Chainlink aggregator from which the relayer will fetch prices. + * @return relayerAddress The address of the newly deployed relayer contract. + */ + function deployRelayer( + address rateFeedId, + address chainlinkAggregator + ) public onlyOwner returns (address relayerAddress) { + address expectedAddress = computedRelayerAddress(rateFeedId, chainlinkAggregator); + + if (address(deployedRelayers[rateFeedId]) == expectedAddress || expectedAddress.code.length > 0) { + revert ContractAlreadyExists(expectedAddress, rateFeedId, chainlinkAggregator); + } + + if (address(deployedRelayers[rateFeedId]) != address(0)) { + revert RelayerForFeedExists(rateFeedId); + } + + bytes32 salt = _getSalt(); + ChainlinkRelayerV1 relayer = new ChainlinkRelayerV1{ salt: salt }(rateFeedId, sortedOracles, chainlinkAggregator); + + if (address(relayer) != expectedAddress) { + revert UnexpectedAddress(expectedAddress, address(relayer)); + } + + deployedRelayers[rateFeedId] = relayer; + rateFeeds.push(rateFeedId); + + emit RelayerDeployed(address(relayer), rateFeedId, chainlinkAggregator); + + return address(relayer); + } + + /** + * @notice Removes a relayer from the list of deployed relayers. + * @param rateFeedId The rate feed whose relayer should be removed. + */ + function removeRelayer(address rateFeedId) public onlyOwner { + address relayerAddress = address(deployedRelayers[rateFeedId]); + + if (relayerAddress == address(0)) { + revert NoRelayerForRateFeedId(rateFeedId); + } + + delete deployedRelayers[rateFeedId]; + + uint256 lastRateFeedIndex = rateFeeds.length - 1; + + for (uint256 i = 0; i <= lastRateFeedIndex; i++) { + if (rateFeeds[i] == rateFeedId) { + rateFeeds[i] = rateFeeds[lastRateFeedIndex]; + rateFeeds.pop(); + break; + } + } + + emit RelayerRemoved(relayerAddress, rateFeedId); + } + + /** + * @notice Removes the current relayer and redeploys a new one with a different + * Chainlink aggregator (and/or different bytecode if the factory + * has been upgraded since the last deployment of the relayer). + * @param rateFeedId The rate feed for which the relayer should be redeployed. + * @param chainlinkAggregator Address of the Chainlink aggregator the new relayer version will fetch prices from. + * @return relayerAddress The address of the newly deployed relayer contract. + */ + function redeployRelayer( + address rateFeedId, + address chainlinkAggregator + ) external onlyOwner returns (address relayerAddress) { + removeRelayer(rateFeedId); + return deployRelayer(rateFeedId, chainlinkAggregator); + } + + /** + * @notice Returns the address of the currently deployed relayer for a given rate feed ID. + * @param rateFeedId The rate feed ID whose relayer we want to get. + * @return relayerAddress Address of the relayer contract. + */ + function getRelayer(address rateFeedId) public view returns (address relayerAddress) { + return address(deployedRelayers[rateFeedId]); + } + + /** + * @notice Returns a list of all currently deployed relayers. + * @return relayerAddresses An array of all relayer contract addresses. + */ + function getRelayers() public view returns (address[] memory relayerAddresses) { + address[] memory relayers = new address[](rateFeeds.length); + for (uint256 i = 0; i < rateFeeds.length; i++) { + relayers[i] = address(deployedRelayers[rateFeeds[i]]); + } + return relayers; + } + + /** + * @notice Returns the salt used for CREATE2 deployment of relayer contracts. + * @return salt The `bytes32` constant `keccak256("mento.chainlinkRelayer")`. + * @dev We're using CREATE2 and all the data we want to use for address + * generation is included in the init code and constructor arguments, so a + * constant salt is enough. + */ + function _getSalt() internal pure returns (bytes32 salt) { + return keccak256("mento.chainlinkRelayer"); + } + + /** + * @notice Computes the expected CREATE2 address for given relayer parameters. + * @param rateFeedId The rate feed ID. + * @param chainlinkAggregator Address of the Chainlink aggregator. + * @dev See https://eips.ethereum.org/EIPS/eip-1014. + */ + function computedRelayerAddress(address rateFeedId, address chainlinkAggregator) public view returns (address) { + bytes32 salt = _getSalt(); + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + address(this), + salt, + keccak256( + abi.encodePacked( + type(ChainlinkRelayerV1).creationCode, + abi.encode(rateFeedId, sortedOracles, chainlinkAggregator) + ) + ) + ) + ) + ) + ) + ); + } +} diff --git a/contracts/oracles/ChainlinkRelayerV1.sol b/contracts/oracles/ChainlinkRelayerV1.sol index 1230d7dd..a2ca2987 100644 --- a/contracts/oracles/ChainlinkRelayerV1.sol +++ b/contracts/oracles/ChainlinkRelayerV1.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity 0.8.18; import "../interfaces/IChainlinkRelayer.sol"; import "foundry-chainlink-toolkit/src/interfaces/feeds/AggregatorV3Interface.sol"; @@ -34,35 +34,27 @@ interface ISortedOraclesMin { */ contract ChainlinkRelayerV1 is IChainlinkRelayer { /** - * @notice The number of digits after the decimal point in FixidityLib - * values, as used by SortedOracles. + * @notice The number of digits after the decimal point in FixidityLib values, as used by SortedOracles. * @dev See contracts/common/FixidityLib.sol */ uint256 public constant FIXIDITY_DECIMALS = 24; + /// @notice The rateFeedId this relayer relays for. address public immutable rateFeedId; + /// @notice The address of the SortedOracles contract to report to. address public immutable sortedOracles; - /** - * @notice The address of the Chainlink aggregator this contract fetches - * data from. - */ + + /// @notice The address of the Chainlink aggregator this contract fetches data from. address public immutable chainlinkAggregator; - /** - * @notice Used when a new price's timestamp is not newer than the most recent - * SortedOracles timestamp. - */ + /// @notice Used when a new price's timestamp is not newer than the most recent SortedOracles timestamp. error TimestampNotNew(); - /** - * @notice Used when a new price's timestamp would be considered expired by - * SortedOracles. - */ + + /// @notice Used when a new price's timestamp would be considered expired by SortedOracles. error ExpiredTimestamp(); - /** - * @notice Used when a negative price is returned by the Chainlink - * aggregator. - */ + + /// @notice Used when a negative price is returned by the Chainlink aggregator. error NegativePrice(); /** @@ -82,15 +74,11 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { } /** - * @notice Relays data from the configured Chainlink aggregator to - * SortedOracles. - * @dev Checks the price is non-negative (Chainlink uses `int256` rather - * than `uint256`. - * @dev Converts the price to a Fixidity value, as expected by - * SortedOracles. + * @notice Relays data from the configured Chainlink aggregator to SortedOracles. + * @dev Checks the price is non-negative (Chainlink uses `int256` rather than `uint256`. + * @dev Converts the price to a Fixidity value, as expected by SortedOracles. * @dev Performs checks on the timestamp, will revert if any fails: - * - The timestamp should be strictly newer than the most recent - * timestamp in SortedOracles. + * - The timestamp should be strictly newer than the most recent timestamp in SortedOracles. * - The timestamp should not be considered expired by SortedOracles. */ function relay() external { @@ -105,7 +93,7 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { } } - if (isTimestampExpired(timestamp)) { + if (_isTimestampExpired(timestamp)) { revert ExpiredTimestamp(); } @@ -113,7 +101,7 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { revert NegativePrice(); } - uint256 report = chainlinkToFixidity(price); + uint256 report = _chainlinkToFixidity(price); // This contract is built for a setup where it is the only reporter for the // given `rateFeedId`. As such, we don't need to compute and provide @@ -123,12 +111,11 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { } /** - * @notice Checks if a Chainlink price's timestamp would be expired in - * SortedOracles. + * @notice Checks if a Chainlink price's timestamp would be expired in SortedOracles. * @param timestamp The timestamp returned by the Chainlink aggregator. * @return `true` if expired based on SortedOracles expiry parameter. */ - function isTimestampExpired(uint256 timestamp) internal view returns (bool) { + function _isTimestampExpired(uint256 timestamp) internal view returns (bool) { return block.timestamp - timestamp >= ISortedOraclesMin(sortedOracles).getTokenReportExpirySeconds(rateFeedId); } @@ -137,7 +124,7 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @param price An price from the Chainlink aggregator. * @return The converted Fixidity value (with 24 decimals). */ - function chainlinkToFixidity(int256 price) internal view returns (uint256) { + function _chainlinkToFixidity(int256 price) internal view returns (uint256) { uint256 chainlinkDecimals = uint256(AggregatorV3Interface(chainlinkAggregator).decimals()); return uint256(price) * 10**(FIXIDITY_DECIMALS - chainlinkDecimals); } diff --git a/foundry.toml b/foundry.toml index 230d2233..2a9ca20c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,7 +10,7 @@ gas_reports = ["*"] optimizer = true optimizer_runs = 10_000 legacy = true -no_match_contract = "ForkTest" +no_match_contract = "(ForkTest)|(GovernanceGasTest)" via_ir = true [profile.ci] diff --git a/test/oracles/ChainlinkRelayer.t.sol b/test/oracles/ChainlinkRelayer.t.sol index 5abf292d..c91ae3e0 100644 --- a/test/oracles/ChainlinkRelayer.t.sol +++ b/test/oracles/ChainlinkRelayer.t.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, private-vars-leading-underscore +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase, one-contract-per-file +pragma solidity ^0.5.17; -import "../utils/BaseTest.t.sol"; -import "contracts/common/SortedOracles.sol"; -import "../mocks/MockAggregatorV3.sol"; -import "contracts/interfaces/IChainlinkRelayer.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { SortedOracles } from "contracts/common/SortedOracles.sol"; +import { MockAggregatorV3 } from "../mocks/MockAggregatorV3.sol"; +import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; contract ChainlinkRelayerTest is BaseTest { bytes constant TIMESTAMP_NOT_NEW_ERROR = abi.encodeWithSignature("TimestampNotNew()"); @@ -16,7 +15,7 @@ contract ChainlinkRelayerTest is BaseTest { SortedOracles sortedOracles; MockAggregatorV3 chainlinkAggregator; IChainlinkRelayer relayer; - address rateFeedId = address(0xbeef); + address rateFeedId = actor("rateFeed"); int256 aPrice = 420000000; uint256 expectedReport = 4200000000000000000000000; uint256 aReport = 4100000000000000000000000; diff --git a/test/oracles/ChainlinkRelayerFactory.t.sol b/test/oracles/ChainlinkRelayerFactory.t.sol new file mode 100644 index 00000000..f6b02fa1 --- /dev/null +++ b/test/oracles/ChainlinkRelayerFactory.t.sol @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, private-vars-leading-underscore +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase, one-contract-per-file +pragma solidity ^0.5.17; + +import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { IChainlinkRelayerFactory } from "contracts/interfaces/IChainlinkRelayerFactory.sol"; +import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; + +contract ChainlinkRelayerFactoryTest is BaseTest { + IChainlinkRelayerFactory relayerFactory; + address owner = actor("owner"); + address nonOwner = actor("nonOwner"); + address mockSortedOracles = actor("sortedOracles"); + address[3] mockAggregators = [actor("aggregator1"), actor("aggregator2"), actor("aggregator3")]; + address[3] rateFeeds = [actor("rateFeed1"), actor("rateFeed2"), actor("rateFeed3")]; + address mockAggregator = mockAggregators[0]; + address aRateFeed = rateFeeds[0]; + + event RelayerDeployed( + address indexed relayerAddress, + address indexed rateFeedId, + address indexed chainlinkAggregator + ); + event RelayerRemoved(address indexed relayerAddress, address indexed rateFeedId); + + function setUp() public { + relayerFactory = IChainlinkRelayerFactory(factory.createContract("ChainlinkRelayerFactory", abi.encode(false))); + vm.prank(owner); + relayerFactory.initialize(mockSortedOracles); + } + + function expectedRelayerAddress( + address rateFeedId, + address sortedOracles, + address chainlinkAggregator, + address relayerFactoryAddress + ) public returns (address expectedAddress) { + bytes32 salt = keccak256("mento.chainlinkRelayer"); + return + address( + uint160( + uint256( + keccak256( + abi.encodePacked( + bytes1(0xff), + relayerFactoryAddress, + salt, + keccak256( + abi.encodePacked( + vm.getCode(factory.contractPath("ChainlinkRelayerV1")), + abi.encode(rateFeedId, sortedOracles, chainlinkAggregator) + ) + ) + ) + ) + ) + ) + ); + } + + function contractAlreadyExistsError( + address relayerAddress, + address rateFeedId, + address aggregator + ) public pure returns (bytes memory ContractAlreadyExistsError) { + return + abi.encodeWithSignature("ContractAlreadyExists(address,address,address)", relayerAddress, rateFeedId, aggregator); + } + + function relayerForFeedExistsError(address rateFeedId) public pure returns (bytes memory RelayerForFeedExistsError) { + return abi.encodeWithSignature("RelayerForFeedExists(address)", rateFeedId); + } + + function noRelayerForRateFeedId(address rateFeedId) public pure returns (bytes memory NoRelayerForRateFeedIdError) { + return abi.encodeWithSignature("NoRelayerForRateFeedId(address)", rateFeedId); + } +} + +contract ChainlinkRelayerFactoryTest_initialize is ChainlinkRelayerFactoryTest { + function test_setsSortedOracles() public { + address realSortedOracles = relayerFactory.sortedOracles(); + assertEq(realSortedOracles, mockSortedOracles); + } + + function test_setsOwner() public { + address realOwner = Ownable(address(relayerFactory)).owner(); + assertEq(realOwner, owner); + } +} + +contract ChainlinkRelayerFactoryTest_transferOwnership is ChainlinkRelayerFactoryTest { + function test_setsNewOwner() public { + vm.prank(owner); + Ownable(address(relayerFactory)).transferOwnership(nonOwner); + address realOwner = Ownable(address(relayerFactory)).owner(); + assertEq(realOwner, nonOwner); + } + + function test_failsWhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + Ownable(address(relayerFactory)).transferOwnership(nonOwner); + } +} + +contract ChainlinkRelayerFactoryTest_renounceOwnership is ChainlinkRelayerFactoryTest { + function test_setsOwnerToZeroAddress() public { + vm.prank(owner); + Ownable(address(relayerFactory)).renounceOwnership(); + address realOwner = Ownable(address(relayerFactory)).owner(); + assertEq(realOwner, address(0)); + } + + function test_failsWhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + Ownable(address(relayerFactory)).renounceOwnership(); + } +} + +contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTest { + function test_setsRateFeed() public { + vm.prank(owner); + IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, mockAggregator)); + + address rateFeed = relayer.rateFeedId(); + assertEq(rateFeed, aRateFeed); + } + + function test_setsAggregator() public { + vm.prank(owner); + IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, mockAggregator)); + + address aggregator = relayer.chainlinkAggregator(); + assertEq(aggregator, mockAggregator); + } + + function test_setsSortedOracles() public { + vm.prank(owner); + IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, mockAggregator)); + + address sortedOracles = relayer.sortedOracles(); + assertEq(sortedOracles, mockSortedOracles); + } + + function test_deploysToTheCorrectAddress() public { + vm.prank(owner); + address relayer = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + + address expectedAddress = expectedRelayerAddress({ + rateFeedId: aRateFeed, + sortedOracles: mockSortedOracles, + chainlinkAggregator: mockAggregator, + relayerFactoryAddress: address(relayerFactory) + }); + + assertEq(relayer, expectedAddress); + } + + function test_emitsRelayerDeployedEvent() public { + address expectedAddress = expectedRelayerAddress( + aRateFeed, + mockSortedOracles, + mockAggregator, + address(relayerFactory) + ); + // solhint-disable-next-line func-named-parameters + vm.expectEmit(true, true, true, false, address(relayerFactory)); + emit RelayerDeployed({ + relayerAddress: expectedAddress, + rateFeedId: aRateFeed, + chainlinkAggregator: mockAggregator + }); + vm.prank(owner); + relayerFactory.deployRelayer(aRateFeed, mockAggregator); + } + + function test_remembersTheRelayerAddress() public { + vm.prank(owner); + address relayer = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + address storedAddress = relayerFactory.getRelayer(aRateFeed); + assertEq(storedAddress, relayer); + } + + function test_revertsWhenDeployingTheSameRelayer() public { + vm.prank(owner); + address relayer = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + vm.expectRevert(contractAlreadyExistsError(relayer, aRateFeed, mockAggregator)); + vm.prank(owner); + relayerFactory.deployRelayer(aRateFeed, mockAggregator); + } + + function test_revertsWhenDeployingForTheSameRateFeed() public { + vm.prank(owner); + relayerFactory.deployRelayer(aRateFeed, mockAggregators[0]); + vm.expectRevert(relayerForFeedExistsError(aRateFeed)); + vm.prank(owner); + relayerFactory.deployRelayer(aRateFeed, mockAggregators[1]); + } + + function test_revertsWhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + relayerFactory.deployRelayer(aRateFeed, mockAggregator); + } +} + +contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest { + function test_emptyWhenNoRelayers() public { + address[] memory relayers = relayerFactory.getRelayers(); + assertEq(relayers.length, 0); + } + + function test_returnsRelayerWhenThereIsOne() public { + vm.prank(owner); + address relayerAddress = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + address[] memory relayers = relayerFactory.getRelayers(); + assertEq(relayers.length, 1); + assertEq(relayers[0], relayerAddress); + } + + function test_returnsMultipleRelayersWhenThereAreMore() public { + vm.prank(owner); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], mockAggregators[0]); + vm.prank(owner); + address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + vm.prank(owner); + address relayerAddress3 = relayerFactory.deployRelayer(rateFeeds[2], mockAggregators[2]); + address[] memory relayers = relayerFactory.getRelayers(); + assertEq(relayers.length, 3); + assertEq(relayers[0], relayerAddress1); + assertEq(relayers[1], relayerAddress2); + assertEq(relayers[2], relayerAddress3); + } + + function test_returnsADifferentRelayerAfterRedeployment() public { + vm.prank(owner); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], mockAggregators[0]); + vm.prank(owner); + relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + vm.prank(owner); + address relayerAddress2 = relayerFactory.redeployRelayer(rateFeeds[1], mockAggregators[2]); + address[] memory relayers = relayerFactory.getRelayers(); + assertEq(relayers.length, 2); + assertEq(relayers[0], relayerAddress1); + assertEq(relayers[1], relayerAddress2); + } + + function test_doesntReturnARemovedRelayer() public { + vm.prank(owner); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], mockAggregators[0]); + vm.prank(owner); + address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + vm.prank(owner); + relayerFactory.deployRelayer(rateFeeds[2], mockAggregators[2]); + vm.prank(owner); + relayerFactory.removeRelayer(rateFeeds[2]); + address[] memory relayers = relayerFactory.getRelayers(); + assertEq(relayers.length, 2); + assertEq(relayers[0], relayerAddress1); + assertEq(relayers[1], relayerAddress2); + } +} + +contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTest { + address relayerAddress; + + function setUp() public { + super.setUp(); + + vm.prank(owner); + relayerAddress = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + } + + function test_removesTheRelayer() public { + vm.prank(owner); + relayerFactory.removeRelayer(aRateFeed); + vm.prank(owner); + address relayer = relayerFactory.getRelayer(aRateFeed); + assertEq(relayer, address(0)); + } + + function test_emitsRelayerRemovedEvent() public { + // solhint-disable-next-line func-named-parameters + vm.expectEmit(true, true, true, false, address(relayerFactory)); + emit RelayerRemoved({ relayerAddress: relayerAddress, rateFeedId: aRateFeed }); + vm.prank(owner); + relayerFactory.removeRelayer(aRateFeed); + } + + function test_doesntRemoveOtherRelayers() public { + vm.prank(owner); + address newRelayerAddress = relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + vm.prank(owner); + relayerFactory.removeRelayer(aRateFeed); + address[] memory relayers = relayerFactory.getRelayers(); + + assertEq(relayers.length, 1); + assertEq(relayers[0], newRelayerAddress); + } + + function test_revertsOnNonexistentRelayer() public { + vm.expectRevert(noRelayerForRateFeedId(rateFeeds[1])); + vm.prank(owner); + relayerFactory.removeRelayer(rateFeeds[1]); + } + + function test_revertsWhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + relayerFactory.removeRelayer(aRateFeed); + } +} + +contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryTest { + address oldAddress; + + function setUp() public { + super.setUp(); + vm.prank(owner); + oldAddress = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + } + + function test_setsRateFeedOnNewRelayer() public { + vm.prank(owner); + IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1])); + + address rateFeed = relayer.rateFeedId(); + assertEq(rateFeed, aRateFeed); + } + + function test_setsAggregatorOnNewRelayer() public { + vm.prank(owner); + IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1])); + + address aggregator = relayer.chainlinkAggregator(); + assertEq(aggregator, mockAggregators[1]); + } + + function test_setsSortedOraclesOnNewRelayer() public { + vm.prank(owner); + IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1])); + + address sortedOracles = relayer.sortedOracles(); + assertEq(sortedOracles, mockSortedOracles); + } + + function test_deploysToTheCorrectNewAddress() public { + vm.prank(owner); + address relayer = relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + + address expectedAddress = expectedRelayerAddress( + aRateFeed, + mockSortedOracles, + mockAggregators[1], + address(relayerFactory) + ); + + assertEq(relayer, expectedAddress); + } + + function test_emitsRelayerRemovedAndDeployedEvents() public { + address expectedAddress = expectedRelayerAddress({ + rateFeedId: aRateFeed, + sortedOracles: mockSortedOracles, + chainlinkAggregator: mockAggregators[1], + relayerFactoryAddress: address(relayerFactory) + }); + // solhint-disable-next-line func-named-parameters + vm.expectEmit(true, true, true, false, address(relayerFactory)); + emit RelayerRemoved({ relayerAddress: oldAddress, rateFeedId: aRateFeed }); + // solhint-disable-next-line func-named-parameters + vm.expectEmit(true, true, true, false, address(relayerFactory)); + emit RelayerDeployed({ + relayerAddress: expectedAddress, + rateFeedId: aRateFeed, + chainlinkAggregator: mockAggregators[1] + }); + vm.prank(owner); + relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + } + + function test_remembersTheNewRelayerAddress() public { + vm.prank(owner); + address relayer = relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + address storedAddress = relayerFactory.getRelayer(aRateFeed); + assertEq(storedAddress, relayer); + } + + function test_revertsWhenDeployingTheSameExactRelayer() public { + vm.expectRevert(contractAlreadyExistsError(oldAddress, aRateFeed, mockAggregator)); + vm.prank(owner); + relayerFactory.redeployRelayer(aRateFeed, mockAggregator); + } + + function test_revertsWhenCalledByNonOwner() public { + vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(nonOwner); + relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + } +} diff --git a/test/utils/Factory.sol b/test/utils/Factory.sol index d4c51e61..51c8d3dc 100644 --- a/test/utils/Factory.sol +++ b/test/utils/Factory.sol @@ -32,12 +32,16 @@ contract Factory { } function createContract(string memory _contract, bytes memory args) public returns (address addr) { - string memory path = string(abi.encodePacked("out/", _contract, ".sol", "/", _contract, ".json")); + string memory path = contractPath(_contract); addr = createFromPath(path, args); console.log("Deployed %s to %s", _contract, addr); return addr; } + function contractPath(string memory _contract) public pure returns (string memory) { + return string(abi.encodePacked("out/", _contract, ".sol", "/", _contract, ".json")); + } + function createAt( string memory _contract, address dest, From bf70520831ecc53868d4baf8ae528bc3fd84d723 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 8 Aug 2024 11:51:32 +0200 Subject: [PATCH 05/38] feat: proxy ChainlinkRelayerFactory (#481) ### Description This PR sets up upgradeability for the ChainlinkRelayerFactory (see #476). TODO: * [x] integration test of the ChainlinkRelayerFactory ### Tested Integration tests that: - Setup a proxied ChainlinkRelayerFactory - Ensure the TransparentUpgradeableProxy/ProxyAdmin pattern works correctly - Sets up a new rate feed with an enabled circuit breaker - Tests that price reports coming from the relayer correctly trigger/reset the circuit breaker. ### Related issues - Fixes #459 --------- Co-authored-by: chapati Co-authored-by: bowd Co-authored-by: boqdan <304771+bowd@users.noreply.github.com> Co-authored-by: baroooo --- .prettierrc.yml | 2 +- .../interfaces/IChainlinkRelayerFactory.sol | 2 + contracts/interfaces/IProxyAdmin.sol | 8 + contracts/interfaces/ITransparentProxy.sol | 12 ++ contracts/oracles/ChainlinkRelayerFactory.sol | 9 +- .../oracles/ChainlinkRelayerFactoryProxy.sol | 15 ++ .../ChainlinkRelayerFactoryProxyAdmin.sol | 6 + .../ChainlinkRelayerIntegration.t.sol | 202 ++++++++++++++++++ 8 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 contracts/interfaces/IProxyAdmin.sol create mode 100644 contracts/interfaces/ITransparentProxy.sol create mode 100644 contracts/oracles/ChainlinkRelayerFactoryProxy.sol create mode 100644 contracts/oracles/ChainlinkRelayerFactoryProxyAdmin.sol create mode 100644 test/integration/ChainlinkRelayerIntegration.t.sol diff --git a/.prettierrc.yml b/.prettierrc.yml index a28a3534..1b16eb34 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -24,6 +24,6 @@ overrides: - files: [test/**/*.sol] options: compiler: "" - - files: [contracts/oracles/ChainlinkRelayerFactory.sol, contracts/oracles/ChainlinkRelayerV1.sol] + - files: [contracts/oracles/Chainlink*.sol] options: compiler: 0.8.18 diff --git a/contracts/interfaces/IChainlinkRelayerFactory.sol b/contracts/interfaces/IChainlinkRelayerFactory.sol index 6b9c372f..368c6f2c 100644 --- a/contracts/interfaces/IChainlinkRelayerFactory.sol +++ b/contracts/interfaces/IChainlinkRelayerFactory.sol @@ -34,4 +34,6 @@ interface IChainlinkRelayerFactory { function getRelayer(address rateFeedId) external view returns (address); function getRelayers() external view returns (address[] memory); + + function computedRelayerAddress(address rateFeedId, address chainlinkAggregator) external returns (address); } diff --git a/contracts/interfaces/IProxyAdmin.sol b/contracts/interfaces/IProxyAdmin.sol new file mode 100644 index 00000000..b7c69481 --- /dev/null +++ b/contracts/interfaces/IProxyAdmin.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.5.13 <0.8.19; + +interface IProxyAdmin { + function getProxyImplementation(address proxy) external view returns (address); + + function getProxyAdmin(address proxy) external view returns (address); +} diff --git a/contracts/interfaces/ITransparentProxy.sol b/contracts/interfaces/ITransparentProxy.sol new file mode 100644 index 00000000..ab3c0aa9 --- /dev/null +++ b/contracts/interfaces/ITransparentProxy.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >=0.5.13 <0.8.19; + +interface ITransparentProxy { + function implementation() external view returns (address); + + function changeAdmin(address) external; + + function upgradeTo(address) external; + + function upgradeToAndCall(address, bytes calldata) external payable; +} diff --git a/contracts/oracles/ChainlinkRelayerFactory.sol b/contracts/oracles/ChainlinkRelayerFactory.sol index a595ba59..a4a7e37f 100644 --- a/contracts/oracles/ChainlinkRelayerFactory.sol +++ b/contracts/oracles/ChainlinkRelayerFactory.sol @@ -8,7 +8,6 @@ import { IChainlinkRelayerFactory } from "../interfaces/IChainlinkRelayerFactory /** * @title ChainlinkRelayerFactory * @notice The ChainlinkRelayerFactory creates and keeps track of ChainlinkRelayers. - * TODO: choose a proxy implementation and make this contract upgradeable */ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable { /// @notice Address of the SortedOracles contract. @@ -52,6 +51,14 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable */ error NoRelayerForRateFeedId(address rateFeedId); + /** + * @notice Constructor for the logic contract. + * @param disable If `true`, disables the initializer. + * @dev This contract is meant to be deployed with an upgradeable proxy in + * front of it. Set `disable` to `true` in production environments to disable + * contract initialization on the logic contract, only allowing initialization + * on the proxy. + */ constructor(bool disable) { if (disable) { _disableInitializers(); diff --git a/contracts/oracles/ChainlinkRelayerFactoryProxy.sol b/contracts/oracles/ChainlinkRelayerFactoryProxy.sol new file mode 100644 index 00000000..dafd2273 --- /dev/null +++ b/contracts/oracles/ChainlinkRelayerFactoryProxy.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.18; + +// solhint-disable max-line-length +import { + TransparentUpgradeableProxy +} from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract ChainlinkRelayerFactoryProxy is TransparentUpgradeableProxy { + constructor( + address _logic, + address admin_, + bytes memory _data + ) payable TransparentUpgradeableProxy(_logic, admin_, _data) {} +} diff --git a/contracts/oracles/ChainlinkRelayerFactoryProxyAdmin.sol b/contracts/oracles/ChainlinkRelayerFactoryProxyAdmin.sol new file mode 100644 index 00000000..13c16e7c --- /dev/null +++ b/contracts/oracles/ChainlinkRelayerFactoryProxyAdmin.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.18; + +import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract ChainlinkRelayerFactoryProxyAdmin is ProxyAdmin {} diff --git a/test/integration/ChainlinkRelayerIntegration.t.sol b/test/integration/ChainlinkRelayerIntegration.t.sol new file mode 100644 index 00000000..609dc202 --- /dev/null +++ b/test/integration/ChainlinkRelayerIntegration.t.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import { Ownable } from "openzeppelin-contracts/ownership/Ownable.sol"; + +import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; +import { MockAggregatorV3 } from "../mocks/MockAggregatorV3.sol"; + +import { IChainlinkRelayerFactory } from "contracts/interfaces/IChainlinkRelayerFactory.sol"; +import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; +import { IProxyAdmin } from "contracts/interfaces/IProxyAdmin.sol"; +import { ITransparentProxy } from "contracts/interfaces/ITransparentProxy.sol"; + +contract ChainlinkRelayerIntegration is IntegrationTest { + address owner = actor("owner"); + + IChainlinkRelayerFactory relayerFactoryImplementation; + IChainlinkRelayerFactory relayerFactory; + IProxyAdmin proxyAdmin; + ITransparentProxy proxy; + + function setUp() public { + IntegrationTest.setUp(); + + proxyAdmin = IProxyAdmin(factory.createContract("ChainlinkRelayerFactoryProxyAdmin", "")); + relayerFactoryImplementation = IChainlinkRelayerFactory( + factory.createContract("ChainlinkRelayerFactory", abi.encode(true)) + ); + proxy = ITransparentProxy( + factory.createContract( + "ChainlinkRelayerFactoryProxy", + abi.encode( + address(relayerFactoryImplementation), + address(proxyAdmin), + abi.encodeWithSignature("initialize(address)", address(sortedOracles)) + ) + ) + ); + relayerFactory = IChainlinkRelayerFactory(address(proxy)); + vm.startPrank(address(factory)); + Ownable(address(proxyAdmin)).transferOwnership(owner); + Ownable(address(relayerFactory)).transferOwnership(owner); + vm.stopPrank(); + } +} + +contract ChainlinkRelayerIntegration_ProxySetup is ChainlinkRelayerIntegration { + function test_proxyOwnedByAdmin() public { + address admin = proxyAdmin.getProxyAdmin(address(proxy)); + assertEq(admin, address(proxyAdmin)); + } + + function test_adminOwnedByOwner() public { + address realOwner = Ownable(address(proxyAdmin)).owner(); + assertEq(realOwner, owner); + } + + function test_adminCantCallImplementation() public { + vm.prank(address(proxyAdmin)); + vm.expectRevert("TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + relayerFactory.sortedOracles(); + } + + function test_nonAdminCantCallProxy() public { + vm.prank(owner); + vm.expectRevert(); + proxy.implementation(); + } + + function test_implementationOwnedByOwner() public { + address realOwner = Ownable(address(relayerFactory)).owner(); + assertEq(realOwner, owner); + } + + function test_implementationSetCorrectly() public { + address implementation = proxyAdmin.getProxyImplementation(address(proxy)); + assertEq(implementation, address(relayerFactoryImplementation)); + } + + function test_implementationNotInitializable() public { + vm.expectRevert("Initializable: contract is already initialized"); + relayerFactoryImplementation.initialize(address(sortedOracles)); + } +} + +contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelayerIntegration { + // Fictional rate feed ID + address rateFeedId = address(bytes20(keccak256(("cUSD/FOO")))); + + MockAggregatorV3 chainlinkAggregator; + IChainlinkRelayer chainlinkRelayer; + + function setUp() public { + super.setUp(); + + setUpRelayer(); + setUpCircuitBreaker(); + } + + function setUpRelayer() public { + chainlinkAggregator = new MockAggregatorV3(); + vm.prank(owner); + chainlinkRelayer = IChainlinkRelayer(relayerFactory.deployRelayer(rateFeedId, address(chainlinkAggregator))); + + vm.prank(deployer); + sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer)); + } + + function setUpCircuitBreaker() public { + setUpBreakerBox(); + setUpBreaker(); + } + + function setUpBreakerBox() public { + vm.startPrank(deployer); + breakerBox.addRateFeed(rateFeedId); + breakerBox.toggleBreaker(address(valueDeltaBreaker), rateFeedId, true); + vm.stopPrank(); + } + + function setUpBreaker() public { + address[] memory rateFeeds = new address[](1); + rateFeeds[0] = rateFeedId; + uint256[] memory thresholds = new uint256[](1); + thresholds[0] = 10**23; // 10% + uint256[] memory cooldownTimes = new uint256[](1); + cooldownTimes[0] = 1 minutes; + uint256[] memory referenceValues = new uint256[](1); + referenceValues[0] = 10**24; + + vm.startPrank(deployer); + valueDeltaBreaker.setRateChangeThresholds(rateFeeds, thresholds); + valueDeltaBreaker.setCooldownTimes(rateFeeds, cooldownTimes); + valueDeltaBreaker.setReferenceValues(rateFeeds, referenceValues); + vm.stopPrank(); + } + + function test_initiallyNoPrice() public { + (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); + uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(price, 0); + assertEq(denominator, 0); + assertEq(uint256(tradingMode), 0); + } + + function test_passesPriceFromAggregatorToSortedOracles() public { + chainlinkAggregator.setRoundData(10**8, block.timestamp - 1); + chainlinkRelayer.relay(); + (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); + uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(price, 10**24); + assertEq(denominator, 10**24); + assertEq(uint256(tradingMode), 0); + } + + function test_whenPriceBeyondThresholdIsRelayed_breakerShouldTrigger() public { + chainlinkAggregator.setRoundData(12 * 10**7, block.timestamp - 1); + chainlinkRelayer.relay(); + (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); + uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(price, 12 * 10**23); + assertEq(denominator, 10**24); + assertEq(uint256(tradingMode), 3); + } + + function test_whenPriceBeyondThresholdIsRelayedThenRecovers_breakerShouldTriggerThenRecover() public { + chainlinkAggregator.setRoundData(12 * 10**7, block.timestamp - 1); + chainlinkRelayer.relay(); + uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(uint256(tradingMode), 3); + + vm.warp(now + 1 minutes + 1); + + chainlinkAggregator.setRoundData(105 * 10**6, block.timestamp - 1); + chainlinkRelayer.relay(); + (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(price, 105 * 10**22); + assertEq(denominator, 10**24); + assertEq(uint256(tradingMode), 0); + } + + function test_whenPriceBeyondThresholdIsRelayedAndCooldownIsntReached_breakerShouldTriggerAndNotRecover() public { + chainlinkAggregator.setRoundData(12 * 10**7, block.timestamp - 1); + chainlinkRelayer.relay(); + uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(uint256(tradingMode), 3); + + vm.warp(now + 1 minutes - 1); + + chainlinkAggregator.setRoundData(105 * 10**6, block.timestamp - 1); + chainlinkRelayer.relay(); + (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); + assertEq(price, 105 * 10**22); + assertEq(denominator, 10**24); + assertEq(uint256(tradingMode), 3); + } +} From 31e5f017987f76fbf3451afd264fa62ae153e064 Mon Sep 17 00:00:00 2001 From: Denvil J Clarke <60730266+denviljclarke@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:01:44 +0100 Subject: [PATCH 06/38] feat: add PUSO + remove cPHP proxy contract (#497) adds the PSO Proxy contract + removes PHP --- .../{StableTokenPHPProxy.sol => StableTokenPSOProxy.sol} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename contracts/legacy/proxies/{StableTokenPHPProxy.sol => StableTokenPSOProxy.sol} (79%) diff --git a/contracts/legacy/proxies/StableTokenPHPProxy.sol b/contracts/legacy/proxies/StableTokenPSOProxy.sol similarity index 79% rename from contracts/legacy/proxies/StableTokenPHPProxy.sol rename to contracts/legacy/proxies/StableTokenPSOProxy.sol index 976a488b..40c00c7d 100644 --- a/contracts/legacy/proxies/StableTokenPHPProxy.sol +++ b/contracts/legacy/proxies/StableTokenPSOProxy.sol @@ -4,6 +4,6 @@ pragma solidity ^0.5.13; import "../../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenPHPProxy is Proxy { +contract StableTokenPSOProxy is Proxy { } From 38dae010e7f69e50ec20ff4bbd1314a4f58f6f28 Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:14:47 +0200 Subject: [PATCH 07/38] Feat: Make ChainlinkRelayer support implicit pairs (#487) ### Description Late last week it became apparent that Chainlink won't provide all derived rates that we need, and that we will need to compute implicit rates on-chain. This PR extends the relayer such that it can aggregate up to 4 chainlink date feeds into a single price. Each data feed can be inverted (1 / p) to make the dominos line up. ### Other changes Upgraded the tests to solidity 0.8 and fixed the issue with deploying SortedOracles via the factory by pre-deploying libraries as well because then factory links them automatically when calling `vm.getCode`. ### Tested Tests have been extended, and I've also used a slightly new pattern that I didn't use before which I like, and might not be evident from reading the code. Specifically for the relayer we have 4 scenarios depending on how many price feeds it aggregates, so I wrote test contracts that extend each other in such a way where test cases from N-1 price feeds get executed also in the test for N price feeds. ### Related issues N/A ### Backwards compatibility Not backwards compatible but not deployed yet. ### Documentation N/A --------- Co-authored-by: Marcin Chrzanowski Co-authored-by: Martin Co-authored-by: chapati Co-authored-by: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Co-authored-by: baroooo --- .gitmodules | 4 + contracts/interfaces/IChainlinkRelayer.sol | 15 +- .../interfaces/IChainlinkRelayerFactory.sol | 27 +- contracts/oracles/ChainlinkRelayerFactory.sol | 57 +- contracts/oracles/ChainlinkRelayerV1.sol | 269 ++++++- lib/prb-math | 1 + remappings.txt | 3 +- .../ChainlinkRelayerIntegration.t.sol | 59 +- test/mocks/MockAggregatorV3.sol | 11 +- test/oracles/ChainlinkRelayer.t.sol | 115 --- test/oracles/ChainlinkRelayerFactory.t.sol | 232 +++--- test/oracles/ChainlinkRelayerV1.t.sol | 697 ++++++++++++++++++ test/utils/BaseTest.next.sol | 8 +- test/utils/BaseTest.t.sol | 6 + test/utils/Factory.sol | 20 + 15 files changed, 1254 insertions(+), 270 deletions(-) create mode 160000 lib/prb-math delete mode 100644 test/oracles/ChainlinkRelayer.t.sol create mode 100644 test/oracles/ChainlinkRelayerV1.t.sol diff --git a/.gitmodules b/.gitmodules index 498e8d0f..fb49d1d4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,7 @@ [submodule "lib/foundry-chainlink-toolkit"] path = lib/foundry-chainlink-toolkit url = https://github.com/smartcontractkit/foundry-chainlink-toolkit +[submodule "lib/prb-math"] + branch = "release-v4" + path = lib/prb-math + url = https://github.com/PaulRBerg/prb-math diff --git a/contracts/interfaces/IChainlinkRelayer.sol b/contracts/interfaces/IChainlinkRelayer.sol index a360be25..47fdd1b5 100644 --- a/contracts/interfaces/IChainlinkRelayer.sol +++ b/contracts/interfaces/IChainlinkRelayer.sol @@ -1,12 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.5.13 <0.9; +pragma experimental ABIEncoderV2; interface IChainlinkRelayer { + /** + * @notice Struct used to represent a segment in the price path. + * @member aggregator The address of the Chainlink aggregator. + * @member invert Wether to invert the aggregator's price feed, i.e. convert CELO/USD to USD/CELO. + */ + struct ChainlinkAggregator { + address aggregator; + bool invert; + } + function rateFeedId() external returns (address); + function rateFeedDescription() external returns (string memory); + function sortedOracles() external returns (address); - function chainlinkAggregator() external returns (address); + function getAggregators() external returns (ChainlinkAggregator[] memory); function relay() external; } diff --git a/contracts/interfaces/IChainlinkRelayerFactory.sol b/contracts/interfaces/IChainlinkRelayerFactory.sol index 368c6f2c..2cac2d99 100644 --- a/contracts/interfaces/IChainlinkRelayerFactory.sol +++ b/contracts/interfaces/IChainlinkRelayerFactory.sol @@ -1,17 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity >=0.5.13 <0.9; +pragma experimental ABIEncoderV2; + +import "./IChainlinkRelayer.sol"; interface IChainlinkRelayerFactory { /** * @notice Emitted when a relayer is deployed. * @param relayerAddress Address of the newly deployed relayer. * @param rateFeedId Rate feed ID for which the relayer will report. - * @param chainlinkAggregator Address of the Chainlink aggregator the relayer will fetch prices from. + * @param rateFeedDescription Human-readable rate feed, which the relayer will report on, i.e. "CELO/USD" + * @param aggregators List of ChainlinkAggregator that the relayer chains. */ event RelayerDeployed( address indexed relayerAddress, address indexed rateFeedId, - address indexed chainlinkAggregator + string rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] aggregators ); /** @@ -25,15 +30,27 @@ interface IChainlinkRelayerFactory { function sortedOracles() external returns (address); - function deployRelayer(address rateFeedId, address chainlinkAggregator) external returns (address); + function deployRelayer( + address rateFeedId, + string calldata rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators + ) external returns (address); function removeRelayer(address rateFeedId) external; - function redeployRelayer(address rateFeedId, address chainlinkAggregator) external returns (address); + function redeployRelayer( + address rateFeedId, + string calldata rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators + ) external returns (address); function getRelayer(address rateFeedId) external view returns (address); function getRelayers() external view returns (address[] memory); - function computedRelayerAddress(address rateFeedId, address chainlinkAggregator) external returns (address); + function computedRelayerAddress( + address rateFeedId, + string calldata rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators + ) external returns (address); } diff --git a/contracts/oracles/ChainlinkRelayerFactory.sol b/contracts/oracles/ChainlinkRelayerFactory.sol index a4a7e37f..43faaa3e 100644 --- a/contracts/oracles/ChainlinkRelayerFactory.sol +++ b/contracts/oracles/ChainlinkRelayerFactory.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.18; import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import { ChainlinkRelayerV1 } from "./ChainlinkRelayerV1.sol"; +import { IChainlinkRelayer } from "../interfaces/IChainlinkRelayer.sol"; import { IChainlinkRelayerFactory } from "../interfaces/IChainlinkRelayerFactory.sol"; /** @@ -26,9 +27,8 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Thrown when trying to deploy a relayer to an address that already has code. * @param contractAddress Address at which the relayer could not be deployed. * @param rateFeedId Rate feed ID for which the relayer would have reported. - * @param chainlinkAggregator Address of the Chainlink aggregator the relayer would have fetched prices from. */ - error ContractAlreadyExists(address contractAddress, address rateFeedId, address chainlinkAggregator); + error ContractAlreadyExists(address contractAddress, address rateFeedId); /** * @notice Thrown when trying to deploy a relayer for a rate feed ID that already has a relayer. @@ -77,25 +77,33 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable /** * @notice Deploys a new relayer contract. * @param rateFeedId The rate feed ID for which the relayer will report. - * @param chainlinkAggregator The Chainlink aggregator from which the relayer will fetch prices. + * @param rateFeedDescription Human-readable rate feed, which the relayer will report on, i.e. "CELO/USD". + * @param aggregators Array of ChainlinkAggregator structs defining the price path. + * See contract-level @dev comment in the ChainlinkRelayerV1 contract, + * for an explanation on price paths. * @return relayerAddress The address of the newly deployed relayer contract. */ function deployRelayer( address rateFeedId, - address chainlinkAggregator + string calldata rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) public onlyOwner returns (address relayerAddress) { - address expectedAddress = computedRelayerAddress(rateFeedId, chainlinkAggregator); - - if (address(deployedRelayers[rateFeedId]) == expectedAddress || expectedAddress.code.length > 0) { - revert ContractAlreadyExists(expectedAddress, rateFeedId, chainlinkAggregator); - } - if (address(deployedRelayers[rateFeedId]) != address(0)) { revert RelayerForFeedExists(rateFeedId); } + address expectedAddress = computedRelayerAddress(rateFeedId, rateFeedDescription, aggregators); + if (expectedAddress.code.length > 0) { + revert ContractAlreadyExists(expectedAddress, rateFeedId); + } + bytes32 salt = _getSalt(); - ChainlinkRelayerV1 relayer = new ChainlinkRelayerV1{ salt: salt }(rateFeedId, sortedOracles, chainlinkAggregator); + ChainlinkRelayerV1 relayer = new ChainlinkRelayerV1{ salt: salt }( + rateFeedId, + rateFeedDescription, + sortedOracles, + aggregators + ); if (address(relayer) != expectedAddress) { revert UnexpectedAddress(expectedAddress, address(relayer)); @@ -104,7 +112,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable deployedRelayers[rateFeedId] = relayer; rateFeeds.push(rateFeedId); - emit RelayerDeployed(address(relayer), rateFeedId, chainlinkAggregator); + emit RelayerDeployed(address(relayer), rateFeedId, rateFeedDescription, aggregators); return address(relayer); } @@ -139,16 +147,18 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Removes the current relayer and redeploys a new one with a different * Chainlink aggregator (and/or different bytecode if the factory * has been upgraded since the last deployment of the relayer). - * @param rateFeedId The rate feed for which the relayer should be redeployed. - * @param chainlinkAggregator Address of the Chainlink aggregator the new relayer version will fetch prices from. + * @param rateFeedId The rate feed ID for which the relayer will report. + * @param rateFeedDescription Human-readable rate feed, which the relayer will report on, i.e. "CELO/USD". + * @param aggregators Array of ChainlinkAggregator structs defining the price path. * @return relayerAddress The address of the newly deployed relayer contract. */ function redeployRelayer( address rateFeedId, - address chainlinkAggregator + string calldata rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) external onlyOwner returns (address relayerAddress) { removeRelayer(rateFeedId); - return deployRelayer(rateFeedId, chainlinkAggregator); + return deployRelayer(rateFeedId, rateFeedDescription, aggregators); } /** @@ -156,7 +166,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @param rateFeedId The rate feed ID whose relayer we want to get. * @return relayerAddress Address of the relayer contract. */ - function getRelayer(address rateFeedId) public view returns (address relayerAddress) { + function getRelayer(address rateFeedId) external view returns (address relayerAddress) { return address(deployedRelayers[rateFeedId]); } @@ -164,7 +174,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Returns a list of all currently deployed relayers. * @return relayerAddresses An array of all relayer contract addresses. */ - function getRelayers() public view returns (address[] memory relayerAddresses) { + function getRelayers() external view returns (address[] memory relayerAddresses) { address[] memory relayers = new address[](rateFeeds.length); for (uint256 i = 0; i < rateFeeds.length; i++) { relayers[i] = address(deployedRelayers[rateFeeds[i]]); @@ -186,10 +196,15 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable /** * @notice Computes the expected CREATE2 address for given relayer parameters. * @param rateFeedId The rate feed ID. - * @param chainlinkAggregator Address of the Chainlink aggregator. + * @param rateFeedDescription The human readable description of the reported rate feed. + * @param aggregators Array of ChainlinkAggregator structs defining the price path. * @dev See https://eips.ethereum.org/EIPS/eip-1014. */ - function computedRelayerAddress(address rateFeedId, address chainlinkAggregator) public view returns (address) { + function computedRelayerAddress( + address rateFeedId, + string calldata rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators + ) public view returns (address) { bytes32 salt = _getSalt(); return address( @@ -203,7 +218,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable keccak256( abi.encodePacked( type(ChainlinkRelayerV1).creationCode, - abi.encode(rateFeedId, sortedOracles, chainlinkAggregator) + abi.encode(rateFeedId, rateFeedDescription, sortedOracles, aggregators) ) ) ) diff --git a/contracts/oracles/ChainlinkRelayerV1.sol b/contracts/oracles/ChainlinkRelayerV1.sol index a2ca2987..8ebce3ca 100644 --- a/contracts/oracles/ChainlinkRelayerV1.sol +++ b/contracts/oracles/ChainlinkRelayerV1.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.18; import "../interfaces/IChainlinkRelayer.sol"; import "foundry-chainlink-toolkit/src/interfaces/feeds/AggregatorV3Interface.sol"; +import { UD60x18, ud, intoUint256 } from "prb/math/UD60x18.sol"; /** * @notice The minimal subset of the SortedOracles interface needed by the @@ -19,25 +20,44 @@ interface ISortedOraclesMin { address greaterKey ) external; + function getRates(address rateFeedId) + external + returns ( + address[] memory, + uint256[] memory, + uint256[] memory + ); + function medianTimestamp(address rateFeedId) external view returns (uint256); function getTokenReportExpirySeconds(address rateFeedId) external view returns (uint256); + + function removeExpiredReports(address rateFeedId, uint256 n) external; } /** * @title ChainlinkRelayer - * @notice The ChainlinkRelayer relays rate feed data from a Chainlink price feed to - * the SortedOracles contract. A separate instance should be deployed for each - * rate feed. - * @dev Assumes that it itself is the only reporter for the given SortedOracles - * feed. + * @notice The ChainlinkRelayer relays rate feed data from a Chainlink price feed, or + * an aggregation of multiple Chainlink price feeds to the SortedOracles contract. + * A separate instance should be deployed for each rate feed. + * @dev Assumes that it is the only reporter for the given SortedOracles feed. + * This contract aggregates multiple Chainlink price feeds in order to provide derived rate feeds + * to the rest of the protocol. This is needed because it is more efficient for oracle providers + * to report FX rates against the dollar and crypto-asset rates against the dollar, + * instead of all possible combinations. + * For example, for the Philippine Peso, Chainlink reports PHP/USD, but does not report CELO/PHP + * which is required to pay for gas in a PHP stable token. But using both PHP/USD and CELO/USD, + * one can create a path: CELO/USD * inverse(PHP/USD) = CELO/PHP. + * Because of this we can provide up to four Chainlink price sources with inversion settings + * to the relayer, a price path. The path segments are chained through multiplication and + * inversion to derive the rate. */ contract ChainlinkRelayerV1 is IChainlinkRelayer { /** * @notice The number of digits after the decimal point in FixidityLib values, as used by SortedOracles. * @dev See contracts/common/FixidityLib.sol */ - uint256 public constant FIXIDITY_DECIMALS = 24; + uint256 private constant UD60X18_TO_FIXIDITY_SCALE = 1e6; // 10 ** (24 - 18) /// @notice The rateFeedId this relayer relays for. address public immutable rateFeedId; @@ -45,8 +65,42 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { /// @notice The address of the SortedOracles contract to report to. address public immutable sortedOracles; - /// @notice The address of the Chainlink aggregator this contract fetches data from. - address public immutable chainlinkAggregator; + /** + * @dev We store an array of up to four IChainlinkRelayer.ChainlinkAggregator structs + * in the following immutable variables. + * aggregator stores the i-th ChainlinkAggregator.aggregator member. + * invert stores the i-th ChainlinkAggregator.invert member. + * aggregatorCount stores the length of the array. + * These are built back up into an in-memory array in the buildAggregatorArray function. + */ + + /// @notice The addresses of the Chainlink aggregators this contract fetches data from. + address private immutable aggregator0; + address private immutable aggregator1; + address private immutable aggregator2; + address private immutable aggregator3; + + /// @notice The invert setting for each aggregator, if true it flips the rate feed, i.e. CELO/USD -> USD/CELO. + bool private immutable invert0; + bool private immutable invert1; + bool private immutable invert2; + bool private immutable invert3; + + /// @notice The number of aggregators provided during construction 1 <= aggregatorCount <= 4. + uint256 private immutable aggregatorCount; + + /** + * @notice Human-readable description of the rate feed. + * @dev Should only be used off-chain for easier debugging / UI generation, + * thus the only storage related gas spend occurs in the constructor. + */ + string public rateFeedDescription; + + /// @notice Used when an empty array of aggregators is passed into the constructor. + error NoAggregators(); + + /// @notice Used when more than four aggregators are passed into the constructor. + error TooManyAggregators(); /// @notice Used when a new price's timestamp is not newer than the most recent SortedOracles timestamp. error TimestampNotNew(); @@ -54,23 +108,72 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { /// @notice Used when a new price's timestamp would be considered expired by SortedOracles. error ExpiredTimestamp(); - /// @notice Used when a negative price is returned by the Chainlink aggregator. - error NegativePrice(); + /// @notice Used when a negative or zero price is returned by the Chainlink aggregator. + error InvalidPrice(); + + /** + * @notice Used when trying to recover from a lesser/greater revert and there are + * too many existing reports in SortedOracles. + */ + error TooManyExistingReports(); + + /** + * @notice Used in the constructor when a ChainlinkAggregator + * has address(0) for an aggregator. + */ + error InvalidAggregator(); /** * @notice Initializes the contract and sets immutable parameters. * @param _rateFeedId ID of the rate feed this relayer instance relays for. + * @param _rateFeedDescription The human-readable description of the reported rate feed. * @param _sortedOracles Address of the SortedOracles contract to relay to. - * @param _chainlinkAggregator Address of the Chainlink price feed to fetch data from. + * @param _aggregators Array of ChainlinkAggregator structs defining the price path. */ constructor( address _rateFeedId, + string memory _rateFeedDescription, address _sortedOracles, - address _chainlinkAggregator + ChainlinkAggregator[] memory _aggregators ) { rateFeedId = _rateFeedId; sortedOracles = _sortedOracles; - chainlinkAggregator = _chainlinkAggregator; + rateFeedDescription = _rateFeedDescription; + + aggregatorCount = _aggregators.length; + if (aggregatorCount == 0) { + revert NoAggregators(); + } + + if (aggregatorCount > 4) { + revert TooManyAggregators(); + } + + ChainlinkAggregator[] memory aggregators = new ChainlinkAggregator[](4); + for (uint256 i = 0; i < _aggregators.length; i++) { + if (_aggregators[i].aggregator == address(0)) { + revert InvalidAggregator(); + } + + aggregators[i] = _aggregators[i]; + } + + aggregator0 = aggregators[0].aggregator; + aggregator1 = aggregators[1].aggregator; + aggregator2 = aggregators[2].aggregator; + aggregator3 = aggregators[3].aggregator; + invert0 = aggregators[0].invert; + invert1 = aggregators[1].invert; + invert2 = aggregators[2].invert; + invert3 = aggregators[3].invert; + } + + /** + * @notice Get the Chainlink aggregators and their invert settings. + * @return An array of ChainlinkAggregator segments that compose the price path. + */ + function getAggregators() external view returns (ChainlinkAggregator[] memory) { + return buildAggregatorArray(); } /** @@ -78,36 +181,128 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @dev Checks the price is non-negative (Chainlink uses `int256` rather than `uint256`. * @dev Converts the price to a Fixidity value, as expected by SortedOracles. * @dev Performs checks on the timestamp, will revert if any fails: - * - The timestamp should be strictly newer than the most recent timestamp in SortedOracles. - * - The timestamp should not be considered expired by SortedOracles. + * - The most recent Chainlink timestamp should be strictly newer than the most + * recent timestamp in SortedOracles. + * - The oldest Chainlink timestamp should not be considered expired by SortedOracles. */ function relay() external { ISortedOraclesMin _sortedOracles = ISortedOraclesMin(sortedOracles); - (, int256 price, , uint256 timestamp, ) = AggregatorV3Interface(chainlinkAggregator).latestRoundData(); + ChainlinkAggregator[] memory aggregators = buildAggregatorArray(); - uint256 lastTimestamp = _sortedOracles.medianTimestamp(rateFeedId); + (UD60x18 report, uint256 timestamp) = readChainlinkAggregator(aggregators[0]); + uint256 oldestChainlinkTs = timestamp; + uint256 newestChainlinkTs = timestamp; - if (lastTimestamp > 0) { - if (timestamp <= lastTimestamp) { - revert TimestampNotNew(); - } + UD60x18 nextReport; + for (uint256 i = 1; i < aggregators.length; i++) { + (nextReport, timestamp) = readChainlinkAggregator(aggregators[i]); + report = report.mul(nextReport); + oldestChainlinkTs = timestamp < oldestChainlinkTs ? timestamp : oldestChainlinkTs; + newestChainlinkTs = timestamp > newestChainlinkTs ? timestamp : newestChainlinkTs; + } + + uint256 lastReportTs = _sortedOracles.medianTimestamp(rateFeedId); + + if (lastReportTs > 0 && newestChainlinkTs <= lastReportTs) { + revert TimestampNotNew(); } - if (_isTimestampExpired(timestamp)) { + if (isTimestampExpired(oldestChainlinkTs)) { revert ExpiredTimestamp(); } - if (price < 0) { - revert NegativePrice(); + reportRate(intoUint256(report) * UD60X18_TO_FIXIDITY_SCALE); + } + + /** + * @notice Report by looking up existing reports and building the lesser and greater keys. + * @dev Depending on the state in SortedOracles we can be in the: + * - Happy path: No reports, or a single report from this relayer. + * We can report with lesser and greater keys as address(0) + * - Unhappy path: There are reports from other oracles. + * We restrain this path by only computing lesser and greater keys when there is + * at most one report from a different oracle. + * We also attempt to expire reports in order to get back to the happy path. + * @param rate The rate to report. + */ + function reportRate(uint256 rate) internal { + (address[] memory oracles, uint256[] memory rates, ) = ISortedOraclesMin(sortedOracles).getRates(rateFeedId); + uint256 numRates = oracles.length; + + if (numRates == 0 || (numRates == 1 && oracles[0] == address(this))) { + // Happy path: SortedOracles is empty, or there is a single report from this relayer. + ISortedOraclesMin(sortedOracles).report(rateFeedId, rate, address(0), address(0)); + return; + } + + if (numRates > 2 || (numRates == 2 && oracles[0] != address(this) && oracles[1] != address(this))) { + revert TooManyExistingReports(); + } + + // At this point we have ensured that either: + // - There is a single report from another oracle. + // - There are two reports and one is from this relayer. + + address otherOracle; + uint256 otherRate; + + if (numRates == 1 || oracles[0] != address(this)) { + otherOracle = oracles[0]; + otherRate = rates[0]; + } else { + otherOracle = oracles[1]; + otherRate = rates[1]; + } + + address lesserKey; + address greaterKey; + + if (otherRate < rate) { + lesserKey = otherOracle; + } else { + greaterKey = otherOracle; } - uint256 report = _chainlinkToFixidity(price); + ISortedOraclesMin(sortedOracles).report(rateFeedId, rate, lesserKey, greaterKey); + ISortedOraclesMin(sortedOracles).removeExpiredReports(rateFeedId, 1); + } - // This contract is built for a setup where it is the only reporter for the - // given `rateFeedId`. As such, we don't need to compute and provide - // `lesserKey`/`greaterKey` each time, the "null pointer" `address(0)` will - // correctly place the report in SortedOracles' sorted linked list. - ISortedOraclesMin(sortedOracles).report(rateFeedId, report, address(0), address(0)); + /** + * @notice Read and validate a Chainlink report from an aggregator. + * It inverts the value if necessary. + * @return price UD60x18 report value. + * @return timestamp uint256 timestamp of the report. + */ + function readChainlinkAggregator(ChainlinkAggregator memory aggCfg) internal view returns (UD60x18, uint256) { + (, int256 _price, , uint256 timestamp, ) = AggregatorV3Interface(aggCfg.aggregator).latestRoundData(); + if (_price <= 0) { + revert InvalidPrice(); + } + UD60x18 price = chainlinkToUD60x18(_price, aggCfg.aggregator); + if (aggCfg.invert) { + price = price.inv(); + } + return (price, timestamp); + } + + /** + * @notice Compose immutable variables into an in-memory array for better handling. + * @return aggregators An array of ChainlinkAggregator structs. + */ + function buildAggregatorArray() internal view returns (ChainlinkAggregator[] memory aggregators) { + aggregators = new ChainlinkAggregator[](aggregatorCount); + unchecked { + aggregators[0] = ChainlinkAggregator(aggregator0, invert0); + if (aggregatorCount > 1) { + aggregators[1] = ChainlinkAggregator(aggregator1, invert1); + if (aggregatorCount > 2) { + aggregators[2] = ChainlinkAggregator(aggregator2, invert2); + if (aggregatorCount > 3) { + aggregators[3] = ChainlinkAggregator(aggregator3, invert3); + } + } + } + } } /** @@ -115,17 +310,17 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @param timestamp The timestamp returned by the Chainlink aggregator. * @return `true` if expired based on SortedOracles expiry parameter. */ - function _isTimestampExpired(uint256 timestamp) internal view returns (bool) { + function isTimestampExpired(uint256 timestamp) internal view returns (bool) { return block.timestamp - timestamp >= ISortedOraclesMin(sortedOracles).getTokenReportExpirySeconds(rateFeedId); } /** - * @notice Converts a Chainlink price to an unwrapped Fixidity value. - * @param price An price from the Chainlink aggregator. - * @return The converted Fixidity value (with 24 decimals). + * @notice Converts a Chainlink price to a UD60x18 value. + * @param price A price from the Chainlink aggregator. + * @return The converted UD60x18 value. */ - function _chainlinkToFixidity(int256 price) internal view returns (uint256) { - uint256 chainlinkDecimals = uint256(AggregatorV3Interface(chainlinkAggregator).decimals()); - return uint256(price) * 10**(FIXIDITY_DECIMALS - chainlinkDecimals); + function chainlinkToUD60x18(int256 price, address aggregator) internal view returns (UD60x18) { + uint256 chainlinkDecimals = uint256(AggregatorV3Interface(aggregator).decimals()); + return ud(uint256(price) * 10**(18 - chainlinkDecimals)); } } diff --git a/lib/prb-math b/lib/prb-math new file mode 160000 index 00000000..1edf08dd --- /dev/null +++ b/lib/prb-math @@ -0,0 +1 @@ +Subproject commit 1edf08dd73eb1ace0042459ba719b8ea4a55c0e0 diff --git a/remappings.txt b/remappings.txt index f897e6cb..863374e5 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,4 +4,5 @@ forge-std/=lib/celo-foundry/lib/forge-std/src/ ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/ test/=test/ contracts/=contracts/ -safe-contracts/=lib/safe-contracts/ \ No newline at end of file +safe-contracts/=lib/safe-contracts/ +prb/math/=lib/prb-math/src/ diff --git a/test/integration/ChainlinkRelayerIntegration.t.sol b/test/integration/ChainlinkRelayerIntegration.t.sol index 609dc202..a1d21198 100644 --- a/test/integration/ChainlinkRelayerIntegration.t.sol +++ b/test/integration/ChainlinkRelayerIntegration.t.sol @@ -86,6 +86,59 @@ contract ChainlinkRelayerIntegration_ProxySetup is ChainlinkRelayerIntegration { } } +contract ChainlinkRelayerIntegration_ReportAfterRedeploy is ChainlinkRelayerIntegration { + // Fictional rate feed ID + address rateFeedId = address(bytes20(keccak256(("cUSD/FOO")))); + + MockAggregatorV3 chainlinkAggregator0; + MockAggregatorV3 chainlinkAggregator1; + + function setUp() public { + super.setUp(); + + chainlinkAggregator0 = new MockAggregatorV3(8); + chainlinkAggregator1 = new MockAggregatorV3(8); + } + + function test_reportAfterRedeploy() public { + IChainlinkRelayer.ChainlinkAggregator[] memory aggregatorList0 = new IChainlinkRelayer.ChainlinkAggregator[](1); + aggregatorList0[0] = IChainlinkRelayer.ChainlinkAggregator(address(chainlinkAggregator0), false); + IChainlinkRelayer.ChainlinkAggregator[] memory aggregatorList1 = new IChainlinkRelayer.ChainlinkAggregator[](1); + aggregatorList1[0] = IChainlinkRelayer.ChainlinkAggregator(address(chainlinkAggregator1), false); + + vm.prank(owner); + IChainlinkRelayer chainlinkRelayer0 = IChainlinkRelayer( + relayerFactory.deployRelayer(rateFeedId, "cUSD/FOO", aggregatorList0) + ); + + vm.prank(deployer); + sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer0)); + + chainlinkAggregator0.setRoundData(1000000, block.timestamp); + chainlinkRelayer0.relay(); + + vm.warp(block.timestamp + 100); + + vm.prank(owner); + IChainlinkRelayer chainlinkRelayer1 = IChainlinkRelayer( + relayerFactory.redeployRelayer(rateFeedId, "cUSD/FOO", aggregatorList1) + ); + + vm.prank(deployer); + sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer1)); + + chainlinkAggregator1.setRoundData(1000000, block.timestamp); + chainlinkRelayer1.relay(); + assertEq(sortedOracles.numRates(rateFeedId), 2); + + vm.warp(block.timestamp + 1000); + + chainlinkAggregator1.setRoundData(1000000, block.timestamp); + chainlinkRelayer1.relay(); + assertEq(sortedOracles.numRates(rateFeedId), 1); + } +} + contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelayerIntegration { // Fictional rate feed ID address rateFeedId = address(bytes20(keccak256(("cUSD/FOO")))); @@ -101,9 +154,11 @@ contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelay } function setUpRelayer() public { - chainlinkAggregator = new MockAggregatorV3(); + chainlinkAggregator = new MockAggregatorV3(8); + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = new IChainlinkRelayer.ChainlinkAggregator[](1); + aggregators[0] = IChainlinkRelayer.ChainlinkAggregator(address(chainlinkAggregator), false); vm.prank(owner); - chainlinkRelayer = IChainlinkRelayer(relayerFactory.deployRelayer(rateFeedId, address(chainlinkAggregator))); + chainlinkRelayer = IChainlinkRelayer(relayerFactory.deployRelayer(rateFeedId, "CELO/USD", aggregators)); vm.prank(deployer); sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer)); diff --git a/test/mocks/MockAggregatorV3.sol b/test/mocks/MockAggregatorV3.sol index cb636d4e..13c6b5db 100644 --- a/test/mocks/MockAggregatorV3.sol +++ b/test/mocks/MockAggregatorV3.sol @@ -1,9 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.8.19; contract MockAggregatorV3 { int256 public _answer; uint256 public _updatedAt; + uint8 public decimals; + + constructor(uint8 _decimals) public { + decimals = _decimals; + } function setRoundData(int256 answer, uint256 updatedAt) external { _answer = answer; @@ -23,8 +28,4 @@ contract MockAggregatorV3 { { return (uint80(0), _answer, uint256(0), _updatedAt, uint80(0)); } - - function decimals() external view returns (uint8) { - return 8; - } } diff --git a/test/oracles/ChainlinkRelayer.t.sol b/test/oracles/ChainlinkRelayer.t.sol deleted file mode 100644 index c91ae3e0..00000000 --- a/test/oracles/ChainlinkRelayer.t.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, private-vars-leading-underscore -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase, one-contract-per-file -pragma solidity ^0.5.17; - -import { BaseTest } from "../utils/BaseTest.t.sol"; -import { SortedOracles } from "contracts/common/SortedOracles.sol"; -import { MockAggregatorV3 } from "../mocks/MockAggregatorV3.sol"; -import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; - -contract ChainlinkRelayerTest is BaseTest { - bytes constant TIMESTAMP_NOT_NEW_ERROR = abi.encodeWithSignature("TimestampNotNew()"); - bytes constant EXPIRED_TIMESTAMP_ERROR = abi.encodeWithSignature("ExpiredTimestamp()"); - bytes constant NEGATIVE_PRICE_ERROR = abi.encodeWithSignature("NegativePrice()"); - SortedOracles sortedOracles; - MockAggregatorV3 chainlinkAggregator; - IChainlinkRelayer relayer; - address rateFeedId = actor("rateFeed"); - int256 aPrice = 420000000; - uint256 expectedReport = 4200000000000000000000000; - uint256 aReport = 4100000000000000000000000; - uint256 expirySeconds = 600; - - function setUp() public { - sortedOracles = new SortedOracles(true); - chainlinkAggregator = new MockAggregatorV3(); - relayer = IChainlinkRelayer( - factory.createContract( - "ChainlinkRelayerV1", - abi.encode(rateFeedId, address(sortedOracles), address(chainlinkAggregator)) - ) - ); - sortedOracles.addOracle(rateFeedId, address(relayer)); - sortedOracles.setTokenReportExpiry(rateFeedId, expirySeconds); - } -} - -contract ChainlinkRelayerTest_constructor is ChainlinkRelayerTest { - function test_constructorSetsRateFeedId() public { - address _rateFeedId = relayer.rateFeedId(); - assertEq(_rateFeedId, rateFeedId); - } - - function test_constructorSetsSortedOracles() public { - address _sortedOracles = relayer.sortedOracles(); - assertEq(_sortedOracles, address(sortedOracles)); - } - - function test_constructorSetsAggregator() public { - address _chainlinkAggregator = relayer.chainlinkAggregator(); - assertEq(_chainlinkAggregator, address(chainlinkAggregator)); - } -} - -contract ChainlinkRelayerTest_relay is ChainlinkRelayerTest { - function setUp() public { - super.setUp(); - chainlinkAggregator.setRoundData(int256(aPrice), uint256(block.timestamp)); - } - - function test_relaysTheRate() public { - relayer.relay(); - (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); - assertEq(medianRate, expectedReport); - } - - function testFuzz_convertsChainlinkToFixidityCorrectly(int256 x) public { - vm.assume(x >= 0); - vm.assume(uint256(x) < uint256(2**256 - 1) / (10**(24 - 8))); - chainlinkAggregator.setRoundData(x, uint256(block.timestamp)); - relayer.relay(); - (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); - assertEq(medianRate, uint256(x) * 10**(24 - 8)); - } - - function test_revertsOnNegativePrice() public { - chainlinkAggregator.setRoundData(-1 * aPrice, block.timestamp); - vm.expectRevert(NEGATIVE_PRICE_ERROR); - relayer.relay(); - } - - function test_revertsOnEarlierTimestamp() public { - vm.prank(address(relayer)); - sortedOracles.report(rateFeedId, aReport, address(0), address(0)); - uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); - chainlinkAggregator.setRoundData(aPrice, latestTimestamp - 1); - vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); - relayer.relay(); - } - - function test_revertsOnRepeatTimestamp() public { - vm.prank(address(relayer)); - sortedOracles.report(rateFeedId, aReport, address(0), address(0)); - uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); - chainlinkAggregator.setRoundData(aPrice, latestTimestamp); - vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); - relayer.relay(); - } - - function test_revertsOnExpiredTimestamp() public { - vm.prank(address(relayer)); - sortedOracles.report(rateFeedId, aReport, address(0), address(0)); - chainlinkAggregator.setRoundData(aPrice, block.timestamp + 1); - vm.warp(block.timestamp + expirySeconds + 1); - vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); - relayer.relay(); - } - - function test_revertsWhenFirstTimestampIsExpired() public { - chainlinkAggregator.setRoundData(aPrice, block.timestamp); - vm.warp(block.timestamp + expirySeconds); - vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); - relayer.relay(); - } -} diff --git a/test/oracles/ChainlinkRelayerFactory.t.sol b/test/oracles/ChainlinkRelayerFactory.t.sol index f6b02fa1..be288157 100644 --- a/test/oracles/ChainlinkRelayerFactory.t.sol +++ b/test/oracles/ChainlinkRelayerFactory.t.sol @@ -1,42 +1,67 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, private-vars-leading-underscore // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase, one-contract-per-file -pragma solidity ^0.5.17; +pragma solidity ^0.8.18; -import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import { BaseTest } from "../utils/BaseTest.t.sol"; +import { Ownable } from "openzeppelin-contracts-next/contracts/access/Ownable.sol"; +import { BaseTest } from "../utils/BaseTest.next.sol"; import { IChainlinkRelayerFactory } from "contracts/interfaces/IChainlinkRelayerFactory.sol"; import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; +import { ChainlinkRelayerFactory } from "contracts/oracles/ChainlinkRelayerFactory.sol"; contract ChainlinkRelayerFactoryTest is BaseTest { IChainlinkRelayerFactory relayerFactory; - address owner = actor("owner"); - address nonOwner = actor("nonOwner"); - address mockSortedOracles = actor("sortedOracles"); - address[3] mockAggregators = [actor("aggregator1"), actor("aggregator2"), actor("aggregator3")]; - address[3] rateFeeds = [actor("rateFeed1"), actor("rateFeed2"), actor("rateFeed3")]; - address mockAggregator = mockAggregators[0]; + address owner = makeAddr("owner"); + address nonOwner = makeAddr("nonOwner"); + address mockSortedOracles = makeAddr("sortedOracles"); + address[4] mockAggregators = [ + makeAddr("aggregator1"), + makeAddr("aggregator2"), + makeAddr("aggregator3"), + makeAddr("aggregator4") + ]; + address[3] rateFeeds = [makeAddr("rateFeed1"), makeAddr("rateFeed2"), makeAddr("rateFeed3")]; address aRateFeed = rateFeeds[0]; + string aRateFeedDescription = "CELO/USD"; event RelayerDeployed( address indexed relayerAddress, address indexed rateFeedId, - address indexed chainlinkAggregator + string rateFeedDescription, + IChainlinkRelayer.ChainlinkAggregator[] aggregators ); event RelayerRemoved(address indexed relayerAddress, address indexed rateFeedId); - function setUp() public { - relayerFactory = IChainlinkRelayerFactory(factory.createContract("ChainlinkRelayerFactory", abi.encode(false))); + function oneAggregator(uint256 aggregatorIndex) + internal + view + returns (IChainlinkRelayer.ChainlinkAggregator[] memory aggregators) + { + aggregators = new IChainlinkRelayer.ChainlinkAggregator[](1); + aggregators[0] = IChainlinkRelayer.ChainlinkAggregator(mockAggregators[aggregatorIndex], false); + } + + function fourAggregators() internal view returns (IChainlinkRelayer.ChainlinkAggregator[] memory aggregators) { + aggregators = new IChainlinkRelayer.ChainlinkAggregator[](4); + aggregators[0] = IChainlinkRelayer.ChainlinkAggregator(mockAggregators[0], false); + aggregators[1] = IChainlinkRelayer.ChainlinkAggregator(mockAggregators[1], false); + aggregators[2] = IChainlinkRelayer.ChainlinkAggregator(mockAggregators[2], false); + aggregators[3] = IChainlinkRelayer.ChainlinkAggregator(mockAggregators[3], false); + } + + function setUp() public virtual { + relayerFactory = IChainlinkRelayerFactory(new ChainlinkRelayerFactory(false)); vm.prank(owner); relayerFactory.initialize(mockSortedOracles); } function expectedRelayerAddress( address rateFeedId, + string memory rateFeedDescription, address sortedOracles, - address chainlinkAggregator, + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators, address relayerFactoryAddress - ) public returns (address expectedAddress) { + ) internal view returns (address expectedAddress) { bytes32 salt = keccak256("mento.chainlinkRelayer"); return address( @@ -50,7 +75,7 @@ contract ChainlinkRelayerFactoryTest is BaseTest { keccak256( abi.encodePacked( vm.getCode(factory.contractPath("ChainlinkRelayerV1")), - abi.encode(rateFeedId, sortedOracles, chainlinkAggregator) + abi.encode(rateFeedId, rateFeedDescription, sortedOracles, aggregators) ) ) ) @@ -60,13 +85,12 @@ contract ChainlinkRelayerFactoryTest is BaseTest { ); } - function contractAlreadyExistsError( - address relayerAddress, - address rateFeedId, - address aggregator - ) public pure returns (bytes memory ContractAlreadyExistsError) { - return - abi.encodeWithSignature("ContractAlreadyExists(address,address,address)", relayerAddress, rateFeedId, aggregator); + function contractAlreadyExistsError(address relayerAddress, address rateFeedId) + public + pure + returns (bytes memory ContractAlreadyExistsError) + { + return abi.encodeWithSignature("ContractAlreadyExists(address,address)", relayerAddress, rateFeedId); } function relayerForFeedExistsError(address rateFeedId) public pure returns (bytes memory RelayerForFeedExistsError) { @@ -121,89 +145,117 @@ contract ChainlinkRelayerFactoryTest_renounceOwnership is ChainlinkRelayerFactor } contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTest { + IChainlinkRelayer relayer; + function test_setsRateFeed() public { vm.prank(owner); - IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, mockAggregator)); - + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address rateFeed = relayer.rateFeedId(); assertEq(rateFeed, aRateFeed); } - function test_setsAggregator() public { + function test_setsRateFeedDescription() public { vm.prank(owner); - IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, mockAggregator)); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + string memory rateFeedDescription = relayer.rateFeedDescription(); + assertEq(rateFeedDescription, aRateFeedDescription); + } - address aggregator = relayer.chainlinkAggregator(); - assertEq(aggregator, mockAggregator); + function test_setsAggregators() public { + vm.prank(owner); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + IChainlinkRelayer.ChainlinkAggregator[] memory expectedAggregators = fourAggregators(); + IChainlinkRelayer.ChainlinkAggregator[] memory actualAggregators = relayer.getAggregators(); + assertEq(expectedAggregators.length, actualAggregators.length); + for (uint256 i = 0; i < expectedAggregators.length; i++) { + assertEq(expectedAggregators[i].aggregator, actualAggregators[i].aggregator); + assertEq(expectedAggregators[i].invert, actualAggregators[i].invert); + } } function test_setsSortedOracles() public { vm.prank(owner); - IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, mockAggregator)); - + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address sortedOracles = relayer.sortedOracles(); assertEq(sortedOracles, mockSortedOracles); } function test_deploysToTheCorrectAddress() public { vm.prank(owner); - address relayer = relayerFactory.deployRelayer(aRateFeed, mockAggregator); - + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address expectedAddress = expectedRelayerAddress({ rateFeedId: aRateFeed, + rateFeedDescription: aRateFeedDescription, sortedOracles: mockSortedOracles, - chainlinkAggregator: mockAggregator, + aggregators: fourAggregators(), relayerFactoryAddress: address(relayerFactory) }); - assertEq(relayer, expectedAddress); + assertEq(address(relayer), expectedAddress); } function test_emitsRelayerDeployedEvent() public { - address expectedAddress = expectedRelayerAddress( - aRateFeed, - mockSortedOracles, - mockAggregator, - address(relayerFactory) - ); + address expectedAddress = expectedRelayerAddress({ + rateFeedId: aRateFeed, + rateFeedDescription: aRateFeedDescription, + sortedOracles: mockSortedOracles, + aggregators: fourAggregators(), + relayerFactoryAddress: address(relayerFactory) + }); // solhint-disable-next-line func-named-parameters - vm.expectEmit(true, true, true, false, address(relayerFactory)); + vm.expectEmit(true, true, true, true, address(relayerFactory)); emit RelayerDeployed({ relayerAddress: expectedAddress, rateFeedId: aRateFeed, - chainlinkAggregator: mockAggregator + rateFeedDescription: aRateFeedDescription, + aggregators: fourAggregators() }); vm.prank(owner); - relayerFactory.deployRelayer(aRateFeed, mockAggregator); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } function test_remembersTheRelayerAddress() public { vm.prank(owner); - address relayer = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address storedAddress = relayerFactory.getRelayer(aRateFeed); - assertEq(storedAddress, relayer); + assertEq(storedAddress, address(relayer)); + } + + function test_revertsWhenDeployingToAddressWithCode() public { + vm.prank(owner); + address futureAddress = expectedRelayerAddress( + aRateFeed, + aRateFeedDescription, + address(mockSortedOracles), + fourAggregators(), + address(relayerFactory) + ); + vm.etch(futureAddress, abi.encode("This is a great contract's bytecode")); + vm.expectRevert(contractAlreadyExistsError(address(futureAddress), aRateFeed)); + vm.prank(owner); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } function test_revertsWhenDeployingTheSameRelayer() public { vm.prank(owner); - address relayer = relayerFactory.deployRelayer(aRateFeed, mockAggregator); - vm.expectRevert(contractAlreadyExistsError(relayer, aRateFeed, mockAggregator)); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + vm.expectRevert(relayerForFeedExistsError(aRateFeed)); vm.prank(owner); - relayerFactory.deployRelayer(aRateFeed, mockAggregator); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } function test_revertsWhenDeployingForTheSameRateFeed() public { vm.prank(owner); - relayerFactory.deployRelayer(aRateFeed, mockAggregators[0]); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); vm.expectRevert(relayerForFeedExistsError(aRateFeed)); vm.prank(owner); - relayerFactory.deployRelayer(aRateFeed, mockAggregators[1]); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, oneAggregator(0)); } function test_revertsWhenCalledByNonOwner() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(nonOwner); - relayerFactory.deployRelayer(aRateFeed, mockAggregator); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } } @@ -215,7 +267,7 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_returnsRelayerWhenThereIsOne() public { vm.prank(owner); - address relayerAddress = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + address relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 1); assertEq(relayers[0], relayerAddress); @@ -223,11 +275,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_returnsMultipleRelayersWhenThereAreMore() public { vm.prank(owner); - address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], mockAggregators[0]); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); vm.prank(owner); - address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); vm.prank(owner); - address relayerAddress3 = relayerFactory.deployRelayer(rateFeeds[2], mockAggregators[2]); + address relayerAddress3 = relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, oneAggregator(2)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 3); assertEq(relayers[0], relayerAddress1); @@ -237,11 +289,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_returnsADifferentRelayerAfterRedeployment() public { vm.prank(owner); - address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], mockAggregators[0]); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); vm.prank(owner); - relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); vm.prank(owner); - address relayerAddress2 = relayerFactory.redeployRelayer(rateFeeds[1], mockAggregators[2]); + address relayerAddress2 = relayerFactory.redeployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(2)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 2); assertEq(relayers[0], relayerAddress1); @@ -250,11 +302,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_doesntReturnARemovedRelayer() public { vm.prank(owner); - address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], mockAggregators[0]); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); vm.prank(owner); - address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); vm.prank(owner); - relayerFactory.deployRelayer(rateFeeds[2], mockAggregators[2]); + relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, oneAggregator(2)); vm.prank(owner); relayerFactory.removeRelayer(rateFeeds[2]); address[] memory relayers = relayerFactory.getRelayers(); @@ -267,11 +319,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTest { address relayerAddress; - function setUp() public { + function setUp() public override { super.setUp(); vm.prank(owner); - relayerAddress = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); } function test_removesTheRelayer() public { @@ -292,7 +344,7 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes function test_doesntRemoveOtherRelayers() public { vm.prank(owner); - address newRelayerAddress = relayerFactory.deployRelayer(rateFeeds[1], mockAggregators[1]); + address newRelayerAddress = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); vm.prank(owner); relayerFactory.removeRelayer(aRateFeed); address[] memory relayers = relayerFactory.getRelayers(); @@ -317,31 +369,42 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryTest { address oldAddress; - function setUp() public { + function setUp() public override { super.setUp(); vm.prank(owner); - oldAddress = relayerFactory.deployRelayer(aRateFeed, mockAggregator); + oldAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); } function test_setsRateFeedOnNewRelayer() public { vm.prank(owner); - IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1])); + IChainlinkRelayer relayer = IChainlinkRelayer( + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + ); address rateFeed = relayer.rateFeedId(); - assertEq(rateFeed, aRateFeed); + assertEq(rateFeed, rateFeeds[0]); } function test_setsAggregatorOnNewRelayer() public { vm.prank(owner); - IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1])); + IChainlinkRelayer relayer = IChainlinkRelayer( + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + ); - address aggregator = relayer.chainlinkAggregator(); - assertEq(aggregator, mockAggregators[1]); + IChainlinkRelayer.ChainlinkAggregator[] memory expectedAggregators = oneAggregator(1); + IChainlinkRelayer.ChainlinkAggregator[] memory actualAggregators = relayer.getAggregators(); + assertEq(expectedAggregators.length, actualAggregators.length); + for (uint256 i = 0; i < expectedAggregators.length; i++) { + assertEq(expectedAggregators[i].aggregator, actualAggregators[i].aggregator); + assertEq(expectedAggregators[i].invert, actualAggregators[i].invert); + } } function test_setsSortedOraclesOnNewRelayer() public { vm.prank(owner); - IChainlinkRelayer relayer = IChainlinkRelayer(relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1])); + IChainlinkRelayer relayer = IChainlinkRelayer( + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + ); address sortedOracles = relayer.sortedOracles(); assertEq(sortedOracles, mockSortedOracles); @@ -349,23 +412,27 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function test_deploysToTheCorrectNewAddress() public { vm.prank(owner); - address relayer = relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + IChainlinkRelayer relayer = IChainlinkRelayer( + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + ); address expectedAddress = expectedRelayerAddress( aRateFeed, + aRateFeedDescription, mockSortedOracles, - mockAggregators[1], + oneAggregator(1), address(relayerFactory) ); - assertEq(relayer, expectedAddress); + assertEq(address(relayer), expectedAddress); } function test_emitsRelayerRemovedAndDeployedEvents() public { address expectedAddress = expectedRelayerAddress({ rateFeedId: aRateFeed, + rateFeedDescription: aRateFeedDescription, sortedOracles: mockSortedOracles, - chainlinkAggregator: mockAggregators[1], + aggregators: oneAggregator(1), relayerFactoryAddress: address(relayerFactory) }); // solhint-disable-next-line func-named-parameters @@ -376,28 +443,29 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT emit RelayerDeployed({ relayerAddress: expectedAddress, rateFeedId: aRateFeed, - chainlinkAggregator: mockAggregators[1] + rateFeedDescription: aRateFeedDescription, + aggregators: oneAggregator(1) }); vm.prank(owner); - relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); } function test_remembersTheNewRelayerAddress() public { vm.prank(owner); - address relayer = relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + address relayer = relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); address storedAddress = relayerFactory.getRelayer(aRateFeed); assertEq(storedAddress, relayer); } function test_revertsWhenDeployingTheSameExactRelayer() public { - vm.expectRevert(contractAlreadyExistsError(oldAddress, aRateFeed, mockAggregator)); + vm.expectRevert(contractAlreadyExistsError(oldAddress, aRateFeed)); vm.prank(owner); - relayerFactory.redeployRelayer(aRateFeed, mockAggregator); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); } function test_revertsWhenCalledByNonOwner() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(nonOwner); - relayerFactory.redeployRelayer(aRateFeed, mockAggregators[1]); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); } } diff --git a/test/oracles/ChainlinkRelayerV1.t.sol b/test/oracles/ChainlinkRelayerV1.t.sol new file mode 100644 index 00000000..ad248ee6 --- /dev/null +++ b/test/oracles/ChainlinkRelayerV1.t.sol @@ -0,0 +1,697 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.8.18; + +import { console } from "forge-std-next/console.sol"; +import "../utils/BaseTest.next.sol"; +import "../mocks/MockAggregatorV3.sol"; +import "contracts/interfaces/IChainlinkRelayer.sol"; +import "contracts/oracles/ChainlinkRelayerV1.sol"; + +import { UD60x18, ud, intoUint256 } from "prb/math/UD60x18.sol"; + +interface ISortedOracles { + function addOracle(address, address) external; + + function removeOracle( + address, + address, + uint256 + ) external; + + function report( + address, + uint256, + address, + address + ) external; + + function setTokenReportExpiry(address, uint256) external; + + function medianRate(address) external returns (uint256, uint256); + + function medianTimestamp(address token) external returns (uint256); + + function getRates(address rateFeedId) + external + returns ( + address[] memory, + uint256[] memory, + uint256[] memory + ); +} + +contract ChainlinkRelayerV1Test is BaseTest { + bytes constant TIMESTAMP_NOT_NEW_ERROR = abi.encodeWithSignature("TimestampNotNew()"); + bytes constant EXPIRED_TIMESTAMP_ERROR = abi.encodeWithSignature("ExpiredTimestamp()"); + bytes constant INVALID_PRICE_ERROR = abi.encodeWithSignature("InvalidPrice()"); + bytes constant INVALID_AGGREGATOR = abi.encodeWithSignature("InvalidAggregator()"); + bytes constant NO_AGGREGATORS = abi.encodeWithSignature("NoAggregators()"); + bytes constant TOO_MANY_AGGREGATORS = abi.encodeWithSignature("TooManyAggregators()"); + bytes constant TOO_MANY_EXISTING_REPORTS = abi.encodeWithSignature("TooManyExistingReports()"); + + ISortedOracles sortedOracles; + + IChainlinkRelayer.ChainlinkAggregator aggregator0; + IChainlinkRelayer.ChainlinkAggregator aggregator1; + IChainlinkRelayer.ChainlinkAggregator aggregator2; + IChainlinkRelayer.ChainlinkAggregator aggregator3; + + MockAggregatorV3 mockAggregator0; + MockAggregatorV3 mockAggregator1; + MockAggregatorV3 mockAggregator2; + MockAggregatorV3 mockAggregator3; + + bool invert0 = false; + bool invert1 = true; + bool invert2 = false; + bool invert3 = true; + + int256 aggregatorPrice0; + int256 aggregatorPrice1; + int256 aggregatorPrice2; + int256 aggregatorPrice3; + + uint256 expectedReport; + uint256 aReport = 4100000000000000000000000; + + IChainlinkRelayer relayer; + address rateFeedId = makeAddr("rateFeed"); + uint256 expirySeconds = 600; + + function setUpRelayer(uint256 aggregatorsCount) internal { + aggregator0 = IChainlinkRelayer.ChainlinkAggregator(address(mockAggregator0), invert0); + aggregator1 = IChainlinkRelayer.ChainlinkAggregator(address(mockAggregator1), invert1); + aggregator2 = IChainlinkRelayer.ChainlinkAggregator(address(mockAggregator2), invert2); + aggregator3 = IChainlinkRelayer.ChainlinkAggregator(address(mockAggregator3), invert3); + + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = new IChainlinkRelayer.ChainlinkAggregator[]( + aggregatorsCount + ); + aggregators[0] = aggregator0; + if (aggregatorsCount > 1) { + aggregators[1] = aggregator1; + if (aggregatorsCount > 2) { + aggregators[2] = aggregator2; + if (aggregatorsCount > 3) { + aggregators[3] = aggregator3; + } + } + } + + relayer = IChainlinkRelayer(new ChainlinkRelayerV1(rateFeedId, "CELO/USD", address(sortedOracles), aggregators)); + sortedOracles.addOracle(rateFeedId, address(relayer)); + } + + function setUp() public virtual { + sortedOracles = ISortedOracles( + factory.createFromPath("contracts/common/SortedOracles.sol:SortedOracles", abi.encode(true), address(this)) + ); + sortedOracles.setTokenReportExpiry(rateFeedId, expirySeconds); + + mockAggregator0 = new MockAggregatorV3(8); + mockAggregator1 = new MockAggregatorV3(12); + mockAggregator2 = new MockAggregatorV3(8); + mockAggregator3 = new MockAggregatorV3(6); + } + + function setAggregatorPrices() public { + mockAggregator0.setRoundData(aggregatorPrice0, uint256(block.timestamp)); + mockAggregator1.setRoundData(aggregatorPrice1, uint256(block.timestamp)); + mockAggregator2.setRoundData(aggregatorPrice2, uint256(block.timestamp)); + mockAggregator3.setRoundData(aggregatorPrice3, uint256(block.timestamp)); + } +} + +contract ChainlinkRelayerV1Test_constructor_invalid is ChainlinkRelayerV1Test { + function test_constructorRevertsWhenAggregatorsIsEmpty() public { + vm.expectRevert(NO_AGGREGATORS); + new ChainlinkRelayerV1( + rateFeedId, + "CELO/USD", + address(sortedOracles), + new IChainlinkRelayer.ChainlinkAggregator[](0) + ); + } + + function test_constructorRevertsWhenTooManyAggregators() public { + vm.expectRevert(TOO_MANY_AGGREGATORS); + new ChainlinkRelayerV1( + rateFeedId, + "CELO/USD", + address(sortedOracles), + new IChainlinkRelayer.ChainlinkAggregator[](5) + ); + } + + function test_constructorRevertsWhenAggregatorsIsInvalid() public { + vm.expectRevert(INVALID_AGGREGATOR); + new ChainlinkRelayerV1( + rateFeedId, + "CELO/USD", + address(sortedOracles), + new IChainlinkRelayer.ChainlinkAggregator[](1) + ); + } +} + +contract ChainlinkRelayerV1Test_constructor_single is ChainlinkRelayerV1Test { + function setUpRelayer() internal virtual { + setUpRelayer(1); + } + + function setUp() public override { + super.setUp(); + setUpRelayer(); + } + + function test_constructorSetsRateFeedId() public { + address _rateFeedId = relayer.rateFeedId(); + assertEq(_rateFeedId, rateFeedId); + } + + function test_constructorSetsSortedOracles() public { + address _sortedOracles = relayer.sortedOracles(); + assertEq(_sortedOracles, address(sortedOracles)); + } + + function test_constructorSetsRateFeedDescription() public { + string memory rateFeedDescription = relayer.rateFeedDescription(); + assertEq(rateFeedDescription, "CELO/USD"); + } + + function test_constructorSetsAggregators() public virtual { + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = relayer.getAggregators(); + assertEq(aggregators.length, 1); + assertEq(aggregators[0].aggregator, address(aggregator0.aggregator)); + assertEq(aggregators[0].invert, aggregator0.invert); + } +} + +contract ChainlinkRelayerV1Test_constructor_double is ChainlinkRelayerV1Test_constructor_single { + function setUpRelayer() internal override { + setUpRelayer(2); + } + + function test_constructorSetsAggregators() public override { + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = relayer.getAggregators(); + assertEq(aggregators.length, 2); + assertEq(aggregators[0].aggregator, address(aggregator0.aggregator)); + assertEq(aggregators[1].aggregator, address(aggregator1.aggregator)); + assertEq(aggregators[0].invert, aggregator0.invert); + assertEq(aggregators[1].invert, aggregator1.invert); + } +} + +contract ChainlinkRelayerV1Test_constructor_triple is ChainlinkRelayerV1Test_constructor_single { + function setUpRelayer() internal override { + setUpRelayer(3); + } + + function test_constructorSetsAggregators() public override { + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = relayer.getAggregators(); + assertEq(aggregators.length, 3); + assertEq(aggregators[0].aggregator, address(aggregator0.aggregator)); + assertEq(aggregators[1].aggregator, address(aggregator1.aggregator)); + assertEq(aggregators[2].aggregator, address(aggregator2.aggregator)); + assertEq(aggregators[0].invert, aggregator0.invert); + assertEq(aggregators[1].invert, aggregator1.invert); + assertEq(aggregators[2].invert, aggregator2.invert); + } +} + +contract ChainlinkRelayerV1Test_constructor_full is ChainlinkRelayerV1Test_constructor_single { + function setUpRelayer() internal override { + setUpRelayer(4); + } + + function test_constructorSetsAggregators() public override { + IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = relayer.getAggregators(); + assertEq(aggregators.length, 4); + assertEq(aggregators[0].aggregator, address(aggregator0.aggregator)); + assertEq(aggregators[1].aggregator, address(aggregator1.aggregator)); + assertEq(aggregators[2].aggregator, address(aggregator2.aggregator)); + assertEq(aggregators[3].aggregator, address(aggregator3.aggregator)); + assertEq(aggregators[0].invert, aggregator0.invert); + assertEq(aggregators[1].invert, aggregator1.invert); + assertEq(aggregators[2].invert, aggregator2.invert); + assertEq(aggregators[3].invert, aggregator3.invert); + } +} + +contract ChainlinkRelayerV1Test_fuzz_single is ChainlinkRelayerV1Test { + function setUp() public override { + super.setUp(); + setUpRelayer(1); + } + + function testFuzz_convertsChainlinkToUD60x18Correctly(int256 _rate) public { + int256 rate = bound(_rate, 1, type(int256).max / 10**18); + mockAggregator0.setRoundData(rate, uint256(block.timestamp)); + + relayer.relay(); + (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); + assertEq(medianRate, uint256(rate) * 10**(24 - mockAggregator0.decimals())); + } +} + +contract ChainlinkRelayerV1Test_fuzz_full is ChainlinkRelayerV1Test { + function setUp() public override { + super.setUp(); + setUpRelayer(4); + } + + function testFuzz_calculatesReportsCorrectly( + int256 _aggregatorPrice0, + int256 _aggregatorPrice1, + int256 _aggregatorPrice2, + int256 _aggregatorPrice3, + bool _invert0, + bool _invert1, + bool _invert2, + bool _invert3 + ) public { + aggregatorPrice0 = bound(_aggregatorPrice0, 1, 1e5 * 1e6); + aggregatorPrice1 = bound(_aggregatorPrice1, 1, 1e5 * 1e6); + aggregatorPrice2 = bound(_aggregatorPrice2, 1, 1e5 * 1e6); + aggregatorPrice3 = bound(_aggregatorPrice3, 1, 1e5 * 1e6); + + invert0 = _invert0; + invert1 = _invert1; + invert2 = _invert2; + invert3 = _invert3; + + setUpRelayer(4); + setAggregatorPrices(); + + relayer.relay(); + (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); + assertEq(medianRate, expectedPriceInFixidity()); + } + + function expectedPriceInFixidity() internal view returns (uint256) { + UD60x18 price0 = ud(uint256(aggregatorPrice0) * 1e10); + if (invert0) { + price0 = ud(1e18).div(price0); + } + UD60x18 price1 = ud(uint256(aggregatorPrice1) * 1e6); + if (invert1) { + price1 = ud(1e18).div(price1); + } + UD60x18 price2 = ud(uint256(aggregatorPrice2) * 1e10); + if (invert2) { + price2 = ud(1e18).div(price2); + } + UD60x18 price3 = ud(uint256(aggregatorPrice3) * 1e12); + if (invert3) { + price3 = ud(1e18).div(price3); + } + return intoUint256(price0.mul(price1).mul(price2).mul(price3)) * 1e6; // 1e18 -> 1e24 + } +} + +contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { + modifier withReport(uint256 report) { + vm.warp(2); + vm.prank(address(relayer)); + sortedOracles.report(rateFeedId, report, address(0), address(0)); + _; + } + + function setUpRelayer() internal virtual { + setUpRelayer(1); + } + + function setUpExpectations() internal virtual { + aggregatorPrice0 = 420000000; + expectedReport = 4200000000000000000000000; + } + + function setUp() public override { + super.setUp(); + setUpRelayer(); + setUpExpectations(); + setAggregatorPrices(); + } + + function relayAndLogGas(string memory label) internal { + uint256 gasBefore = gasleft(); + relayer.relay(); + uint256 gasCost = gasBefore - gasleft(); + console.log("RelayerV1[%s] cost = %d", label, gasCost); + } + + function test_relaysTheRate() public { + relayAndLogGas("happy path"); + // relayer.relay(); + (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); + assertEq(medianRate, expectedReport); + } + + function test_relaysTheRate_withLesserGreater_whenLesser_andCantExpireDirectly() public { + address oldRelayer = makeAddr("oldRelayer"); + sortedOracles.addOracle(rateFeedId, oldRelayer); + + vm.prank(oldRelayer); + sortedOracles.report(rateFeedId, expectedReport + 2, address(0), address(0)); + + vm.warp(block.timestamp + 200); // Not enough to be able to expire the first report + setAggregatorPrices(); // Update timestamps + relayAndLogGas("other report no expiry"); + // relayer.relay(); + + (address[] memory oracles, uint256[] memory rates, ) = sortedOracles.getRates(rateFeedId); + assertEq(oracles.length, 2); + assertEq(oracles[1], address(relayer)); + assertEq(rates[1], expectedReport); + + vm.warp(block.timestamp + 400); // First report should be expired + setAggregatorPrices(); // Update timestamps + relayAndLogGas("2 reports with expiry"); + // relayer.relay(); + + // Next report should also cause an expiry, resulting in only one report remaining + // from this oracle. + (oracles, rates, ) = sortedOracles.getRates(rateFeedId); + assertEq(oracles.length, 1); + assertEq(oracles[0], address(relayer)); + assertEq(rates[0], expectedReport); + } + + function test_relaysTheRate_withLesserGreater_whenGreater_andCantExpireDirectly() public { + address oldRelayer = makeAddr("oldRelayer"); + sortedOracles.addOracle(rateFeedId, oldRelayer); + + vm.prank(oldRelayer); + sortedOracles.report(rateFeedId, expectedReport - 2, address(0), address(0)); + + vm.warp(block.timestamp + 200); // Not enough to be able to expire the first report + setAggregatorPrices(); // Update timestamps + relayAndLogGas("other report no expiry"); + // relayer.relay(); + + (address[] memory oracles, uint256[] memory rates, ) = sortedOracles.getRates(rateFeedId); + assertEq(oracles.length, 2); + assertEq(oracles[0], address(relayer)); + assertEq(rates[0], expectedReport); + + vm.warp(block.timestamp + 400); // First report should be expired + setAggregatorPrices(); // Update timestamps + relayAndLogGas("2 reports with expiry"); + // relayer.relay(); + + // Next report should also cause an expiry, resulting in only one report remaining + // from this oracle. + (oracles, rates, ) = sortedOracles.getRates(rateFeedId); + assertEq(oracles.length, 1); + assertEq(oracles[0], address(relayer)); + assertEq(rates[0], expectedReport); + } + + function test_relaysTheRate_withLesserGreater_whenLesser_andCanExpire() public { + address oldRelayer = makeAddr("oldRelayer"); + sortedOracles.addOracle(rateFeedId, oldRelayer); + + vm.prank(oldRelayer); + sortedOracles.report(rateFeedId, expectedReport + 2, address(0), address(0)); + + vm.warp(block.timestamp + 600); // Not enough to be able to expire the first report + setAggregatorPrices(); // Update timestamps + relayAndLogGas("other report with expiry"); + + (address[] memory oracles, uint256[] memory rates, ) = sortedOracles.getRates(rateFeedId); + assertEq(oracles.length, 1); + assertEq(oracles[0], address(relayer)); + assertEq(rates[0], expectedReport); + } + + function test_relaysTheRate_withLesserGreater_whenGreater_andCanExpire() public { + address oldRelayer = makeAddr("oldRelayer"); + sortedOracles.addOracle(rateFeedId, oldRelayer); + + vm.prank(oldRelayer); + sortedOracles.report(rateFeedId, expectedReport - 2, address(0), address(0)); + + vm.warp(block.timestamp + 600); // Not enough to be able to expire the first report + setAggregatorPrices(); // Update timestamps + relayAndLogGas("other report with expiry"); + // relayer.relay(); + + (address[] memory oracles, uint256[] memory rates, ) = sortedOracles.getRates(rateFeedId); + assertEq(oracles.length, 1); + assertEq(oracles[0], address(relayer)); + assertEq(rates[0], expectedReport); + } + + function test_revertsWhenComputingLesserGreaterWithTooManyReporters() public { + address oracle0 = makeAddr("oracle0"); + address oracle1 = makeAddr("oracle1"); + sortedOracles.addOracle(rateFeedId, oracle0); + sortedOracles.addOracle(rateFeedId, oracle1); + + vm.prank(oracle0); + sortedOracles.report(rateFeedId, expectedReport - 2, address(0), address(0)); + vm.prank(oracle1); + sortedOracles.report(rateFeedId, expectedReport, oracle0, address(0)); + + vm.warp(block.timestamp + 100); // Not enough to be able to expire the first report + setAggregatorPrices(); // Update timestamps + vm.expectRevert(TOO_MANY_EXISTING_REPORTS); + relayer.relay(); + } + + function test_revertsOnNegativePrice0() public { + mockAggregator0.setRoundData(-1 * aggregatorPrice0, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnZeroPrice0() public { + mockAggregator0.setRoundData(0, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnEarlierTimestamp() public virtual withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + mockAggregator0.setRoundData(aggregatorPrice0, latestTimestamp - 1); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_relaysOnRecentTimestamp0() public withReport(aReport) { + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + skip(50); + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + relayer.relay(); + uint256 timestamp = sortedOracles.medianTimestamp(rateFeedId); + assertEq(timestamp, block.timestamp); + } + + function test_revertsOnRepeatTimestamp0() public withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + mockAggregator0.setRoundData(aggregatorPrice0, latestTimestamp); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_revertsWhenOldestTimestampIsExpired() public virtual withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp + 1); + vm.warp(block.timestamp + expirySeconds + 1); + vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); + relayer.relay(); + } +} + +contract ChainlinkRelayerV1Test_relay_double is ChainlinkRelayerV1Test_relay_single { + function setUpExpectations() internal virtual override { + aggregatorPrice0 = 420000000; // 4.2 * 1e8 + aggregatorPrice1 = 2000000000000; // 2 * 1e12 + // ^ results in 4.2 * 1 / 2 = 2.1 + expectedReport = 2100000000000000000000000; + } + + function setUpRelayer() internal virtual override { + setUpRelayer(2); + } + + function test_revertsOnNegativePrice1() public { + mockAggregator1.setRoundData(-1 * aggregatorPrice1, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnZeroPrice1() public { + mockAggregator1.setRoundData(0, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnEarlierTimestamp() public virtual override withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + + mockAggregator0.setRoundData(aggregatorPrice0, latestTimestamp - 1); + mockAggregator1.setRoundData(aggregatorPrice1, latestTimestamp - 1); + + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_relaysOnRecentTimestamp1() public withReport(aReport) { + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + skip(50); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); + relayer.relay(); + uint256 timestamp = sortedOracles.medianTimestamp(rateFeedId); + assertEq(timestamp, block.timestamp); + } + + function test_revertsOnRepeatTimestamp1() public withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + mockAggregator1.setRoundData(aggregatorPrice1, latestTimestamp); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_revertsWhenOldestTimestampIsExpired() public virtual override withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + vm.warp(block.timestamp + expirySeconds + 1); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); + vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); + relayer.relay(); + } +} + +contract ChainlinkRelayerV1Test_relay_triple is ChainlinkRelayerV1Test_relay_double { + function setUpExpectations() internal virtual override { + aggregatorPrice0 = 420000000; // 4.2 * 1e8 + aggregatorPrice1 = 2000000000000; // 2 * 1e12 + aggregatorPrice2 = 300000000; // 3 * 1e8 + // ^ results in 4.2 * (1 / 2) * 3 = 6.3 + expectedReport = 6300000000000000000000000; + } + + function setUpRelayer() internal virtual override { + setUpRelayer(3); + } + + function test_revertsOnNegativePrice2() public { + mockAggregator2.setRoundData(-1 * aggregatorPrice2, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnZeroPrice2() public { + mockAggregator2.setRoundData(0, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnEarlierTimestamp() public virtual override withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + + mockAggregator0.setRoundData(aggregatorPrice0, latestTimestamp - 1); + mockAggregator1.setRoundData(aggregatorPrice1, latestTimestamp - 1); + mockAggregator2.setRoundData(aggregatorPrice2, latestTimestamp - 1); + + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_relaysOnRecentTimestamp2() public withReport(aReport) { + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + skip(50); + mockAggregator2.setRoundData(aggregatorPrice0, block.timestamp); + relayer.relay(); + uint256 timestamp = sortedOracles.medianTimestamp(rateFeedId); + assertEq(timestamp, block.timestamp); + } + + function test_revertsOnRepeatTimestamp2() public withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + mockAggregator2.setRoundData(aggregatorPrice2, latestTimestamp); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_revertsWhenOldestTimestampIsExpired() public virtual override withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); + vm.warp(block.timestamp + expirySeconds + 1); + mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp); + vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); + relayer.relay(); + } +} + +contract ChainlinkRelayerV1Test_relay_full is ChainlinkRelayerV1Test_relay_triple { + function setUpExpectations() internal override { + aggregatorPrice0 = 420000000; // 4.2 * 1e8 + aggregatorPrice1 = 2000000000000; // 2 * 1e12 + aggregatorPrice2 = 300000000; // 3 * 1e8 + aggregatorPrice3 = 5000000; // 5 * 1e6 + // ^ results in 4.2 * (1 / 2) * 3 * (1/5) = 1.26 + // in the tests price1 and price3 are inverted + expectedReport = 1260000000000000000000000; + } + + function setUpRelayer() internal override { + setUpRelayer(4); + } + + function test_revertsOnNegativePrice3() public { + mockAggregator3.setRoundData(-1 * aggregatorPrice3, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnZeroPrice3() public { + mockAggregator3.setRoundData(0, block.timestamp); + vm.expectRevert(INVALID_PRICE_ERROR); + relayer.relay(); + } + + function test_revertsOnEarlierTimestamp() public override withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + + mockAggregator0.setRoundData(aggregatorPrice0, latestTimestamp - 1); + mockAggregator1.setRoundData(aggregatorPrice1, latestTimestamp - 1); + mockAggregator2.setRoundData(aggregatorPrice2, latestTimestamp - 1); + mockAggregator3.setRoundData(aggregatorPrice3, latestTimestamp - 1); + + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_relaysOnRecentTimestamp3() public withReport(aReport) { + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + skip(50); + mockAggregator3.setRoundData(aggregatorPrice3, block.timestamp); + relayer.relay(); + uint256 timestamp = sortedOracles.medianTimestamp(rateFeedId); + assertEq(timestamp, block.timestamp); + } + + function test_revertsOnRepeatTimestamp3() public withReport(aReport) { + uint256 latestTimestamp = sortedOracles.medianTimestamp(rateFeedId); + mockAggregator3.setRoundData(aggregatorPrice3, latestTimestamp); + vm.expectRevert(TIMESTAMP_NOT_NEW_ERROR); + relayer.relay(); + } + + function test_revertsWhenOldestTimestampIsExpired() public override withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); + mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp); + vm.warp(block.timestamp + expirySeconds + 1); + mockAggregator2.setRoundData(aggregatorPrice3, block.timestamp); + + vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); + relayer.relay(); + } +} diff --git a/test/utils/BaseTest.next.sol b/test/utils/BaseTest.next.sol index f3acf106..c0cc0e11 100644 --- a/test/utils/BaseTest.next.sol +++ b/test/utils/BaseTest.next.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; +pragma solidity ^0.8.18; import { Test } from "forge-std-next/Test.sol"; import { IRegistry } from "contracts/common/interfaces/IRegistry.sol"; @@ -24,5 +24,11 @@ contract BaseTest is Test { factory.createAt("Registry", REGISTRY_ADDRESS, abi.encode(true)); vm.prank(deployer); IRegistryInit(REGISTRY_ADDRESS).initialize(); + + // Deploy required libraries so that vm.getCode will automatically link + factory.createFromPath( + "contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian", + abi.encodePacked() + ); } } diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol index 28e920e2..b4d22682 100644 --- a/test/utils/BaseTest.t.sol +++ b/test/utils/BaseTest.t.sol @@ -22,5 +22,11 @@ contract BaseTest is Test { factory.createAt("Registry", REGISTRY_ADDRESS, abi.encode(true)); vm.prank(deployer); Registry(REGISTRY_ADDRESS).initialize(); + + // Deploy required libraries so that vm.getCode will automatically link + factory.createFromPath( + "contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian", + abi.encodePacked() + ); } } diff --git a/test/utils/Factory.sol b/test/utils/Factory.sol index 51c8d3dc..976cbb3d 100644 --- a/test/utils/Factory.sol +++ b/test/utils/Factory.sol @@ -8,6 +8,10 @@ interface MiniVM { function etch(address _addr, bytes calldata _code) external; function getCode(string calldata _path) external view returns (bytes memory); + + function getDeployedCode(string calldata _path) external view returns (bytes memory); + + function prank(address pranker) external; } /** @@ -31,6 +35,22 @@ contract Factory { return addr; } + function createFromPath( + string memory _path, + bytes memory args, + address deployer + ) public returns (address) { + bytes memory bytecode = abi.encodePacked(VM.getCode(_path), args); + address addr; + + VM.prank(deployer); + // solhint-disable-next-line no-inline-assembly + assembly { + addr := create(0, add(bytecode, 0x20), mload(bytecode)) + } + return addr; + } + function createContract(string memory _contract, bytes memory args) public returns (address addr) { string memory path = contractPath(_contract); addr = createFromPath(path, args); From 7f9f7b9d26ca1f2e99c2ff8926cf56a5f9ec094d Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:29:44 +0200 Subject: [PATCH 08/38] feat/cosmetic: Move Proxy Contracts around (#498) ### Description When we restructured the repo a while ago I moved the proxy contracts to the legacy folder thinking that we might not deploy named proxies in the future, and maybe upgrade to a more modern Proxy implementation, but we have continued to add the proxies inside the `legacy` folder which doesn't feel great as that folder is meant to be removed (and probably can be at this point). So in order to embrace what we're doing I propose collocating the proxy contracts to the `contracts/tokens` directory. And as an addition to that, I propose collocating all proxies with their contracts like Martin also did in the `contracts/oracles` for the new ChainlinkRelayerFactory contract and proxy. So I also moved around the proxies in `contracts/proxies`. This shouldn't really effect anything else. ### Other changes - Renamed stabletokenPSO to stabletokenPHP ### Tested Ran tests, all is well. ### Related issues N/A ### Backwards compatibility YES ### Documentation N/A --------- Co-authored-by: Bayological <6872903+bayological@users.noreply.github.com> Co-authored-by: denviljclarke <60730266+denviljclarke@users.noreply.github.com> --- contracts/{proxies => common}/SortedOraclesProxy.sol | 2 +- contracts/{proxies => swap}/BiPoolManagerProxy.sol | 0 contracts/{proxies => swap}/BrokerProxy.sol | 0 contracts/{proxies => swap}/ReserveProxy.sol | 0 contracts/{legacy/proxies => tokens}/StableTokenBRLProxy.sol | 2 +- contracts/{legacy/proxies => tokens}/StableTokenCOPProxy.sol | 2 +- contracts/{legacy/proxies => tokens}/StableTokenEURProxy.sol | 2 +- contracts/{legacy/proxies => tokens}/StableTokenINRProxy.sol | 2 +- contracts/{legacy/proxies => tokens}/StableTokenKESProxy.sol | 2 +- .../StableTokenPHPProxy.sol} | 4 ++-- contracts/{legacy/proxies => tokens}/StableTokenProxy.sol | 2 +- contracts/{legacy/proxies => tokens}/StableTokenXOFProxy.sol | 2 +- 12 files changed, 10 insertions(+), 10 deletions(-) rename contracts/{proxies => common}/SortedOraclesProxy.sol (84%) rename contracts/{proxies => swap}/BiPoolManagerProxy.sol (100%) rename contracts/{proxies => swap}/BrokerProxy.sol (100%) rename contracts/{proxies => swap}/ReserveProxy.sol (100%) rename contracts/{legacy/proxies => tokens}/StableTokenBRLProxy.sol (83%) rename contracts/{legacy/proxies => tokens}/StableTokenCOPProxy.sol (83%) rename contracts/{legacy/proxies => tokens}/StableTokenEURProxy.sol (83%) rename contracts/{legacy/proxies => tokens}/StableTokenINRProxy.sol (83%) rename contracts/{legacy/proxies => tokens}/StableTokenKESProxy.sol (83%) rename contracts/{legacy/proxies/StableTokenPSOProxy.sol => tokens/StableTokenPHPProxy.sol} (62%) rename contracts/{legacy/proxies => tokens}/StableTokenProxy.sol (82%) rename contracts/{legacy/proxies => tokens}/StableTokenXOFProxy.sol (83%) diff --git a/contracts/proxies/SortedOraclesProxy.sol b/contracts/common/SortedOraclesProxy.sol similarity index 84% rename from contracts/proxies/SortedOraclesProxy.sol rename to contracts/common/SortedOraclesProxy.sol index 3d0e4727..eba26a76 100644 --- a/contracts/proxies/SortedOraclesProxy.sol +++ b/contracts/common/SortedOraclesProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "./Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract SortedOraclesProxy is Proxy { diff --git a/contracts/proxies/BiPoolManagerProxy.sol b/contracts/swap/BiPoolManagerProxy.sol similarity index 100% rename from contracts/proxies/BiPoolManagerProxy.sol rename to contracts/swap/BiPoolManagerProxy.sol diff --git a/contracts/proxies/BrokerProxy.sol b/contracts/swap/BrokerProxy.sol similarity index 100% rename from contracts/proxies/BrokerProxy.sol rename to contracts/swap/BrokerProxy.sol diff --git a/contracts/proxies/ReserveProxy.sol b/contracts/swap/ReserveProxy.sol similarity index 100% rename from contracts/proxies/ReserveProxy.sol rename to contracts/swap/ReserveProxy.sol diff --git a/contracts/legacy/proxies/StableTokenBRLProxy.sol b/contracts/tokens/StableTokenBRLProxy.sol similarity index 83% rename from contracts/legacy/proxies/StableTokenBRLProxy.sol rename to contracts/tokens/StableTokenBRLProxy.sol index 66d8389a..4ebbde82 100644 --- a/contracts/legacy/proxies/StableTokenBRLProxy.sol +++ b/contracts/tokens/StableTokenBRLProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenBRLProxy is Proxy { diff --git a/contracts/legacy/proxies/StableTokenCOPProxy.sol b/contracts/tokens/StableTokenCOPProxy.sol similarity index 83% rename from contracts/legacy/proxies/StableTokenCOPProxy.sol rename to contracts/tokens/StableTokenCOPProxy.sol index 5ca9e0ea..9abe6e85 100644 --- a/contracts/legacy/proxies/StableTokenCOPProxy.sol +++ b/contracts/tokens/StableTokenCOPProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenCOPProxy is Proxy { diff --git a/contracts/legacy/proxies/StableTokenEURProxy.sol b/contracts/tokens/StableTokenEURProxy.sol similarity index 83% rename from contracts/legacy/proxies/StableTokenEURProxy.sol rename to contracts/tokens/StableTokenEURProxy.sol index fb521dc6..53ee6ec7 100644 --- a/contracts/legacy/proxies/StableTokenEURProxy.sol +++ b/contracts/tokens/StableTokenEURProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenEURProxy is Proxy { diff --git a/contracts/legacy/proxies/StableTokenINRProxy.sol b/contracts/tokens/StableTokenINRProxy.sol similarity index 83% rename from contracts/legacy/proxies/StableTokenINRProxy.sol rename to contracts/tokens/StableTokenINRProxy.sol index 4ca49a76..f1e8f42a 100644 --- a/contracts/legacy/proxies/StableTokenINRProxy.sol +++ b/contracts/tokens/StableTokenINRProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenINRProxy is Proxy { diff --git a/contracts/legacy/proxies/StableTokenKESProxy.sol b/contracts/tokens/StableTokenKESProxy.sol similarity index 83% rename from contracts/legacy/proxies/StableTokenKESProxy.sol rename to contracts/tokens/StableTokenKESProxy.sol index 10481a81..6b40487a 100644 --- a/contracts/legacy/proxies/StableTokenKESProxy.sol +++ b/contracts/tokens/StableTokenKESProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenKESProxy is Proxy { diff --git a/contracts/legacy/proxies/StableTokenPSOProxy.sol b/contracts/tokens/StableTokenPHPProxy.sol similarity index 62% rename from contracts/legacy/proxies/StableTokenPSOProxy.sol rename to contracts/tokens/StableTokenPHPProxy.sol index 40c00c7d..4f8b6a76 100644 --- a/contracts/legacy/proxies/StableTokenPSOProxy.sol +++ b/contracts/tokens/StableTokenPHPProxy.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenPSOProxy is Proxy { +contract StableTokenPHPProxy is Proxy { } diff --git a/contracts/legacy/proxies/StableTokenProxy.sol b/contracts/tokens/StableTokenProxy.sol similarity index 82% rename from contracts/legacy/proxies/StableTokenProxy.sol rename to contracts/tokens/StableTokenProxy.sol index ff74aa55..0767929f 100644 --- a/contracts/legacy/proxies/StableTokenProxy.sol +++ b/contracts/tokens/StableTokenProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenProxy is Proxy { diff --git a/contracts/legacy/proxies/StableTokenXOFProxy.sol b/contracts/tokens/StableTokenXOFProxy.sol similarity index 83% rename from contracts/legacy/proxies/StableTokenXOFProxy.sol rename to contracts/tokens/StableTokenXOFProxy.sol index 72d594c7..958813d5 100644 --- a/contracts/legacy/proxies/StableTokenXOFProxy.sol +++ b/contracts/tokens/StableTokenXOFProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "../common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ contract StableTokenXOFProxy is Proxy { From 6feadf3bdc6fb0cf1cb481ecd0c9e1d5a36469e2 Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Thu, 22 Aug 2024 13:21:57 +0200 Subject: [PATCH 09/38] feat: Add relayerDeployer ACL role to the factory (#499) ### Description We realised that it would be better operationally to separate the proxy ownership ACL from the deployer ACL, that way we can keep using the `mento-deployer` account for relayer deploy, and use the multisig only for proxy updates. ### Other changes N/A ### Tested Tests added ### Related issues N/A ### Backwards compatibility No, but no mainnet deployment yet. ### Documentation N/A --- .../interfaces/IChainlinkRelayerFactory.sol | 13 +- contracts/oracles/ChainlinkRelayerFactory.sol | 35 ++++- .../ChainlinkRelayerIntegration.t.sol | 4 +- test/oracles/ChainlinkRelayerFactory.t.sol | 137 +++++++++++------- 4 files changed, 133 insertions(+), 56 deletions(-) diff --git a/contracts/interfaces/IChainlinkRelayerFactory.sol b/contracts/interfaces/IChainlinkRelayerFactory.sol index 2cac2d99..d6dd7362 100644 --- a/contracts/interfaces/IChainlinkRelayerFactory.sol +++ b/contracts/interfaces/IChainlinkRelayerFactory.sol @@ -19,6 +19,13 @@ interface IChainlinkRelayerFactory { IChainlinkRelayer.ChainlinkAggregator[] aggregators ); + /** + * @notice Emitted when the relayer deployer is updated. + * @param newRelayerDeployer Address of the new relayer deployer. + * @param oldRelayerDeployer Address of the old relayer deployer. + */ + event RelayerDeployerUpdated(address indexed newRelayerDeployer, address indexed oldRelayerDeployer); + /** * @notice Emitted when a relayer is removed. * @param relayerAddress Address of the removed relayer. @@ -26,10 +33,14 @@ interface IChainlinkRelayerFactory { */ event RelayerRemoved(address indexed relayerAddress, address indexed rateFeedId); - function initialize(address _sortedOracles) external; + function initialize(address _sortedOracles, address _relayerDeployer) external; function sortedOracles() external returns (address); + function setRelayerDeployer(address _relayerDeployer) external; + + function relayerDeployer() external returns (address); + function deployRelayer( address rateFeedId, string calldata rateFeedDescription, diff --git a/contracts/oracles/ChainlinkRelayerFactory.sol b/contracts/oracles/ChainlinkRelayerFactory.sol index 43faaa3e..ef40b1d1 100644 --- a/contracts/oracles/ChainlinkRelayerFactory.sol +++ b/contracts/oracles/ChainlinkRelayerFactory.sol @@ -23,6 +23,11 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable */ address[] public rateFeeds; + /** + * @notice Account that is allowed to deploy relayers. + */ + address public relayerDeployer; + /** * @notice Thrown when trying to deploy a relayer to an address that already has code. * @param contractAddress Address at which the relayer could not be deployed. @@ -51,6 +56,17 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable */ error NoRelayerForRateFeedId(address rateFeedId); + /// @notice Thrown when a non-deployer tries to call a deployer-only function. + error NotAllowed(); + + /// @notice Modifier to restrict a function to the deployer. + modifier onlyDeployer() { + if (msg.sender != relayerDeployer && msg.sender != owner()) { + revert NotAllowed(); + } + _; + } + /** * @notice Constructor for the logic contract. * @param disable If `true`, disables the initializer. @@ -69,9 +85,20 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Initializes the factory. * @param _sortedOracles The SortedOracles instance deployed relayers should report to. */ - function initialize(address _sortedOracles) external initializer { + function initialize(address _sortedOracles, address _relayerDeployer) external initializer { __Ownable_init(); sortedOracles = _sortedOracles; + relayerDeployer = _relayerDeployer; + } + + /** + * @notice Sets the address of the relayer deployer. + * @param newRelayerDeployer The address of the relayer deployer. + */ + function setRelayerDeployer(address newRelayerDeployer) external onlyOwner { + address oldRelayerDeployer = relayerDeployer; + relayerDeployer = newRelayerDeployer; + emit RelayerDeployerUpdated(newRelayerDeployer, oldRelayerDeployer); } /** @@ -87,7 +114,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable address rateFeedId, string calldata rateFeedDescription, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators - ) public onlyOwner returns (address relayerAddress) { + ) public onlyDeployer returns (address relayerAddress) { if (address(deployedRelayers[rateFeedId]) != address(0)) { revert RelayerForFeedExists(rateFeedId); } @@ -121,7 +148,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Removes a relayer from the list of deployed relayers. * @param rateFeedId The rate feed whose relayer should be removed. */ - function removeRelayer(address rateFeedId) public onlyOwner { + function removeRelayer(address rateFeedId) public onlyDeployer { address relayerAddress = address(deployedRelayers[rateFeedId]); if (relayerAddress == address(0)) { @@ -156,7 +183,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable address rateFeedId, string calldata rateFeedDescription, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators - ) external onlyOwner returns (address relayerAddress) { + ) external onlyDeployer returns (address relayerAddress) { removeRelayer(rateFeedId); return deployRelayer(rateFeedId, rateFeedDescription, aggregators); } diff --git a/test/integration/ChainlinkRelayerIntegration.t.sol b/test/integration/ChainlinkRelayerIntegration.t.sol index a1d21198..4df546b0 100644 --- a/test/integration/ChainlinkRelayerIntegration.t.sol +++ b/test/integration/ChainlinkRelayerIntegration.t.sol @@ -35,7 +35,7 @@ contract ChainlinkRelayerIntegration is IntegrationTest { abi.encode( address(relayerFactoryImplementation), address(proxyAdmin), - abi.encodeWithSignature("initialize(address)", address(sortedOracles)) + abi.encodeWithSignature("initialize(address,address)", address(sortedOracles), owner) ) ) ); @@ -82,7 +82,7 @@ contract ChainlinkRelayerIntegration_ProxySetup is ChainlinkRelayerIntegration { function test_implementationNotInitializable() public { vm.expectRevert("Initializable: contract is already initialized"); - relayerFactoryImplementation.initialize(address(sortedOracles)); + relayerFactoryImplementation.initialize(address(sortedOracles), address(this)); } } diff --git a/test/oracles/ChainlinkRelayerFactory.t.sol b/test/oracles/ChainlinkRelayerFactory.t.sol index be288157..c80812d3 100644 --- a/test/oracles/ChainlinkRelayerFactory.t.sol +++ b/test/oracles/ChainlinkRelayerFactory.t.sol @@ -12,7 +12,9 @@ import { ChainlinkRelayerFactory } from "contracts/oracles/ChainlinkRelayerFacto contract ChainlinkRelayerFactoryTest is BaseTest { IChainlinkRelayerFactory relayerFactory; address owner = makeAddr("owner"); + address relayerDeployer = makeAddr("relayerDeployer"); address nonOwner = makeAddr("nonOwner"); + address nonDeployer = makeAddr("nonDeployer"); address mockSortedOracles = makeAddr("sortedOracles"); address[4] mockAggregators = [ makeAddr("aggregator1"), @@ -24,6 +26,8 @@ contract ChainlinkRelayerFactoryTest is BaseTest { address aRateFeed = rateFeeds[0]; string aRateFeedDescription = "CELO/USD"; + bytes constant NOT_ALLOWED_ERROR = abi.encodeWithSignature("NotAllowed()"); + event RelayerDeployed( address indexed relayerAddress, address indexed rateFeedId, @@ -31,6 +35,7 @@ contract ChainlinkRelayerFactoryTest is BaseTest { IChainlinkRelayer.ChainlinkAggregator[] aggregators ); event RelayerRemoved(address indexed relayerAddress, address indexed rateFeedId); + event RelayerDeployerUpdated(address indexed newRelayerDeployer, address indexed oldRelayerDeployer); function oneAggregator(uint256 aggregatorIndex) internal @@ -52,7 +57,7 @@ contract ChainlinkRelayerFactoryTest is BaseTest { function setUp() public virtual { relayerFactory = IChainlinkRelayerFactory(new ChainlinkRelayerFactory(false)); vm.prank(owner); - relayerFactory.initialize(mockSortedOracles); + relayerFactory.initialize(mockSortedOracles, relayerDeployer); } function expectedRelayerAddress( @@ -129,6 +134,25 @@ contract ChainlinkRelayerFactoryTest_transferOwnership is ChainlinkRelayerFactor } } +contract ChainlinkRelayerFactoryTest_setRelayerDeployer is ChainlinkRelayerFactoryTest { + function test_setRelayerDeployer() public { + address newDeployer = makeAddr("newDeployer"); + vm.expectEmit(true, true, false, false, address(relayerFactory)); + emit RelayerDeployerUpdated({ newRelayerDeployer: newDeployer, oldRelayerDeployer: relayerDeployer }); + vm.prank(owner); + relayerFactory.setRelayerDeployer(newDeployer); + address realRelayerDeployer = relayerFactory.relayerDeployer(); + assertEq(realRelayerDeployer, newDeployer); + } + + function test_failsWhenCalledByNonOwner() public { + address newDeployer = makeAddr("newDeployer"); + vm.prank(nonOwner); + vm.expectRevert("Ownable: caller is not the owner"); + relayerFactory.setRelayerDeployer(newDeployer); + } +} + contract ChainlinkRelayerFactoryTest_renounceOwnership is ChainlinkRelayerFactoryTest { function test_setsOwnerToZeroAddress() public { vm.prank(owner); @@ -148,21 +172,21 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes IChainlinkRelayer relayer; function test_setsRateFeed() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address rateFeed = relayer.rateFeedId(); assertEq(rateFeed, aRateFeed); } function test_setsRateFeedDescription() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); string memory rateFeedDescription = relayer.rateFeedDescription(); assertEq(rateFeedDescription, aRateFeedDescription); } function test_setsAggregators() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); IChainlinkRelayer.ChainlinkAggregator[] memory expectedAggregators = fourAggregators(); IChainlinkRelayer.ChainlinkAggregator[] memory actualAggregators = relayer.getAggregators(); @@ -174,14 +198,14 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes } function test_setsSortedOracles() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address sortedOracles = relayer.sortedOracles(); assertEq(sortedOracles, mockSortedOracles); } function test_deploysToTheCorrectAddress() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address expectedAddress = expectedRelayerAddress({ rateFeedId: aRateFeed, @@ -210,19 +234,19 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes rateFeedDescription: aRateFeedDescription, aggregators: fourAggregators() }); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } function test_remembersTheRelayerAddress() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); address storedAddress = relayerFactory.getRelayer(aRateFeed); assertEq(storedAddress, address(relayer)); } function test_revertsWhenDeployingToAddressWithCode() public { - vm.prank(owner); + vm.prank(relayerDeployer); address futureAddress = expectedRelayerAddress( aRateFeed, aRateFeedDescription, @@ -232,29 +256,34 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes ); vm.etch(futureAddress, abi.encode("This is a great contract's bytecode")); vm.expectRevert(contractAlreadyExistsError(address(futureAddress), aRateFeed)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } function test_revertsWhenDeployingTheSameRelayer() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); vm.expectRevert(relayerForFeedExistsError(aRateFeed)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } function test_revertsWhenDeployingForTheSameRateFeed() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); vm.expectRevert(relayerForFeedExistsError(aRateFeed)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, oneAggregator(0)); } - function test_revertsWhenCalledByNonOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); + function test_revertsWhenCalledByNonDeployer() public { + vm.expectRevert(NOT_ALLOWED_ERROR); + vm.prank(nonDeployer); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); + } + + function test_worksWhenCalledByOwner() public { + vm.prank(owner); relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); } } @@ -266,7 +295,7 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest } function test_returnsRelayerWhenThereIsOne() public { - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 1); @@ -274,11 +303,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest } function test_returnsMultipleRelayersWhenThereAreMore() public { - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress3 = relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, oneAggregator(2)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 3); @@ -288,11 +317,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest } function test_returnsADifferentRelayerAfterRedeployment() public { - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress2 = relayerFactory.redeployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(2)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 2); @@ -301,13 +330,13 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest } function test_doesntReturnARemovedRelayer() public { - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); - vm.prank(owner); + vm.prank(relayerDeployer); address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, oneAggregator(2)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.removeRelayer(rateFeeds[2]); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 2); @@ -322,14 +351,14 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes function setUp() public override { super.setUp(); - vm.prank(owner); + vm.prank(relayerDeployer); relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); } function test_removesTheRelayer() public { - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.removeRelayer(aRateFeed); - vm.prank(owner); + vm.prank(relayerDeployer); address relayer = relayerFactory.getRelayer(aRateFeed); assertEq(relayer, address(0)); } @@ -338,14 +367,14 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes // solhint-disable-next-line func-named-parameters vm.expectEmit(true, true, true, false, address(relayerFactory)); emit RelayerRemoved({ relayerAddress: relayerAddress, rateFeedId: aRateFeed }); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.removeRelayer(aRateFeed); } function test_doesntRemoveOtherRelayers() public { - vm.prank(owner); + vm.prank(relayerDeployer); address newRelayerAddress = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.removeRelayer(aRateFeed); address[] memory relayers = relayerFactory.getRelayers(); @@ -355,13 +384,18 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes function test_revertsOnNonexistentRelayer() public { vm.expectRevert(noRelayerForRateFeedId(rateFeeds[1])); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.removeRelayer(rateFeeds[1]); } - function test_revertsWhenCalledByNonOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); + function test_revertsWhenCalledByNonDeployer() public { + vm.expectRevert(NOT_ALLOWED_ERROR); + vm.prank(nonDeployer); + relayerFactory.removeRelayer(aRateFeed); + } + + function test_worksWhenCalledByOwner() public { + vm.prank(owner); relayerFactory.removeRelayer(aRateFeed); } } @@ -371,12 +405,12 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function setUp() public override { super.setUp(); - vm.prank(owner); + vm.prank(relayerDeployer); oldAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); } function test_setsRateFeedOnNewRelayer() public { - vm.prank(owner); + vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) ); @@ -386,7 +420,7 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT } function test_setsAggregatorOnNewRelayer() public { - vm.prank(owner); + vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) ); @@ -401,7 +435,7 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT } function test_setsSortedOraclesOnNewRelayer() public { - vm.prank(owner); + vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) ); @@ -411,7 +445,7 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT } function test_deploysToTheCorrectNewAddress() public { - vm.prank(owner); + vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) ); @@ -446,12 +480,12 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT rateFeedDescription: aRateFeedDescription, aggregators: oneAggregator(1) }); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); } function test_remembersTheNewRelayerAddress() public { - vm.prank(owner); + vm.prank(relayerDeployer); address relayer = relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); address storedAddress = relayerFactory.getRelayer(aRateFeed); assertEq(storedAddress, relayer); @@ -459,13 +493,18 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function test_revertsWhenDeployingTheSameExactRelayer() public { vm.expectRevert(contractAlreadyExistsError(oldAddress, aRateFeed)); - vm.prank(owner); + vm.prank(relayerDeployer); relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); } - function test_revertsWhenCalledByNonOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - vm.prank(nonOwner); + function test_revertsWhenCalledByNonDeployer() public { + vm.expectRevert(NOT_ALLOWED_ERROR); + vm.prank(nonDeployer); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); + } + + function test_worksWhenCalledByOwner() public { + vm.prank(owner); relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); } } From f0d5316a5facb23251079b5f23f3108701382268 Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:28:03 +0200 Subject: [PATCH 10/38] Revert "feat: remove maxTimestampSpread and only rely on expiry" (#504) This reverts commit ac1c5406441c01119eaf9c0de54512f7cfe1ff19. ### Description Brings back the maxTimestampSpread setting. A visual explanation here: image We need it because CELO/USD has a heartbeat of 24hr and PHP/USD 5minutes, therefore we could be in a situation where the spread between reports is quite large, and if we rely only on max timestamp spread we could be reporting with stale data. The rule of thumb is: - Set `maxTimestampSpread` by considering the largest heartbeat frequency. - Set `tokenExpiry` in SortedOracles by considering the shortest heartbeat frequency. ### Other changes N/A ### Tested Yup ### Related issues N/A ### Backwards compatibility N/A ### Documentation N/A --------- Co-authored-by: chapati Co-authored-by: philbow61 <80156619+philbow61@users.noreply.github.com> --- contracts/interfaces/IChainlinkRelayer.sol | 2 + .../interfaces/IChainlinkRelayerFactory.sol | 3 + contracts/oracles/ChainlinkRelayerFactory.sol | 16 ++- contracts/oracles/ChainlinkRelayerV1.sol | 34 +++++- .../ChainlinkRelayerIntegration.t.sol | 6 +- test/oracles/ChainlinkRelayerFactory.t.sol | 87 ++++++++------- test/oracles/ChainlinkRelayerV1.t.sol | 102 +++++++++++++----- 7 files changed, 180 insertions(+), 70 deletions(-) diff --git a/contracts/interfaces/IChainlinkRelayer.sol b/contracts/interfaces/IChainlinkRelayer.sol index 47fdd1b5..fe4b9e68 100644 --- a/contracts/interfaces/IChainlinkRelayer.sol +++ b/contracts/interfaces/IChainlinkRelayer.sol @@ -21,5 +21,7 @@ interface IChainlinkRelayer { function getAggregators() external returns (ChainlinkAggregator[] memory); + function maxTimestampSpread() external returns (uint256); + function relay() external; } diff --git a/contracts/interfaces/IChainlinkRelayerFactory.sol b/contracts/interfaces/IChainlinkRelayerFactory.sol index d6dd7362..844c318c 100644 --- a/contracts/interfaces/IChainlinkRelayerFactory.sol +++ b/contracts/interfaces/IChainlinkRelayerFactory.sol @@ -44,6 +44,7 @@ interface IChainlinkRelayerFactory { function deployRelayer( address rateFeedId, string calldata rateFeedDescription, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) external returns (address); @@ -52,6 +53,7 @@ interface IChainlinkRelayerFactory { function redeployRelayer( address rateFeedId, string calldata rateFeedDescription, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) external returns (address); @@ -62,6 +64,7 @@ interface IChainlinkRelayerFactory { function computedRelayerAddress( address rateFeedId, string calldata rateFeedDescription, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) external returns (address); } diff --git a/contracts/oracles/ChainlinkRelayerFactory.sol b/contracts/oracles/ChainlinkRelayerFactory.sol index ef40b1d1..9e87a6cb 100644 --- a/contracts/oracles/ChainlinkRelayerFactory.sol +++ b/contracts/oracles/ChainlinkRelayerFactory.sol @@ -105,6 +105,8 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Deploys a new relayer contract. * @param rateFeedId The rate feed ID for which the relayer will report. * @param rateFeedDescription Human-readable rate feed, which the relayer will report on, i.e. "CELO/USD". + * @param maxTimestampSpread Max difference in milliseconds between the earliest and + * latest timestamp of all aggregators in the price path. * @param aggregators Array of ChainlinkAggregator structs defining the price path. * See contract-level @dev comment in the ChainlinkRelayerV1 contract, * for an explanation on price paths. @@ -113,13 +115,14 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable function deployRelayer( address rateFeedId, string calldata rateFeedDescription, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) public onlyDeployer returns (address relayerAddress) { if (address(deployedRelayers[rateFeedId]) != address(0)) { revert RelayerForFeedExists(rateFeedId); } - address expectedAddress = computedRelayerAddress(rateFeedId, rateFeedDescription, aggregators); + address expectedAddress = computedRelayerAddress(rateFeedId, rateFeedDescription, maxTimestampSpread, aggregators); if (expectedAddress.code.length > 0) { revert ContractAlreadyExists(expectedAddress, rateFeedId); } @@ -129,6 +132,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable rateFeedId, rateFeedDescription, sortedOracles, + maxTimestampSpread, aggregators ); @@ -176,16 +180,19 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * has been upgraded since the last deployment of the relayer). * @param rateFeedId The rate feed ID for which the relayer will report. * @param rateFeedDescription Human-readable rate feed, which the relayer will report on, i.e. "CELO/USD". + * @param maxTimestampSpread Max difference in milliseconds between the earliest and + * latest timestamp of all aggregators in the price path. * @param aggregators Array of ChainlinkAggregator structs defining the price path. * @return relayerAddress The address of the newly deployed relayer contract. */ function redeployRelayer( address rateFeedId, string calldata rateFeedDescription, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) external onlyDeployer returns (address relayerAddress) { removeRelayer(rateFeedId); - return deployRelayer(rateFeedId, rateFeedDescription, aggregators); + return deployRelayer(rateFeedId, rateFeedDescription, maxTimestampSpread, aggregators); } /** @@ -224,12 +231,15 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable * @notice Computes the expected CREATE2 address for given relayer parameters. * @param rateFeedId The rate feed ID. * @param rateFeedDescription The human readable description of the reported rate feed. + * @param maxTimestampSpread Max difference in milliseconds between the earliest and + * latest timestamp of all aggregators in the price path. * @param aggregators Array of ChainlinkAggregator structs defining the price path. * @dev See https://eips.ethereum.org/EIPS/eip-1014. */ function computedRelayerAddress( address rateFeedId, string calldata rateFeedDescription, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] calldata aggregators ) public view returns (address) { bytes32 salt = _getSalt(); @@ -245,7 +255,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable keccak256( abi.encodePacked( type(ChainlinkRelayerV1).creationCode, - abi.encode(rateFeedId, rateFeedDescription, sortedOracles, aggregators) + abi.encode(rateFeedId, rateFeedDescription, sortedOracles, maxTimestampSpread, aggregators) ) ) ) diff --git a/contracts/oracles/ChainlinkRelayerV1.sol b/contracts/oracles/ChainlinkRelayerV1.sol index 8ebce3ca..6e08cccb 100644 --- a/contracts/oracles/ChainlinkRelayerV1.sol +++ b/contracts/oracles/ChainlinkRelayerV1.sol @@ -89,6 +89,13 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { /// @notice The number of aggregators provided during construction 1 <= aggregatorCount <= 4. uint256 private immutable aggregatorCount; + /** + * @notice Maximum timestamp deviation allowed between all report timestamps pulled + * from the Chainlink aggregators. + * @dev Only relevant when aggregatorCount > 1. + */ + uint256 public immutable maxTimestampSpread; + /** * @notice Human-readable description of the rate feed. * @dev Should only be used off-chain for easier debugging / UI generation, @@ -102,6 +109,10 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { /// @notice Used when more than four aggregators are passed into the constructor. error TooManyAggregators(); + /// @notice Used when a) there is more than 1 aggregator and the maxTimestampSpread is 0, + /// OR b) when there is only 1 aggregator and the maxTimestampSpread is not 0. + error InvalidMaxTimestampSpread(); + /// @notice Used when a new price's timestamp is not newer than the most recent SortedOracles timestamp. error TimestampNotNew(); @@ -110,7 +121,11 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { /// @notice Used when a negative or zero price is returned by the Chainlink aggregator. error InvalidPrice(); - + /** + * @notice Used when the spread between the earliest and latest timestamp + * of the aggregators is above the maximum allowed. + */ + error TimestampSpreadTooHigh(); /** * @notice Used when trying to recover from a lesser/greater revert and there are * too many existing reports in SortedOracles. @@ -128,16 +143,20 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @param _rateFeedId ID of the rate feed this relayer instance relays for. * @param _rateFeedDescription The human-readable description of the reported rate feed. * @param _sortedOracles Address of the SortedOracles contract to relay to. + * @param _maxTimestampSpread Max difference in milliseconds between the earliest and + * latest timestamp of all aggregators in the price path. * @param _aggregators Array of ChainlinkAggregator structs defining the price path. */ constructor( address _rateFeedId, string memory _rateFeedDescription, address _sortedOracles, + uint256 _maxTimestampSpread, ChainlinkAggregator[] memory _aggregators ) { rateFeedId = _rateFeedId; sortedOracles = _sortedOracles; + maxTimestampSpread = _maxTimestampSpread; rateFeedDescription = _rateFeedDescription; aggregatorCount = _aggregators.length; @@ -149,6 +168,10 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { revert TooManyAggregators(); } + if ((aggregatorCount > 1 && _maxTimestampSpread == 0) || (aggregatorCount == 1 && _maxTimestampSpread != 0)) { + revert InvalidMaxTimestampSpread(); + } + ChainlinkAggregator[] memory aggregators = new ChainlinkAggregator[](4); for (uint256 i = 0; i < _aggregators.length; i++) { if (_aggregators[i].aggregator == address(0)) { @@ -183,7 +206,8 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @dev Performs checks on the timestamp, will revert if any fails: * - The most recent Chainlink timestamp should be strictly newer than the most * recent timestamp in SortedOracles. - * - The oldest Chainlink timestamp should not be considered expired by SortedOracles. + * - The most recent Chainlink timestamp should not be considered expired by SortedOracles. + * - The spread between aggregator timestamps is less than the maxTimestampSpread. */ function relay() external { ISortedOraclesMin _sortedOracles = ISortedOraclesMin(sortedOracles); @@ -201,13 +225,17 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { newestChainlinkTs = timestamp > newestChainlinkTs ? timestamp : newestChainlinkTs; } + if (newestChainlinkTs - oldestChainlinkTs > maxTimestampSpread) { + revert TimestampSpreadTooHigh(); + } + uint256 lastReportTs = _sortedOracles.medianTimestamp(rateFeedId); if (lastReportTs > 0 && newestChainlinkTs <= lastReportTs) { revert TimestampNotNew(); } - if (isTimestampExpired(oldestChainlinkTs)) { + if (isTimestampExpired(newestChainlinkTs)) { revert ExpiredTimestamp(); } diff --git a/test/integration/ChainlinkRelayerIntegration.t.sol b/test/integration/ChainlinkRelayerIntegration.t.sol index 4df546b0..2d722ad4 100644 --- a/test/integration/ChainlinkRelayerIntegration.t.sol +++ b/test/integration/ChainlinkRelayerIntegration.t.sol @@ -108,7 +108,7 @@ contract ChainlinkRelayerIntegration_ReportAfterRedeploy is ChainlinkRelayerInte vm.prank(owner); IChainlinkRelayer chainlinkRelayer0 = IChainlinkRelayer( - relayerFactory.deployRelayer(rateFeedId, "cUSD/FOO", aggregatorList0) + relayerFactory.deployRelayer(rateFeedId, "cUSD/FOO", 0, aggregatorList0) ); vm.prank(deployer); @@ -121,7 +121,7 @@ contract ChainlinkRelayerIntegration_ReportAfterRedeploy is ChainlinkRelayerInte vm.prank(owner); IChainlinkRelayer chainlinkRelayer1 = IChainlinkRelayer( - relayerFactory.redeployRelayer(rateFeedId, "cUSD/FOO", aggregatorList1) + relayerFactory.redeployRelayer(rateFeedId, "cUSD/FOO", 0, aggregatorList1) ); vm.prank(deployer); @@ -158,7 +158,7 @@ contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelay IChainlinkRelayer.ChainlinkAggregator[] memory aggregators = new IChainlinkRelayer.ChainlinkAggregator[](1); aggregators[0] = IChainlinkRelayer.ChainlinkAggregator(address(chainlinkAggregator), false); vm.prank(owner); - chainlinkRelayer = IChainlinkRelayer(relayerFactory.deployRelayer(rateFeedId, "CELO/USD", aggregators)); + chainlinkRelayer = IChainlinkRelayer(relayerFactory.deployRelayer(rateFeedId, "CELO/USD", 0, aggregators)); vm.prank(deployer); sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer)); diff --git a/test/oracles/ChainlinkRelayerFactory.t.sol b/test/oracles/ChainlinkRelayerFactory.t.sol index c80812d3..e4491523 100644 --- a/test/oracles/ChainlinkRelayerFactory.t.sol +++ b/test/oracles/ChainlinkRelayerFactory.t.sol @@ -64,6 +64,7 @@ contract ChainlinkRelayerFactoryTest is BaseTest { address rateFeedId, string memory rateFeedDescription, address sortedOracles, + uint256 maxTimestampSpread, IChainlinkRelayer.ChainlinkAggregator[] memory aggregators, address relayerFactoryAddress ) internal view returns (address expectedAddress) { @@ -80,7 +81,7 @@ contract ChainlinkRelayerFactoryTest is BaseTest { keccak256( abi.encodePacked( vm.getCode(factory.contractPath("ChainlinkRelayerV1")), - abi.encode(rateFeedId, rateFeedDescription, sortedOracles, aggregators) + abi.encode(rateFeedId, rateFeedDescription, sortedOracles, maxTimestampSpread, aggregators) ) ) ) @@ -173,21 +174,28 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes function test_setsRateFeed() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); address rateFeed = relayer.rateFeedId(); assertEq(rateFeed, aRateFeed); } function test_setsRateFeedDescription() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); string memory rateFeedDescription = relayer.rateFeedDescription(); assertEq(rateFeedDescription, aRateFeedDescription); } + function test_setsMaxTimestampSpread() public { + vm.prank(owner); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); + uint256 maxTimestampSpread = relayer.maxTimestampSpread(); + assertEq(maxTimestampSpread, 300); + } + function test_setsAggregators() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); IChainlinkRelayer.ChainlinkAggregator[] memory expectedAggregators = fourAggregators(); IChainlinkRelayer.ChainlinkAggregator[] memory actualAggregators = relayer.getAggregators(); assertEq(expectedAggregators.length, actualAggregators.length); @@ -199,18 +207,19 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes function test_setsSortedOracles() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); address sortedOracles = relayer.sortedOracles(); assertEq(sortedOracles, mockSortedOracles); } function test_deploysToTheCorrectAddress() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); address expectedAddress = expectedRelayerAddress({ rateFeedId: aRateFeed, rateFeedDescription: aRateFeedDescription, sortedOracles: mockSortedOracles, + maxTimestampSpread: 300, aggregators: fourAggregators(), relayerFactoryAddress: address(relayerFactory) }); @@ -223,6 +232,7 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes rateFeedId: aRateFeed, rateFeedDescription: aRateFeedDescription, sortedOracles: mockSortedOracles, + maxTimestampSpread: 300, aggregators: fourAggregators(), relayerFactoryAddress: address(relayerFactory) }); @@ -235,12 +245,12 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes aggregators: fourAggregators() }); vm.prank(relayerDeployer); - relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators()); } function test_remembersTheRelayerAddress() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); address storedAddress = relayerFactory.getRelayer(aRateFeed); assertEq(storedAddress, address(relayer)); } @@ -251,40 +261,41 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes aRateFeed, aRateFeedDescription, address(mockSortedOracles), + 300, fourAggregators(), address(relayerFactory) ); vm.etch(futureAddress, abi.encode("This is a great contract's bytecode")); vm.expectRevert(contractAlreadyExistsError(address(futureAddress), aRateFeed)); vm.prank(relayerDeployer); - relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators()); } function test_revertsWhenDeployingTheSameRelayer() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); vm.expectRevert(relayerForFeedExistsError(aRateFeed)); vm.prank(relayerDeployer); - relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators()); } function test_revertsWhenDeployingForTheSameRateFeed() public { vm.prank(relayerDeployer); - relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators())); + relayer = IChainlinkRelayer(relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators())); vm.expectRevert(relayerForFeedExistsError(aRateFeed)); vm.prank(relayerDeployer); - relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, oneAggregator(0)); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, oneAggregator(0)); } function test_revertsWhenCalledByNonDeployer() public { vm.expectRevert(NOT_ALLOWED_ERROR); vm.prank(nonDeployer); - relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators()); } function test_worksWhenCalledByOwner() public { vm.prank(owner); - relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, fourAggregators()); + relayerFactory.deployRelayer(aRateFeed, aRateFeedDescription, 300, fourAggregators()); } } @@ -296,7 +307,7 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_returnsRelayerWhenThereIsOne() public { vm.prank(relayerDeployer); - address relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + address relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 1); assertEq(relayers[0], relayerAddress); @@ -304,11 +315,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_returnsMultipleRelayersWhenThereAreMore() public { vm.prank(relayerDeployer); - address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); vm.prank(relayerDeployer); - address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); + address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, 0, oneAggregator(1)); vm.prank(relayerDeployer); - address relayerAddress3 = relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, oneAggregator(2)); + address relayerAddress3 = relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, 0, oneAggregator(2)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 3); assertEq(relayers[0], relayerAddress1); @@ -318,11 +329,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_returnsADifferentRelayerAfterRedeployment() public { vm.prank(relayerDeployer); - address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); vm.prank(relayerDeployer); - relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); + relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, 0, oneAggregator(1)); vm.prank(relayerDeployer); - address relayerAddress2 = relayerFactory.redeployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(2)); + address relayerAddress2 = relayerFactory.redeployRelayer(rateFeeds[1], aRateFeedDescription, 0, oneAggregator(2)); address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 2); assertEq(relayers[0], relayerAddress1); @@ -331,11 +342,11 @@ contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest function test_doesntReturnARemovedRelayer() public { vm.prank(relayerDeployer); - address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + address relayerAddress1 = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); vm.prank(relayerDeployer); - address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); + address relayerAddress2 = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, 0, oneAggregator(1)); vm.prank(relayerDeployer); - relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, oneAggregator(2)); + relayerFactory.deployRelayer(rateFeeds[2], aRateFeedDescription, 0, oneAggregator(2)); vm.prank(relayerDeployer); relayerFactory.removeRelayer(rateFeeds[2]); address[] memory relayers = relayerFactory.getRelayers(); @@ -352,7 +363,7 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes super.setUp(); vm.prank(relayerDeployer); - relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + relayerAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); } function test_removesTheRelayer() public { @@ -373,7 +384,7 @@ contract ChainlinkRelayerFactoryTest_removeRelayer is ChainlinkRelayerFactoryTes function test_doesntRemoveOtherRelayers() public { vm.prank(relayerDeployer); - address newRelayerAddress = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, oneAggregator(1)); + address newRelayerAddress = relayerFactory.deployRelayer(rateFeeds[1], aRateFeedDescription, 0, oneAggregator(1)); vm.prank(relayerDeployer); relayerFactory.removeRelayer(aRateFeed); address[] memory relayers = relayerFactory.getRelayers(); @@ -406,13 +417,13 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function setUp() public override { super.setUp(); vm.prank(relayerDeployer); - oldAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + oldAddress = relayerFactory.deployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); } function test_setsRateFeedOnNewRelayer() public { vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)) ); address rateFeed = relayer.rateFeedId(); @@ -422,7 +433,7 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function test_setsAggregatorOnNewRelayer() public { vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)) ); IChainlinkRelayer.ChainlinkAggregator[] memory expectedAggregators = oneAggregator(1); @@ -437,7 +448,7 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function test_setsSortedOraclesOnNewRelayer() public { vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)) ); address sortedOracles = relayer.sortedOracles(); @@ -447,13 +458,14 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function test_deploysToTheCorrectNewAddress() public { vm.prank(relayerDeployer); IChainlinkRelayer relayer = IChainlinkRelayer( - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)) + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)) ); address expectedAddress = expectedRelayerAddress( aRateFeed, aRateFeedDescription, mockSortedOracles, + 0, oneAggregator(1), address(relayerFactory) ); @@ -466,6 +478,7 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT rateFeedId: aRateFeed, rateFeedDescription: aRateFeedDescription, sortedOracles: mockSortedOracles, + maxTimestampSpread: 0, aggregators: oneAggregator(1), relayerFactoryAddress: address(relayerFactory) }); @@ -481,12 +494,12 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT aggregators: oneAggregator(1) }); vm.prank(relayerDeployer); - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)); } function test_remembersTheNewRelayerAddress() public { vm.prank(relayerDeployer); - address relayer = relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); + address relayer = relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)); address storedAddress = relayerFactory.getRelayer(aRateFeed); assertEq(storedAddress, relayer); } @@ -494,17 +507,17 @@ contract ChainlinkRelayerFactoryTest_redeployRelayer is ChainlinkRelayerFactoryT function test_revertsWhenDeployingTheSameExactRelayer() public { vm.expectRevert(contractAlreadyExistsError(oldAddress, aRateFeed)); vm.prank(relayerDeployer); - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(0)); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(0)); } function test_revertsWhenCalledByNonDeployer() public { vm.expectRevert(NOT_ALLOWED_ERROR); vm.prank(nonDeployer); - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)); } function test_worksWhenCalledByOwner() public { vm.prank(owner); - relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, oneAggregator(1)); + relayerFactory.redeployRelayer(rateFeeds[0], aRateFeedDescription, 0, oneAggregator(1)); } } diff --git a/test/oracles/ChainlinkRelayerV1.t.sol b/test/oracles/ChainlinkRelayerV1.t.sol index ad248ee6..9730f75f 100644 --- a/test/oracles/ChainlinkRelayerV1.t.sol +++ b/test/oracles/ChainlinkRelayerV1.t.sol @@ -43,13 +43,15 @@ interface ISortedOracles { } contract ChainlinkRelayerV1Test is BaseTest { - bytes constant TIMESTAMP_NOT_NEW_ERROR = abi.encodeWithSignature("TimestampNotNew()"); bytes constant EXPIRED_TIMESTAMP_ERROR = abi.encodeWithSignature("ExpiredTimestamp()"); bytes constant INVALID_PRICE_ERROR = abi.encodeWithSignature("InvalidPrice()"); - bytes constant INVALID_AGGREGATOR = abi.encodeWithSignature("InvalidAggregator()"); - bytes constant NO_AGGREGATORS = abi.encodeWithSignature("NoAggregators()"); - bytes constant TOO_MANY_AGGREGATORS = abi.encodeWithSignature("TooManyAggregators()"); - bytes constant TOO_MANY_EXISTING_REPORTS = abi.encodeWithSignature("TooManyExistingReports()"); + bytes constant INVALID_AGGREGATOR_ERROR = abi.encodeWithSignature("InvalidAggregator()"); + bytes constant INVALID_MAX_TIMESTAMP_SPREAD_ERROR = abi.encodeWithSignature("InvalidMaxTimestampSpread()"); + bytes constant NO_AGGREGATORS_ERROR = abi.encodeWithSignature("NoAggregators()"); + bytes constant TIMESTAMP_NOT_NEW_ERROR = abi.encodeWithSignature("TimestampNotNew()"); + bytes constant TIMESTAMP_SPREAD_TOO_HIGH_ERROR = abi.encodeWithSignature("TimestampSpreadTooHigh()"); + bytes constant TOO_MANY_AGGREGATORS_ERROR = abi.encodeWithSignature("TooManyAggregators()"); + bytes constant TOO_MANY_EXISTING_REPORTS_ERROR = abi.encodeWithSignature("TooManyExistingReports()"); ISortedOracles sortedOracles; @@ -100,7 +102,10 @@ contract ChainlinkRelayerV1Test is BaseTest { } } - relayer = IChainlinkRelayer(new ChainlinkRelayerV1(rateFeedId, "CELO/USD", address(sortedOracles), aggregators)); + uint256 maxTimestampSpread = aggregatorsCount > 1 ? 300 : 0; + relayer = IChainlinkRelayer( + new ChainlinkRelayerV1(rateFeedId, "CELO/USD", address(sortedOracles), maxTimestampSpread, aggregators) + ); sortedOracles.addOracle(rateFeedId, address(relayer)); } @@ -126,31 +131,56 @@ contract ChainlinkRelayerV1Test is BaseTest { contract ChainlinkRelayerV1Test_constructor_invalid is ChainlinkRelayerV1Test { function test_constructorRevertsWhenAggregatorsIsEmpty() public { - vm.expectRevert(NO_AGGREGATORS); + vm.expectRevert(NO_AGGREGATORS_ERROR); new ChainlinkRelayerV1( rateFeedId, "CELO/USD", address(sortedOracles), + 0, new IChainlinkRelayer.ChainlinkAggregator[](0) ); } function test_constructorRevertsWhenTooManyAggregators() public { - vm.expectRevert(TOO_MANY_AGGREGATORS); + vm.expectRevert(TOO_MANY_AGGREGATORS_ERROR); new ChainlinkRelayerV1( rateFeedId, "CELO/USD", address(sortedOracles), + 300, new IChainlinkRelayer.ChainlinkAggregator[](5) ); } function test_constructorRevertsWhenAggregatorsIsInvalid() public { - vm.expectRevert(INVALID_AGGREGATOR); + vm.expectRevert(INVALID_AGGREGATOR_ERROR); new ChainlinkRelayerV1( rateFeedId, "CELO/USD", address(sortedOracles), + 0, + new IChainlinkRelayer.ChainlinkAggregator[](1) + ); + } + + function test_constructorRevertsWhenNoTimestampSpreadButMultipleAggregators() public { + vm.expectRevert(INVALID_MAX_TIMESTAMP_SPREAD_ERROR); + new ChainlinkRelayerV1( + rateFeedId, + "CELO/USD", + address(sortedOracles), + 0, + new IChainlinkRelayer.ChainlinkAggregator[](2) + ); + } + + function test_constructorRevertsWhenTimestampSpreadPositiveButSingleAggregator() public { + vm.expectRevert(INVALID_MAX_TIMESTAMP_SPREAD_ERROR); + new ChainlinkRelayerV1( + rateFeedId, + "CELO/USD", + address(sortedOracles), + 300, new IChainlinkRelayer.ChainlinkAggregator[](1) ); } @@ -457,7 +487,7 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { vm.warp(block.timestamp + 100); // Not enough to be able to expire the first report setAggregatorPrices(); // Update timestamps - vm.expectRevert(TOO_MANY_EXISTING_REPORTS); + vm.expectRevert(TOO_MANY_EXISTING_REPORTS_ERROR); relayer.relay(); } @@ -497,7 +527,7 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { relayer.relay(); } - function test_revertsWhenOldestTimestampIsExpired() public virtual withReport(aReport) { + function test_revertsWhenMostRecentTimestampIsExpired() public virtual withReport(aReport) { mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp + 1); vm.warp(block.timestamp + expirySeconds + 1); vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); @@ -556,13 +586,21 @@ contract ChainlinkRelayerV1Test_relay_double is ChainlinkRelayerV1Test_relay_sin relayer.relay(); } - function test_revertsWhenOldestTimestampIsExpired() public virtual override withReport(aReport) { - mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + function test_revertsWhenMostRecentTimestampIsExpired() public virtual override withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp + 1); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp + 1); vm.warp(block.timestamp + expirySeconds + 1); - mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); relayer.relay(); } + + function test_revertsWhenTimestampSpreadTooLarge() public virtual { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + vm.warp(block.timestamp + 301); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); + vm.expectRevert(TIMESTAMP_SPREAD_TOO_HIGH_ERROR); + relayer.relay(); + } } contract ChainlinkRelayerV1Test_relay_triple is ChainlinkRelayerV1Test_relay_double { @@ -618,14 +656,22 @@ contract ChainlinkRelayerV1Test_relay_triple is ChainlinkRelayerV1Test_relay_dou relayer.relay(); } - function test_revertsWhenOldestTimestampIsExpired() public virtual override withReport(aReport) { - mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); - mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); + function test_revertsWhenMostRecentTimestampIsExpired() public virtual override withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp + 1); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp + 1); + mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp + 1); vm.warp(block.timestamp + expirySeconds + 1); - mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp); vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); relayer.relay(); } + + function test_revertsWhenTimestampSpreadTooLarge() public virtual override { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + vm.warp(block.timestamp + 301); + mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp); + vm.expectRevert(TIMESTAMP_SPREAD_TOO_HIGH_ERROR); + relayer.relay(); + } } contract ChainlinkRelayerV1Test_relay_full is ChainlinkRelayerV1Test_relay_triple { @@ -684,14 +730,22 @@ contract ChainlinkRelayerV1Test_relay_full is ChainlinkRelayerV1Test_relay_tripl relayer.relay(); } - function test_revertsWhenOldestTimestampIsExpired() public override withReport(aReport) { - mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); - mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp); - mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp); - vm.warp(block.timestamp + expirySeconds + 1); - mockAggregator2.setRoundData(aggregatorPrice3, block.timestamp); + function test_revertsWhenMostRecentTimestampIsExpired() public override withReport(aReport) { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp + 1); + mockAggregator1.setRoundData(aggregatorPrice1, block.timestamp + 1); + mockAggregator2.setRoundData(aggregatorPrice2, block.timestamp + 1); + mockAggregator2.setRoundData(aggregatorPrice3, block.timestamp + 1); + vm.warp(block.timestamp + expirySeconds + 1); vm.expectRevert(EXPIRED_TIMESTAMP_ERROR); relayer.relay(); } + + function test_revertsWhenTimestampSpreadTooLarge() public virtual override { + mockAggregator0.setRoundData(aggregatorPrice0, block.timestamp); + vm.warp(block.timestamp + 301); + mockAggregator3.setRoundData(aggregatorPrice3, block.timestamp); + vm.expectRevert(TIMESTAMP_SPREAD_TOO_HIGH_ERROR); + relayer.relay(); + } } From 45a551d3a108a9bdb685e815fbafd1f61597c1a6 Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:58:45 +0200 Subject: [PATCH 11/38] =?UTF-8?q?THE=20BIG=20ONE=20=F0=9F=90=98=20?= =?UTF-8?q?=F0=9F=92=A3=20=F0=9F=9A=9D=20(#501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description As I told some of you at dinner, I've had a bad case of insomnia over the last week. This PR resulted from a couple of late night coding sessions and the incessant need to make things nicer in our repos. It's a big PR but 𝚫code is negative so 🤷 . What happens in this mondo-PR: 1. All `celo` contracts are removed form the repo and replaced with the `@celo/contracts` NPM package. With a small caveat. 2. All `interfaces` are made to work with `0.8` and cover more of the contract functions. 3. All `tests` contracts are updated to `0.8` and use `deployCode` helpers to deploy lower-version contracts, and the use the interfaces to interact with them. 4. ~We make use of this https://github.com/foundry-rs/foundry/pull/8668 to improve compile time.~ 5. Update `solhint` and `prettier` to a recent version and fix/ignore all issues. 6. Fix solc compiler warnings. 7. Fix/ignore slither > informational warnings. 8. Slight Refactor of ForkTests to make them better to work with. 9. Restructuring of the tests folder. #### `@celo/contracts` The only caveat of using the `@celo/contracts` package is that `UsingRegistry.sol` in there needs to import things from `mento-core` and I didn't manage to get it working ok with remappings, so I kept a copy of `UsingRegistry.sol` in `contracts/common`. It's only used in `swap/Reserve.sol` and when we remove it from there we can completely kill `common`. #### The foundry WIP PR A better solution was found here #502, which removes the need for `via-ir` completely. ~The reason it takes so long to compile our code is because we need `via-ir` for `Airgrab.sol` and `StableTokenV2.sol`, and `via-ir` is super slow. But with the compiler restrictions implemented in https://github.com/foundry-rs/foundry/pull/8668 we can have multiple compiler profile settings for subgraphs of the source-graph, which compile in parallel with different settings.~ ~You can easily install that version of foundry locally, (you have to have rust installed tho):~ ``` foundryup -P 8668 ``` ~With this version of foundry and the settings in `foundry.toml`, if you're not working in a part of the source graph that contains `Airgrab.sol` or `StableTokenV2.sol`, compilation will happen without `via-ir` and will be super snappy. However if you do touch that source graph it will still take noticeably long. Right now on my machine full clean compilation takes 80seconds. It used to take >3minutes.~ #### ForkTest Refactoring Our fork tests can get a bit heavy because they're running test assertions for all the exchanges in a single test call. I've refactor it a bit and split out exchange assertions into their own tests contracts that need to be manually created when new exchanges are deployed. There's a chain-level assertion on the number of exchanges that helps us catch this and keep them up to date. This work was continued here #503 for a much cleaner solution. ### Other changes > _Describe any minor or "drive-by" changes here._ no minor changes here, no :)) ### Tested Tested? Tested! ### Related issues Fixes the issues in my head. ### Backwards compatibility What even is that? ### Documentation Holy Bible --------- Co-authored-by: Bayological <6872903+bayological@users.noreply.github.com> Co-authored-by: chapati Co-authored-by: philbow61 <80156619+philbow61@users.noreply.github.com> --- .github/workflows/echidna.yaml | 21 +- .github/workflows/lint_test.yaml | 4 +- .github/workflows/slither.yaml | 13 +- .github/workflows/storage-layout.yaml | 12 + .gitmodules | 12 +- .husky/pre-push | 2 +- .prettierrc.yml | 4 +- .solhint.json | 18 +- .solhint.test.json | 22 +- contracts/common/CalledByVm.sol | 9 - contracts/common/ExternalCall.sol | 27 - contracts/common/FixidityLib.sol | 289 ----- contracts/common/Freezable.sol | 13 - contracts/common/Freezer.sol | 37 - contracts/common/GoldToken.sol | 260 ----- contracts/common/ICeloGovernance.sol | 24 - contracts/common/Initializable.sol | 18 - contracts/common/MultiSig.sol | 379 ------- contracts/common/Proxy.sol | 155 --- contracts/common/ReentrancyGuard.sol | 33 - contracts/common/Registry.sol | 98 -- contracts/common/SortedOracles.sol | 408 -------- contracts/common/SortedOraclesProxy.sol | 9 - contracts/common/UsingPrecompiles.sol | 266 ----- contracts/common/UsingRegistry.sol | 12 +- .../interfaces/ICeloVersionedContract.sol | 21 - contracts/common/interfaces/IFreezer.sol | 6 - contracts/common/interfaces/IRegistry.sol | 16 - .../common/linkedlists/AddressLinkedList.sol | 108 -- .../linkedlists/AddressSortedLinkedList.sol | 152 --- .../AddressSortedLinkedListWithMedian.sol | 163 --- .../linkedlists/IntegerSortedLinkedList.sol | 121 --- contracts/common/linkedlists/LinkedList.sol | 166 --- .../common/linkedlists/SortedLinkedList.sol | 212 ---- .../SortedLinkedListWithMedian.sol | 271 ----- contracts/governance/Airgrab.sol | 7 +- contracts/governance/Emission.sol | 7 +- contracts/governance/GovernanceFactory.sol | 117 +-- contracts/governance/MentoGovernor.sol | 32 +- contracts/governance/MentoToken.sol | 10 +- contracts/governance/locking/Locking.sol | 2 +- contracts/governance/locking/LockingBase.sol | 10 +- .../governance/locking/LockingRelock.sol | 16 +- contracts/governance/locking/LockingVotes.sol | 10 +- .../governance/locking/libs/LibBrokenLine.sol | 56 +- .../governance/locking/libs/LibIntMapping.sol | 12 +- contracts/import.sol | 15 + contracts/interfaces/IBiPoolManager.sol | 104 +- contracts/interfaces/IBreaker.sol | 2 +- contracts/interfaces/IBreakerBox.sol | 94 +- contracts/interfaces/IBroker.sol | 40 +- .../IProxy.sol => interfaces/ICeloProxy.sol} | 6 +- contracts/interfaces/ICeloToken.sol | 6 +- .../IERC20.sol} | 14 +- .../{legacy => }/interfaces/IExchange.sol | 18 +- contracts/interfaces/IExchangeProvider.sol | 2 +- contracts/interfaces/IGoldToken.sol | 31 + contracts/interfaces/IMedianDeltaBreaker.sol | 47 + contracts/interfaces/IOwnable.sol | 10 + contracts/interfaces/IPricingModule.sol | 4 +- contracts/interfaces/IReserve.sol | 86 +- contracts/interfaces/ISortedOracles.sol | 47 +- contracts/interfaces/IStableTokenV2.sol | 18 +- contracts/interfaces/ITradingLimits.sol | 38 + contracts/interfaces/IValueDeltaBreaker.sol | 36 + contracts/legacy/Exchange.sol | 453 -------- contracts/legacy/ExchangeBRL.sol | 33 - contracts/legacy/ExchangeEUR.sol | 33 - contracts/legacy/GrandaMento.sol | 602 ----------- contracts/legacy/ReserveSpenderMultiSig.sol | 12 - contracts/legacy/StableToken.sol | 589 ----------- contracts/legacy/StableTokenBRL.sol | 33 - contracts/legacy/StableTokenEUR.sol | 33 - contracts/legacy/StableTokenXOF.sol | 33 - contracts/legacy/interfaces/IStableToken.sol | 58 - contracts/legacy/proxies/ExchangeBRLProxy.sol | 9 - contracts/legacy/proxies/ExchangeProxy.sol | 9 - contracts/legacy/proxies/GrandaMentoProxy.sol | 9 - .../proxies/ReserveSpenderMultiSigProxy.sol | 9 - contracts/libraries/TradingLimits.sol | 53 +- contracts/oracles/BreakerBox.sol | 18 +- contracts/oracles/ChainlinkRelayerFactory.sol | 1 + contracts/oracles/ChainlinkRelayerV1.sol | 25 +- .../oracles/breakers/MedianDeltaBreaker.sol | 10 +- .../oracles/breakers/ValueDeltaBreaker.sol | 10 +- contracts/oracles/breakers/WithThreshold.sol | 6 +- contracts/swap/BiPoolManager.sol | 41 +- contracts/swap/BiPoolManagerProxy.sol | 6 +- contracts/swap/Broker.sol | 49 +- contracts/swap/BrokerProxy.sol | 6 +- .../swap/ConstantProductPricingModule.sol | 2 +- contracts/swap/ConstantSumPricingModule.sol | 2 +- contracts/swap/Reserve.sol | 45 +- contracts/swap/ReserveProxy.sol | 6 +- contracts/tokens/StableTokenBRLProxy.sol | 6 +- contracts/tokens/StableTokenCOPProxy.sol | 6 +- contracts/tokens/StableTokenEURProxy.sol | 6 +- contracts/tokens/StableTokenINRProxy.sol | 6 +- contracts/tokens/StableTokenKESProxy.sol | 6 +- contracts/tokens/StableTokenPHPProxy.sol | 9 - .../StableTokenPSOProxy.sol} | 6 +- contracts/tokens/StableTokenProxy.sol | 6 +- contracts/tokens/StableTokenV2.sol | 25 +- contracts/tokens/StableTokenXOFProxy.sol | 6 +- .../tokens/patched/ERC20PermitUpgradeable.sol | 4 + contracts/tokens/patched/ERC20Upgradeable.sol | 37 +- echidna.yaml | 6 +- foundry.toml | 28 +- lib/celo-foundry | 1 - lib/forge-std | 1 + lib/forge-std-next | 1 - lib/mento-std | 1 + package.json | 18 +- remappings.txt | 10 +- slither.config.json | 5 +- test/echidna/EchidnaFixidityLib.sol | 8 +- test/echidna/EchidnaStableToken.sol | 68 -- test/fork-tests/BaseForkTest.t.sol | 435 -------- test/fork-tests/EnvForkTest.t.sol | 12 - test/fork-tests/TokenUpgrade.t.sol | 42 - test/fork/BaseForkTest.sol | 136 +++ test/fork/ChainForkTest.sol | 82 ++ test/fork/EnvForkTest.t.sol | 97 ++ test/fork/ExchangeForkTest.sol | 226 ++++ .../TestAsserts.sol} | 219 ++-- test/fork/TokenUpgrade.t.sol | 40 + .../Utils.t.sol => fork/Utils.sol} | 259 ++--- test/governance/Locking/Locking.fuzz.t.sol | 79 -- .../GovernanceIntegration.gas.t.sol | 14 +- .../governance}/GovernanceIntegration.t.sol | 39 +- .../governance}/LockingIntegration.fuzz.t.sol | 20 +- .../governance}/Proposals.sol | 34 +- .../protocol/BrokerGas.md} | 0 .../protocol}/BrokerGas.t.sol | 66 +- .../{ => protocol}/BrokerIntegration.t.sol | 65 +- .../ChainlinkRelayerIntegration.t.sol | 124 ++- .../CircuitBreakerIntegration.t.sol | 114 +- .../ConstantSumIntegration.t.sol | 75 +- .../protocol/ProtocolTest.sol} | 230 ++-- .../{ => protocol}/eXOFIntegration.t.sol | 132 +-- test/legacy/Exchange.t.sol | 725 ------------- test/legacy/StableToken.t.sol | 758 -------------- test/mocks/MockBreakerBox.sol | 26 - test/mocks/MockLocking.sol | 12 - test/mocks/MockStableToken.sol | 91 -- test/oracles/SortedOracles.t.sol | 594 ----------- test/oracles/breakers/WithThreshold.t.sol | 97 -- test/swap/ConstantProductPricingModule.t.sol | 111 -- test/tokens/StableTokenV1V2GasPayment.t.sol | 123 --- test/{ => unit}/governance/Airgrab.t.sol | 8 +- test/{ => unit}/governance/Emission.t.sol | 14 +- .../governance/GovernanceFactory.t.sol | 29 +- .../governance/GovernanceTest.sol} | 4 +- .../governance/Locking/LibBrokenLine.t.sol | 6 +- .../governance/Locking/Locking.fuzz.t.sol | 75 ++ .../governance/Locking/LockingTest.sol} | 12 +- .../governance/Locking/delegateTo.t.sol | 4 +- .../governance/Locking/locking.t.sol | 9 +- .../governance/Locking/relock.t.sol | 4 +- .../{ => unit}/governance/MentoGovernor.t.sol | 17 +- test/{ => unit}/governance/MentoToken.t.sol | 22 +- test/{ => unit}/libraries/TradingLimits.t.sol | 150 +-- test/{ => unit}/oracles/BreakerBox.t.sol | 284 +++-- .../oracles/ChainlinkRelayerFactory.t.sol | 31 +- .../oracles/ChainlinkRelayerV1.t.sol | 54 +- .../oracles/breakers/MedianDeltaBreaker.t.sol | 208 ++-- .../oracles/breakers/ValueDeltaBreaker.t.sol | 118 ++- .../oracles/breakers/WithCooldown.t.sol | 41 +- .../unit/oracles/breakers/WithThreshold.t.sol | 107 ++ test/{ => unit}/swap/BiPoolManager.t.sol | 155 ++- test/{ => unit}/swap/Broker.t.sol | 129 ++- .../swap/ConstantProductPricingModule.t.sol | 50 + .../swap/ConstantSumPricingModule.t.sol | 26 +- test/{ => unit}/swap/Reserve.t.sol | 236 ++--- test/{ => unit}/tokens/StableTokenV2.t.sol | 18 +- test/utils/Arrays.sol | 320 ------ test/utils/BaseTest.next.sol | 34 - test/utils/BaseTest.t.sol | 32 - test/utils/Chain.sol | 95 -- test/utils/DummyErc20.sol | 13 - test/utils/Factory.sol | 74 -- test/utils/GetCode.sol | 22 - test/utils/TestERC20.sol | 14 - test/utils/Token.sol | 13 - test/utils/TokenHelpers.t.sol | 74 -- test/utils/VmExtension.sol | 9 +- test/utils/WithRegistry.sol | 20 + test/utils/WithRegistry.t.sol | 19 - .../harnesses}/GovernanceFactoryHarness.t.sol | 2 +- .../utils/harnesses/ITradingLimitsHarness.sol | 23 + test/utils/harnesses/IWithCooldownHarness.sol | 14 + .../utils/harnesses/IWithThresholdHarness.sol | 20 + .../LockingHarness.sol} | 14 +- test/utils/harnesses/TradingLimitsHarness.sol | 36 + test/utils/harnesses/WithCooldownHarness.sol | 14 + test/utils/harnesses/WithThresholdHarness.sol | 14 + test/{ => utils}/mocks/MockAggregatorV3.sol | 12 +- test/{ => utils}/mocks/MockBreaker.sol | 14 +- test/utils/mocks/MockBreakerBox.sol | 24 + test/{ => utils}/mocks/MockERC20.sol | 8 +- .../mocks/MockExchangeProvider.sol | 16 +- test/utils/mocks/MockLocking.sol | 7 + test/{ => utils}/mocks/MockMentoToken.sol | 0 test/{ => utils}/mocks/MockOwnable.sol | 0 test/{ => utils}/mocks/MockPricingModule.sol | 18 +- test/{ => utils}/mocks/MockReserve.sol | 15 +- test/{ => utils}/mocks/MockSortedOracles.sol | 18 +- test/{ => utils}/mocks/MockVeMento.sol | 0 test/utils/mocks/TestERC20.sol | 18 + test/utils/mocks/USDC.sol | 12 + yarn.lock | 988 +++++++----------- 211 files changed, 3754 insertions(+), 11903 deletions(-) delete mode 100644 contracts/common/CalledByVm.sol delete mode 100644 contracts/common/ExternalCall.sol delete mode 100644 contracts/common/FixidityLib.sol delete mode 100644 contracts/common/Freezable.sol delete mode 100644 contracts/common/Freezer.sol delete mode 100644 contracts/common/GoldToken.sol delete mode 100644 contracts/common/ICeloGovernance.sol delete mode 100644 contracts/common/Initializable.sol delete mode 100644 contracts/common/MultiSig.sol delete mode 100644 contracts/common/Proxy.sol delete mode 100644 contracts/common/ReentrancyGuard.sol delete mode 100644 contracts/common/Registry.sol delete mode 100644 contracts/common/SortedOracles.sol delete mode 100644 contracts/common/SortedOraclesProxy.sol delete mode 100644 contracts/common/UsingPrecompiles.sol delete mode 100644 contracts/common/interfaces/ICeloVersionedContract.sol delete mode 100644 contracts/common/interfaces/IFreezer.sol delete mode 100644 contracts/common/interfaces/IRegistry.sol delete mode 100644 contracts/common/linkedlists/AddressLinkedList.sol delete mode 100644 contracts/common/linkedlists/AddressSortedLinkedList.sol delete mode 100644 contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol delete mode 100644 contracts/common/linkedlists/IntegerSortedLinkedList.sol delete mode 100644 contracts/common/linkedlists/LinkedList.sol delete mode 100644 contracts/common/linkedlists/SortedLinkedList.sol delete mode 100644 contracts/common/linkedlists/SortedLinkedListWithMedian.sol create mode 100644 contracts/import.sol rename contracts/{common/interfaces/IProxy.sol => interfaces/ICeloProxy.sol} (64%) rename contracts/{common/interfaces/IERC20Metadata.sol => interfaces/IERC20.sol} (89%) rename contracts/{legacy => }/interfaces/IExchange.sol (60%) create mode 100644 contracts/interfaces/IGoldToken.sol create mode 100644 contracts/interfaces/IMedianDeltaBreaker.sol create mode 100644 contracts/interfaces/IOwnable.sol create mode 100644 contracts/interfaces/ITradingLimits.sol create mode 100644 contracts/interfaces/IValueDeltaBreaker.sol delete mode 100644 contracts/legacy/Exchange.sol delete mode 100644 contracts/legacy/ExchangeBRL.sol delete mode 100644 contracts/legacy/ExchangeEUR.sol delete mode 100644 contracts/legacy/GrandaMento.sol delete mode 100644 contracts/legacy/ReserveSpenderMultiSig.sol delete mode 100644 contracts/legacy/StableToken.sol delete mode 100644 contracts/legacy/StableTokenBRL.sol delete mode 100644 contracts/legacy/StableTokenEUR.sol delete mode 100644 contracts/legacy/StableTokenXOF.sol delete mode 100644 contracts/legacy/interfaces/IStableToken.sol delete mode 100644 contracts/legacy/proxies/ExchangeBRLProxy.sol delete mode 100644 contracts/legacy/proxies/ExchangeProxy.sol delete mode 100644 contracts/legacy/proxies/GrandaMentoProxy.sol delete mode 100644 contracts/legacy/proxies/ReserveSpenderMultiSigProxy.sol delete mode 100644 contracts/tokens/StableTokenPHPProxy.sol rename contracts/{legacy/proxies/ExchangeEURProxy.sol => tokens/StableTokenPSOProxy.sol} (59%) delete mode 160000 lib/celo-foundry create mode 160000 lib/forge-std delete mode 160000 lib/forge-std-next create mode 160000 lib/mento-std delete mode 100644 test/echidna/EchidnaStableToken.sol delete mode 100644 test/fork-tests/BaseForkTest.t.sol delete mode 100644 test/fork-tests/EnvForkTest.t.sol delete mode 100644 test/fork-tests/TokenUpgrade.t.sol create mode 100644 test/fork/BaseForkTest.sol create mode 100644 test/fork/ChainForkTest.sol create mode 100644 test/fork/EnvForkTest.t.sol create mode 100644 test/fork/ExchangeForkTest.sol rename test/{fork-tests/TestAsserts.t.sol => fork/TestAsserts.sol} (73%) create mode 100644 test/fork/TokenUpgrade.t.sol rename test/{fork-tests/Utils.t.sol => fork/Utils.sol} (65%) delete mode 100644 test/governance/Locking/Locking.fuzz.t.sol rename test/{governance/IntegrationTests => integration/governance}/GovernanceIntegration.gas.t.sol (96%) rename test/{governance/IntegrationTests => integration/governance}/GovernanceIntegration.t.sol (95%) rename test/{governance/IntegrationTests => integration/governance}/LockingIntegration.fuzz.t.sol (90%) rename test/{governance/IntegrationTests => integration/governance}/Proposals.sol (84%) rename test/{gas/GasComparison.md => integration/protocol/BrokerGas.md} (100%) rename test/{gas => integration/protocol}/BrokerGas.t.sol (61%) rename test/integration/{ => protocol}/BrokerIntegration.t.sol (86%) rename test/integration/{ => protocol}/ChainlinkRelayerIntegration.t.sol (69%) rename test/integration/{ => protocol}/CircuitBreakerIntegration.t.sol (78%) rename test/integration/{ => protocol}/ConstantSumIntegration.t.sol (74%) rename test/{utils/IntegrationTest.t.sol => integration/protocol/ProtocolTest.sol} (72%) rename test/integration/{ => protocol}/eXOFIntegration.t.sol (71%) delete mode 100644 test/legacy/Exchange.t.sol delete mode 100644 test/legacy/StableToken.t.sol delete mode 100644 test/mocks/MockBreakerBox.sol delete mode 100644 test/mocks/MockLocking.sol delete mode 100644 test/mocks/MockStableToken.sol delete mode 100644 test/oracles/SortedOracles.t.sol delete mode 100644 test/oracles/breakers/WithThreshold.t.sol delete mode 100644 test/swap/ConstantProductPricingModule.t.sol delete mode 100644 test/tokens/StableTokenV1V2GasPayment.t.sol rename test/{ => unit}/governance/Airgrab.t.sol (97%) rename test/{ => unit}/governance/Emission.t.sol (94%) rename test/{ => unit}/governance/GovernanceFactory.t.sol (93%) rename test/{governance/TestSetup.sol => unit/governance/GovernanceTest.sol} (88%) rename test/{ => unit}/governance/Locking/LibBrokenLine.t.sol (99%) create mode 100644 test/unit/governance/Locking/Locking.fuzz.t.sol rename test/{governance/Locking/Base.t.sol => unit/governance/Locking/LockingTest.sol} (71%) rename test/{ => unit}/governance/Locking/delegateTo.t.sol (98%) rename test/{ => unit}/governance/Locking/locking.t.sol (97%) rename test/{ => unit}/governance/Locking/relock.t.sol (99%) rename test/{ => unit}/governance/MentoGovernor.t.sol (97%) rename test/{ => unit}/governance/MentoToken.t.sol (92%) rename test/{ => unit}/libraries/TradingLimits.t.sol (60%) rename test/{ => unit}/oracles/BreakerBox.t.sol (77%) rename test/{ => unit}/oracles/ChainlinkRelayerFactory.t.sol (96%) rename test/{ => unit}/oracles/ChainlinkRelayerV1.t.sol (95%) rename test/{ => unit}/oracles/breakers/MedianDeltaBreaker.t.sol (76%) rename test/{ => unit}/oracles/breakers/ValueDeltaBreaker.t.sol (75%) rename test/{ => unit}/oracles/breakers/WithCooldown.t.sol (61%) create mode 100644 test/unit/oracles/breakers/WithThreshold.t.sol rename test/{ => unit}/swap/BiPoolManager.t.sol (87%) rename test/{ => unit}/swap/Broker.t.sol (86%) create mode 100644 test/unit/swap/ConstantProductPricingModule.t.sol rename test/{ => unit}/swap/ConstantSumPricingModule.t.sol (86%) rename test/{ => unit}/swap/Reserve.t.sol (85%) rename test/{ => unit}/tokens/StableTokenV2.t.sol (96%) delete mode 100644 test/utils/Arrays.sol delete mode 100644 test/utils/BaseTest.next.sol delete mode 100644 test/utils/BaseTest.t.sol delete mode 100644 test/utils/Chain.sol delete mode 100644 test/utils/DummyErc20.sol delete mode 100644 test/utils/Factory.sol delete mode 100644 test/utils/GetCode.sol delete mode 100644 test/utils/TestERC20.sol delete mode 100644 test/utils/Token.sol delete mode 100644 test/utils/TokenHelpers.t.sol create mode 100644 test/utils/WithRegistry.sol delete mode 100644 test/utils/WithRegistry.t.sol rename test/{governance => utils/harnesses}/GovernanceFactoryHarness.t.sol (80%) create mode 100644 test/utils/harnesses/ITradingLimitsHarness.sol create mode 100644 test/utils/harnesses/IWithCooldownHarness.sol create mode 100644 test/utils/harnesses/IWithThresholdHarness.sol rename test/utils/{TestLocking.sol => harnesses/LockingHarness.sol} (75%) create mode 100644 test/utils/harnesses/TradingLimitsHarness.sol create mode 100644 test/utils/harnesses/WithCooldownHarness.sol create mode 100644 test/utils/harnesses/WithThresholdHarness.sol rename test/{ => utils}/mocks/MockAggregatorV3.sol (67%) rename test/{ => utils}/mocks/MockBreaker.sol (66%) create mode 100644 test/utils/mocks/MockBreakerBox.sol rename test/{ => utils}/mocks/MockERC20.sol (78%) rename test/{ => utils}/mocks/MockExchangeProvider.sol (87%) create mode 100644 test/utils/mocks/MockLocking.sol rename test/{ => utils}/mocks/MockMentoToken.sol (100%) rename test/{ => utils}/mocks/MockOwnable.sol (100%) rename test/{ => utils}/mocks/MockPricingModule.sol (68%) rename test/{ => utils}/mocks/MockReserve.sol (86%) rename test/{ => utils}/mocks/MockSortedOracles.sol (79%) rename test/{ => utils}/mocks/MockVeMento.sol (100%) create mode 100644 test/utils/mocks/TestERC20.sol create mode 100644 test/utils/mocks/USDC.sol diff --git a/.github/workflows/echidna.yaml b/.github/workflows/echidna.yaml index 528480a2..a79c75bf 100644 --- a/.github/workflows/echidna.yaml +++ b/.github/workflows/echidna.yaml @@ -36,15 +36,32 @@ jobs: - uses: actions/checkout@v3 with: submodules: recursive + + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "yarn" + node-version: "20" + + - name: "Install the Node.js dependencies" + run: "yarn install --immutable" + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 + - name: "Build for echidna" - run: forge build --build-info --skip */test/**/*.t.sol */script/** + run: | + forge build --build-info --skip \ + "test/fork/**/*" \ + "test/integration/**/*" \ + "test/unit/**/*" \ + "test/utils/**/*" \ + "script/**/" + - name: "Run Echidna" uses: crytic/echidna-action@v2 with: files: . - solc-version: 0.5.17 contract: ${{ matrix.contract }} config: echidna.yaml test-mode: assertion diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index 283aa8da..b71d29c2 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -34,7 +34,7 @@ jobs: run: "yarn install --immutable" - name: "Lint the contracts" - run: "yarn lint:check" + run: "yarn lint" - name: "Add lint summary" run: | @@ -50,7 +50,7 @@ jobs: - name: "Build the contracts" run: | forge --version - forge build --sizes + forge build --sizes --skip test/**/* - name: "Add test summary" run: | diff --git a/.github/workflows/slither.yaml b/.github/workflows/slither.yaml index 9ac922e9..f49d0679 100644 --- a/.github/workflows/slither.yaml +++ b/.github/workflows/slither.yaml @@ -20,11 +20,20 @@ jobs: uses: "actions/checkout@v3" with: submodules: "recursive" + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "yarn" + node-version: "20" + + - name: "Install the Node.js dependencies" + run: "yarn install --immutable" - name: Run Slither - uses: crytic/slither-action@v0.3.1 + uses: crytic/slither-action@v0.4.0 id: slither with: sarif: results.sarif + fail-on: "low" # continue-on-error: true # ----------------------- # Ideally, we'd like to continue on error to allow uploading the SARIF file here. @@ -36,6 +45,6 @@ jobs: # know it failed. # ----------------------- - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.slither.outputs.sarif }} diff --git a/.github/workflows/storage-layout.yaml b/.github/workflows/storage-layout.yaml index 4f5aff50..17dc1806 100644 --- a/.github/workflows/storage-layout.yaml +++ b/.github/workflows/storage-layout.yaml @@ -1,4 +1,8 @@ name: "Storage Layout" + +env: + FOUNDRY_PROFILE: "ci" + on: workflow_dispatch: push: @@ -32,6 +36,14 @@ jobs: uses: onbjerg/foundry-toolchain@v1 with: version: "nightly" + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "yarn" + node-version: "20" + + - name: "Install the Node.js dependencies" + run: "yarn install --immutable" - name: Check storage layout uses: Rubilmax/foundry-storage-check@v3.2.1 with: diff --git a/.gitmodules b/.gitmodules index fb49d1d4..29a1c406 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,18 +1,12 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts -[submodule "lib/celo-foundry"] - path = lib/celo-foundry - url = https://github.com/bowd/celo-foundry [submodule "lib/openzeppelin-contracts-next"] path = lib/openzeppelin-contracts-next url = https://github.com/OpenZeppelin/openzeppelin-contracts [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable -[submodule "lib/forge-std-next"] - path = lib/forge-std-next - url = https://github.com/foundry-rs/forge-std [submodule "lib/safe-contracts"] path = lib/safe-contracts url = https://github.com/safe-global/safe-contracts @@ -23,3 +17,9 @@ branch = "release-v4" path = lib/prb-math url = https://github.com/PaulRBerg/prb-math +[submodule "lib/mento-std"] + path = lib/mento-std + url = https://github.com/mento-protocol/mento-std +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.husky/pre-push b/.husky/pre-push index 7403c9b0..6cdaab7b 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -yarn lint:check +yarn lint diff --git a/.prettierrc.yml b/.prettierrc.yml index 1b16eb34..c33ae4d0 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -6,12 +6,12 @@ singleQuote: false tabWidth: 2 trailingComma: all +plugins: ["prettier-plugin-solidity"] + overrides: - files: ["*.sol"] options: compiler: 0.5.17 - tabWidth: 2 - printWidth: 120 - files: [contracts/tokens/patched/*.sol] options: compiler: 0.8.18 diff --git a/.solhint.json b/.solhint.json index d7d06441..4d94dc31 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,10 +2,17 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { + "no-global-import": "off", + "no-console": "off", "code-complexity": ["error", 8], "compiler-version": ["error", ">=0.5.13"], - "func-visibility": ["error", { "ignoreConstructors": true }], - "max-line-length": ["error", 120], + "func-visibility": [ + "error", + { + "ignoreConstructors": true + } + ], + "max-line-length": ["error", 121], "not-rely-on-time": "off", "function-max-lines": ["error", 120], "no-empty-blocks": "off", @@ -15,6 +22,11 @@ "endOfLine": "auto" } ], - "reason-string": ["warn", { "maxLength": 64 }] + "reason-string": [ + "warn", + { + "maxLength": 64 + } + ] } } diff --git a/.solhint.test.json b/.solhint.test.json index 35eb7c0c..6d67cf31 100644 --- a/.solhint.test.json +++ b/.solhint.test.json @@ -2,12 +2,21 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { + "one-contract-per-file": "off", + "no-global-import": "off", + "no-console": "off", "code-complexity": ["error", 8], "compiler-version": ["error", ">=0.5.13"], - "func-visibility": ["error", { "ignoreConstructors": true }], - "max-line-length": ["error", 120], + "func-visibility": [ + "error", + { + "ignoreConstructors": true + } + ], + "max-line-length": ["error", 121], "not-rely-on-time": "off", - "function-max-lines": ["error", 120], + "function-max-lines": ["error", 121], + "gas-custom-errors": "off", "max-states-count": "off", "var-name-mixedcase": "off", "func-name-mixedcase": "off", @@ -21,6 +30,11 @@ "endOfLine": "auto" } ], - "reason-string": ["warn", { "maxLength": 64 }] + "reason-string": [ + "warn", + { + "maxLength": 64 + } + ] } } diff --git a/contracts/common/CalledByVm.sol b/contracts/common/CalledByVm.sol deleted file mode 100644 index 0a913fb1..00000000 --- a/contracts/common/CalledByVm.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.13 <0.8.19; - -contract CalledByVm { - modifier onlyVm() { - require(msg.sender == address(0), "Only VM can call"); - _; - } -} diff --git a/contracts/common/ExternalCall.sol b/contracts/common/ExternalCall.sol deleted file mode 100644 index 2d4a3c59..00000000 --- a/contracts/common/ExternalCall.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/utils/Address.sol"; - -library ExternalCall { - /** - * @notice Executes external call. - * @param destination The address to call. - * @param value The CELO value to be sent. - * @param data The data to be sent. - * @return The call return value. - */ - function execute( - address destination, - uint256 value, - bytes memory data - ) internal returns (bytes memory) { - if (data.length > 0) require(Address.isContract(destination), "Invalid contract address"); - bool success; - bytes memory returnData; - // solhint-disable-next-line avoid-call-value, avoid-low-level-calls - (success, returnData) = destination.call.value(value)(data); - require(success, "Transaction execution failed."); - return returnData; - } -} diff --git a/contracts/common/FixidityLib.sol b/contracts/common/FixidityLib.sol deleted file mode 100644 index bd02bec6..00000000 --- a/contracts/common/FixidityLib.sol +++ /dev/null @@ -1,289 +0,0 @@ -pragma solidity ^0.5.13; - -/** - * @title FixidityLib - * @author Gadi Guy, Alberto Cuesta Canada - * @notice This library provides fixed point arithmetic with protection against - * overflow. - * All operations are done with uint256 and the operands must have been created - * with any of the newFrom* functions, which shift the comma digits() to the - * right and check for limits, or with wrap() which expects a number already - * in the internal representation of a fraction. - * When using this library be sure to use maxNewFixed() as the upper limit for - * creation of fixed point numbers. - * @dev All contained functions are pure and thus marked internal to be inlined - * on consuming contracts at compile time for gas efficiency. - */ -library FixidityLib { - struct Fraction { - uint256 value; - } - - /** - * @notice Number of positions that the comma is shifted to the right. - */ - function digits() internal pure returns (uint8) { - return 24; - } - - uint256 private constant FIXED1_UINT = 1000000000000000000000000; - - /** - * @notice This is 1 in the fixed point units used in this library. - * @dev Test fixed1() equals 10^digits() - * Hardcoded to 24 digits. - */ - function fixed1() internal pure returns (Fraction memory) { - return Fraction(FIXED1_UINT); - } - - /** - * @notice Wrap a uint256 that represents a 24-decimal fraction in a Fraction - * struct. - * @param x Number that already represents a 24-decimal fraction. - * @return A Fraction struct with contents x. - */ - function wrap(uint256 x) internal pure returns (Fraction memory) { - return Fraction(x); - } - - /** - * @notice Unwraps the uint256 inside of a Fraction struct. - */ - function unwrap(Fraction memory x) internal pure returns (uint256) { - return x.value; - } - - /** - * @notice The amount of decimals lost on each multiplication operand. - * @dev Test mulPrecision() equals sqrt(fixed1) - */ - function mulPrecision() internal pure returns (uint256) { - return 1000000000000; - } - - /** - * @notice Maximum value that can be converted to fixed point. Optimize for deployment. - * @dev - * Test maxNewFixed() equals maxUint256() / fixed1() - */ - function maxNewFixed() internal pure returns (uint256) { - return 115792089237316195423570985008687907853269984665640564; - } - - /** - * @notice Converts a uint256 to fixed point Fraction - * @dev Test newFixed(0) returns 0 - * Test newFixed(1) returns fixed1() - * Test newFixed(maxNewFixed()) returns maxNewFixed() * fixed1() - * Test newFixed(maxNewFixed()+1) fails - */ - function newFixed(uint256 x) internal pure returns (Fraction memory) { - require(x <= maxNewFixed(), "can't create fixidity number larger than maxNewFixed()"); - return Fraction(x * FIXED1_UINT); - } - - /** - * @notice Converts a uint256 in the fixed point representation of this - * library to a non decimal. All decimal digits will be truncated. - */ - function fromFixed(Fraction memory x) internal pure returns (uint256) { - return x.value / FIXED1_UINT; - } - - /** - * @notice Converts two uint256 representing a fraction to fixed point units, - * equivalent to multiplying dividend and divisor by 10^digits(). - * @param numerator numerator must be <= maxNewFixed() - * @param denominator denominator must be <= maxNewFixed() and denominator can't be 0 - * @dev - * Test newFixedFraction(1,0) fails - * Test newFixedFraction(0,1) returns 0 - * Test newFixedFraction(1,1) returns fixed1() - * Test newFixedFraction(1,fixed1()) returns 1 - */ - function newFixedFraction(uint256 numerator, uint256 denominator) internal pure returns (Fraction memory) { - Fraction memory convertedNumerator = newFixed(numerator); - Fraction memory convertedDenominator = newFixed(denominator); - return divide(convertedNumerator, convertedDenominator); - } - - /** - * @notice Returns the integer part of a fixed point number. - * @dev - * Test integer(0) returns 0 - * Test integer(fixed1()) returns fixed1() - * Test integer(newFixed(maxNewFixed())) returns maxNewFixed()*fixed1() - */ - function integer(Fraction memory x) internal pure returns (Fraction memory) { - return Fraction((x.value / FIXED1_UINT) * FIXED1_UINT); // Can't overflow - } - - /** - * @notice Returns the fractional part of a fixed point number. - * In the case of a negative number the fractional is also negative. - * @dev - * Test fractional(0) returns 0 - * Test fractional(fixed1()) returns 0 - * Test fractional(fixed1()-1) returns 10^24-1 - */ - function fractional(Fraction memory x) internal pure returns (Fraction memory) { - return Fraction(x.value - (x.value / FIXED1_UINT) * FIXED1_UINT); // Can't overflow - } - - /** - * @notice x+y. - * @dev The maximum value that can be safely used as an addition operator is defined as - * maxFixedAdd = maxUint256()-1 / 2, or - * 57896044618658097711785492504343953926634992332820282019728792003956564819967. - * Test add(maxFixedAdd,maxFixedAdd) equals maxFixedAdd + maxFixedAdd - * Test add(maxFixedAdd+1,maxFixedAdd+1) throws - */ - function add(Fraction memory x, Fraction memory y) internal pure returns (Fraction memory) { - uint256 z = x.value + y.value; - require(z >= x.value, "add overflow detected"); - return Fraction(z); - } - - /** - * @notice x-y. - * @dev - * Test subtract(6, 10) fails - */ - function subtract(Fraction memory x, Fraction memory y) internal pure returns (Fraction memory) { - require(x.value >= y.value, "substraction underflow detected"); - return Fraction(x.value - y.value); - } - - /** - * @notice x*y. If any of the operators is higher than the max multiplier value it - * might overflow. - * @dev The maximum value that can be safely used as a multiplication operator - * (maxFixedMul) is calculated as sqrt(maxUint256()*fixed1()), - * or 340282366920938463463374607431768211455999999999999 - * Test multiply(0,0) returns 0 - * Test multiply(maxFixedMul,0) returns 0 - * Test multiply(0,maxFixedMul) returns 0 - * Test multiply(fixed1()/mulPrecision(),fixed1()*mulPrecision()) returns fixed1() - * Test multiply(maxFixedMul,maxFixedMul) is around maxUint256() - * Test multiply(maxFixedMul+1,maxFixedMul+1) fails - */ - // solhint-disable-next-line code-complexity - function multiply(Fraction memory x, Fraction memory y) internal pure returns (Fraction memory) { - if (x.value == 0 || y.value == 0) return Fraction(0); - if (y.value == FIXED1_UINT) return x; - if (x.value == FIXED1_UINT) return y; - - // Separate into integer and fractional parts - // x = x1 + x2, y = y1 + y2 - uint256 x1 = integer(x).value / FIXED1_UINT; - uint256 x2 = fractional(x).value; - uint256 y1 = integer(y).value / FIXED1_UINT; - uint256 y2 = fractional(y).value; - - // (x1 + x2) * (y1 + y2) = (x1 * y1) + (x1 * y2) + (x2 * y1) + (x2 * y2) - uint256 x1y1 = x1 * y1; - if (x1 != 0) require(x1y1 / x1 == y1, "overflow x1y1 detected"); - - // x1y1 needs to be multiplied back by fixed1 - // solhint-disable-next-line var-name-mixedcase - uint256 fixed_x1y1 = x1y1 * FIXED1_UINT; - if (x1y1 != 0) require(fixed_x1y1 / x1y1 == FIXED1_UINT, "overflow x1y1 * fixed1 detected"); - x1y1 = fixed_x1y1; - - uint256 x2y1 = x2 * y1; - if (x2 != 0) require(x2y1 / x2 == y1, "overflow x2y1 detected"); - - uint256 x1y2 = x1 * y2; - if (x1 != 0) require(x1y2 / x1 == y2, "overflow x1y2 detected"); - - x2 = x2 / mulPrecision(); - y2 = y2 / mulPrecision(); - uint256 x2y2 = x2 * y2; - if (x2 != 0) require(x2y2 / x2 == y2, "overflow x2y2 detected"); - - // result = fixed1() * x1 * y1 + x1 * y2 + x2 * y1 + x2 * y2 / fixed1(); - Fraction memory result = Fraction(x1y1); - result = add(result, Fraction(x2y1)); // Add checks for overflow - result = add(result, Fraction(x1y2)); // Add checks for overflow - result = add(result, Fraction(x2y2)); // Add checks for overflow - return result; - } - - /** - * @notice 1/x - * @dev - * Test reciprocal(0) fails - * Test reciprocal(fixed1()) returns fixed1() - * Test reciprocal(fixed1()*fixed1()) returns 1 // Testing how the fractional is truncated - * Test reciprocal(1+fixed1()*fixed1()) returns 0 // Testing how the fractional is truncated - * Test reciprocal(newFixedFraction(1, 1e24)) returns newFixed(1e24) - */ - function reciprocal(Fraction memory x) internal pure returns (Fraction memory) { - require(x.value != 0, "can't call reciprocal(0)"); - return Fraction((FIXED1_UINT * FIXED1_UINT) / x.value); // Can't overflow - } - - /** - * @notice x/y. If the dividend is higher than the max dividend value, it - * might overflow. You can use multiply(x,reciprocal(y)) instead. - * @dev The maximum value that can be safely used as a dividend (maxNewFixed) is defined as - * divide(maxNewFixed,newFixedFraction(1,fixed1())) is around maxUint256(). - * This yields the value 115792089237316195423570985008687907853269984665640564. - * Test maxNewFixed equals maxUint256()/fixed1() - * Test divide(maxNewFixed,1) equals maxNewFixed*(fixed1) - * Test divide(maxNewFixed+1,multiply(mulPrecision(),mulPrecision())) throws - * Test divide(fixed1(),0) fails - * Test divide(maxNewFixed,1) = maxNewFixed*(10^digits()) - * Test divide(maxNewFixed+1,1) throws - */ - function divide(Fraction memory x, Fraction memory y) internal pure returns (Fraction memory) { - require(y.value != 0, "can't divide by 0"); - // solhint-disable-next-line var-name-mixedcase - uint256 X = x.value * FIXED1_UINT; - require(X / FIXED1_UINT == x.value, "overflow at divide"); - return Fraction(X / y.value); - } - - /** - * @notice x > y - */ - function gt(Fraction memory x, Fraction memory y) internal pure returns (bool) { - return x.value > y.value; - } - - /** - * @notice x >= y - */ - function gte(Fraction memory x, Fraction memory y) internal pure returns (bool) { - return x.value >= y.value; - } - - /** - * @notice x < y - */ - function lt(Fraction memory x, Fraction memory y) internal pure returns (bool) { - return x.value < y.value; - } - - /** - * @notice x <= y - */ - function lte(Fraction memory x, Fraction memory y) internal pure returns (bool) { - return x.value <= y.value; - } - - /** - * @notice x == y - */ - function equals(Fraction memory x, Fraction memory y) internal pure returns (bool) { - return x.value == y.value; - } - - /** - * @notice x <= 1 - */ - function isProperFraction(Fraction memory x) internal pure returns (bool) { - return lte(x, fixed1()); - } -} diff --git a/contracts/common/Freezable.sol b/contracts/common/Freezable.sol deleted file mode 100644 index 1eb888ff..00000000 --- a/contracts/common/Freezable.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./UsingRegistry.sol"; - -contract Freezable is UsingRegistry { - // onlyWhenNotFrozen functions can only be called when `frozen` is false, otherwise they will - // revert. - modifier onlyWhenNotFrozen() { - require(!getFreezer().isFrozen(address(this)), "can't call when contract is frozen"); - _; - } -} diff --git a/contracts/common/Freezer.sol b/contracts/common/Freezer.sol deleted file mode 100644 index 2763e798..00000000 --- a/contracts/common/Freezer.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; - -import "./Initializable.sol"; -import "./interfaces/IFreezer.sol"; - -contract Freezer is Ownable, Initializable, IFreezer { - mapping(address => bool) public isFrozen; - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - function initialize() external initializer { - _transferOwnership(msg.sender); - } - - /** - * @notice Freezes the target contract, disabling `onlyWhenNotFrozen` functions. - * @param target The address of the contract to freeze. - */ - function freeze(address target) external onlyOwner { - isFrozen[target] = true; - } - - /** - * @notice Unfreezes the contract, enabling `onlyWhenNotFrozen` functions. - * @param target The address of the contract to freeze. - */ - function unfreeze(address target) external onlyOwner { - isFrozen[target] = false; - } -} diff --git a/contracts/common/GoldToken.sol b/contracts/common/GoldToken.sol deleted file mode 100644 index bad37d55..00000000 --- a/contracts/common/GoldToken.sol +++ /dev/null @@ -1,260 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; - -import "./UsingRegistry.sol"; -import "./CalledByVm.sol"; -import "./Initializable.sol"; -import "../interfaces/ICeloToken.sol"; -import "./interfaces/ICeloVersionedContract.sol"; - -contract GoldToken is Initializable, CalledByVm, UsingRegistry, IERC20, ICeloToken, ICeloVersionedContract { - using SafeMath for uint256; - - // Address of the TRANSFER precompiled contract. - // solhint-disable state-visibility - address constant TRANSFER = address(0xff - 2); - string constant NAME = "Celo native asset"; - string constant SYMBOL = "CELO"; - uint8 constant DECIMALS = 18; - uint256 internal totalSupply_; - // solhint-enable state-visibility - - mapping(address => mapping(address => uint256)) internal allowed; - - event Transfer(address indexed from, address indexed to, uint256 value); - - event TransferComment(string comment); - - event Approval(address indexed owner, address indexed spender, uint256 value); - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 1, 1, 1); - } - - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param registryAddress Address of the Registry contract. - */ - function initialize(address registryAddress) external initializer { - totalSupply_ = 0; - _transferOwnership(msg.sender); - setRegistry(registryAddress); - } - - /** - * @notice Transfers CELO from one address to another. - * @param to The address to transfer CELO to. - * @param value The amount of CELO to transfer. - * @return True if the transaction succeeds. - */ - // solhint-disable-next-line no-simple-event-func-name - function transfer(address to, uint256 value) external returns (bool) { - return _transfer(to, value); - } - - /** - * @notice Transfers CELO from one address to another with a comment. - * @param to The address to transfer CELO to. - * @param value The amount of CELO to transfer. - * @param comment The transfer comment - * @return True if the transaction succeeds. - */ - function transferWithComment( - address to, - uint256 value, - string calldata comment - ) external returns (bool) { - bool succeeded = _transfer(to, value); - emit TransferComment(comment); - return succeeded; - } - - /** - * @notice Approve a user to transfer CELO on behalf of another user. - * @param spender The address which is being approved to spend CELO. - * @param value The amount of CELO approved to the spender. - * @return True if the transaction succeeds. - */ - function approve(address spender, uint256 value) external returns (bool) { - require(spender != address(0), "cannot set allowance for 0"); - allowed[msg.sender][spender] = value; - emit Approval(msg.sender, spender, value); - return true; - } - - /** - * @notice Increases the allowance of another user. - * @param spender The address which is being approved to spend CELO. - * @param value The increment of the amount of CELO approved to the spender. - * @return True if the transaction succeeds. - */ - function increaseAllowance(address spender, uint256 value) external returns (bool) { - require(spender != address(0), "cannot set allowance for 0"); - uint256 oldValue = allowed[msg.sender][spender]; - uint256 newValue = oldValue.add(value); - allowed[msg.sender][spender] = newValue; - emit Approval(msg.sender, spender, newValue); - return true; - } - - /** - * @notice Decreases the allowance of another user. - * @param spender The address which is being approved to spend CELO. - * @param value The decrement of the amount of CELO approved to the spender. - * @return True if the transaction succeeds. - */ - function decreaseAllowance(address spender, uint256 value) external returns (bool) { - uint256 oldValue = allowed[msg.sender][spender]; - uint256 newValue = oldValue.sub(value); - allowed[msg.sender][spender] = newValue; - emit Approval(msg.sender, spender, newValue); - return true; - } - - /** - * @notice Transfers CELO from one address to another on behalf of a user. - * @param from The address to transfer CELO from. - * @param to The address to transfer CELO to. - * @param value The amount of CELO to transfer. - * @return True if the transaction succeeds. - */ - function transferFrom( - address from, - address to, - uint256 value - ) external returns (bool) { - require(to != address(0), "transfer attempted to reserved address 0x0"); - require(value <= balanceOf(from), "transfer value exceeded balance of sender"); - require(value <= allowed[from][msg.sender], "transfer value exceeded sender's allowance for recipient"); - - bool success; - // solhint-disable-next-line avoid-call-value, avoid-low-level-calls - (success, ) = TRANSFER.call.value(0).gas(gasleft())(abi.encode(from, to, value)); - require(success, "CELO transfer failed"); - - allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); - emit Transfer(from, to, value); - return true; - } - - /** - * @notice Mints new CELO and gives it to 'to'. - * @param to The account for which to mint tokens. - * @param value The amount of CELO to mint. - */ - function mint(address to, uint256 value) external onlyVm returns (bool) { - if (value == 0) { - return true; - } - - require(to != address(0), "mint attempted to reserved address 0x0"); - totalSupply_ = totalSupply_.add(value); - - bool success; - // solhint-disable-next-line avoid-call-value, avoid-low-level-calls - (success, ) = TRANSFER.call.value(0).gas(gasleft())(abi.encode(address(0), to, value)); - require(success, "CELO transfer failed"); - - emit Transfer(address(0), to, value); - return true; - } - - /** - * @return The name of the CELO token. - */ - function name() external view returns (string memory) { - return NAME; - } - - /** - * @return The symbol of the CELO token. - */ - function symbol() external view returns (string memory) { - return SYMBOL; - } - - /** - * @return The number of decimal places to which CELO is divisible. - */ - function decimals() external view returns (uint8) { - return DECIMALS; - } - - /** - * @return The total amount of CELO in existence. - */ - function totalSupply() external view returns (uint256) { - return totalSupply_; - } - - /** - * @notice Gets the amount of owner's CELO allowed to be spent by spender. - * @param owner The owner of the CELO. - * @param spender The spender of the CELO. - * @return The amount of CELO owner is allowing spender to spend. - */ - function allowance(address owner, address spender) external view returns (uint256) { - return allowed[owner][spender]; - } - - /** - * @notice Increases the variable for total amount of CELO in existence. - * @param amount The amount to increase counter by - */ - function increaseSupply(uint256 amount) external onlyVm { - totalSupply_ = totalSupply_.add(amount); - } - - /** - * @notice Gets the balance of the specified address. - * @param owner The address to query the balance of. - * @return The balance of the specified address. - */ - function balanceOf(address owner) public view returns (uint256) { - return owner.balance; - } - - /** - * @notice internal CELO transfer from one address to another. - * @param to The address to transfer CELO to. - * @param value The amount of CELO to transfer. - * @return True if the transaction succeeds. - */ - function _transfer(address to, uint256 value) internal returns (bool) { - require(to != address(0), "transfer attempted to reserved address 0x0"); - require(value <= balanceOf(msg.sender), "transfer value exceeded balance of sender"); - - bool success; - // solhint-disable-next-line avoid-call-value, avoid-low-level-calls - (success, ) = TRANSFER.call.value(0).gas(gasleft())(abi.encode(msg.sender, to, value)); - require(success, "CELO transfer failed"); - emit Transfer(msg.sender, to, value); - return true; - } -} diff --git a/contracts/common/ICeloGovernance.sol b/contracts/common/ICeloGovernance.sol deleted file mode 100644 index 93bfd7a3..00000000 --- a/contracts/common/ICeloGovernance.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -interface ICeloGovernance { - struct Transaction { - uint256 value; - address destination; - bytes data; - } - - function minDeposit() external view returns (uint256); - - function dequeued(uint256 index) external view returns (uint256); - - function execute(uint256 proposalId, uint256 index) external; - - function propose( - uint256[] calldata values, - address[] calldata destinations, - bytes calldata data, - uint256[] calldata dataLengths, - string calldata descriptionUrl - ) external payable returns (uint256); -} diff --git a/contracts/common/Initializable.sol b/contracts/common/Initializable.sol deleted file mode 100644 index b129c736..00000000 --- a/contracts/common/Initializable.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -contract Initializable { - bool public initialized; - - constructor(bool testingDeployment) public { - if (!testingDeployment) { - initialized = true; - } - } - - modifier initializer() { - require(!initialized, "contract already initialized"); - initialized = true; - _; - } -} diff --git a/contracts/common/MultiSig.sol b/contracts/common/MultiSig.sol deleted file mode 100644 index 8c194af1..00000000 --- a/contracts/common/MultiSig.sol +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; -/* solhint-disable no-inline-assembly, avoid-low-level-calls, func-name-mixedcase, func-order */ - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import "./ExternalCall.sol"; -import "./Initializable.sol"; - -/** - * @title Multisignature wallet - Allows multiple parties to agree on transactions before - * execution. - * @author Stefan George - - * @dev NOTE: This contract has its limitations and is not viable for every - * multi-signature setup. On a case by case basis, evaluate whether this is the - * correct contract for your use case. - * In particular, this contract doesn't have an atomic "add owners and increase - * requirement" operation. - * This can be tricky, for example, in a situation where a MultiSig starts out - * owned by a single owner. Safely increasing the owner set and requirement at - * the same time is not trivial. One way to work around this situation is to - * first add a second address controlled by the original owner, increase the - * requirement, and then replace the auxillary address with the intended second - * owner. - * Again, this is just one example, in general make sure to verify this contract - * will support your intended usage. The goal of this contract is to offer a - * simple, minimal multi-signature API that's easy to understand even for novice - * Solidity users. - */ -contract MultiSig is Initializable { - using SafeMath for uint256; - /* - * Events - */ - event Confirmation(address indexed sender, uint256 indexed transactionId); - event Revocation(address indexed sender, uint256 indexed transactionId); - event Submission(uint256 indexed transactionId); - event Execution(uint256 indexed transactionId, bytes returnData); - event Deposit(address indexed sender, uint256 value); - event OwnerAddition(address indexed owner); - event OwnerRemoval(address indexed owner); - event RequirementChange(uint256 required); - event InternalRequirementChange(uint256 internalRequired); - - /* - * Constants - */ - uint256 public constant MAX_OWNER_COUNT = 50; - - /* - * Storage - */ - mapping(uint256 => Transaction) public transactions; - mapping(uint256 => mapping(address => bool)) public confirmations; - mapping(address => bool) public isOwner; - address[] public owners; - uint256 public required; - uint256 public internalRequired; - uint256 public transactionCount; - - struct Transaction { - address destination; - uint256 value; - bytes data; - bool executed; - } - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /* - * Modifiers - */ - modifier onlyWallet() { - require(msg.sender == address(this), "msg.sender was not multisig wallet"); - _; - } - - modifier ownerDoesNotExist(address owner) { - require(!isOwner[owner], "owner already existed"); - _; - } - - modifier ownerExists(address owner) { - require(isOwner[owner], "owner does not exist"); - _; - } - - modifier transactionExists(uint256 transactionId) { - require(transactions[transactionId].destination != address(0), "transaction does not exist"); - _; - } - - modifier confirmed(uint256 transactionId, address owner) { - require(confirmations[transactionId][owner], "transaction was not confirmed for owner"); - _; - } - - modifier notConfirmed(uint256 transactionId, address owner) { - require(!confirmations[transactionId][owner], "transaction was already confirmed for owner"); - _; - } - - modifier notExecuted(uint256 transactionId) { - require(!transactions[transactionId].executed, "transaction was executed already"); - _; - } - - modifier notNull(address _address) { - require(_address != address(0), "address was null"); - _; - } - - modifier validRequirement(uint256 ownerCount, uint256 _required) { - require( - ownerCount <= MAX_OWNER_COUNT && _required <= ownerCount && _required != 0 && ownerCount != 0, - "invalid requirement" - ); - _; - } - - /// @dev Fallback function allows to deposit ether. - function() external payable { - if (msg.value > 0) emit Deposit(msg.sender, msg.value); - } - - /* - * Public functions - */ - /// @dev Contract constructor sets initial owners and required number of confirmations. - /// @param _owners List of initial owners. - /// @param _required Number of required confirmations for external transactions. - /// @param _internalRequired Number of required confirmations for internal transactions. - function initialize( - address[] calldata _owners, - uint256 _required, - uint256 _internalRequired - ) - external - initializer - validRequirement(_owners.length, _required) - validRequirement(_owners.length, _internalRequired) - { - for (uint256 i = 0; i < _owners.length; i = i.add(1)) { - require(!isOwner[_owners[i]] && _owners[i] != address(0), "owner was null or already given owner status"); - isOwner[_owners[i]] = true; - } - owners = _owners; - required = _required; - internalRequired = _internalRequired; - } - - /// @dev Allows to add a new owner. Transaction has to be sent by wallet. - /// @param owner Address of new owner. - function addOwner(address owner) - external - onlyWallet - ownerDoesNotExist(owner) - notNull(owner) - validRequirement(owners.length.add(1), internalRequired) - { - isOwner[owner] = true; - owners.push(owner); - emit OwnerAddition(owner); - } - - /// @dev Allows to remove an owner. Transaction has to be sent by wallet. - /// @param owner Address of owner. - function removeOwner(address owner) external onlyWallet ownerExists(owner) { - isOwner[owner] = false; - for (uint256 i = 0; i < owners.length.sub(1); i = i.add(1)) - if (owners[i] == owner) { - owners[i] = owners[owners.length.sub(1)]; - break; - } - owners.length = owners.length.sub(1); - if (required > owners.length) changeRequirement(owners.length); - if (internalRequired > owners.length) changeInternalRequirement(owners.length); - emit OwnerRemoval(owner); - } - - /// @dev Allows to replace an owner with a new owner. Transaction has to be sent by wallet. - /// @param owner Address of owner to be replaced. - /// @param newOwner Address of new owner. - function replaceOwner(address owner, address newOwner) - external - onlyWallet - ownerExists(owner) - notNull(newOwner) - ownerDoesNotExist(newOwner) - { - for (uint256 i = 0; i < owners.length; i = i.add(1)) - if (owners[i] == owner) { - owners[i] = newOwner; - break; - } - isOwner[owner] = false; - isOwner[newOwner] = true; - emit OwnerRemoval(owner); - emit OwnerAddition(newOwner); - } - - /// @dev Allows to change the number of required confirmations. Transaction has to be sent by - /// wallet. - /// @param _required Number of required confirmations. - function changeRequirement(uint256 _required) public onlyWallet validRequirement(owners.length, _required) { - required = _required; - emit RequirementChange(_required); - } - - /// @dev Allows to change the number of required confirmations. Transaction has to be sent by - /// wallet. - /// @param _internalRequired Number of required confirmations for interal txs. - function changeInternalRequirement(uint256 _internalRequired) - public - onlyWallet - validRequirement(owners.length, _internalRequired) - { - internalRequired = _internalRequired; - emit InternalRequirementChange(_internalRequired); - } - - /// @dev Allows an owner to submit and confirm a transaction. - /// @param destination Transaction target address. - /// @param value Transaction ether value. - /// @param data Transaction data payload. - /// @return Returns transaction ID. - function submitTransaction( - address destination, - uint256 value, - bytes calldata data - ) external returns (uint256 transactionId) { - transactionId = addTransaction(destination, value, data); - confirmTransaction(transactionId); - } - - /// @dev Allows an owner to confirm a transaction. - /// @param transactionId Transaction ID. - function confirmTransaction(uint256 transactionId) - public - ownerExists(msg.sender) - transactionExists(transactionId) - notConfirmed(transactionId, msg.sender) - { - confirmations[transactionId][msg.sender] = true; - emit Confirmation(msg.sender, transactionId); - if (isConfirmed(transactionId)) { - executeTransaction(transactionId); - } - } - - /// @dev Allows an owner to revoke a confirmation for a transaction. - /// @param transactionId Transaction ID. - function revokeConfirmation(uint256 transactionId) - external - ownerExists(msg.sender) - confirmed(transactionId, msg.sender) - notExecuted(transactionId) - { - confirmations[transactionId][msg.sender] = false; - emit Revocation(msg.sender, transactionId); - } - - /// @dev Allows anyone to execute a confirmed transaction. - /// @param transactionId Transaction ID. - function executeTransaction(uint256 transactionId) - public - ownerExists(msg.sender) - confirmed(transactionId, msg.sender) - notExecuted(transactionId) - { - require(isConfirmed(transactionId), "Transaction not confirmed."); - Transaction storage txn = transactions[transactionId]; - txn.executed = true; - bytes memory returnData = ExternalCall.execute(txn.destination, txn.value, txn.data); - emit Execution(transactionId, returnData); - } - - /// @dev Returns the confirmation status of a transaction. - /// @param transactionId Transaction ID. - /// @return Confirmation status. - function isConfirmed(uint256 transactionId) public view returns (bool) { - uint256 count = 0; - for (uint256 i = 0; i < owners.length; i = i.add(1)) { - if (confirmations[transactionId][owners[i]]) count = count.add(1); - bool isInternal = transactions[transactionId].destination == address(this); - if ((isInternal && count == internalRequired) || (!isInternal && count == required)) return true; - } - return false; - } - - /* - * Internal functions - */ - /// @dev Adds a new transaction to the transaction mapping, if transaction does not exist yet. - /// @param destination Transaction target address. - /// @param value Transaction ether value. - /// @param data Transaction data payload. - /// @return Returns transaction ID. - function addTransaction( - address destination, - uint256 value, - bytes memory data - ) internal notNull(destination) returns (uint256 transactionId) { - transactionId = transactionCount; - transactions[transactionId] = Transaction({ destination: destination, value: value, data: data, executed: false }); - transactionCount = transactionCount.add(1); - emit Submission(transactionId); - } - - /* - * Web3 call functions - */ - /// @dev Returns number of confirmations of a transaction. - /// @param transactionId Transaction ID. - /// @return Number of confirmations. - function getConfirmationCount(uint256 transactionId) external view returns (uint256 count) { - for (uint256 i = 0; i < owners.length; i = i.add(1)) - if (confirmations[transactionId][owners[i]]) count = count.add(1); - } - - /// @dev Returns total number of transactions after filters are applied. - /// @param pending Include pending transactions. - /// @param executed Include executed transactions. - /// @return Total number of transactions after filters are applied. - function getTransactionCount(bool pending, bool executed) external view returns (uint256 count) { - for (uint256 i = 0; i < transactionCount; i = i.add(1)) - if ((pending && !transactions[i].executed) || (executed && transactions[i].executed)) count = count.add(1); - } - - /// @dev Returns list of owners. - /// @return List of owner addresses. - function getOwners() external view returns (address[] memory) { - return owners; - } - - /// @dev Returns array with owner addresses, which confirmed transaction. - /// @param transactionId Transaction ID. - /// @return Returns array of owner addresses. - function getConfirmations(uint256 transactionId) external view returns (address[] memory _confirmations) { - address[] memory confirmationsTemp = new address[](owners.length); - uint256 count = 0; - uint256 i; - for (i = 0; i < owners.length; i = i.add(1)) - if (confirmations[transactionId][owners[i]]) { - confirmationsTemp[count] = owners[i]; - count = count.add(1); - } - _confirmations = new address[](count); - for (i = 0; i < count; i = i.add(1)) _confirmations[i] = confirmationsTemp[i]; - } - - /// @dev Returns list of transaction IDs in defined range. - /// @param from Index start position of transaction array. - /// @param to Index end position of transaction array. - /// @param pending Include pending transactions. - /// @param executed Include executed transactions. - /// @return Returns array of transaction IDs. - function getTransactionIds( - uint256 from, - uint256 to, - bool pending, - bool executed - ) external view returns (uint256[] memory _transactionIds) { - uint256[] memory transactionIdsTemp = new uint256[](transactionCount); - uint256 count = 0; - uint256 i; - for (i = 0; i < transactionCount; i = i.add(1)) - if ((pending && !transactions[i].executed) || (executed && transactions[i].executed)) { - transactionIdsTemp[count] = i; - count = count.add(1); - } - _transactionIds = new uint256[](to.sub(from)); - for (i = from; i < to; i = i.add(1)) _transactionIds[i.sub(from)] = transactionIdsTemp[i]; - } -} diff --git a/contracts/common/Proxy.sol b/contracts/common/Proxy.sol deleted file mode 100644 index a0631d97..00000000 --- a/contracts/common/Proxy.sol +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.17; -/* solhint-disable no-inline-assembly, no-complex-fallback, avoid-low-level-calls */ - -import "openzeppelin-solidity/contracts/utils/Address.sol"; - -/** - * @title A Proxy utilizing the Unstructured Storage pattern. - */ -contract Proxy { - // Used to store the address of the owner. - bytes32 private constant OWNER_POSITION = bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1); - // Used to store the address of the implementation contract. - bytes32 private constant IMPLEMENTATION_POSITION = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1); - - event OwnerSet(address indexed owner); - event ImplementationSet(address indexed implementation); - - constructor() public { - _setOwner(msg.sender); - } - - /** - * @notice Throws if called by any account other than the owner. - */ - modifier onlyOwner() { - require(msg.sender == _getOwner(), "sender was not owner"); - _; - } - - /** - * @notice Delegates calls to the implementation contract. - */ - function() external payable { - bytes32 implementationPosition = IMPLEMENTATION_POSITION; - - address implementationAddress; - - // Load the address of the implementation contract from an explicit storage slot. - assembly { - implementationAddress := sload(implementationPosition) - } - - // Avoid checking if address is a contract or executing delegated call when - // implementation address is 0x0 - require(implementationAddress != address(0), "No Implementation set"); - require(Address.isContract(implementationAddress), "Invalid contract address"); - - assembly { - // Extract the position of the transaction data (i.e. function ID and arguments). - let newCallDataPosition := mload(0x40) - mstore(0x40, add(newCallDataPosition, calldatasize)) - calldatacopy(newCallDataPosition, 0, calldatasize) - - // Call the smart contract at `implementationAddress` in the context of the proxy contract, - // with the same msg.sender and value. - let delegatecallSuccess := delegatecall(gas, implementationAddress, newCallDataPosition, calldatasize, 0, 0) - - // Copy the return value of the call so it can be returned. - let returnDataSize := returndatasize - let returnDataPosition := mload(0x40) - mstore(0x40, add(returnDataPosition, returnDataSize)) - returndatacopy(returnDataPosition, 0, returnDataSize) - - // Revert or return depending on whether or not the call was successful. - switch delegatecallSuccess - case 0 { - revert(returnDataPosition, returnDataSize) - } - default { - return(returnDataPosition, returnDataSize) - } - } - } - - /** - * @notice Transfers ownership of Proxy to a new owner. - * @param newOwner Address of the new owner account. - */ - function _transferOwnership(address newOwner) external onlyOwner { - _setOwner(newOwner); - } - - /** - * @notice Sets the address of the implementation contract and calls into it. - * @param implementation Address of the new target contract. - * @param callbackData The abi-encoded function call to perform in the implementation - * contract. - * @dev Throws if the initialization callback fails. - * @dev If the target contract does not need initialization, use - * setImplementation instead. - */ - function _setAndInitializeImplementation(address implementation, bytes calldata callbackData) - external - payable - onlyOwner - { - _setImplementation(implementation); - bool success; - bytes memory returnValue; - (success, returnValue) = implementation.delegatecall(callbackData); - require(success, "initialization callback failed"); - } - - /** - * @notice Returns the implementation address. - */ - function _getImplementation() external view returns (address implementation) { - bytes32 implementationPosition = IMPLEMENTATION_POSITION; - // Load the address of the implementation contract from an explicit storage slot. - assembly { - implementation := sload(implementationPosition) - } - } - - /** - * @notice Sets the address of the implementation contract. - * @param implementation Address of the new target contract. - * @dev If the target contract needs to be initialized, call - * setAndInitializeImplementation instead. - */ - function _setImplementation(address implementation) public onlyOwner { - bytes32 implementationPosition = IMPLEMENTATION_POSITION; - - require(Address.isContract(implementation), "Invalid contract address"); - - // Store the address of the implementation contract in an explicit storage slot. - assembly { - sstore(implementationPosition, implementation) - } - - emit ImplementationSet(implementation); - } - - /** - * @notice Returns the Proxy owner's address. - */ - function _getOwner() public view returns (address owner) { - bytes32 position = OWNER_POSITION; - // Load the address of the contract owner from an explicit storage slot. - assembly { - owner := sload(position) - } - } - - function _setOwner(address newOwner) internal { - require(newOwner != address(0), "owner cannot be 0"); - bytes32 position = OWNER_POSITION; - // Store the address of the contract owner in an explicit storage slot. - assembly { - sstore(position, newOwner) - } - emit OwnerSet(newOwner); - } -} diff --git a/contracts/common/ReentrancyGuard.sol b/contracts/common/ReentrancyGuard.sol deleted file mode 100644 index 8731a69b..00000000 --- a/contracts/common/ReentrancyGuard.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -/** - * @title Helps contracts guard against reentrancy attacks. - * @author Remco Bloemen , Eenae - * @dev If you mark a function `nonReentrant`, you should also - * mark it `external`. - */ -contract ReentrancyGuard { - /// @dev counter to allow mutex lock with only one SSTORE operation - uint256 private _guardCounter; - - constructor() internal { - // The counter starts at one to prevent changing it from zero to a non-zero - // value, which is a more expensive operation. - _guardCounter = 1; - } - - /** - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and make it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - _guardCounter += 1; - uint256 localCounter = _guardCounter; - _; - require(localCounter == _guardCounter, "reentrant call"); - } -} diff --git a/contracts/common/Registry.sol b/contracts/common/Registry.sol deleted file mode 100644 index 8f8b61d5..00000000 --- a/contracts/common/Registry.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; - -import "./interfaces/IRegistry.sol"; -import "./Initializable.sol"; - -/** - * @title Routes identifiers to addresses. - */ -contract Registry is IRegistry, Ownable, Initializable { - using SafeMath for uint256; - - mapping(bytes32 => address) public registry; - - event RegistryUpdated(string identifier, bytes32 indexed identifierHash, address indexed addr); - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - */ - function initialize() external initializer { - _transferOwnership(msg.sender); - } - - /** - * @notice Associates the given address with the given identifier. - * @param identifier Identifier of contract whose address we want to set. - * @param addr Address of contract. - */ - function setAddressFor(string calldata identifier, address addr) external onlyOwner { - bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); - registry[identifierHash] = addr; - emit RegistryUpdated(identifier, identifierHash, addr); - } - - /** - * @notice Gets address associated with the given identifierHash. - * @param identifierHash Identifier hash of contract whose address we want to look up. - * @dev Throws if address not set. - */ - function getAddressForOrDie(bytes32 identifierHash) external view returns (address) { - require(registry[identifierHash] != address(0), "identifier has no registry entry"); - return registry[identifierHash]; - } - - /** - * @notice Gets address associated with the given identifierHash. - * @param identifierHash Identifier hash of contract whose address we want to look up. - */ - function getAddressFor(bytes32 identifierHash) external view returns (address) { - return registry[identifierHash]; - } - - /** - * @notice Gets address associated with the given identifier. - * @param identifier Identifier of contract whose address we want to look up. - * @dev Throws if address not set. - */ - function getAddressForStringOrDie(string calldata identifier) external view returns (address) { - bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); - require(registry[identifierHash] != address(0), "identifier has no registry entry"); - return registry[identifierHash]; - } - - /** - * @notice Gets address associated with the given identifier. - * @param identifier Identifier of contract whose address we want to look up. - */ - function getAddressForString(string calldata identifier) external view returns (address) { - bytes32 identifierHash = keccak256(abi.encodePacked(identifier)); - return registry[identifierHash]; - } - - /** - * @notice Iterates over provided array of identifiers, getting the address for each. - * Returns true if `sender` matches the address of one of the provided identifiers. - * @param identifierHashes Array of hashes of approved identifiers. - * @param sender Address in question to verify membership. - * @return True if `sender` corresponds to the address of any of `identifiers` - * registry entries. - */ - function isOneOf(bytes32[] calldata identifierHashes, address sender) external view returns (bool) { - for (uint256 i = 0; i < identifierHashes.length; i = i.add(1)) { - if (registry[identifierHashes[i]] == sender) { - return true; - } - } - return false; - } -} diff --git a/contracts/common/SortedOracles.sol b/contracts/common/SortedOracles.sol deleted file mode 100644 index d2b02d75..00000000 --- a/contracts/common/SortedOracles.sol +++ /dev/null @@ -1,408 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; - -import "../interfaces/ISortedOracles.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../interfaces/IBreakerBox.sol"; - -import "../common/FixidityLib.sol"; -import "../common/Initializable.sol"; -import "../common/linkedlists/AddressSortedLinkedListWithMedian.sol"; -import "../common/linkedlists/SortedLinkedListWithMedian.sol"; - -/** - * @title SortedOracles - * - * @notice This contract stores a collection of exchange rate reports between mento - * collateral assets and other currencies. The most recent exchange rates - * are gathered off-chain by oracles, who then use the `report` function to - * submit the rates to this contract. Before submitting a rate report, an - * oracle's address must be added to the `isOracle` mapping for a specific - * rateFeedId, with the flag set to true. While submitting a report requires - * an address to be added to the mapping, no additional permissions are needed - * to read the reports, the calculated median rate, or the list of oracles. - * - * @dev A unique rateFeedId identifies each exchange rate. In the initial implementation - * of this contract, the rateFeedId was set as the address of the mento stable - * asset contract that used the rate. However, this implementation has since - * been updated, and the rateFeedId now refers to an address derived from the - * concatenation of the mento collateral asset and the currency symbols' hash. - * This change enables the contract to store multiple exchange rates for a - * single stable asset. As a result of this change, there may be instances - * where the term "token" is used in the contract code. These useages of the term - * "token" are actually referring to the rateFeedId. You can refer to the Mento - * protocol documentation to learn more about the rateFeedId and how it is derived. - * - */ -contract SortedOracles is ISortedOracles, ICeloVersionedContract, Ownable, Initializable { - using SafeMath for uint256; - using AddressSortedLinkedListWithMedian for SortedLinkedListWithMedian.List; - using FixidityLib for FixidityLib.Fraction; - - uint256 private constant FIXED1_UINT = 1e24; - - // Maps a rateFeedID to a sorted list of report values. - mapping(address => SortedLinkedListWithMedian.List) private rates; - // Maps a rateFeedID to a sorted list of report timestamps. - mapping(address => SortedLinkedListWithMedian.List) private timestamps; - mapping(address => mapping(address => bool)) public isOracle; - mapping(address => address[]) public oracles; - - // `reportExpirySeconds` is the fallback value used to determine reporting - // frequency. Initially it was the _only_ value but we later introduced - // the per token mapping in `tokenReportExpirySeconds`. If a token - // doesn't have a value in the mapping (i.e. it's 0), the fallback is used. - // See: #getTokenReportExpirySeconds - uint256 public reportExpirySeconds; - // Maps a rateFeedId to its report expiry time in seconds. - mapping(address => uint256) public tokenReportExpirySeconds; - - IBreakerBox public breakerBox; - - event OracleAdded(address indexed token, address indexed oracleAddress); - event OracleRemoved(address indexed token, address indexed oracleAddress); - event OracleReported(address indexed token, address indexed oracle, uint256 timestamp, uint256 value); - event OracleReportRemoved(address indexed token, address indexed oracle); - event MedianUpdated(address indexed token, uint256 value); - event ReportExpirySet(uint256 reportExpiry); - event TokenReportExpirySet(address token, uint256 reportExpiry); - event BreakerBoxUpdated(address indexed newBreakerBox); - - modifier onlyOracle(address token) { - require(isOracle[token][msg.sender], "sender was not an oracle for token addr"); - _; - } - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 1, 2, 1); - } - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param _reportExpirySeconds The number of seconds before a report is considered expired. - */ - function initialize(uint256 _reportExpirySeconds) external initializer { - _transferOwnership(msg.sender); - setReportExpiry(_reportExpirySeconds); - } - - /** - * @notice Sets the report expiry parameter. - * @param _reportExpirySeconds The number of seconds before a report is considered expired. - */ - function setReportExpiry(uint256 _reportExpirySeconds) public onlyOwner { - require(_reportExpirySeconds > 0, "report expiry seconds must be > 0"); - require(_reportExpirySeconds != reportExpirySeconds, "reportExpirySeconds hasn't changed"); - reportExpirySeconds = _reportExpirySeconds; - emit ReportExpirySet(_reportExpirySeconds); - } - - /** - * @notice Sets the report expiry parameter for a rateFeedId. - * @param _token The rateFeedId for which the expiry is to be set. - * @param _reportExpirySeconds The number of seconds before a report is considered expired. - */ - function setTokenReportExpiry(address _token, uint256 _reportExpirySeconds) external onlyOwner { - require(_reportExpirySeconds > 0, "report expiry seconds must be > 0"); - require(_reportExpirySeconds != tokenReportExpirySeconds[_token], "token reportExpirySeconds hasn't changed"); - tokenReportExpirySeconds[_token] = _reportExpirySeconds; - emit TokenReportExpirySet(_token, _reportExpirySeconds); - } - - /** - * @notice Sets the address of the BreakerBox. - * @param newBreakerBox The new BreakerBox address. - */ - function setBreakerBox(IBreakerBox newBreakerBox) public onlyOwner { - require(address(newBreakerBox) != address(0), "BreakerBox address must be set"); - breakerBox = newBreakerBox; - emit BreakerBoxUpdated(address(newBreakerBox)); - } - - /** - * @notice Adds a new Oracle for a specified rate feed. - * @param token The rateFeedId that the specified oracle is permitted to report. - * @param oracleAddress The address of the oracle. - */ - function addOracle(address token, address oracleAddress) external onlyOwner { - // solhint-disable-next-line reason-string - require( - token != address(0) && oracleAddress != address(0) && !isOracle[token][oracleAddress], - "token addr was null or oracle addr was null or oracle addr is already an oracle for token addr" - ); - isOracle[token][oracleAddress] = true; - oracles[token].push(oracleAddress); - emit OracleAdded(token, oracleAddress); - } - - /** - * @notice Removes an Oracle from a specified rate feed. - * @param token The rateFeedId that the specified oracle is no longer permitted to report. - * @param oracleAddress The address of the oracle. - * @param index The index of `oracleAddress` in the list of oracles. - */ - function removeOracle( - address token, - address oracleAddress, - uint256 index - ) external onlyOwner { - // solhint-disable-next-line reason-string - require( - token != address(0) && - oracleAddress != address(0) && - oracles[token].length > index && - oracles[token][index] == oracleAddress, - "token addr null or oracle addr null or index of token oracle not mapped to oracle addr" - ); - isOracle[token][oracleAddress] = false; - oracles[token][index] = oracles[token][oracles[token].length.sub(1)]; - oracles[token].length = oracles[token].length.sub(1); - if (reportExists(token, oracleAddress)) { - removeReport(token, oracleAddress); - } - emit OracleRemoved(token, oracleAddress); - } - - /** - * @notice Removes a report that is expired. - * @param token The rateFeedId of the report to be removed. - * @param n The number of expired reports to remove, at most (deterministic upper gas bound). - */ - function removeExpiredReports(address token, uint256 n) external { - require( - token != address(0) && n < timestamps[token].getNumElements(), - "token addr null or trying to remove too many reports" - ); - for (uint256 i = 0; i < n; i = i.add(1)) { - (bool isExpired, address oldestAddress) = isOldestReportExpired(token); - if (isExpired) { - removeReport(token, oldestAddress); - } else { - break; - } - } - } - - /** - * @notice Check if last report is expired. - * @param token The rateFeedId of the reports to be checked. - * @return bool A bool indicating if the last report is expired. - * @return address Oracle address of the last report. - */ - function isOldestReportExpired(address token) public view returns (bool, address) { - // solhint-disable-next-line reason-string - require(token != address(0)); - address oldest = timestamps[token].getTail(); - uint256 timestamp = timestamps[token].getValue(oldest); - // solhint-disable-next-line not-rely-on-time - if (now.sub(timestamp) >= getTokenReportExpirySeconds(token)) { - return (true, oldest); - } - return (false, oldest); - } - - /** - * @notice Updates an oracle value and the median. - * @param token The rateFeedId for the rate that is being reported. - * @param value The number of stable asset that equate to one unit of collateral asset, for the - * specified rateFeedId, expressed as a fixidity value. - * @param lesserKey The element which should be just left of the new oracle value. - * @param greaterKey The element which should be just right of the new oracle value. - * @dev Note that only one of `lesserKey` or `greaterKey` needs to be correct to reduce friction. - */ - function report( - address token, - uint256 value, - address lesserKey, - address greaterKey - ) external onlyOracle(token) { - uint256 originalMedian = rates[token].getMedianValue(); - if (rates[token].contains(msg.sender)) { - rates[token].update(msg.sender, value, lesserKey, greaterKey); - - // Rather than update the timestamp, we remove it and re-add it at the - // head of the list later. The reason for this is that we need to handle - // a few different cases: - // 1. This oracle is the only one to report so far. lesserKey = address(0) - // 2. Other oracles have reported since this one's last report. lesserKey = getHead() - // 3. Other oracles have reported, but the most recent is this one. - // lesserKey = key immediately after getHead() - // - // However, if we just remove this timestamp, timestamps[token].getHead() - // does the right thing in all cases. - timestamps[token].remove(msg.sender); - } else { - rates[token].insert(msg.sender, value, lesserKey, greaterKey); - } - timestamps[token].insert( - msg.sender, - // solhint-disable-next-line not-rely-on-time - now, - timestamps[token].getHead(), - address(0) - ); - emit OracleReported(token, msg.sender, now, value); - uint256 newMedian = rates[token].getMedianValue(); - if (newMedian != originalMedian) { - emit MedianUpdated(token, newMedian); - } - - if (address(breakerBox) != address(0)) { - breakerBox.checkAndSetBreakers(token); - } - } - - /** - * @notice Returns the number of rates that are currently stored for a specifed rateFeedId. - * @param token The rateFeedId for which to retrieve the number of rates. - * @return uint256 The number of reported oracle rates stored for the given rateFeedId. - */ - function numRates(address token) public view returns (uint256) { - return rates[token].getNumElements(); - } - - /** - * @notice Returns the median of the currently stored rates for a specified rateFeedId. - * @param token The rateFeedId of the rates for which the median value is being retrieved. - * @return uint256 The median exchange rate for rateFeedId. - * @return fixidity - */ - function medianRate(address token) external view returns (uint256, uint256) { - return (rates[token].getMedianValue(), numRates(token) == 0 ? 0 : FIXED1_UINT); - } - - /** - * @notice Gets all elements from the doubly linked list. - * @param token The rateFeedId for which the collateral asset exchange rate is being reported. - * @return keys Keys of an unpacked list of elements from largest to smallest. - * @return values Values of an unpacked list of elements from largest to smallest. - * @return relations Relations of an unpacked list of elements from largest to smallest. - */ - function getRates(address token) - external - view - returns ( - address[] memory, - uint256[] memory, - SortedLinkedListWithMedian.MedianRelation[] memory - ) - { - return rates[token].getElements(); - } - - /** - * @notice Returns the number of timestamps. - * @param token The rateFeedId for which the collateral asset exchange rate is being reported. - * @return uint256 The number of oracle report timestamps for the specified rateFeedId. - */ - function numTimestamps(address token) public view returns (uint256) { - return timestamps[token].getNumElements(); - } - - /** - * @notice Returns the median timestamp. - * @param token The rateFeedId for which the collateral asset exchange rate is being reported. - * @return uint256 The median report timestamp for the specified rateFeedId. - */ - function medianTimestamp(address token) external view returns (uint256) { - return timestamps[token].getMedianValue(); - } - - /** - * @notice Gets all elements from the doubly linked list. - * @param token The rateFeedId for which the collateral asset exchange rate is being reported. - * @return keys Keys of nn unpacked list of elements from largest to smallest. - * @return values Values of an unpacked list of elements from largest to smallest. - * @return relations Relations of an unpacked list of elements from largest to smallest. - */ - function getTimestamps(address token) - external - view - returns ( - address[] memory, - uint256[] memory, - SortedLinkedListWithMedian.MedianRelation[] memory - ) - { - return timestamps[token].getElements(); - } - - /** - * @notice Checks if a report exists for a specified rateFeedId from a given oracle. - * @param token The rateFeedId to be checked. - * @param oracle The oracle whose report should be checked. - * @return bool True if a report exists, false otherwise. - */ - function reportExists(address token, address oracle) internal view returns (bool) { - return rates[token].contains(oracle) && timestamps[token].contains(oracle); - } - - /** - * @notice Returns the list of oracles for a speficied rateFeedId. - * @param token The rateFeedId whose oracles should be returned. - * @return address[] A list of oracles for the given rateFeedId. - */ - function getOracles(address token) external view returns (address[] memory) { - return oracles[token]; - } - - /** - * @notice Returns the expiry for specified rateFeedId if it exists, if not the default is returned. - * @param token The rateFeedId. - * @return The report expiry in seconds. - */ - function getTokenReportExpirySeconds(address token) public view returns (uint256) { - if (tokenReportExpirySeconds[token] == 0) { - return reportExpirySeconds; - } - - return tokenReportExpirySeconds[token]; - } - - /** - * @notice Removes an oracle value and updates the median. - * @param token The rateFeedId for which the collateral asset exchange rate is being reported. - * @param oracle The oracle whose value should be removed. - * @dev This can be used to delete elements for oracles that have been removed. - * However, a > 1 elements reports list should always be maintained - */ - function removeReport(address token, address oracle) private { - if (numTimestamps(token) == 1 && reportExists(token, oracle)) return; - uint256 originalMedian = rates[token].getMedianValue(); - rates[token].remove(oracle); - timestamps[token].remove(oracle); - emit OracleReportRemoved(token, oracle); - uint256 newMedian = rates[token].getMedianValue(); - if (newMedian != originalMedian) { - emit MedianUpdated(token, newMedian); - if (address(breakerBox) != address(0)) { - breakerBox.checkAndSetBreakers(token); - } - } - } -} diff --git a/contracts/common/SortedOraclesProxy.sol b/contracts/common/SortedOraclesProxy.sol deleted file mode 100644 index eba26a76..00000000 --- a/contracts/common/SortedOraclesProxy.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./Proxy.sol"; - -/* solhint-disable-next-line no-empty-blocks */ -contract SortedOraclesProxy is Proxy { - -} diff --git a/contracts/common/UsingPrecompiles.sol b/contracts/common/UsingPrecompiles.sol deleted file mode 100644 index b0aca188..00000000 --- a/contracts/common/UsingPrecompiles.sol +++ /dev/null @@ -1,266 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable state-visibility -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "./interfaces/ICeloVersionedContract.sol"; - -contract UsingPrecompiles { - using SafeMath for uint256; - - address constant TRANSFER = address(0xff - 2); - address constant FRACTION_MUL = address(0xff - 3); - address constant PROOF_OF_POSSESSION = address(0xff - 4); - address constant GET_VALIDATOR = address(0xff - 5); - address constant NUMBER_VALIDATORS = address(0xff - 6); - address constant EPOCH_SIZE = address(0xff - 7); - address constant BLOCK_NUMBER_FROM_HEADER = address(0xff - 8); - address constant HASH_HEADER = address(0xff - 9); - address constant GET_PARENT_SEAL_BITMAP = address(0xff - 10); - address constant GET_VERIFIED_SEAL_BITMAP = address(0xff - 11); - - /** - * @notice calculate a * b^x for fractions a, b to `decimals` precision - * @param aNumerator Numerator of first fraction - * @param aDenominator Denominator of first fraction - * @param bNumerator Numerator of exponentiated fraction - * @param bDenominator Denominator of exponentiated fraction - * @param exponent exponent to raise b to - * @param _decimals precision - * @return Numerator of the computed quantity (not reduced). - * @return Denominator of the computed quantity (not reduced). - */ - function fractionMulExp( - uint256 aNumerator, - uint256 aDenominator, - uint256 bNumerator, - uint256 bDenominator, - uint256 exponent, - uint256 _decimals - ) public view returns (uint256, uint256) { - require(aDenominator != 0 && bDenominator != 0, "a denominator is zero"); - uint256 returnNumerator; - uint256 returnDenominator; - bool success; - bytes memory out; - (success, out) = FRACTION_MUL.staticcall( - abi.encodePacked(aNumerator, aDenominator, bNumerator, bDenominator, exponent, _decimals) - ); - require(success, "error calling fractionMulExp precompile"); - returnNumerator = getUint256FromBytes(out, 0); - returnDenominator = getUint256FromBytes(out, 32); - return (returnNumerator, returnDenominator); - } - - /** - * @notice Returns the current epoch size in blocks. - * @return The current epoch size in blocks. - */ - function getEpochSize() public view returns (uint256) { - bytes memory out; - bool success; - (success, out) = EPOCH_SIZE.staticcall(abi.encodePacked()); - require(success, "error calling getEpochSize precompile"); - return getUint256FromBytes(out, 0); - } - - /** - * @notice Returns the epoch number at a block. - * @param blockNumber Block number where epoch number is calculated. - * @return Epoch number. - */ - function getEpochNumberOfBlock(uint256 blockNumber) public view returns (uint256) { - return epochNumberOfBlock(blockNumber, getEpochSize()); - } - - /** - * @notice Returns the epoch number at a block. - * @return Current epoch number. - */ - function getEpochNumber() public view returns (uint256) { - return getEpochNumberOfBlock(block.number); - } - - /** - * @notice Returns the epoch number at a block. - * @param blockNumber Block number where epoch number is calculated. - * @param epochSize The epoch size in blocks. - * @return Epoch number. - */ - function epochNumberOfBlock(uint256 blockNumber, uint256 epochSize) internal pure returns (uint256) { - // Follows GetEpochNumber from celo-blockchain/blob/master/consensus/istanbul/utils.go - uint256 epochNumber = blockNumber / epochSize; - if (blockNumber % epochSize == 0) { - return epochNumber; - } else { - return epochNumber.add(1); - } - } - - /** - * @notice Gets a validator address from the current validator set. - * @param index Index of requested validator in the validator set. - * @return Address of validator at the requested index. - */ - function validatorSignerAddressFromCurrentSet(uint256 index) public view returns (address) { - bytes memory out; - bool success; - (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, uint256(block.number))); - require(success, "error calling validatorSignerAddressFromCurrentSet precompile"); - return address(getUint256FromBytes(out, 0)); - } - - /** - * @notice Gets a validator address from the validator set at the given block number. - * @param index Index of requested validator in the validator set. - * @param blockNumber Block number to retrieve the validator set from. - * @return Address of validator at the requested index. - */ - function validatorSignerAddressFromSet(uint256 index, uint256 blockNumber) public view returns (address) { - bytes memory out; - bool success; - (success, out) = GET_VALIDATOR.staticcall(abi.encodePacked(index, blockNumber)); - require(success, "error calling validatorSignerAddressFromSet precompile"); - return address(getUint256FromBytes(out, 0)); - } - - /** - * @notice Gets the size of the current elected validator set. - * @return Size of the current elected validator set. - */ - function numberValidatorsInCurrentSet() public view returns (uint256) { - bytes memory out; - bool success; - (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(uint256(block.number))); - require(success, "error calling numberValidatorsInCurrentSet precompile"); - return getUint256FromBytes(out, 0); - } - - /** - * @notice Gets the size of the validator set that must sign the given block number. - * @param blockNumber Block number to retrieve the validator set from. - * @return Size of the validator set. - */ - function numberValidatorsInSet(uint256 blockNumber) public view returns (uint256) { - bytes memory out; - bool success; - (success, out) = NUMBER_VALIDATORS.staticcall(abi.encodePacked(blockNumber)); - require(success, "error calling numberValidatorsInSet precompile"); - return getUint256FromBytes(out, 0); - } - - /** - * @notice Checks a BLS proof of possession. - * @param sender The address signed by the BLS key to generate the proof of possession. - * @param blsKey The BLS public key that the validator is using for consensus, should pass proof - * of possession. 48 bytes. - * @param blsPop The BLS public key proof-of-possession, which consists of a signature on the - * account address. 96 bytes. - * @return True upon success. - */ - function checkProofOfPossession( - address sender, - bytes memory blsKey, - bytes memory blsPop - ) public view returns (bool) { - bool success; - (success, ) = PROOF_OF_POSSESSION.staticcall(abi.encodePacked(sender, blsKey, blsPop)); - return success; - } - - /** - * @notice Parses block number out of header. - * @param header RLP encoded header - * @return Block number. - */ - function getBlockNumberFromHeader(bytes memory header) public view returns (uint256) { - bytes memory out; - bool success; - (success, out) = BLOCK_NUMBER_FROM_HEADER.staticcall(abi.encodePacked(header)); - require(success, "error calling getBlockNumberFromHeader precompile"); - return getUint256FromBytes(out, 0); - } - - /** - * @notice Computes hash of header. - * @param header RLP encoded header - * @return Header hash. - */ - function hashHeader(bytes memory header) public view returns (bytes32) { - bytes memory out; - bool success; - (success, out) = HASH_HEADER.staticcall(abi.encodePacked(header)); - require(success, "error calling hashHeader precompile"); - return getBytes32FromBytes(out, 0); - } - - /** - * @notice Gets the parent seal bitmap from the header at the given block number. - * @param blockNumber Block number to retrieve. Must be within 4 epochs of the current number. - * @return Bitmap parent seal with set bits at indices corresponding to signing validators. - */ - function getParentSealBitmap(uint256 blockNumber) public view returns (bytes32) { - bytes memory out; - bool success; - (success, out) = GET_PARENT_SEAL_BITMAP.staticcall(abi.encodePacked(blockNumber)); - require(success, "error calling getParentSealBitmap precompile"); - return getBytes32FromBytes(out, 0); - } - - /** - * @notice Verifies the BLS signature on the header and returns the seal bitmap. - * The validator set used for verification is retrieved based on the parent hash field of the - * header. If the parent hash is not in the blockchain, verification fails. - * @param header RLP encoded header - * @return Bitmap parent seal with set bits at indices correspoinding to signing validators. - */ - function getVerifiedSealBitmapFromHeader(bytes memory header) public view returns (bytes32) { - bytes memory out; - bool success; - (success, out) = GET_VERIFIED_SEAL_BITMAP.staticcall(abi.encodePacked(header)); - require(success, "error calling getVerifiedSealBitmapFromHeader precompile"); - return getBytes32FromBytes(out, 0); - } - - /** - * @notice Converts bytes to uint256. - * @param bs byte[] data - * @param start offset into byte data to convert - * @return uint256 data - */ - function getUint256FromBytes(bytes memory bs, uint256 start) internal pure returns (uint256) { - return uint256(getBytes32FromBytes(bs, start)); - } - - /** - * @notice Converts bytes to bytes32. - * @param bs byte[] data - * @param start offset into byte data to convert - * @return bytes32 data - */ - function getBytes32FromBytes(bytes memory bs, uint256 start) internal pure returns (bytes32) { - require(bs.length >= start.add(32), "slicing out of range"); - bytes32 x; - // solhint-disable-next-line no-inline-assembly - assembly { - x := mload(add(bs, add(start, 32))) - } - return x; - } - - /** - * @notice Returns the minimum number of required signers for a given block number. - * @dev Computed in celo-blockchain as int(math.Ceil(float64(2*valSet.Size()) / 3)) - */ - function minQuorumSize(uint256 blockNumber) public view returns (uint256) { - return numberValidatorsInSet(blockNumber).mul(2).add(2).div(3); - } - - /** - * @notice Computes byzantine quorum from current validator set size - * @return Byzantine quorum of validators. - */ - function minQuorumSizeInCurrentSet() public view returns (uint256) { - return minQuorumSize(block.number); - } -} diff --git a/contracts/common/UsingRegistry.sol b/contracts/common/UsingRegistry.sol index ab0cf311..1ff264bf 100644 --- a/contracts/common/UsingRegistry.sol +++ b/contracts/common/UsingRegistry.sol @@ -4,11 +4,11 @@ pragma solidity ^0.5.13; import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; -import "./interfaces/IFreezer.sol"; -import "./interfaces/IRegistry.sol"; +import "celo/contracts/common/interfaces/IFreezer.sol"; +import "celo/contracts/common/interfaces/IRegistry.sol"; -import "../legacy/interfaces/IExchange.sol"; -import "../legacy/interfaces/IStableToken.sol"; +import "../interfaces/IExchange.sol"; +import "../interfaces/IStableTokenV2.sol"; import "../interfaces/IReserve.sol"; import "../interfaces/ISortedOracles.sol"; @@ -77,7 +77,7 @@ contract UsingRegistry is Ownable { return ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); } - function getStableToken() internal view returns (IStableToken) { - return IStableToken(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); + function getStableToken() internal view returns (IStableTokenV2) { + return IStableTokenV2(registry.getAddressForOrDie(STABLE_TOKEN_REGISTRY_ID)); } } diff --git a/contracts/common/interfaces/ICeloVersionedContract.sol b/contracts/common/interfaces/ICeloVersionedContract.sol deleted file mode 100644 index 71ff1f84..00000000 --- a/contracts/common/interfaces/ICeloVersionedContract.sol +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -interface ICeloVersionedContract { - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ); -} diff --git a/contracts/common/interfaces/IFreezer.sol b/contracts/common/interfaces/IFreezer.sol deleted file mode 100644 index 090b6e9f..00000000 --- a/contracts/common/interfaces/IFreezer.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.13 <0.8.19; - -interface IFreezer { - function isFrozen(address) external view returns (bool); -} diff --git a/contracts/common/interfaces/IRegistry.sol b/contracts/common/interfaces/IRegistry.sol deleted file mode 100644 index 826713b8..00000000 --- a/contracts/common/interfaces/IRegistry.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.17 <0.8.19; - -interface IRegistry { - function setAddressFor(string calldata, address) external; - - function getAddressForOrDie(bytes32) external view returns (address); - - function getAddressFor(bytes32) external view returns (address); - - function getAddressForStringOrDie(string calldata identifier) external view returns (address); - - function getAddressForString(string calldata identifier) external view returns (address); - - function isOneOf(bytes32[] calldata, address) external view returns (bool); -} diff --git a/contracts/common/linkedlists/AddressLinkedList.sol b/contracts/common/linkedlists/AddressLinkedList.sol deleted file mode 100644 index f07c3695..00000000 --- a/contracts/common/linkedlists/AddressLinkedList.sol +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import "./LinkedList.sol"; - -/** - * @title Maintains a doubly linked list keyed by address. - * @dev Following the `next` pointers will lead you to the head, rather than the tail. - */ -library AddressLinkedList { - using LinkedList for LinkedList.List; - using SafeMath for uint256; - - function toBytes(address a) public pure returns (bytes32) { - return bytes32(uint256(a) << 96); - } - - function toAddress(bytes32 b) public pure returns (address) { - return address(uint256(b) >> 96); - } - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param previousKey The key of the element that comes before the element to insert. - * @param nextKey The key of the element that comes after the element to insert. - */ - function insert( - LinkedList.List storage list, - address key, - address previousKey, - address nextKey - ) public { - list.insert(toBytes(key), toBytes(previousKey), toBytes(nextKey)); - } - - /** - * @notice Inserts an element at the end of the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - */ - function push(LinkedList.List storage list, address key) public { - list.insert(toBytes(key), bytes32(0), list.tail); - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(LinkedList.List storage list, address key) public { - list.remove(toBytes(key)); - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param previousKey The key of the element that comes before the updated element. - * @param nextKey The key of the element that comes after the updated element. - */ - function update( - LinkedList.List storage list, - address key, - address previousKey, - address nextKey - ) public { - list.update(toBytes(key), toBytes(previousKey), toBytes(nextKey)); - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(LinkedList.List storage list, address key) public view returns (bool) { - return list.elements[toBytes(key)].exists; - } - - /** - * @notice Returns the N greatest elements of the list. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to return. - * @return The keys of the greatest elements. - * @dev Reverts if n is greater than the number of elements in the list. - */ - function headN(LinkedList.List storage list, uint256 n) public view returns (address[] memory) { - bytes32[] memory byteKeys = list.headN(n); - address[] memory keys = new address[](n); - for (uint256 i = 0; i < n; i = i.add(1)) { - keys[i] = toAddress(byteKeys[i]); - } - return keys; - } - - /** - * @notice Gets all element keys from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return All element keys from head to tail. - */ - function getKeys(LinkedList.List storage list) public view returns (address[] memory) { - return headN(list, list.numElements); - } -} diff --git a/contracts/common/linkedlists/AddressSortedLinkedList.sol b/contracts/common/linkedlists/AddressSortedLinkedList.sol deleted file mode 100644 index 2a48a005..00000000 --- a/contracts/common/linkedlists/AddressSortedLinkedList.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/Math.sol"; -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import "./SortedLinkedList.sol"; - -/** - * @title Maintains a sorted list of unsigned ints keyed by address. - */ -library AddressSortedLinkedList { - using SafeMath for uint256; - using SortedLinkedList for SortedLinkedList.List; - - function toBytes(address a) public pure returns (bytes32) { - return bytes32(uint256(a) << 96); - } - - function toAddress(bytes32 b) public pure returns (address) { - return address(uint256(b) >> 96); - } - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param value The element value. - * @param lesserKey The key of the element less than the element to insert. - * @param greaterKey The key of the element greater than the element to insert. - */ - function insert( - SortedLinkedList.List storage list, - address key, - uint256 value, - address lesserKey, - address greaterKey - ) public { - list.insert(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(SortedLinkedList.List storage list, address key) public { - list.remove(toBytes(key)); - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param value The element value. - * @param lesserKey The key of the element will be just left of `key` after the update. - * @param greaterKey The key of the element will be just right of `key` after the update. - * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. - */ - function update( - SortedLinkedList.List storage list, - address key, - uint256 value, - address lesserKey, - address greaterKey - ) public { - list.update(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(SortedLinkedList.List storage list, address key) public view returns (bool) { - return list.contains(toBytes(key)); - } - - /** - * @notice Returns the value for a particular key in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return The element value. - */ - function getValue(SortedLinkedList.List storage list, address key) public view returns (uint256) { - return list.getValue(toBytes(key)); - } - - /** - * @notice Gets all elements from the doubly linked list. - * @return Array of all keys in the list. - * @return Values corresponding to keys, which will be ordered largest to smallest. - */ - function getElements(SortedLinkedList.List storage list) public view returns (address[] memory, uint256[] memory) { - bytes32[] memory byteKeys = list.getKeys(); - address[] memory keys = new address[](byteKeys.length); - uint256[] memory values = new uint256[](byteKeys.length); - for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { - keys[i] = toAddress(byteKeys[i]); - values[i] = list.values[byteKeys[i]]; - } - return (keys, values); - } - - /** - * @notice Returns the minimum of `max` and the number of elements in the list > threshold. - * @param list A storage pointer to the underlying list. - * @param threshold The number that the element must exceed to be included. - * @param max The maximum number returned by this function. - * @return The minimum of `max` and the number of elements in the list > threshold. - */ - function numElementsGreaterThan( - SortedLinkedList.List storage list, - uint256 threshold, - uint256 max - ) public view returns (uint256) { - uint256 revisedMax = Math.min(max, list.list.numElements); - bytes32 key = list.list.head; - for (uint256 i = 0; i < revisedMax; i = i.add(1)) { - if (list.getValue(key) < threshold) { - return i; - } - key = list.list.elements[key].previousKey; - } - return revisedMax; - } - - /** - * @notice Returns the N greatest elements of the list. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to return. - * @return The keys of the greatest elements. - */ - function headN(SortedLinkedList.List storage list, uint256 n) public view returns (address[] memory) { - bytes32[] memory byteKeys = list.headN(n); - address[] memory keys = new address[](n); - for (uint256 i = 0; i < n; i = i.add(1)) { - keys[i] = toAddress(byteKeys[i]); - } - return keys; - } - - /** - * @notice Gets all element keys from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return All element keys from head to tail. - */ - function getKeys(SortedLinkedList.List storage list) public view returns (address[] memory) { - return headN(list, list.list.numElements); - } -} diff --git a/contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol b/contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol deleted file mode 100644 index 4d3dd61b..00000000 --- a/contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import "./SortedLinkedListWithMedian.sol"; - -/** - * @title Maintains a sorted list of unsigned ints keyed by address. - */ -library AddressSortedLinkedListWithMedian { - using SafeMath for uint256; - using SortedLinkedListWithMedian for SortedLinkedListWithMedian.List; - - function toBytes(address a) public pure returns (bytes32) { - return bytes32(uint256(a) << 96); - } - - function toAddress(bytes32 b) public pure returns (address) { - return address(uint256(b) >> 96); - } - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param value The element value. - * @param lesserKey The key of the element less than the element to insert. - * @param greaterKey The key of the element greater than the element to insert. - */ - function insert( - SortedLinkedListWithMedian.List storage list, - address key, - uint256 value, - address lesserKey, - address greaterKey - ) public { - list.insert(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(SortedLinkedListWithMedian.List storage list, address key) public { - list.remove(toBytes(key)); - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param value The element value. - * @param lesserKey The key of the element will be just left of `key` after the update. - * @param greaterKey The key of the element will be just right of `key` after the update. - * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. - */ - function update( - SortedLinkedListWithMedian.List storage list, - address key, - uint256 value, - address lesserKey, - address greaterKey - ) public { - list.update(toBytes(key), value, toBytes(lesserKey), toBytes(greaterKey)); - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(SortedLinkedListWithMedian.List storage list, address key) public view returns (bool) { - return list.contains(toBytes(key)); - } - - /** - * @notice Returns the value for a particular key in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return The element value. - */ - function getValue(SortedLinkedListWithMedian.List storage list, address key) public view returns (uint256) { - return list.getValue(toBytes(key)); - } - - /** - * @notice Returns the median value of the sorted list. - * @param list A storage pointer to the underlying list. - * @return The median value. - */ - function getMedianValue(SortedLinkedListWithMedian.List storage list) public view returns (uint256) { - return list.getValue(list.median); - } - - /** - * @notice Returns the key of the first element in the list. - * @param list A storage pointer to the underlying list. - * @return The key of the first element in the list. - */ - function getHead(SortedLinkedListWithMedian.List storage list) external view returns (address) { - return toAddress(list.getHead()); - } - - /** - * @notice Returns the key of the median element in the list. - * @param list A storage pointer to the underlying list. - * @return The key of the median element in the list. - */ - function getMedian(SortedLinkedListWithMedian.List storage list) external view returns (address) { - return toAddress(list.getMedian()); - } - - /** - * @notice Returns the key of the last element in the list. - * @param list A storage pointer to the underlying list. - * @return The key of the last element in the list. - */ - function getTail(SortedLinkedListWithMedian.List storage list) external view returns (address) { - return toAddress(list.getTail()); - } - - /** - * @notice Returns the number of elements in the list. - * @param list A storage pointer to the underlying list. - * @return The number of elements in the list. - */ - function getNumElements(SortedLinkedListWithMedian.List storage list) external view returns (uint256) { - return list.getNumElements(); - } - - /** - * @notice Gets all elements from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return Array of all keys in the list. - * @return Values corresponding to keys, which will be ordered largest to smallest. - * @return Array of relations to median of corresponding list elements. - */ - function getElements(SortedLinkedListWithMedian.List storage list) - public - view - returns ( - address[] memory, - uint256[] memory, - SortedLinkedListWithMedian.MedianRelation[] memory - ) - { - bytes32[] memory byteKeys = list.getKeys(); - address[] memory keys = new address[](byteKeys.length); - uint256[] memory values = new uint256[](byteKeys.length); - // prettier-ignore - SortedLinkedListWithMedian.MedianRelation[] memory relations = - new SortedLinkedListWithMedian.MedianRelation[](keys.length); - for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { - keys[i] = toAddress(byteKeys[i]); - values[i] = list.getValue(byteKeys[i]); - relations[i] = list.relation[byteKeys[i]]; - } - return (keys, values, relations); - } -} diff --git a/contracts/common/linkedlists/IntegerSortedLinkedList.sol b/contracts/common/linkedlists/IntegerSortedLinkedList.sol deleted file mode 100644 index 3c18cfc3..00000000 --- a/contracts/common/linkedlists/IntegerSortedLinkedList.sol +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import "./SortedLinkedList.sol"; - -/** - * @title Maintains a sorted list of unsigned ints keyed by uint256. - */ -library IntegerSortedLinkedList { - using SafeMath for uint256; - using SortedLinkedList for SortedLinkedList.List; - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param value The element value. - * @param lesserKey The key of the element less than the element to insert. - * @param greaterKey The key of the element greater than the element to insert. - */ - function insert( - SortedLinkedList.List storage list, - uint256 key, - uint256 value, - uint256 lesserKey, - uint256 greaterKey - ) public { - list.insert(bytes32(key), value, bytes32(lesserKey), bytes32(greaterKey)); - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(SortedLinkedList.List storage list, uint256 key) public { - list.remove(bytes32(key)); - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param value The element value. - * @param lesserKey The key of the element will be just left of `key` after the update. - * @param greaterKey The key of the element will be just right of `key` after the update. - * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. - */ - function update( - SortedLinkedList.List storage list, - uint256 key, - uint256 value, - uint256 lesserKey, - uint256 greaterKey - ) public { - list.update(bytes32(key), value, bytes32(lesserKey), bytes32(greaterKey)); - } - - /** - * @notice Inserts an element at the end of the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - */ - function push(SortedLinkedList.List storage list, uint256 key) public { - list.push(bytes32(key)); - } - - /** - * @notice Removes N elements from the head of the list and returns their keys. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to pop. - * @return The keys of the popped elements. - */ - function popN(SortedLinkedList.List storage list, uint256 n) public returns (uint256[] memory) { - bytes32[] memory byteKeys = list.popN(n); - uint256[] memory keys = new uint256[](byteKeys.length); - for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { - keys[i] = uint256(byteKeys[i]); - } - return keys; - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(SortedLinkedList.List storage list, uint256 key) public view returns (bool) { - return list.contains(bytes32(key)); - } - - /** - * @notice Returns the value for a particular key in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return The element value. - */ - function getValue(SortedLinkedList.List storage list, uint256 key) public view returns (uint256) { - return list.getValue(bytes32(key)); - } - - /** - * @notice Gets all elements from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return Array of all keys in the list. - * @return Values corresponding to keys, which will be ordered largest to smallest. - */ - function getElements(SortedLinkedList.List storage list) public view returns (uint256[] memory, uint256[] memory) { - bytes32[] memory byteKeys = list.getKeys(); - uint256[] memory keys = new uint256[](byteKeys.length); - uint256[] memory values = new uint256[](byteKeys.length); - for (uint256 i = 0; i < byteKeys.length; i = i.add(1)) { - keys[i] = uint256(byteKeys[i]); - values[i] = list.values[byteKeys[i]]; - } - return (keys, values); - } -} diff --git a/contracts/common/linkedlists/LinkedList.sol b/contracts/common/linkedlists/LinkedList.sol deleted file mode 100644 index 738dd632..00000000 --- a/contracts/common/linkedlists/LinkedList.sol +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -/** - * @title Maintains a doubly linked list keyed by bytes32. - * @dev Following the `next` pointers will lead you to the head, rather than the tail. - */ -library LinkedList { - using SafeMath for uint256; - - struct Element { - bytes32 previousKey; - bytes32 nextKey; - bool exists; - } - - struct List { - bytes32 head; - bytes32 tail; - uint256 numElements; - mapping(bytes32 => Element) elements; - } - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param previousKey The key of the element that comes before the element to insert. - * @param nextKey The key of the element that comes after the element to insert. - */ - function insert( - List storage list, - bytes32 key, - bytes32 previousKey, - bytes32 nextKey - ) internal { - require(key != bytes32(0), "Key must be defined"); - require(!contains(list, key), "Can't insert an existing element"); - require(previousKey != key && nextKey != key, "Key cannot be the same as previousKey or nextKey"); - - Element storage element = list.elements[key]; - element.exists = true; - - if (list.numElements == 0) { - list.tail = key; - list.head = key; - } else { - require(previousKey != bytes32(0) || nextKey != bytes32(0), "Either previousKey or nextKey must be defined"); - - element.previousKey = previousKey; - element.nextKey = nextKey; - - if (previousKey != bytes32(0)) { - require(contains(list, previousKey), "If previousKey is defined, it must exist in the list"); - Element storage previousElement = list.elements[previousKey]; - require(previousElement.nextKey == nextKey, "previousKey must be adjacent to nextKey"); - previousElement.nextKey = key; - } else { - list.tail = key; - } - - if (nextKey != bytes32(0)) { - require(contains(list, nextKey), "If nextKey is defined, it must exist in the list"); - Element storage nextElement = list.elements[nextKey]; - require(nextElement.previousKey == previousKey, "previousKey must be adjacent to nextKey"); - nextElement.previousKey = key; - } else { - list.head = key; - } - } - - list.numElements = list.numElements.add(1); - } - - /** - * @notice Inserts an element at the tail of the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - */ - function push(List storage list, bytes32 key) internal { - insert(list, key, bytes32(0), list.tail); - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(List storage list, bytes32 key) internal { - Element storage element = list.elements[key]; - require(key != bytes32(0) && contains(list, key), "key not in list"); - if (element.previousKey != bytes32(0)) { - Element storage previousElement = list.elements[element.previousKey]; - previousElement.nextKey = element.nextKey; - } else { - list.tail = element.nextKey; - } - - if (element.nextKey != bytes32(0)) { - Element storage nextElement = list.elements[element.nextKey]; - nextElement.previousKey = element.previousKey; - } else { - list.head = element.previousKey; - } - - delete list.elements[key]; - list.numElements = list.numElements.sub(1); - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param previousKey The key of the element that comes before the updated element. - * @param nextKey The key of the element that comes after the updated element. - */ - function update( - List storage list, - bytes32 key, - bytes32 previousKey, - bytes32 nextKey - ) internal { - require(key != bytes32(0) && key != previousKey && key != nextKey && contains(list, key), "key on in list"); - remove(list, key); - insert(list, key, previousKey, nextKey); - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(List storage list, bytes32 key) internal view returns (bool) { - return list.elements[key].exists; - } - - /** - * @notice Returns the keys of the N elements at the head of the list. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to return. - * @return The keys of the N elements at the head of the list. - * @dev Reverts if n is greater than the number of elements in the list. - */ - function headN(List storage list, uint256 n) internal view returns (bytes32[] memory) { - require(n <= list.numElements, "not enough elements"); - bytes32[] memory keys = new bytes32[](n); - bytes32 key = list.head; - for (uint256 i = 0; i < n; i = i.add(1)) { - keys[i] = key; - key = list.elements[key].previousKey; - } - return keys; - } - - /** - * @notice Gets all element keys from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return All element keys from head to tail. - */ - function getKeys(List storage list) internal view returns (bytes32[] memory) { - return headN(list, list.numElements); - } -} diff --git a/contracts/common/linkedlists/SortedLinkedList.sol b/contracts/common/linkedlists/SortedLinkedList.sol deleted file mode 100644 index cfa95cfc..00000000 --- a/contracts/common/linkedlists/SortedLinkedList.sol +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "./LinkedList.sol"; - -/** - * @title Maintains a sorted list of unsigned ints keyed by bytes32. - */ -library SortedLinkedList { - using SafeMath for uint256; - using LinkedList for LinkedList.List; - - struct List { - LinkedList.List list; - mapping(bytes32 => uint256) values; - } - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param value The element value. - * @param lesserKey The key of the element less than the element to insert. - * @param greaterKey The key of the element greater than the element to insert. - */ - function insert( - List storage list, - bytes32 key, - uint256 value, - bytes32 lesserKey, - bytes32 greaterKey - ) internal { - require(key != bytes32(0) && key != lesserKey && key != greaterKey && !contains(list, key), "invalid key"); - require( - (lesserKey != bytes32(0) || greaterKey != bytes32(0)) || list.list.numElements == 0, - "greater and lesser key zero" - ); - require(contains(list, lesserKey) || lesserKey == bytes32(0), "invalid lesser key"); - require(contains(list, greaterKey) || greaterKey == bytes32(0), "invalid greater key"); - (lesserKey, greaterKey) = getLesserAndGreater(list, value, lesserKey, greaterKey); - list.list.insert(key, lesserKey, greaterKey); - list.values[key] = value; - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(List storage list, bytes32 key) internal { - list.list.remove(key); - list.values[key] = 0; - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param value The element value. - * @param lesserKey The key of the element will be just left of `key` after the update. - * @param greaterKey The key of the element will be just right of `key` after the update. - * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. - */ - function update( - List storage list, - bytes32 key, - uint256 value, - bytes32 lesserKey, - bytes32 greaterKey - ) internal { - remove(list, key); - insert(list, key, value, lesserKey, greaterKey); - } - - /** - * @notice Inserts an element at the tail of the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - */ - function push(List storage list, bytes32 key) internal { - insert(list, key, 0, bytes32(0), list.list.tail); - } - - /** - * @notice Removes N elements from the head of the list and returns their keys. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to pop. - * @return The keys of the popped elements. - */ - function popN(List storage list, uint256 n) internal returns (bytes32[] memory) { - require(n <= list.list.numElements, "not enough elements"); - bytes32[] memory keys = new bytes32[](n); - for (uint256 i = 0; i < n; i = i.add(1)) { - bytes32 key = list.list.head; - keys[i] = key; - remove(list, key); - } - return keys; - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(List storage list, bytes32 key) internal view returns (bool) { - return list.list.contains(key); - } - - /** - * @notice Returns the value for a particular key in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return The element value. - */ - function getValue(List storage list, bytes32 key) internal view returns (uint256) { - return list.values[key]; - } - - /** - * @notice Gets all elements from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return Array of all keys in the list. - * @return Values corresponding to keys, which will be ordered largest to smallest. - */ - function getElements(List storage list) internal view returns (bytes32[] memory, uint256[] memory) { - bytes32[] memory keys = getKeys(list); - uint256[] memory values = new uint256[](keys.length); - for (uint256 i = 0; i < keys.length; i = i.add(1)) { - values[i] = list.values[keys[i]]; - } - return (keys, values); - } - - /** - * @notice Gets all element keys from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return All element keys from head to tail. - */ - function getKeys(List storage list) internal view returns (bytes32[] memory) { - return list.list.getKeys(); - } - - /** - * @notice Returns first N greatest elements of the list. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to return. - * @return The keys of the first n elements. - * @dev Reverts if n is greater than the number of elements in the list. - */ - function headN(List storage list, uint256 n) internal view returns (bytes32[] memory) { - return list.list.headN(n); - } - - /** - * @notice Returns the keys of the elements greaterKey than and less than the provided value. - * @param list A storage pointer to the underlying list. - * @param value The element value. - * @param lesserKey The key of the element which could be just left of the new value. - * @param greaterKey The key of the element which could be just right of the new value. - * @return The correct lesserKey keys. - * @return The correct greaterKey keys. - */ - function getLesserAndGreater( - List storage list, - uint256 value, - bytes32 lesserKey, - bytes32 greaterKey - ) private view returns (bytes32, bytes32) { - // Check for one of the following conditions and fail if none are met: - // 1. The value is less than the current lowest value - // 2. The value is greater than the current greatest value - // 3. The value is just greater than the value for `lesserKey` - // 4. The value is just less than the value for `greaterKey` - if (lesserKey == bytes32(0) && isValueBetween(list, value, lesserKey, list.list.tail)) { - return (lesserKey, list.list.tail); - } else if (greaterKey == bytes32(0) && isValueBetween(list, value, list.list.head, greaterKey)) { - return (list.list.head, greaterKey); - } else if ( - lesserKey != bytes32(0) && isValueBetween(list, value, lesserKey, list.list.elements[lesserKey].nextKey) - ) { - return (lesserKey, list.list.elements[lesserKey].nextKey); - } else if ( - greaterKey != bytes32(0) && isValueBetween(list, value, list.list.elements[greaterKey].previousKey, greaterKey) - ) { - return (list.list.elements[greaterKey].previousKey, greaterKey); - } else { - require(false, "get lesser and greater failure"); - } - } - - /** - * @notice Returns whether or not a given element is between two other elements. - * @param list A storage pointer to the underlying list. - * @param value The element value. - * @param lesserKey The key of the element whose value should be lesserKey. - * @param greaterKey The key of the element whose value should be greaterKey. - * @return True if the given element is between the two other elements. - */ - function isValueBetween( - List storage list, - uint256 value, - bytes32 lesserKey, - bytes32 greaterKey - ) private view returns (bool) { - bool isLesser = lesserKey == bytes32(0) || list.values[lesserKey] <= value; - bool isGreater = greaterKey == bytes32(0) || list.values[greaterKey] >= value; - return isLesser && isGreater; - } -} diff --git a/contracts/common/linkedlists/SortedLinkedListWithMedian.sol b/contracts/common/linkedlists/SortedLinkedListWithMedian.sol deleted file mode 100644 index 2f88fed4..00000000 --- a/contracts/common/linkedlists/SortedLinkedListWithMedian.sol +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "./LinkedList.sol"; -import "./SortedLinkedList.sol"; - -/** - * @title Maintains a sorted list of unsigned ints keyed by bytes32. - */ -library SortedLinkedListWithMedian { - using SafeMath for uint256; - using SortedLinkedList for SortedLinkedList.List; - - enum MedianAction { - None, - Lesser, - Greater - } - - enum MedianRelation { - Undefined, - Lesser, - Greater, - Equal - } - - struct List { - SortedLinkedList.List list; - bytes32 median; - mapping(bytes32 => MedianRelation) relation; - } - - /** - * @notice Inserts an element into a doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - * @param value The element value. - * @param lesserKey The key of the element less than the element to insert. - * @param greaterKey The key of the element greater than the element to insert. - */ - function insert( - List storage list, - bytes32 key, - uint256 value, - bytes32 lesserKey, - bytes32 greaterKey - ) internal { - list.list.insert(key, value, lesserKey, greaterKey); - LinkedList.Element storage element = list.list.list.elements[key]; - - MedianAction action = MedianAction.None; - if (list.list.list.numElements == 1) { - list.median = key; - list.relation[key] = MedianRelation.Equal; - } else if (list.list.list.numElements % 2 == 1) { - // When we have an odd number of elements, and the element that we inserted is less than - // the previous median, we need to slide the median down one element, since we had previously - // selected the greater of the two middle elements. - if (element.previousKey == bytes32(0) || list.relation[element.previousKey] == MedianRelation.Lesser) { - action = MedianAction.Lesser; - list.relation[key] = MedianRelation.Lesser; - } else { - list.relation[key] = MedianRelation.Greater; - } - } else { - // When we have an even number of elements, and the element that we inserted is greater than - // the previous median, we need to slide the median up one element, since we always select - // the greater of the two middle elements. - if (element.nextKey == bytes32(0) || list.relation[element.nextKey] == MedianRelation.Greater) { - action = MedianAction.Greater; - list.relation[key] = MedianRelation.Greater; - } else { - list.relation[key] = MedianRelation.Lesser; - } - } - updateMedian(list, action); - } - - /** - * @notice Removes an element from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to remove. - */ - function remove(List storage list, bytes32 key) internal { - MedianAction action = MedianAction.None; - if (list.list.list.numElements == 0) { - list.median = bytes32(0); - } else if (list.list.list.numElements % 2 == 0) { - // When we have an even number of elements, we always choose the higher of the two medians. - // Thus, if the element we're removing is greaterKey than or equal to the median we need to - // slide the median left by one. - if (list.relation[key] == MedianRelation.Greater || list.relation[key] == MedianRelation.Equal) { - action = MedianAction.Lesser; - } - } else { - // When we don't have an even number of elements, we just choose the median value. - // Thus, if the element we're removing is less than or equal to the median, we need to slide - // median right by one. - if (list.relation[key] == MedianRelation.Lesser || list.relation[key] == MedianRelation.Equal) { - action = MedianAction.Greater; - } - } - updateMedian(list, action); - - list.list.remove(key); - } - - /** - * @notice Updates an element in the list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @param value The element value. - * @param lesserKey The key of the element will be just left of `key` after the update. - * @param greaterKey The key of the element will be just right of `key` after the update. - * @dev Note that only one of "lesserKey" or "greaterKey" needs to be correct to reduce friction. - */ - function update( - List storage list, - bytes32 key, - uint256 value, - bytes32 lesserKey, - bytes32 greaterKey - ) internal { - remove(list, key); - insert(list, key, value, lesserKey, greaterKey); - } - - /** - * @notice Inserts an element at the tail of the doubly linked list. - * @param list A storage pointer to the underlying list. - * @param key The key of the element to insert. - */ - function push(List storage list, bytes32 key) internal { - insert(list, key, 0, bytes32(0), list.list.list.tail); - } - - /** - * @notice Removes N elements from the head of the list and returns their keys. - * @param list A storage pointer to the underlying list. - * @param n The number of elements to pop. - * @return The keys of the popped elements. - */ - function popN(List storage list, uint256 n) internal returns (bytes32[] memory) { - require(n <= list.list.list.numElements, "not enough elements"); - bytes32[] memory keys = new bytes32[](n); - for (uint256 i = 0; i < n; i = i.add(1)) { - bytes32 key = list.list.list.head; - keys[i] = key; - remove(list, key); - } - return keys; - } - - /** - * @notice Returns whether or not a particular key is present in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return Whether or not the key is in the sorted list. - */ - function contains(List storage list, bytes32 key) internal view returns (bool) { - return list.list.contains(key); - } - - /** - * @notice Returns the value for a particular key in the sorted list. - * @param list A storage pointer to the underlying list. - * @param key The element key. - * @return The element value. - */ - function getValue(List storage list, bytes32 key) internal view returns (uint256) { - return list.list.values[key]; - } - - /** - * @notice Returns the median value of the sorted list. - * @param list A storage pointer to the underlying list. - * @return The median value. - */ - function getMedianValue(List storage list) internal view returns (uint256) { - return getValue(list, list.median); - } - - /** - * @notice Returns the key of the first element in the list. - * @param list A storage pointer to the underlying list. - * @return The key of the first element in the list. - */ - function getHead(List storage list) internal view returns (bytes32) { - return list.list.list.head; - } - - /** - * @notice Returns the key of the median element in the list. - * @param list A storage pointer to the underlying list. - * @return The key of the median element in the list. - */ - function getMedian(List storage list) internal view returns (bytes32) { - return list.median; - } - - /** - * @notice Returns the key of the last element in the list. - * @param list A storage pointer to the underlying list. - * @return The key of the last element in the list. - */ - function getTail(List storage list) internal view returns (bytes32) { - return list.list.list.tail; - } - - /** - * @notice Returns the number of elements in the list. - * @param list A storage pointer to the underlying list. - * @return The number of elements in the list. - */ - function getNumElements(List storage list) internal view returns (uint256) { - return list.list.list.numElements; - } - - /** - * @notice Gets all elements from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return Array of all keys in the list. - * @return Values corresponding to keys, which will be ordered largest to smallest. - * @return Array of relations to median of corresponding list elements. - */ - function getElements(List storage list) - internal - view - returns ( - bytes32[] memory, - uint256[] memory, - MedianRelation[] memory - ) - { - bytes32[] memory keys = getKeys(list); - uint256[] memory values = new uint256[](keys.length); - MedianRelation[] memory relations = new MedianRelation[](keys.length); - for (uint256 i = 0; i < keys.length; i = i.add(1)) { - values[i] = list.list.values[keys[i]]; - relations[i] = list.relation[keys[i]]; - } - return (keys, values, relations); - } - - /** - * @notice Gets all element keys from the doubly linked list. - * @param list A storage pointer to the underlying list. - * @return All element keys from head to tail. - */ - function getKeys(List storage list) internal view returns (bytes32[] memory) { - return list.list.getKeys(); - } - - /** - * @notice Moves the median pointer right or left of its current value. - * @param list A storage pointer to the underlying list. - * @param action Which direction to move the median pointer. - */ - function updateMedian(List storage list, MedianAction action) private { - LinkedList.Element storage previousMedian = list.list.list.elements[list.median]; - if (action == MedianAction.Lesser) { - list.relation[list.median] = MedianRelation.Greater; - list.median = previousMedian.previousKey; - } else if (action == MedianAction.Greater) { - list.relation[list.median] = MedianRelation.Lesser; - list.median = previousMedian.nextKey; - } - list.relation[list.median] = MedianRelation.Equal; - } -} diff --git a/contracts/governance/Airgrab.sol b/contracts/governance/Airgrab.sol index 853a0450..424b0920 100644 --- a/contracts/governance/Airgrab.sol +++ b/contracts/governance/Airgrab.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable immutable-vars-naming, gas-custom-errors pragma solidity 0.8.18; import { MerkleProof } from "openzeppelin-contracts-next/contracts/utils/cryptography/MerkleProof.sol"; @@ -113,11 +114,7 @@ contract Airgrab is ReentrancyGuard { * @param amount The amount of tokens to be claimed. * @param merkleProof The merkle proof for the account. */ - modifier canClaim( - address account, - uint256 amount, - bytes32[] calldata merkleProof - ) { + modifier canClaim(address account, uint256 amount, bytes32[] calldata merkleProof) { require(block.timestamp <= endTimestamp, "Airgrab: finished"); require(!claimed[account], "Airgrab: already claimed"); bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, amount)))); diff --git a/contracts/governance/Emission.sol b/contracts/governance/Emission.sol index e586ae0b..e29d99c6 100644 --- a/contracts/governance/Emission.sol +++ b/contracts/governance/Emission.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable gas-custom-errors pragma solidity 0.8.18; import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; @@ -54,11 +55,7 @@ contract Emission is OwnableUpgradeable { * @param emissionTarget_ The address of the emission target. * @param emissionSupply_ The total amount of tokens that can be emitted. */ - function initialize( - address mentoToken_, - address emissionTarget_, - uint256 emissionSupply_ - ) public initializer { + function initialize(address mentoToken_, address emissionTarget_, uint256 emissionSupply_) public initializer { emissionStartTime = block.timestamp; mentoToken = MentoToken(mentoToken_); // slither-disable-next-line missing-zero-check diff --git a/contracts/governance/GovernanceFactory.sol b/contracts/governance/GovernanceFactory.sol index 6e5c552b..736cb844 100644 --- a/contracts/governance/GovernanceFactory.sol +++ b/contracts/governance/GovernanceFactory.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; -// solhint-disable max-line-length +// solhint-disable max-line-length, gas-custom-errors // slither-disable-start reentrancy-events import { MentoToken } from "./MentoToken.sol"; @@ -24,7 +24,6 @@ import { import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; import { Ownable } from "openzeppelin-contracts-next/contracts/access/Ownable.sol"; -import { IERC20Upgradeable } from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; /** * @title GovernanceFactory @@ -144,11 +143,11 @@ contract GovernanceFactory is Ownable { } mentoToken = MentoTokenDeployerLib.deploy( // NONCE:2 - allocationRecipients, - allocationAmounts, - emissionPrecalculated, - lockingPrecalculated - ); + allocationRecipients, + allocationAmounts, + emissionPrecalculated, + lockingPrecalculated + ); assert(address(mentoToken) == tokenPrecalculated); @@ -158,15 +157,15 @@ contract GovernanceFactory is Ownable { Emission emissionImpl = EmissionDeployerLib.deploy(); // NONCE:3 // slither-disable-next-line reentrancy-benign TransparentUpgradeableProxy emissionProxy = ProxyDeployerLib.deployProxy( // NONCE:4 - address(emissionImpl), - address(proxyAdmin), - abi.encodeWithSelector( - emissionImpl.initialize.selector, - tokenPrecalculated, /// @param mentoToken_ The address of the MentoToken contract. - governanceTimelockPrecalculated, /// @param governanceTimelock_ The address of the mento treasury contract. - mentoToken.emissionSupply() /// @param emissionSupply_ The total amount of tokens that can be emitted. - ) - ); + address(emissionImpl), + address(proxyAdmin), + abi.encodeWithSelector( + emissionImpl.initialize.selector, + tokenPrecalculated, /// @param mentoToken_ The address of the MentoToken contract. + governanceTimelockPrecalculated, /// @param governanceTimelock_ The address of the mento treasury contract. + mentoToken.emissionSupply() /// @param emissionSupply_ The total amount of tokens that can be emitted. + ) + ); emission = Emission(address(emissionProxy)); assert(address(emission) == emissionPrecalculated); @@ -177,16 +176,16 @@ contract GovernanceFactory is Ownable { airgrabEnds = block.timestamp + AIRGRAB_DURATION; // slither-disable-next-line reentrancy-benign airgrab = AirgrabDeployerLib.deploy( // NONCE:5 - airgrabRoot, - fractalSigner, - FRACTAL_MAX_AGE, - airgrabEnds, - AIRGRAB_LOCK_CLIFF, - AIRGRAB_LOCK_SLOPE, - tokenPrecalculated, - lockingPrecalculated, - payable(governanceTimelockPrecalculated) - ); + airgrabRoot, + fractalSigner, + FRACTAL_MAX_AGE, + airgrabEnds, + AIRGRAB_LOCK_CLIFF, + AIRGRAB_LOCK_SLOPE, + tokenPrecalculated, + lockingPrecalculated, + payable(governanceTimelockPrecalculated) + ); assert(address(airgrab) == airgrabPrecalculated); // ========================================== @@ -196,16 +195,16 @@ contract GovernanceFactory is Ownable { uint32 startingPointWeek = uint32(Locking(lockingImpl).getWeek() - 1); // slither-disable-next-line reentrancy-benign TransparentUpgradeableProxy lockingProxy = ProxyDeployerLib.deployProxy( // NONCE:7 - address(lockingImpl), - address(proxyAdmin), - abi.encodeWithSelector( - lockingImpl.__Locking_init.selector, - address(mentoToken), /// @param _token The token to be locked in exchange for voting power in form of veTokens. - startingPointWeek, /// @param _startingPointWeek The locking epoch start in weeks. We start the locking contract from week 1 with min slope duration of 1 - 0, /// @param _minCliffPeriod minimum cliff period in weeks. - 1 /// @param _minSlopePeriod minimum slope period in weeks. - ) - ); + address(lockingImpl), + address(proxyAdmin), + abi.encodeWithSelector( + lockingImpl.__Locking_init.selector, + address(mentoToken), /// @param _token The token to be locked in exchange for voting power in form of veTokens. + startingPointWeek, /// @param _startingPointWeek The locking epoch start in weeks. We start the locking contract from week 1 with min slope duration of 1 + 0, /// @param _minCliffPeriod minimum cliff period in weeks. + 1 /// @param _minSlopePeriod minimum slope period in weeks. + ) + ); locking = Locking(address(lockingProxy)); assert(address(locking) == lockingPrecalculated); @@ -225,17 +224,17 @@ contract GovernanceFactory is Ownable { // slither-disable-next-line reentrancy-benign TransparentUpgradeableProxy governanceTimelockProxy = ProxyDeployerLib.deployProxy( // NONCE:9 - address(timelockControllerImpl), - address(proxyAdmin), - abi.encodeWithSelector( - timelockControllerImpl.__MentoTimelockController_init.selector, - GOVERNANCE_TIMELOCK_DELAY, /// @param minDelay The minimum delay before a proposal can be executed. - governanceProposers, /// @param proposers List of addresses that are allowed to queue AND cancel operations. - governanceExecutors, /// @param executors List of addresses that are allowed to execute proposals. - address(0), /// @param admin No admin necessary as proposers are preset upon deployment. - watchdogMultiSig /// @param canceller An additional canceller address with the rights to cancel awaiting proposals. - ) - ); + address(timelockControllerImpl), + address(proxyAdmin), + abi.encodeWithSelector( + timelockControllerImpl.__MentoTimelockController_init.selector, + GOVERNANCE_TIMELOCK_DELAY, /// @param minDelay The minimum delay before a proposal can be executed. + governanceProposers, /// @param proposers List of addresses that are allowed to queue AND cancel operations. + governanceExecutors, /// @param executors List of addresses that are allowed to execute proposals. + address(0), /// @param admin No admin necessary as proposers are preset upon deployment. + watchdogMultiSig /// @param canceller An additional canceller address with the rights to cancel awaiting proposals. + ) + ); governanceTimelock = TimelockController(payable(governanceTimelockProxy)); assert(address(governanceTimelock) == governanceTimelockPrecalculated); @@ -245,18 +244,18 @@ contract GovernanceFactory is Ownable { // slither-disable-next-line reentrancy-benign MentoGovernor mentoGovernorImpl = MentoGovernorDeployerLib.deploy(); // NONCE:10 TransparentUpgradeableProxy mentoGovernorProxy = ProxyDeployerLib.deployProxy( // NONCE: 11 - address(mentoGovernorImpl), - address(proxyAdmin), - abi.encodeWithSelector( - mentoGovernorImpl.__MentoGovernor_init.selector, - address(lockingProxy), /// @param veToken The escrowed Mento Token used for voting. - governanceTimelockProxy, /// @param timelockController The timelock controller used by the governor. - GOVERNOR_VOTING_DELAY, /// @param votingDelay_ The delay time in blocks between the proposal creation and the start of voting. - GOVERNOR_VOTING_PERIOD, /// @param votingPeriod_ The voting duration in blocks between the vote start and vote end. - GOVERNOR_PROPOSAL_THRESHOLD, /// @param threshold_ The number of votes required in order for a voter to become a proposer. - GOVERNOR_QUORUM /// @param quorum_ The minimum number of votes in percent of total supply required in order for a proposal to succeed. - ) - ); + address(mentoGovernorImpl), + address(proxyAdmin), + abi.encodeWithSelector( + mentoGovernorImpl.__MentoGovernor_init.selector, + address(lockingProxy), /// @param veToken The escrowed Mento Token used for voting. + governanceTimelockProxy, /// @param timelockController The timelock controller used by the governor. + GOVERNOR_VOTING_DELAY, /// @param votingDelay_ The delay time in blocks between the proposal creation and the start of voting. + GOVERNOR_VOTING_PERIOD, /// @param votingPeriod_ The voting duration in blocks between the vote start and vote end. + GOVERNOR_PROPOSAL_THRESHOLD, /// @param threshold_ The number of votes required in order for a voter to become a proposer. + GOVERNOR_QUORUM /// @param quorum_ The minimum number of votes in percent of total supply required in order for a proposal to succeed. + ) + ); mentoGovernor = MentoGovernor(payable(mentoGovernorProxy)); assert(address(mentoGovernor) == governorPrecalculated); diff --git a/contracts/governance/MentoGovernor.sol b/contracts/governance/MentoGovernor.sol index 866aa356..822063d2 100644 --- a/contracts/governance/MentoGovernor.sol +++ b/contracts/governance/MentoGovernor.sol @@ -83,25 +83,22 @@ contract MentoGovernor is return super.proposalThreshold(); } - function quorum(uint256 blockNumber) - public - view - override(IGovernorUpgradeable, GovernorVotesQuorumFractionUpgradeable) - returns (uint256) - { + function quorum( + uint256 blockNumber + ) public view override(IGovernorUpgradeable, GovernorVotesQuorumFractionUpgradeable) returns (uint256) { return super.quorum(blockNumber); } - function getVotes(address account, uint256 blockNumber) - public - view - override(GovernorUpgradeable, IGovernorUpgradeable) - returns (uint256) - { + function getVotes( + address account, + uint256 blockNumber + ) public view override(GovernorUpgradeable, IGovernorUpgradeable) returns (uint256) { return super.getVotes(account, blockNumber); } - function state(uint256 proposalId) + function state( + uint256 proposalId + ) public view override(GovernorUpgradeable, IGovernorUpgradeable, GovernorTimelockControlUpgradeable) @@ -151,12 +148,9 @@ contract MentoGovernor is return super._executor(); } - function supportsInterface(bytes4 interfaceId) - public - view - override(GovernorUpgradeable, IERC165Upgradeable, GovernorTimelockControlUpgradeable) - returns (bool) - { + function supportsInterface( + bytes4 interfaceId + ) public view override(GovernorUpgradeable, IERC165Upgradeable, GovernorTimelockControlUpgradeable) returns (bool) { return super.supportsInterface(interfaceId); } } diff --git a/contracts/governance/MentoToken.sol b/contracts/governance/MentoToken.sol index 19aa05ec..e304a4a0 100644 --- a/contracts/governance/MentoToken.sol +++ b/contracts/governance/MentoToken.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable gas-custom-errors, immutable-vars-naming pragma solidity 0.8.18; import { ERC20Burnable, ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/extensions/ERC20Burnable.sol"; @@ -51,8 +52,9 @@ contract MentoToken is Ownable, Pausable, ERC20Burnable { locking = locking_; emission = emission_; - uint256 supply = 1_000_000_000 * 10**decimals(); + uint256 supply = 1_000_000_000 * 10 ** decimals(); + // slither-disable-next-line uninitialized-local uint256 totalAllocated; for (uint256 i = 0; i < allocationRecipients_.length; i++) { require(allocationRecipients_[i] != address(0), "MentoToken: allocation recipient is zero address"); @@ -100,11 +102,7 @@ contract MentoToken is Ownable, Pausable, ERC20Burnable { * @param to The account that should receive the tokens * @param amount Amount of tokens that should be transferred */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override { + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { super._beforeTokenTransfer(from, to, amount); require(to != address(this), "MentoToken: cannot transfer tokens to token contract"); diff --git a/contracts/governance/locking/Locking.sol b/contracts/governance/locking/Locking.sol index 5f481a7b..51e169b5 100644 --- a/contracts/governance/locking/Locking.sol +++ b/contracts/governance/locking/Locking.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -// solhint-disable func-name-mixedcase +// solhint-disable func-name-mixedcase, gas-custom-errors import "./LockingBase.sol"; import "./LockingRelock.sol"; diff --git a/contracts/governance/locking/LockingBase.sol b/contracts/governance/locking/LockingBase.sol index 10ccd1e1..f6e8509b 100644 --- a/contracts/governance/locking/LockingBase.sol +++ b/contracts/governance/locking/LockingBase.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -// solhint-disable state-visibility, func-name-mixedcase +// solhint-disable state-visibility, func-name-mixedcase, gas-custom-errors import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; import "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; @@ -31,7 +31,7 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { /** * @dev Basis for locking formula calculations */ - uint32 constant ST_FORMULA_BASIS = 1 * (10**8); + uint32 constant ST_FORMULA_BASIS = 1 * (10 ** 8); /** * @dev ERC20 token that will be locked */ @@ -189,11 +189,7 @@ abstract contract LockingBase is OwnableUpgradeable, IVotesUpgradeable { * @param _delegate address of delegate that owns the voting power * @param time week number till which to update lines */ - function updateLines( - address account, - address _delegate, - uint32 time - ) internal { + function updateLines(address account, address _delegate, uint32 time) internal { totalSupplyLine.update(time); accounts[_delegate].balance.update(time); accounts[account].locked.update(time); diff --git a/contracts/governance/locking/LockingRelock.sol b/contracts/governance/locking/LockingRelock.sol index 401bf974..089f07cb 100644 --- a/contracts/governance/locking/LockingRelock.sol +++ b/contracts/governance/locking/LockingRelock.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// solhint-disable gas-custom-errors pragma solidity 0.8.18; import "./LockingBase.sol"; @@ -96,12 +97,7 @@ abstract contract LockingRelock is LockingBase { * @param delegate address of delegate that owns the voting power * @return residue amount of tokens still locked in the old lock */ - function removeLines( - uint256 id, - address account, - address delegate, - uint32 toTime - ) internal returns (uint96 residue) { + function removeLines(uint256 id, address account, address delegate, uint32 toTime) internal returns (uint96 residue) { updateLines(account, delegate, toTime); uint32 currentBlock = getBlockNumber(); // slither-disable-start unused-return @@ -119,13 +115,7 @@ abstract contract LockingRelock is LockingBase { * @param residue amount of tokens still locked in the old lock * @param newAmount new amount to lock */ - function rebalance( - uint256 id, - address account, - uint96 bias, - uint96 residue, - uint96 newAmount - ) internal { + function rebalance(uint256 id, address account, uint96 bias, uint96 residue, uint96 newAmount) internal { require(residue <= newAmount, "Impossible to relock: less amount, then now is"); uint96 addAmount = newAmount - (residue); uint96 amount = accounts[account].amount; diff --git a/contracts/governance/locking/LockingVotes.sol b/contracts/governance/locking/LockingVotes.sol index 7f5f363b..6d84417a 100644 --- a/contracts/governance/locking/LockingVotes.sol +++ b/contracts/governance/locking/LockingVotes.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -// solhint-disable no-unused-vars +// solhint-disable no-unused-vars, gas-custom-errors import "./LockingBase.sol"; @@ -44,18 +44,14 @@ contract LockingVotes is LockingBase { /** * @dev Returns the delegate that `account` has chosen. */ - function delegates( - address /* account */ - ) external pure override returns (address) { + function delegates(address /* account */) external pure override returns (address) { revert("not implemented"); } /** * @dev Delegates votes from the sender to `delegatee`. */ - function delegate( - address /* delegatee */ - ) external pure override { + function delegate(address /* delegatee */) external pure override { revert("not implemented"); } diff --git a/contracts/governance/locking/libs/LibBrokenLine.sol b/contracts/governance/locking/libs/LibBrokenLine.sol index 8f11cb1a..2f5c3d2a 100644 --- a/contracts/governance/locking/libs/LibBrokenLine.sol +++ b/contracts/governance/locking/libs/LibBrokenLine.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// solhint-disable gas-custom-errors pragma solidity 0.8.18; import "./LibIntMapping.sol"; @@ -62,11 +63,7 @@ library LibBrokenLine { * @param id the id of the line to add * @param line the line to add */ - function _addOneLine( - BrokenLine storage brokenLine, - uint256 id, - Line memory line - ) internal { + function _addOneLine(BrokenLine storage brokenLine, uint256 id, Line memory line) internal { require(line.slope != 0, "Slope == 0, unacceptable value for slope"); require(line.slope <= line.bias, "Slope > bias, unacceptable value for slope"); require(brokenLine.initiatedLines[id].bias == 0, "Line with given id is already exist"); @@ -106,12 +103,7 @@ library LibBrokenLine { * @param line the line to add * @param blockNumber the block number when the line is added */ - function addOneLine( - BrokenLine storage brokenLine, - uint256 id, - Line memory line, - uint32 blockNumber - ) internal { + function addOneLine(BrokenLine storage brokenLine, uint256 id, Line memory line, uint32 blockNumber) internal { _addOneLine(brokenLine, id, line); saveSnapshot(brokenLine, line.start, blockNumber); } @@ -129,14 +121,7 @@ library LibBrokenLine { BrokenLine storage brokenLine, uint256 id, uint32 toTime - ) - internal - returns ( - uint96 bias, - uint96 slope, - uint32 cliff - ) - { + ) internal returns (uint96 bias, uint96 slope, uint32 cliff) { Line memory line = brokenLine.initiatedLines[id]; require(line.bias != 0, "Removing Line, which not exists"); @@ -201,14 +186,7 @@ library LibBrokenLine { uint256 id, uint32 toTime, uint32 blockNumber - ) - internal - returns ( - uint96 bias, - uint96 slope, - uint32 cliff - ) - { + ) internal returns (uint96 bias, uint96 slope, uint32 cliff) { (bias, slope, cliff) = _remove(brokenLine, id, toTime); saveSnapshot(brokenLine, toTime, blockNumber); } @@ -249,11 +227,7 @@ library LibBrokenLine { * @param toBlock the block number to get the value at * @return the y value of the BrokenLine at the given week and block number */ - function actualValue( - BrokenLine storage brokenLine, - uint32 toTime, - uint32 toBlock - ) internal view returns (uint96) { + function actualValue(BrokenLine storage brokenLine, uint32 toTime, uint32 toBlock) internal view returns (uint96) { uint32 fromTime = brokenLine.initial.start; if (fromTime == toTime) { if (brokenLine.history[brokenLine.history.length - 1].blockNumber < toBlock) { @@ -325,7 +299,7 @@ library LibBrokenLine { * @return result the int96 represesntation of the given uint96 value */ function safeInt(uint96 value) internal pure returns (int96 result) { - require(value < 2**95, "int cast error"); + require(value < 2 ** 95, "int cast error"); result = int96(value); } @@ -335,11 +309,7 @@ library LibBrokenLine { * @param epoch The week number of the snapshot * @param blockNumber The block number of the snapshot */ - function saveSnapshot( - BrokenLine storage brokenLine, - uint32 epoch, - uint32 blockNumber - ) internal { + function saveSnapshot(BrokenLine storage brokenLine, uint32 epoch, uint32 blockNumber) internal { brokenLine.history.push( Point({ blockNumber: blockNumber, bias: brokenLine.initial.bias, slope: brokenLine.initial.slope, epoch: epoch }) ); @@ -355,15 +325,7 @@ library LibBrokenLine { * @return slope The slope of the point * @return epoch The week number of the point */ - function binarySearch(Point[] storage history, uint32 toBlock) - internal - view - returns ( - uint96, - uint96, - uint32 - ) - { + function binarySearch(Point[] storage history, uint32 toBlock) internal view returns (uint96, uint96, uint32) { uint256 len = history.length; if (len == 0 || history[0].blockNumber > toBlock) { return (0, 0, 0); diff --git a/contracts/governance/locking/libs/LibIntMapping.sol b/contracts/governance/locking/libs/LibIntMapping.sol index de1e6013..5f2d75a3 100644 --- a/contracts/governance/locking/libs/LibIntMapping.sol +++ b/contracts/governance/locking/libs/LibIntMapping.sol @@ -12,11 +12,7 @@ library LibIntMapping { * @param key Key of the item * @param value Value to add */ - function addToItem( - mapping(uint256 => int96) storage map, - uint256 key, - int96 value - ) internal { + function addToItem(mapping(uint256 => int96) storage map, uint256 key, int96 value) internal { map[key] = map[key] + (value); } @@ -26,11 +22,7 @@ library LibIntMapping { * @param key Key of the item * @param value Value to subtract */ - function subFromItem( - mapping(uint256 => int96) storage map, - uint256 key, - int96 value - ) internal { + function subFromItem(mapping(uint256 => int96) storage map, uint256 key, int96 value) internal { map[key] = map[key] - (value); } } diff --git a/contracts/import.sol b/contracts/import.sol new file mode 100644 index 00000000..e74ef425 --- /dev/null +++ b/contracts/import.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.17; + +/** + * @dev In order for foundry to properly deploy and link contracts via `vm.getCode` + * they must be imported in the `src` (or `contracts`, in our case) folder of the project. + * If we would have this file in the `test` folder, everything builds, but + * `vm.getCode` will complain that it can't find the artifact. + */ +import "celo/contracts/common/Registry.sol"; +import "celo/contracts/common/Freezer.sol"; +import "celo/contracts/stability/SortedOracles.sol"; +import "test/utils/harnesses/WithThresholdHarness.sol"; +import "test/utils/harnesses/WithCooldownHarness.sol"; +import "test/utils/harnesses/TradingLimitsHarness.sol"; diff --git a/contracts/interfaces/IBiPoolManager.sol b/contracts/interfaces/IBiPoolManager.sol index 8bd79f7a..0d17b9f8 100644 --- a/contracts/interfaces/IBiPoolManager.sol +++ b/contracts/interfaces/IBiPoolManager.sol @@ -1,9 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +// solhint-disable func-name-mixedcase +pragma solidity >0.5.13 <0.9; pragma experimental ABIEncoderV2; import { IPricingModule } from "./IPricingModule.sol"; -import { FixidityLib } from "../common/FixidityLib.sol"; +import { IReserve } from "./IReserve.sol"; +import { ISortedOracles } from "./ISortedOracles.sol"; +import { IBreakerBox } from "./IBreakerBox.sol"; +import { IExchangeProvider } from "./IExchangeProvider.sol"; + +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; /** * @title BiPool Manager interface @@ -116,9 +122,9 @@ interface IBiPoolManager { /** * @notice Get all exchange IDs. - * @return exchangeIds List of the exchangeIds. + * @return _exchangeIds List of the exchangeIds. */ - function getExchangeIds() external view returns (bytes32[] memory exchangeIds); + function getExchangeIds() external view returns (bytes32[] memory _exchangeIds); /** * @notice Create a PoolExchange with the provided data. @@ -134,4 +140,94 @@ interface IBiPoolManager { * @return destroyed - true on successful delition. */ function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external returns (bool destroyed); + + /** + * @notice Allows the contract to be upgradable via the proxy. + * @param _broker The address of the broker contract. + * @param _reserve The address of the reserve contract. + * @param _sortedOracles The address of the sorted oracles contract. + * @param _breakerBox The address of the breaker box contract. + */ + function initialize( + address _broker, + IReserve _reserve, + ISortedOracles _sortedOracles, + IBreakerBox _breakerBox + ) external; + + function swapIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) external returns (uint256 amountOut); + + function swapOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) external returns (uint256 amountIn); + + /** + * @notice Updates the pricing modules for a list of identifiers + * @dev This function can only be called by the owner of the contract. + * The number of identifiers and modules provided must be the same. + * @param identifiers An array of identifiers for which the pricing modules are to be set. + * @param addresses An array of module addresses corresponding to each identifier. + */ + function setPricingModules(bytes32[] calldata identifiers, address[] calldata addresses) external; + + // @notice Getters: + function broker() external view returns (address); + + function exchanges(bytes32) external view returns (PoolExchange memory); + + function exchangeIds(uint256) external view returns (bytes32); + + function reserve() external view returns (IReserve); + + function sortedOracles() external view returns (ISortedOracles); + + function breakerBox() external view returns (IBreakerBox); + + function tokenPrecisionMultipliers(address) external view returns (uint256); + + function CONSTANT_SUM() external view returns (bytes32); + + function CONSTANT_PRODUCT() external view returns (bytes32); + + function pricingModules(bytes32) external view returns (address); + + function getExchanges() external view returns (IExchangeProvider.Exchange[] memory); + + function getAmountOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) external view returns (uint256 amountOut); + + function getAmountIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) external view returns (uint256 amountIn); + + /// @notice Setters: + function setBroker(address newBroker) external; + + function setReserve(IReserve newReserve) external; + + function setSortedOracles(ISortedOracles newSortedOracles) external; + + function setBreakerBox(IBreakerBox newBreakerBox) external; + + /// @notice IOwnable: + function transferOwnership(address newOwner) external; + + function renounceOwnership() external; + + function owner() external view returns (address); } diff --git a/contracts/interfaces/IBreaker.sol b/contracts/interfaces/IBreaker.sol index a119dce6..6a70b632 100644 --- a/contracts/interfaces/IBreaker.sol +++ b/contracts/interfaces/IBreaker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; /** * @title Breaker Interface diff --git a/contracts/interfaces/IBreakerBox.sol b/contracts/interfaces/IBreakerBox.sol index 65322a67..f74b7d0e 100644 --- a/contracts/interfaces/IBreakerBox.sol +++ b/contracts/interfaces/IBreakerBox.sol @@ -1,5 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; +pragma experimental ABIEncoderV2; + +import { ISortedOracles } from "./ISortedOracles.sol"; /** * @title Breaker Box Interface @@ -137,4 +140,93 @@ interface IBreakerBox { * @param rateFeedID The address of the rate feed to retrieve the trading mode for. */ function getRateFeedTradingMode(address rateFeedID) external view returns (uint8 tradingMode); + + /** + * @notice Adds a breaker to the end of the list of breakers & the breakerTradingMode mapping. + * @param breaker The address of the breaker to be added. + * @param tradingMode The trading mode of the breaker to be added. + */ + function addBreaker(address breaker, uint8 tradingMode) external; + + /** + * @notice Removes the specified breaker from the list of breakers + * and resets breakerTradingMode mapping + BreakerStatus. + * @param breaker The address of the breaker to be removed. + */ + function removeBreaker(address breaker) external; + + /** + * @notice Enables or disables a breaker for the specified rate feed. + * @param breakerAddress The address of the breaker. + * @param rateFeedID The address of the rateFeed to be toggled. + * @param enable Boolean indicating whether the breaker should be + * enabled or disabled for the given rateFeed. + */ + function toggleBreaker(address breakerAddress, address rateFeedID, bool enable) external; + + /** + * @notice Adds a rateFeedID to the mapping of monitored rateFeedIDs. + * @param rateFeedID The address of the rateFeed to be added. + */ + function addRateFeed(address rateFeedID) external; + + /** + * @notice Adds the specified rateFeedIDs to the mapping of monitored rateFeedIDs. + * @param newRateFeedIDs The array of rateFeed addresses to be added. + */ + function addRateFeeds(address[] calldata newRateFeedIDs) external; + + /** + * @notice Sets dependent rate feeds for a given rate feed. + * @param rateFeedID The address of the rate feed. + * @param dependencies The array of dependent rate feeds. + */ + function setRateFeedDependencies(address rateFeedID, address[] calldata dependencies) external; + + /** + * @notice Removes a rateFeed from the mapping of monitored rateFeeds + * and resets all the BreakerStatus entries for that rateFeed. + * @param rateFeedID The address of the rateFeed to be removed. + */ + function removeRateFeed(address rateFeedID) external; + + /** + * @notice Sets the trading mode for the specified rateFeed. + * @param rateFeedID The address of the rateFeed. + * @param tradingMode The trading mode that should be set. + */ + function setRateFeedTradingMode(address rateFeedID, uint8 tradingMode) external; + + /** + * @notice Returns addresses of rateFeedIDs that have been added. + */ + function getRateFeeds() external view returns (address[] memory); + + /** + * @notice Checks if a breaker is enabled for a specific rate feed. + * @param breaker The address of the breaker we're checking for. + * @param rateFeedID The address of the rateFeed. + */ + function isBreakerEnabled(address breaker, address rateFeedID) external view returns (bool); + + /** + * @notice Sets the address of the sortedOracles contract. + * @param _sortedOracles The new address of the sorted oracles contract. + */ + function setSortedOracles(ISortedOracles _sortedOracles) external; + + /// @notice Public state variable getters: + function breakerTradingMode(address) external view returns (uint8); + + function sortedOracles() external view returns (address); + + function rateFeedStatus(address) external view returns (bool); + + function owner() external view returns (address); + + function rateFeedBreakerStatus(address, address) external view returns (BreakerStatus memory); + + function rateFeedDependencies(address, uint256) external view returns (address); + + function rateFeedTradingMode(address) external view returns (uint8); } diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index c6d57334..ae08b675 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; pragma experimental ABIEncoderV2; -import { TradingLimits } from "../libraries/TradingLimits.sol"; +import { ITradingLimits } from "./ITradingLimits.sol"; /* * @title Broker Interface for trader functions @@ -35,7 +35,7 @@ interface IBroker { * @param token the token to target. * @param config the new trading limits config. */ - event TradingLimitConfigured(bytes32 exchangeId, address token, TradingLimits.Config config); + event TradingLimitConfigured(bytes32 exchangeId, address token, ITradingLimits.Config config); /** * @notice Execute a token swap with fixed amountIn. @@ -115,4 +115,38 @@ interface IBroker { * @return exchangeProviders the addresses of all exchange providers. */ function getExchangeProviders() external view returns (address[] memory exchangeProviders); + + function burnStableTokens(address token, uint256 amount) external returns (bool); + + /** + * @notice Allows the contract to be upgradable via the proxy. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserve The address of the Reserve contract. + */ + function initialize(address[] calldata _exchangeProviders, address _reserve) external; + + /// @notice IOwnable: + function transferOwnership(address newOwner) external; + + function renounceOwnership() external; + + function owner() external view returns (address); + + /// @notice Getters: + function reserve() external view returns (address); + + function isExchangeProvider(address exchangeProvider) external view returns (bool); + + /// @notice Setters: + function addExchangeProvider(address exchangeProvider) external returns (uint256 index); + + function removeExchangeProvider(address exchangeProvider, uint256 index) external; + + function setReserve(address _reserve) external; + + function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; + + function tradingLimitsConfig(bytes32 id) external view returns (ITradingLimits.Config memory); + + function tradingLimitsState(bytes32 id) external view returns (ITradingLimits.State memory); } diff --git a/contracts/common/interfaces/IProxy.sol b/contracts/interfaces/ICeloProxy.sol similarity index 64% rename from contracts/common/interfaces/IProxy.sol rename to contracts/interfaces/ICeloProxy.sol index dee8d628..7410cb95 100644 --- a/contracts/common/interfaces/IProxy.sol +++ b/contracts/interfaces/ICeloProxy.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// slither-disable-start naming-convention pragma solidity >=0.5.17 <0.8.19; -interface IProxy { +interface ICeloProxy { function _getImplementation() external view returns (address); function _getOwner() external view returns (address); @@ -11,4 +12,7 @@ interface IProxy { function _setOwner(address owner) external; function _transferOwnership(address newOwner) external; + + function _setAndInitializeImplementation(address implementation, bytes calldata data) external; } +// slither-disable-end naming-convention diff --git a/contracts/interfaces/ICeloToken.sol b/contracts/interfaces/ICeloToken.sol index 934b2532..8b42386f 100644 --- a/contracts/interfaces/ICeloToken.sol +++ b/contracts/interfaces/ICeloToken.sol @@ -6,11 +6,7 @@ pragma solidity ^0.5.13; * in the absence of interface inheritance is intended as a companion to IERC20.sol. */ interface ICeloToken { - function transferWithComment( - address, - uint256, - string calldata - ) external returns (bool); + function transferWithComment(address, uint256, string calldata) external returns (bool); function name() external view returns (string memory); diff --git a/contracts/common/interfaces/IERC20Metadata.sol b/contracts/interfaces/IERC20.sol similarity index 89% rename from contracts/common/interfaces/IERC20Metadata.sol rename to contracts/interfaces/IERC20.sol index 4c82288c..d951e84b 100644 --- a/contracts/common/interfaces/IERC20Metadata.sol +++ b/contracts/interfaces/IERC20.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >=0.5.13 <0.9.0; -interface IERC20Metadata { +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +interface IERC20 { /** * @dev Returns the name of the token. */ @@ -70,11 +74,7 @@ interface IERC20Metadata { * * Emits a {Transfer} event. */ - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); /** * @dev Emitted when `value` tokens are moved from one account (`from`) to diff --git a/contracts/legacy/interfaces/IExchange.sol b/contracts/interfaces/IExchange.sol similarity index 60% rename from contracts/legacy/interfaces/IExchange.sol rename to contracts/interfaces/IExchange.sol index 1960916a..6e186116 100644 --- a/contracts/legacy/interfaces/IExchange.sol +++ b/contracts/interfaces/IExchange.sol @@ -2,23 +2,11 @@ pragma solidity ^0.5.13; interface IExchange { - function buy( - uint256, - uint256, - bool - ) external returns (uint256); + function buy(uint256, uint256, bool) external returns (uint256); - function sell( - uint256, - uint256, - bool - ) external returns (uint256); + function sell(uint256, uint256, bool) external returns (uint256); - function exchange( - uint256, - uint256, - bool - ) external returns (uint256); + function exchange(uint256, uint256, bool) external returns (uint256); function setUpdateFrequency(uint256) external; diff --git a/contracts/interfaces/IExchangeProvider.sol b/contracts/interfaces/IExchangeProvider.sol index 66b8a6c8..b6f956fa 100644 --- a/contracts/interfaces/IExchangeProvider.sol +++ b/contracts/interfaces/IExchangeProvider.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; pragma experimental ABIEncoderV2; /** diff --git a/contracts/interfaces/IGoldToken.sol b/contracts/interfaces/IGoldToken.sol new file mode 100644 index 00000000..d1dad376 --- /dev/null +++ b/contracts/interfaces/IGoldToken.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { IERC20 } from "./IERC20.sol"; + +interface IGoldToken is IERC20 { + function mint(address, uint256) external returns (bool); + + function burn(uint256) external returns (bool); + + /** + * @notice Transfer token for a specified address + * @param to The address to transfer to. + * @param value The amount to be transferred. + * @param comment The transfer comment. + * @return True if the transaction succeeds. + */ + function transferWithComment(address to, uint256 value, string calldata comment) external returns (bool); + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + function initialize(address registryAddress) external; +} diff --git a/contracts/interfaces/IMedianDeltaBreaker.sol b/contracts/interfaces/IMedianDeltaBreaker.sol new file mode 100644 index 00000000..f371738d --- /dev/null +++ b/contracts/interfaces/IMedianDeltaBreaker.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase +pragma solidity ^0.8; + +import { IBreaker } from "./IBreaker.sol"; +import { IOwnable } from "./IOwnable.sol"; +import { ISortedOracles } from "./ISortedOracles.sol"; + +interface IMedianDeltaBreaker is IBreaker, IOwnable { + function sortedOracles() external view returns (address); + + function breakerBox() external view returns (address); + + function DEFAULT_SMOOTHING_FACTOR() external view returns (uint256); + + function smoothingFactors(address) external view returns (uint256); + + function medianRatesEMA(address) external view returns (uint256); + + function setSortedOracles(ISortedOracles _sortedOracles) external; + + function setBreakerBox(address _breakerBox) external; + + function setCooldownTime(address[] calldata rateFeedIDs, uint256 cooldownTime) external; + + function getCoolDown(address rateFeedID) external view returns (uint256); + + function setDefaultCooldownTime(uint256 cooldownTime) external; + + function setDefaultRateChangeThreshold(uint256) external; + + function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata rateChangeThresholds) external; + + function setSmoothingFactor(address rateFeedID, uint256 smoothingFactor) external; + + function setMedianRateEMA(address rateFeedID) external; + + function getSmoothingFactor(address rateFeedID) external view returns (uint256); + + function defaultCooldownTime() external view returns (uint256); + + function defaultRateChangeThreshold() external view returns (uint256); + + function rateChangeThreshold(address) external view returns (uint256); + + function resetMedianRateEMA(address rateFeedID) external; +} diff --git a/contracts/interfaces/IOwnable.sol b/contracts/interfaces/IOwnable.sol new file mode 100644 index 00000000..0826c9c7 --- /dev/null +++ b/contracts/interfaces/IOwnable.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >0.5.13 <0.9; + +interface IOwnable { + function transferOwnership(address newOwner) external; + + function renounceOwnership() external; + + function owner() external view returns (address); +} diff --git a/contracts/interfaces/IPricingModule.sol b/contracts/interfaces/IPricingModule.sol index 80d1de15..9808dfba 100644 --- a/contracts/interfaces/IPricingModule.sol +++ b/contracts/interfaces/IPricingModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; /** * @title Interface for a Mento Pricing Module. @@ -38,7 +38,7 @@ interface IPricingModule { /** * @notice Retrieve the name of this pricing module. - * @return exchangeName The name of the pricing module. + * @return pricingModuleName The name of the pricing module. */ function name() external view returns (string memory pricingModuleName); } diff --git a/contracts/interfaces/IReserve.sol b/contracts/interfaces/IReserve.sol index fda3fb25..553e28b8 100644 --- a/contracts/interfaces/IReserve.sol +++ b/contracts/interfaces/IReserve.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; interface IReserve { function setTobinTaxStalenessThreshold(uint256) external; @@ -12,11 +12,7 @@ interface IReserve { function transferExchangeGold(address payable, uint256) external returns (bool); - function transferCollateralAsset( - address collateralAsset, - address payable to, - uint256 value - ) external returns (bool); + function transferCollateralAsset(address collateralAsset, address payable to, uint256 value) external returns (bool); function getReserveGoldBalance() external view returns (uint256); @@ -51,4 +47,82 @@ interface IReserve { address payable to, uint256 value ) external returns (bool); + + function initialize( + address registryAddress, + uint256 _tobinTaxStalenessThreshold, + uint256 _spendingRatioForCelo, + uint256 _frozenGold, + uint256 _frozenDays, + bytes32[] calldata _assetAllocationSymbols, + uint256[] calldata _assetAllocationWeights, + uint256 _tobinTax, + uint256 _tobinTaxReserveRatio, + address[] calldata _collateralAssets, + uint256[] calldata _collateralAssetDailySpendingRatios + ) external; + + /// @notice IOwnable: + function transferOwnership(address newOwner) external; + + function renounceOwnership() external; + + function owner() external view returns (address); + + /// @notice Getters: + function registry() external view returns (address); + + function tobinTaxStalenessThreshold() external view returns (uint256); + + function tobinTax() external view returns (uint256); + + function tobinTaxReserveRatio() external view returns (uint256); + + function getDailySpendingRatio() external view returns (uint256); + + function checkIsCollateralAsset(address collateralAsset) external view returns (bool); + + function isToken(address) external view returns (bool); + + function getOtherReserveAddresses() external view returns (address[] memory); + + function getAssetAllocationSymbols() external view returns (bytes32[] memory); + + function getAssetAllocationWeights() external view returns (uint256[] memory); + + function collateralAssetSpendingLimit(address) external view returns (uint256); + + function getExchangeSpenders() external view returns (address[] memory); + + function getUnfrozenBalance() external view returns (uint256); + + function isOtherReserveAddress(address otherReserveAddress) external view returns (bool); + + function isSpender(address spender) external view returns (bool); + + /// @notice Setters: + function setRegistry(address) external; + + function setTobinTax(uint256) external; + + function setTobinTaxReserveRatio(uint256) external; + + function setDailySpendingRatio(uint256 spendingRatio) external; + + function setDailySpendingRatioForCollateralAssets( + address[] calldata _collateralAssets, + uint256[] calldata collateralAssetDailySpendingRatios + ) external; + + function setFrozenGold(uint256 frozenGold, uint256 frozenDays) external; + + function setAssetAllocations(bytes32[] calldata symbols, uint256[] calldata weights) external; + + function removeCollateralAsset(address collateralAsset, uint256 index) external returns (bool); + + function addOtherReserveAddress(address otherReserveAddress) external returns (bool); + + function removeOtherReserveAddress(address otherReserveAddress, uint256 index) external returns (bool); + + function collateralAssets(uint256 index) external view returns (address); } diff --git a/contracts/interfaces/ISortedOracles.sol b/contracts/interfaces/ISortedOracles.sol index ccaaae8c..2a578522 100644 --- a/contracts/interfaces/ISortedOracles.sol +++ b/contracts/interfaces/ISortedOracles.sol @@ -1,23 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >0.5.13 <0.9; -import "../common/linkedlists/SortedLinkedListWithMedian.sol"; +import { IBreakerBox } from "./IBreakerBox.sol"; interface ISortedOracles { + enum MedianRelation { + Undefined, + Lesser, + Greater, + Equal + } + function addOracle(address, address) external; - function removeOracle( - address, - address, - uint256 - ) external; + function removeOracle(address, address, uint256) external; - function report( - address, - uint256, - address, - address - ) external; + function report(address, uint256, address, address) external; function removeExpiredReports(address, uint256) external; @@ -33,12 +31,19 @@ interface ISortedOracles { function getOracles(address) external view returns (address[] memory); - function getTimestamps(address token) - external - view - returns ( - address[] memory, - uint256[] memory, - SortedLinkedListWithMedian.MedianRelation[] memory - ); + function getRates(address token) external view returns (address[] memory, uint256[] memory, MedianRelation[] memory); + + function getTimestamps( + address token + ) external view returns (address[] memory, uint256[] memory, MedianRelation[] memory); + + function initialize(uint256) external; + + function setBreakerBox(IBreakerBox) external; + + function getTokenReportExpirySeconds(address token) external view returns (uint256); + + function oracles(address, uint256) external view returns (address); + + function breakerBox() external view returns (IBreakerBox); } diff --git a/contracts/interfaces/IStableTokenV2.sol b/contracts/interfaces/IStableTokenV2.sol index 218a7a70..c6e0e613 100644 --- a/contracts/interfaces/IStableTokenV2.sol +++ b/contracts/interfaces/IStableTokenV2.sol @@ -12,11 +12,7 @@ interface IStableTokenV2 { function approve(address spender, uint256 amount) external returns (bool); - function transferFrom( - address sender, - address recipient, - uint256 amount - ) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); function mint(address, uint256) external returns (bool); @@ -39,11 +35,7 @@ interface IStableTokenV2 { * @param comment The transfer comment. * @return True if the transaction succeeds. */ - function transferWithComment( - address to, - uint256 value, - string calldata comment - ) external returns (bool); + function transferWithComment(address to, uint256 value, string calldata comment) external returns (bool); /** * @notice Initializes a StableTokenV2. @@ -81,11 +73,7 @@ interface IStableTokenV2 { * @param _validators The address of the Validators contract. * @param _exchange The address of the Exchange contract. */ - function initializeV2( - address _broker, - address _validators, - address _exchange - ) external; + function initializeV2(address _broker, address _validators, address _exchange) external; /** * @notice Gets the address of the Broker contract. diff --git a/contracts/interfaces/ITradingLimits.sol b/contracts/interfaces/ITradingLimits.sol new file mode 100644 index 00000000..5ae37e04 --- /dev/null +++ b/contracts/interfaces/ITradingLimits.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >0.5.13 <0.9; + +interface ITradingLimits { + /** + * @dev The State struct contains the current state of a trading limit config. + * @param lastUpdated0 The timestamp of the last reset of netflow0. + * @param lastUpdated1 The timestamp of the last reset of netflow1. + * @param netflow0 The current netflow of the asset for limit0. + * @param netflow1 The current netflow of the asset for limit1. + * @param netflowGlobal The current netflow of the asset for limitGlobal. + */ + struct State { + uint32 lastUpdated0; + uint32 lastUpdated1; + int48 netflow0; + int48 netflow1; + int48 netflowGlobal; + } + + /** + * @dev The Config struct contains the configuration of trading limits. + * @param timestep0 The time window in seconds for limit0. + * @param timestep1 The time window in seconds for limit1. + * @param limit0 The limit0 for the asset. + * @param limit1 The limit1 for the asset. + * @param limitGlobal The global limit for the asset. + * @param flags A bitfield of flags to enable/disable the individual limits. + */ + struct Config { + uint32 timestep0; + uint32 timestep1; + int48 limit0; + int48 limit1; + int48 limitGlobal; + uint8 flags; + } +} diff --git a/contracts/interfaces/IValueDeltaBreaker.sol b/contracts/interfaces/IValueDeltaBreaker.sol new file mode 100644 index 00000000..eaab2351 --- /dev/null +++ b/contracts/interfaces/IValueDeltaBreaker.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { IBreaker } from "./IBreaker.sol"; +import { IOwnable } from "./IOwnable.sol"; +import { ISortedOracles } from "./ISortedOracles.sol"; + +interface IValueDeltaBreaker is IBreaker, IOwnable { + function sortedOracles() external view returns (address); + + function referenceValues(address) external view returns (uint256); + + function breakerBox() external view returns (address); + + function setSortedOracles(ISortedOracles _sortedOracles) external; + + function setReferenceValues(address[] calldata rateFeedIDs, uint256[] calldata _referenceValues) external; + + function setBreakerBox(address _breakerBox) external; + + function setCooldownTimes(address[] calldata rateFeedIDs, uint256[] calldata cooldownTime) external; + + function getCoolDown(address rateFeedID) external view returns (uint256); + + function setDefaultCooldownTime(uint256 cooldownTime) external; + + function setDefaultRateChangeThreshold(uint256 _rateChangeTreshold) external; + + function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata rateChangeThresholds) external; + + function defaultCooldownTime() external view returns (uint256); + + function defaultRateChangeThreshold() external view returns (uint256); + + function rateChangeThreshold(address) external view returns (uint256); +} diff --git a/contracts/legacy/Exchange.sol b/contracts/legacy/Exchange.sol deleted file mode 100644 index b7158373..00000000 --- a/contracts/legacy/Exchange.sol +++ /dev/null @@ -1,453 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.17; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "./interfaces/IExchange.sol"; -import "../interfaces/ISortedOracles.sol"; -import "../interfaces/IReserve.sol"; -import "../interfaces/IStableTokenV2.sol"; -import "../common/Initializable.sol"; -import "../common/FixidityLib.sol"; -import "../common/Freezable.sol"; -import "../common/UsingRegistry.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/ReentrancyGuard.sol"; - -/** - * @title Contract that allows to exchange StableToken for GoldToken and vice versa - * using a Constant Product Market Maker Model - */ -contract Exchange is - IExchange, - ICeloVersionedContract, - Initializable, - Ownable, - UsingRegistry, - ReentrancyGuard, - Freezable -{ - using SafeMath for uint256; - using FixidityLib for FixidityLib.Fraction; - - event Exchanged(address indexed exchanger, uint256 sellAmount, uint256 buyAmount, bool soldGold); - event UpdateFrequencySet(uint256 updateFrequency); - event MinimumReportsSet(uint256 minimumReports); - event StableTokenSet(address indexed stable); - event SpreadSet(uint256 spread); - event ReserveFractionSet(uint256 reserveFraction); - event BucketsUpdated(uint256 goldBucket, uint256 stableBucket); - - FixidityLib.Fraction public spread; - - // Fraction of the Reserve that is committed to the gold bucket when updating - // buckets. - FixidityLib.Fraction public reserveFraction; - - address public stable; - - // Size of the Uniswap gold bucket - uint256 public goldBucket; - // Size of the Uniswap stable token bucket - uint256 public stableBucket; - - uint256 public lastBucketUpdate = 0; - uint256 public updateFrequency; - uint256 public minimumReports; - - bytes32 public stableTokenRegistryId; - - modifier updateBucketsIfNecessary() { - _updateBucketsIfNecessary(); - _; - } - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 0, 0); - } - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param registryAddress The address of the registry core smart contract. - * @param stableTokenIdentifier String identifier of stabletoken in registry - * @param _spread Spread charged on exchanges - * @param _reserveFraction Fraction to commit to the gold bucket - * @param _updateFrequency The time period that needs to elapse between bucket - * updates - * @param _minimumReports The minimum number of fresh reports that need to be - * present in the oracle to update buckets - * commit to the gold bucket - */ - function initialize( - address registryAddress, - string calldata stableTokenIdentifier, - uint256 _spread, - uint256 _reserveFraction, - uint256 _updateFrequency, - uint256 _minimumReports - ) external initializer { - _transferOwnership(msg.sender); - setRegistry(registryAddress); - stableTokenRegistryId = keccak256(abi.encodePacked(stableTokenIdentifier)); - setSpread(_spread); - setReserveFraction(_reserveFraction); - setUpdateFrequency(_updateFrequency); - setMinimumReports(_minimumReports); - } - - /** - * @notice Ensures stable token address is set in storage and initializes buckets. - * @dev Will revert if stable token is not registered or does not have oracle reports. - */ - function activateStable() external onlyOwner { - require(stable == address(0), "StableToken address already activated"); - _setStableToken(registry.getAddressForOrDie(stableTokenRegistryId)); - _updateBucketsIfNecessary(); - } - - /** - * @notice Exchanges a specific amount of one token for an unspecified amount - * (greater than a threshold) of another. - * @param sellAmount The number of tokens to send to the exchange. - * @param minBuyAmount The minimum number of tokens for the exchange to send in return. - * @param sellGold True if the caller is sending CELO to the exchange, false otherwise. - * @return The number of tokens sent by the exchange. - * @dev The caller must first have approved `sellAmount` to the exchange. - * @dev This function can be frozen via the Freezable interface. - */ - function sell( - uint256 sellAmount, - uint256 minBuyAmount, - bool sellGold - ) public onlyWhenNotFrozen updateBucketsIfNecessary nonReentrant returns (uint256) { - (uint256 buyTokenBucket, uint256 sellTokenBucket) = _getBuyAndSellBuckets(sellGold); - uint256 buyAmount = _getBuyTokenAmount(buyTokenBucket, sellTokenBucket, sellAmount); - - require(buyAmount >= minBuyAmount, "Calculated buyAmount was less than specified minBuyAmount"); - - _exchange(sellAmount, buyAmount, sellGold); - return buyAmount; - } - - /** - * @dev DEPRECATED - Use `buy` or `sell`. - * @notice Exchanges a specific amount of one token for an unspecified amount - * (greater than a threshold) of another. - * @param sellAmount The number of tokens to send to the exchange. - * @param minBuyAmount The minimum number of tokens for the exchange to send in return. - * @param sellGold True if the caller is sending CELO to the exchange, false otherwise. - * @return The number of tokens sent by the exchange. - * @dev The caller must first have approved `sellAmount` to the exchange. - * @dev This function can be frozen via the Freezable interface. - */ - function exchange( - uint256 sellAmount, - uint256 minBuyAmount, - bool sellGold - ) external returns (uint256) { - return sell(sellAmount, minBuyAmount, sellGold); - } - - /** - * @notice Exchanges an unspecified amount (up to a threshold) of one token for - * a specific amount of another. - * @param buyAmount The number of tokens for the exchange to send in return. - * @param maxSellAmount The maximum number of tokens to send to the exchange. - * @param buyGold True if the exchange is sending CELO to the caller, false otherwise. - * @return The number of tokens sent to the exchange. - * @dev The caller must first have approved `maxSellAmount` to the exchange. - * @dev This function can be frozen via the Freezable interface. - */ - function buy( - uint256 buyAmount, - uint256 maxSellAmount, - bool buyGold - ) external onlyWhenNotFrozen updateBucketsIfNecessary nonReentrant returns (uint256) { - bool sellGold = !buyGold; - (uint256 buyTokenBucket, uint256 sellTokenBucket) = _getBuyAndSellBuckets(sellGold); - uint256 sellAmount = _getSellTokenAmount(buyTokenBucket, sellTokenBucket, buyAmount); - - require(sellAmount <= maxSellAmount, "Calculated sellAmount was greater than specified maxSellAmount"); - - _exchange(sellAmount, buyAmount, sellGold); - return sellAmount; - } - - /** - * @notice Exchanges a specific amount of one token for a specific amount of another. - * @param sellAmount The number of tokens to send to the exchange. - * @param buyAmount The number of tokens for the exchange to send in return. - * @param sellGold True if the msg.sender is sending CELO to the exchange, false otherwise. - */ - function _exchange( - uint256 sellAmount, - uint256 buyAmount, - bool sellGold - ) private { - IReserve reserve = IReserve(registry.getAddressForOrDie(RESERVE_REGISTRY_ID)); - - if (sellGold) { - goldBucket = goldBucket.add(sellAmount); - stableBucket = stableBucket.sub(buyAmount); - require(getGoldToken().transferFrom(msg.sender, address(reserve), sellAmount), "Transfer of sell token failed"); - require(IStableTokenV2(stable).mint(msg.sender, buyAmount), "Mint of stable token failed"); - } else { - stableBucket = stableBucket.add(sellAmount); - goldBucket = goldBucket.sub(buyAmount); - require(IERC20(stable).transferFrom(msg.sender, address(this), sellAmount), "Transfer of sell token failed"); - IStableTokenV2(stable).burn(sellAmount); - - require(reserve.transferExchangeGold(msg.sender, buyAmount), "Transfer of buyToken failed"); - } - - emit Exchanged(msg.sender, sellAmount, buyAmount, sellGold); - } - - /** - * @notice Returns the amount of buy tokens a user would get for sellAmount of the sell token. - * @param sellAmount The amount of sellToken the user is selling to the exchange. - * @param sellGold `true` if gold is the sell token. - * @return The corresponding buyToken amount. - */ - function getBuyTokenAmount(uint256 sellAmount, bool sellGold) external view returns (uint256) { - (uint256 buyTokenBucket, uint256 sellTokenBucket) = getBuyAndSellBuckets(sellGold); - return _getBuyTokenAmount(buyTokenBucket, sellTokenBucket, sellAmount); - } - - /** - * @notice Returns the amount of sell tokens a user would need to exchange to receive buyAmount of - * buy tokens. - * @param buyAmount The amount of buyToken the user would like to purchase. - * @param sellGold `true` if gold is the sell token. - * @return The corresponding sellToken amount. - */ - function getSellTokenAmount(uint256 buyAmount, bool sellGold) external view returns (uint256) { - (uint256 buyTokenBucket, uint256 sellTokenBucket) = getBuyAndSellBuckets(sellGold); - return _getSellTokenAmount(buyTokenBucket, sellTokenBucket, buyAmount); - } - - /** - * @notice Returns the buy token and sell token bucket sizes, in order. The ratio of - * the two also represents the exchange rate between the two. - * @param sellGold `true` if gold is the sell token. - * @return buyTokenBucket - * @return sellTokenBucket - */ - function getBuyAndSellBuckets(bool sellGold) public view returns (uint256, uint256) { - uint256 currentGoldBucket = goldBucket; - uint256 currentStableBucket = stableBucket; - - if (shouldUpdateBuckets()) { - (currentGoldBucket, currentStableBucket) = getUpdatedBuckets(); - } - - if (sellGold) { - return (currentStableBucket, currentGoldBucket); - } else { - return (currentGoldBucket, currentStableBucket); - } - } - - /** - * @notice Allows owner to set the update frequency - * @param newUpdateFrequency The new update frequency - */ - function setUpdateFrequency(uint256 newUpdateFrequency) public onlyOwner { - updateFrequency = newUpdateFrequency; - emit UpdateFrequencySet(newUpdateFrequency); - } - - /** - * @notice Allows owner to set the minimum number of reports required - * @param newMininumReports The new update minimum number of reports required - */ - function setMinimumReports(uint256 newMininumReports) public onlyOwner { - minimumReports = newMininumReports; - emit MinimumReportsSet(newMininumReports); - } - - /** - * @notice Allows owner to set the Stable Token address - * @param newStableToken The new address for Stable Token - */ - function setStableToken(address newStableToken) public onlyOwner { - _setStableToken(newStableToken); - } - - /** - * @notice Allows owner to set the spread - * @param newSpread The new value for the spread - */ - function setSpread(uint256 newSpread) public onlyOwner { - spread = FixidityLib.wrap(newSpread); - require(FixidityLib.lte(spread, FixidityLib.fixed1()), "Spread must be less than or equal to 1"); - emit SpreadSet(newSpread); - } - - /** - * @notice Allows owner to set the Reserve Fraction - * @param newReserveFraction The new value for the reserve fraction - */ - function setReserveFraction(uint256 newReserveFraction) public onlyOwner { - reserveFraction = FixidityLib.wrap(newReserveFraction); - require(reserveFraction.lt(FixidityLib.fixed1()), "reserve fraction must be smaller than 1"); - emit ReserveFractionSet(newReserveFraction); - } - - function _setStableToken(address newStableToken) internal { - stable = newStableToken; - emit StableTokenSet(newStableToken); - } - - /** - * @notice Returns the buy token and sell token bucket sizes, in order. The ratio of - * the two also represents the exchange rate between the two. - * @param sellGold `true` if gold is the sell token. - * @return buyTokenBucket - * @return sellTokenBucket - */ - function _getBuyAndSellBuckets(bool sellGold) private view returns (uint256, uint256) { - if (sellGold) { - return (stableBucket, goldBucket); - } else { - return (goldBucket, stableBucket); - } - } - - /** - * @dev Returns the amount of buy tokens a user would get for sellAmount of the sell. - * @param buyTokenBucket The buy token bucket size. - * @param sellTokenBucket The sell token bucket size. - * @param sellAmount The amount the user is selling to the exchange. - * @return The corresponding buy amount. - */ - function _getBuyTokenAmount( - uint256 buyTokenBucket, - uint256 sellTokenBucket, - uint256 sellAmount - ) private view returns (uint256) { - if (sellAmount == 0) return 0; - - FixidityLib.Fraction memory reducedSellAmount = getReducedSellAmount(sellAmount); - FixidityLib.Fraction memory numerator = reducedSellAmount.multiply(FixidityLib.newFixed(buyTokenBucket)); - FixidityLib.Fraction memory denominator = FixidityLib.newFixed(sellTokenBucket).add(reducedSellAmount); - - // Can't use FixidityLib.divide because denominator can easily be greater - // than maxFixedDivisor. - // Fortunately, we expect an integer result, so integer division gives us as - // much precision as we could hope for. - return numerator.unwrap().div(denominator.unwrap()); - } - - /** - * @notice Returns the amount of sell tokens a user would need to exchange to receive buyAmount of - * buy tokens. - * @param buyTokenBucket The buy token bucket size. - * @param sellTokenBucket The sell token bucket size. - * @param buyAmount The amount the user is buying from the exchange. - * @return The corresponding sell amount. - */ - function _getSellTokenAmount( - uint256 buyTokenBucket, - uint256 sellTokenBucket, - uint256 buyAmount - ) private view returns (uint256) { - if (buyAmount == 0) return 0; - - FixidityLib.Fraction memory numerator = FixidityLib.newFixed(buyAmount.mul(sellTokenBucket)); - FixidityLib.Fraction memory denominator = FixidityLib.newFixed(buyTokenBucket.sub(buyAmount)).multiply( - FixidityLib.fixed1().subtract(spread) - ); - - // See comment in _getBuyTokenAmount - return numerator.unwrap().div(denominator.unwrap()); - } - - /** - * @return buyTokenBucket - * @return sellTokenBucket - */ - function getUpdatedBuckets() private view returns (uint256, uint256) { - uint256 updatedGoldBucket = getUpdatedGoldBucket(); - uint256 exchangeRateNumerator; - uint256 exchangeRateDenominator; - (exchangeRateNumerator, exchangeRateDenominator) = getOracleExchangeRate(); - uint256 updatedStableBucket = exchangeRateNumerator.mul(updatedGoldBucket).div(exchangeRateDenominator); - return (updatedGoldBucket, updatedStableBucket); - } - - function getUpdatedGoldBucket() private view returns (uint256) { - uint256 reserveGoldBalance = getReserve().getUnfrozenReserveGoldBalance(); - return reserveFraction.multiply(FixidityLib.newFixed(reserveGoldBalance)).fromFixed(); - } - - /** - * @notice If conditions are met, updates the Uniswap bucket sizes to track - * the price reported by the Oracle. - */ - function _updateBucketsIfNecessary() private { - if (shouldUpdateBuckets()) { - // solhint-disable-next-line not-rely-on-time - lastBucketUpdate = now; - - (goldBucket, stableBucket) = getUpdatedBuckets(); - emit BucketsUpdated(goldBucket, stableBucket); - } - } - - /** - * @notice Calculates the sell amount reduced by the spread. - * @param sellAmount The original sell amount. - * @return The reduced sell amount, computed as (1 - spread) * sellAmount - */ - function getReducedSellAmount(uint256 sellAmount) private view returns (FixidityLib.Fraction memory) { - return FixidityLib.fixed1().subtract(spread).multiply(FixidityLib.newFixed(sellAmount)); - } - - /** - * @notice Checks conditions required for bucket updates. - * @return The Rate numerator - whether or not buckets should be updated. - * @return The rate denominator - whether or not buckets should be updated. - */ - function shouldUpdateBuckets() private view returns (bool) { - ISortedOracles sortedOracles = ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(stable); - // solhint-disable-next-line not-rely-on-time - bool timePassed = now >= lastBucketUpdate.add(updateFrequency); - bool enoughReports = sortedOracles.numRates(stable) >= minimumReports; - // solhint-disable-next-line not-rely-on-time - bool medianReportRecent = sortedOracles.medianTimestamp(stable) > now.sub(updateFrequency); - return timePassed && enoughReports && medianReportRecent && !isReportExpired; - } - - function getOracleExchangeRate() private view returns (uint256, uint256) { - uint256 rateNumerator; - uint256 rateDenominator; - (rateNumerator, rateDenominator) = ISortedOracles(registry.getAddressForOrDie(SORTED_ORACLES_REGISTRY_ID)) - .medianRate(stable); - require(rateDenominator > 0, "exchange rate denominator must be greater than 0"); - return (rateNumerator, rateDenominator); - } -} diff --git a/contracts/legacy/ExchangeBRL.sol b/contracts/legacy/ExchangeBRL.sol deleted file mode 100644 index e6a0d87d..00000000 --- a/contracts/legacy/ExchangeBRL.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./Exchange.sol"; - -contract ExchangeBRL is Exchange { - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Exchange(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @dev This function is overloaded to maintain a distinct version from Exchange.sol. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 0, 0); - } -} diff --git a/contracts/legacy/ExchangeEUR.sol b/contracts/legacy/ExchangeEUR.sol deleted file mode 100644 index ca8683c4..00000000 --- a/contracts/legacy/ExchangeEUR.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./Exchange.sol"; - -contract ExchangeEUR is Exchange { - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Exchange(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @dev This function is overloaded to maintain a distinct version from Exchange.sol. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 0, 0); - } -} diff --git a/contracts/legacy/GrandaMento.sol b/contracts/legacy/GrandaMento.sol deleted file mode 100644 index 19c7e390..00000000 --- a/contracts/legacy/GrandaMento.sol +++ /dev/null @@ -1,602 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; - -import "../common/FixidityLib.sol"; -import "../common/Initializable.sol"; -import "../common/UsingRegistry.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/ReentrancyGuard.sol"; -import "./interfaces/IStableToken.sol"; - -/** - * @title Facilitates large exchanges between CELO stable tokens. - */ -contract GrandaMento is ICeloVersionedContract, Ownable, Initializable, UsingRegistry, ReentrancyGuard { - using FixidityLib for FixidityLib.Fraction; - using SafeMath for uint256; - - // Emitted when a new exchange proposal is created. - event ExchangeProposalCreated( - uint256 indexed proposalId, - address indexed exchanger, - string stableTokenRegistryId, - uint256 sellAmount, - uint256 buyAmount, - bool sellCelo - ); - - // Emitted when an exchange proposal is approved by the approver. - event ExchangeProposalApproved(uint256 indexed proposalId); - - // Emitted when an exchange proposal is cancelled. - event ExchangeProposalCancelled(uint256 indexed proposalId); - - // Emitted when an exchange proposal is executed. - event ExchangeProposalExecuted(uint256 indexed proposalId); - - // Emitted when the approver is set. - event ApproverSet(address approver); - - // Emitted when maxApprovalExchangeRateChange is set. - event MaxApprovalExchangeRateChangeSet(uint256 maxApprovalExchangeRateChange); - - // Emitted when the spread is set. - event SpreadSet(uint256 spread); - - // Emitted when the veto period in seconds is set. - event VetoPeriodSecondsSet(uint256 vetoPeriodSeconds); - - // Emitted when the exchange limits for a stable token are set. - event StableTokenExchangeLimitsSet( - string stableTokenRegistryId, - uint256 minExchangeAmount, - uint256 maxExchangeAmount - ); - - enum ExchangeProposalState { - None, - Proposed, - Approved, - Executed, - Cancelled - } - - struct ExchangeLimits { - // The minimum amount of an asset that can be exchanged in a single proposal. - uint256 minExchangeAmount; - // The maximum amount of an asset that can be exchanged in a single proposal. - uint256 maxExchangeAmount; - } - - struct ExchangeProposal { - // The exchanger/proposer of the exchange proposal. - address payable exchanger; - // The stable token involved in this proposal. This is stored rather than - // the stable token's registry ID in case the contract address is changed - // after a proposal is created, which could affect refunding or burning the - // stable token. - address stableToken; - // The state of the exchange proposal. - ExchangeProposalState state; - // Whether the exchanger is selling CELO and buying stableToken. - bool sellCelo; - // The amount of the sell token being sold. If a stable token is being sold, - // the amount of stable token in "units" is stored rather than the "value." - // This is because stable tokens may experience demurrage/inflation, where - // the amount of stable token "units" doesn't change with time, but the "value" - // does. This is important to ensure the correct inflation-adjusted amount - // of the stable token is transferred out of this contract when a deposit is - // refunded or an exchange selling the stable token is executed. - // See StableToken.sol for more details on what "units" vs "values" are. - uint256 sellAmount; - // The amount of the buy token being bought. For stable tokens, this is - // kept track of as the value, not units. - uint256 buyAmount; - // The price of CELO quoted in stableToken at the time of the exchange proposal - // creation. This is the price used to calculate the buyAmount. Used for a - // safety check when an approval is being made that the price isn't wildly - // different. Recalculating buyAmount is not sufficient because if a stable token - // is being sold that has demurrage enabled, the original value when the stable - // tokens were deposited cannot be calculated. - uint256 celoStableTokenExchangeRate; - // The veto period in seconds at the time the proposal was created. This is kept - // track of on a per-proposal basis to lock-in the veto period for a proposal so - // that changes to the contract's vetoPeriodSeconds do not affect existing - // proposals. - uint256 vetoPeriodSeconds; - // The timestamp (`block.timestamp`) at which the exchange proposal was approved - // in seconds. If the exchange proposal has not ever been approved, is 0. - uint256 approvalTimestamp; - } - - // The address with the authority to approve exchange proposals. - address public approver; - - // The maximum allowed change in the CELO/stable token price when an exchange proposal - // is being approved relative to the rate when the exchange proposal was created. - FixidityLib.Fraction public maxApprovalExchangeRateChange; - - // The percent fee imposed upon an exchange execution. - FixidityLib.Fraction public spread; - - // The period in seconds after an approval during which an exchange proposal can be vetoed. - uint256 public vetoPeriodSeconds; - - // The minimum and maximum amount of the stable token that can be minted or - // burned in a single exchange. Indexed by the stable token registry identifier string. - mapping(string => ExchangeLimits) public stableTokenExchangeLimits; - - // State for all exchange proposals. Indexed by the exchange proposal ID. - mapping(uint256 => ExchangeProposal) public exchangeProposals; - - // An array containing a superset of the IDs of exchange proposals that are currently - // in the Proposed or Approved state. Intended to allow easy viewing of all active - // exchange proposals. It's possible for a proposal ID in this array to no longer be - // active, so filtering is required to find the true set of active proposal IDs. - // A superset is kept because exchange proposal vetoes, intended to be done - // by Governance, effectively go through a multi-day timelock. If the veto - // call was required to provide the index in an array of activeProposalIds to - // remove corresponding to the vetoed exchange proposal, the timelock could result - // in the provided index being stale by the time the veto would be executed. - // Alternative approaches exist, like maintaining a linkedlist of active proposal - // IDs, but this approach was chosen for its low implementation complexity. - uint256[] public activeProposalIdsSuperset; - - // Number of exchange proposals that have ever been created. Used for assigning - // an exchange proposal ID to a new proposal. - uint256 public exchangeProposalCount; - - /** - * @notice Reverts if the sender is not the approver. - */ - modifier onlyApprover() { - require(msg.sender == approver, "Sender must be approver"); - _; - } - - /** - * @notice Sets initialized == true on implementation contracts. - * @param test Set to true to skip implementation initialization. - */ - constructor(bool test) public Initializable(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 1, 0, 1); - } - - /** - * @notice Used in place of the constructor to allow the contract to be upgradable via proxy. - * @param _registry The address of the registry. - * @param _approver The approver that has the ability to approve exchange proposals. - * @param _maxApprovalExchangeRateChange The maximum allowed change in CELO price - * between an exchange proposal's creation and approval. - * @param _spread The spread charged on exchanges. - * @param _vetoPeriodSeconds The length of the veto period in seconds. - */ - function initialize( - address _registry, - address _approver, - uint256 _maxApprovalExchangeRateChange, - uint256 _spread, - uint256 _vetoPeriodSeconds - ) external initializer { - _transferOwnership(msg.sender); - setRegistry(_registry); - setApprover(_approver); - setMaxApprovalExchangeRateChange(_maxApprovalExchangeRateChange); - setSpread(_spread); - setVetoPeriodSeconds(_vetoPeriodSeconds); - } - - /** - * @notice Creates a new exchange proposal and deposits the tokens being sold. - * @dev Stable token value amounts are used for the sellAmount, not unit amounts. - * @param stableTokenRegistryId The string registry ID for the stable token - * involved in the exchange. - * @param sellAmount The amount of the sell token being sold. - * @param sellCelo Whether CELO is being sold. - * @return The proposal identifier for the newly created exchange proposal. - */ - function createExchangeProposal( - string calldata stableTokenRegistryId, - uint256 sellAmount, - bool sellCelo - ) external nonReentrant returns (uint256) { - address stableToken = registry.getAddressForStringOrDie(stableTokenRegistryId); - - // Gets the price of CELO quoted in stableToken. - uint256 celoStableTokenExchangeRate = getOracleExchangeRate(stableToken).unwrap(); - - // Using the current oracle exchange rate, calculate what the buy amount is. - // This takes the spread into consideration. - uint256 buyAmount = getBuyAmount(celoStableTokenExchangeRate, sellAmount, sellCelo); - - // Create new scope to prevent a stack too deep error. - { - // Get the minimum and maximum amount of stable token than can be involved - // in the exchange. This reverts if exchange limits for the stable token have - // not been set. - (uint256 minExchangeAmount, uint256 maxExchangeAmount) = getStableTokenExchangeLimits(stableTokenRegistryId); - // Ensure that the amount of stableToken being bought or sold is within - // the configurable exchange limits. - uint256 stableTokenExchangeAmount = sellCelo ? buyAmount : sellAmount; - require( - stableTokenExchangeAmount <= maxExchangeAmount && stableTokenExchangeAmount >= minExchangeAmount, - "Stable token exchange amount not within limits" - ); - } - - // Deposit the assets being sold. - IERC20 sellToken = sellCelo ? getGoldToken() : IERC20(stableToken); - require(sellToken.transferFrom(msg.sender, address(this), sellAmount), "Transfer in of sell token failed"); - - // Record the proposal. - // Add 1 to the running proposal count, and use the updated proposal count as - // the proposal ID. Proposal IDs intentionally start at 1. - exchangeProposalCount = exchangeProposalCount.add(1); - // For stable tokens, the amount is stored in units to deal with demurrage. - uint256 storedSellAmount = sellCelo ? sellAmount : IStableToken(stableToken).valueToUnits(sellAmount); - exchangeProposals[exchangeProposalCount] = ExchangeProposal({ - exchanger: msg.sender, - stableToken: stableToken, - state: ExchangeProposalState.Proposed, - sellCelo: sellCelo, - sellAmount: storedSellAmount, - buyAmount: buyAmount, - celoStableTokenExchangeRate: celoStableTokenExchangeRate, - vetoPeriodSeconds: vetoPeriodSeconds, - approvalTimestamp: 0 // initial value when not approved yet - }); - // StableToken.unitsToValue (called within getSellTokenAndSellAmount) can - // overflow for very large StableToken amounts. Call it here as a sanity - // check, so that the overflow happens here, blocking proposal creation - // rather than when attempting to execute the proposal, which would lock - // funds in this contract. - getSellTokenAndSellAmount(exchangeProposals[exchangeProposalCount]); - // Push it into the array of active proposals. - activeProposalIdsSuperset.push(exchangeProposalCount); - // Even if stable tokens are being sold, the sellAmount emitted is the "value." - emit ExchangeProposalCreated( - exchangeProposalCount, - msg.sender, - stableTokenRegistryId, - sellAmount, - buyAmount, - sellCelo - ); - return exchangeProposalCount; - } - - /** - * @notice Approves an existing exchange proposal. - * @dev Sender must be the approver. Exchange proposal must be in the Proposed state. - * @param proposalId The identifier of the proposal to approve. - */ - function approveExchangeProposal(uint256 proposalId) external nonReentrant onlyApprover { - ExchangeProposal storage proposal = exchangeProposals[proposalId]; - // Ensure the proposal is in the Proposed state. - require(proposal.state == ExchangeProposalState.Proposed, "Proposal must be in Proposed state"); - // Ensure the change in the current price of CELO quoted in the stable token - // relative to the value when the proposal was created is within the allowed limit. - FixidityLib.Fraction memory currentRate = getOracleExchangeRate(proposal.stableToken); - FixidityLib.Fraction memory proposalRate = FixidityLib.wrap(proposal.celoStableTokenExchangeRate); - (FixidityLib.Fraction memory lesserRate, FixidityLib.Fraction memory greaterRate) = currentRate.lt(proposalRate) - ? (currentRate, proposalRate) - : (proposalRate, currentRate); - FixidityLib.Fraction memory rateChange = greaterRate.subtract(lesserRate).divide(proposalRate); - require( - rateChange.lte(maxApprovalExchangeRateChange), - "CELO exchange rate is too different from the proposed price" - ); - - // Set the time the approval occurred and change the state. - proposal.approvalTimestamp = block.timestamp; - proposal.state = ExchangeProposalState.Approved; - emit ExchangeProposalApproved(proposalId); - } - - /** - * @notice Cancels an exchange proposal. - * @dev Only callable by the exchanger if the proposal is in the Proposed state - * or the owner if the proposal is in the Approved state. - * @param proposalId The identifier of the proposal to cancel. - */ - function cancelExchangeProposal(uint256 proposalId) external nonReentrant { - ExchangeProposal storage proposal = exchangeProposals[proposalId]; - // Require the appropriate state and sender. - // This will also revert if a proposalId is given that does not correspond - // to a previously created exchange proposal. - if (proposal.state == ExchangeProposalState.Proposed) { - require(proposal.exchanger == msg.sender, "Sender must be exchanger"); - } else if (proposal.state == ExchangeProposalState.Approved) { - require(isOwner(), "Sender must be owner"); - } else { - revert("Proposal must be in Proposed or Approved state"); - } - // Mark the proposal as cancelled. Do so prior to refunding as a measure against reentrancy. - proposal.state = ExchangeProposalState.Cancelled; - // Get the token and amount that will be refunded to the proposer. - (IERC20 refundToken, uint256 refundAmount) = getSellTokenAndSellAmount(proposal); - // Finally, transfer out the deposited funds. - require(refundToken.transfer(proposal.exchanger, refundAmount), "Transfer out of refund token failed"); - emit ExchangeProposalCancelled(proposalId); - } - - /** - * @notice Executes an exchange proposal that's been approved and not vetoed. - * @dev Callable by anyone. Reverts if the proposal is not in the Approved state - * or proposal.vetoPeriodSeconds has not elapsed since approval. - * @param proposalId The identifier of the proposal to execute. - */ - function executeExchangeProposal(uint256 proposalId) external nonReentrant { - ExchangeProposal storage proposal = exchangeProposals[proposalId]; - // Require that the proposal is in the Approved state. - require(proposal.state == ExchangeProposalState.Approved, "Proposal must be in Approved state"); - // Require that the veto period has elapsed since the approval time. - require(proposal.approvalTimestamp.add(proposal.vetoPeriodSeconds) <= block.timestamp, "Veto period not elapsed"); - // Mark the proposal as executed. Do so prior to exchanging as a measure against reentrancy. - proposal.state = ExchangeProposalState.Executed; - // Perform the exchange. - (IERC20 sellToken, uint256 sellAmount) = getSellTokenAndSellAmount(proposal); - // If the exchange sells CELO, the CELO is sent to the Reserve from this contract - // and stable token is minted to the exchanger. - if (proposal.sellCelo) { - // Send the CELO from this contract to the reserve. - require(sellToken.transfer(address(getReserve()), sellAmount), "Transfer out of CELO to Reserve failed"); - // Mint stable token to the exchanger. - require( - IStableToken(proposal.stableToken).mint(proposal.exchanger, proposal.buyAmount), - "Stable token mint failed" - ); - } else { - // If the exchange is selling stable token, the stable token is burned from - // this contract and CELO is transferred from the Reserve to the exchanger. - - // Burn the stable token from this contract. - require(IStableToken(proposal.stableToken).burn(sellAmount), "Stable token burn failed"); - // Transfer the CELO from the Reserve to the exchanger. - require( - getReserve().transferExchangeGold(proposal.exchanger, proposal.buyAmount), - "Transfer out of CELO from Reserve failed" - ); - } - emit ExchangeProposalExecuted(proposalId); - } - - /** - * @notice Gets the sell token and the sell amount for a proposal. - * @dev For stable token sell amounts that are stored as units, the value - * is returned. Ensures sell amount is not greater than this contract's balance. - * @param proposal The proposal to get the sell token and sell amount for. - * @return The IERC20 sell token. - * @return The value sell amount. - */ - function getSellTokenAndSellAmount(ExchangeProposal memory proposal) private view returns (IERC20, uint256) { - IERC20 sellToken; - uint256 sellAmount; - if (proposal.sellCelo) { - sellToken = getGoldToken(); - sellAmount = proposal.sellAmount; - } else { - address stableToken = proposal.stableToken; - sellToken = IERC20(stableToken); - // When selling stableToken, the sell amount is stored in units. - // Units must be converted to value when refunding. - sellAmount = IStableToken(stableToken).unitsToValue(proposal.sellAmount); - } - // In the event a precision issue from the unit <-> value calculations results - // in sellAmount being greater than this contract's balance, set the sellAmount - // to the entire balance. - // This check should not be necessary for CELO, but is done so regardless - // for extra certainty that cancelling an exchange proposal can never fail - // if for some reason the CELO balance of this contract is less than the - // recorded sell amount. - uint256 totalBalance = sellToken.balanceOf(address(this)); - if (totalBalance < sellAmount) { - sellAmount = totalBalance; - } - return (sellToken, sellAmount); - } - - /** - * @notice Using the oracle price, charges the spread and calculates the amount of - * the asset being bought. - * @dev Stable token value amounts are used for the sellAmount, not unit amounts. - * Assumes both CELO and the stable token have 18 decimals. - * @param celoStableTokenExchangeRate The unwrapped fraction exchange rate of CELO - * quoted in the stable token. - * @param sellAmount The amount of the sell token being sold. - * @param sellCelo Whether CELO is being sold. - * @return The amount of the asset being bought. - */ - function getBuyAmount( - uint256 celoStableTokenExchangeRate, - uint256 sellAmount, - bool sellCelo - ) public view returns (uint256) { - FixidityLib.Fraction memory exchangeRate = FixidityLib.wrap(celoStableTokenExchangeRate); - // If stableToken is being sold, instead use the price of stableToken - // quoted in CELO. - if (!sellCelo) { - exchangeRate = exchangeRate.reciprocal(); - } - // The sell amount taking the spread into account, ie: - // (1 - spread) * sellAmount - FixidityLib.Fraction memory adjustedSellAmount = FixidityLib.fixed1().subtract(spread).multiply( - FixidityLib.newFixed(sellAmount) - ); - // Calculate the buy amount: - // exchangeRate * adjustedSellAmount - return exchangeRate.multiply(adjustedSellAmount).fromFixed(); - } - - /** - * @notice Removes the proposal ID found at the provided index of activeProposalIdsSuperset - * if the exchange proposal is not active. - * @dev Anyone can call. Reverts if the exchange proposal is active. - * @param index The index of the proposal ID to remove from activeProposalIdsSuperset. - */ - function removeFromActiveProposalIdsSuperset(uint256 index) external { - require(index < activeProposalIdsSuperset.length, "Index out of bounds"); - uint256 proposalId = activeProposalIdsSuperset[index]; - // Require the exchange proposal to be inactive. - require( - exchangeProposals[proposalId].state != ExchangeProposalState.Proposed && - exchangeProposals[proposalId].state != ExchangeProposalState.Approved, - "Exchange proposal not inactive" - ); - // If not removing the last element, overwrite the index with the value of - // the last element. - uint256 lastIndex = activeProposalIdsSuperset.length.sub(1); - if (index < lastIndex) { - activeProposalIdsSuperset[index] = activeProposalIdsSuperset[lastIndex]; - } - // Delete the last element. - activeProposalIdsSuperset.length--; - } - - /** - * @notice Gets the proposal identifiers of exchange proposals in the - * Proposed or Approved state. Returns a version of activeProposalIdsSuperset - * with inactive proposal IDs set as 0. - * @dev Elements with a proposal ID of 0 should be filtered out by the consumer. - * @return An array of active exchange proposals IDs. - */ - function getActiveProposalIds() external view returns (uint256[] memory) { - // Solidity doesn't play well with dynamically sized memory arrays. - // Instead, this array is created with the same length as activeProposalIdsSuperset, - // and will replace elements that are inactive proposal IDs with the value 0. - uint256[] memory activeProposalIds = new uint256[](activeProposalIdsSuperset.length); - - for (uint256 i = 0; i < activeProposalIdsSuperset.length; i = i.add(1)) { - uint256 proposalId = activeProposalIdsSuperset[i]; - if ( - exchangeProposals[proposalId].state == ExchangeProposalState.Proposed || - exchangeProposals[proposalId].state == ExchangeProposalState.Approved - ) { - activeProposalIds[i] = proposalId; - } - } - return activeProposalIds; - } - - /** - * @notice Gets the oracle CELO price quoted in the stable token. - * @dev Reverts if there is not a rate for the provided stable token. - * @param stableToken The stable token to get the oracle price for. - * @return The oracle CELO price quoted in the stable token. - */ - function getOracleExchangeRate(address stableToken) private view returns (FixidityLib.Fraction memory) { - uint256 rateNumerator; - uint256 rateDenominator; - (rateNumerator, rateDenominator) = getSortedOracles().medianRate(stableToken); - // When rateDenominator is 0, it means there are no rates known to SortedOracles. - require(rateDenominator > 0, "No oracle rates present for token"); - return FixidityLib.wrap(rateNumerator).divide(FixidityLib.wrap(rateDenominator)); - } - - /** - * @notice Gets the minimum and maximum amount of a stable token that can be - * involved in a single exchange. - * @dev Reverts if there is no explicit exchange limit for the stable token. - * @param stableTokenRegistryId The string registry ID for the stable token. - * @return Minimum exchange amount. - * @return Maximum exchange amount. - */ - function getStableTokenExchangeLimits(string memory stableTokenRegistryId) public view returns (uint256, uint256) { - ExchangeLimits memory exchangeLimits = stableTokenExchangeLimits[stableTokenRegistryId]; - // Require the configurable stableToken max exchange amount to be > 0. - // This covers the case where a stableToken has never been explicitly permitted. - require(exchangeLimits.maxExchangeAmount > 0, "Max stable token exchange amount must be defined"); - return (exchangeLimits.minExchangeAmount, exchangeLimits.maxExchangeAmount); - } - - /** - * @notice Sets the approver. - * @dev Sender must be owner. New approver is allowed to be address(0). - * @param newApprover The new value for the approver. - */ - function setApprover(address newApprover) public onlyOwner { - approver = newApprover; - emit ApproverSet(newApprover); - } - - /** - * @notice Sets the maximum allowed change in the CELO/stable token price when - * an exchange proposal is being approved relative to the price when the proposal - * was created. - * @dev Sender must be owner. - * @param newMaxApprovalExchangeRateChange The new value for maxApprovalExchangeRateChange - * to be wrapped. - */ - function setMaxApprovalExchangeRateChange(uint256 newMaxApprovalExchangeRateChange) public onlyOwner { - maxApprovalExchangeRateChange = FixidityLib.wrap(newMaxApprovalExchangeRateChange); - emit MaxApprovalExchangeRateChangeSet(newMaxApprovalExchangeRateChange); - } - - /** - * @notice Sets the spread. - * @dev Sender must be owner. - * @param newSpread The new value for the spread to be wrapped. Must be <= fixed 1. - */ - function setSpread(uint256 newSpread) public onlyOwner { - spread = FixidityLib.wrap(newSpread); - require(FixidityLib.lte(spread, FixidityLib.fixed1()), "Spread must be less than or equal to 1"); - emit SpreadSet(newSpread); - } - - /** - * @notice Sets the minimum and maximum amount of the stable token an exchange can involve. - * @dev Sender must be owner. Setting the maxExchangeAmount to 0 effectively disables new - * exchange proposals for the token. - * @param stableTokenRegistryId The registry ID string for the stable token to set limits for. - * @param minExchangeAmount The new minimum exchange amount for the stable token. - * @param maxExchangeAmount The new maximum exchange amount for the stable token. - */ - function setStableTokenExchangeLimits( - string calldata stableTokenRegistryId, - uint256 minExchangeAmount, - uint256 maxExchangeAmount - ) external onlyOwner { - require(minExchangeAmount <= maxExchangeAmount, "Min exchange amount must not be greater than max"); - stableTokenExchangeLimits[stableTokenRegistryId] = ExchangeLimits({ - minExchangeAmount: minExchangeAmount, - maxExchangeAmount: maxExchangeAmount - }); - emit StableTokenExchangeLimitsSet(stableTokenRegistryId, minExchangeAmount, maxExchangeAmount); - } - - /** - * @notice Sets the veto period in seconds. - * @dev Sender must be owner. - * @param newVetoPeriodSeconds The new value for the veto period in seconds. - */ - function setVetoPeriodSeconds(uint256 newVetoPeriodSeconds) public onlyOwner { - // Hardcode a max of 4 weeks. - // A minimum is not enforced for flexibility. A case of interest is if - // Governance were to be set as the `approver`, it would be desirable to - // set the veto period to 0 seconds. - require(newVetoPeriodSeconds <= 4 weeks, "Veto period cannot exceed 4 weeks"); - vetoPeriodSeconds = newVetoPeriodSeconds; - emit VetoPeriodSecondsSet(newVetoPeriodSeconds); - } -} diff --git a/contracts/legacy/ReserveSpenderMultiSig.sol b/contracts/legacy/ReserveSpenderMultiSig.sol deleted file mode 100644 index faab3ac5..00000000 --- a/contracts/legacy/ReserveSpenderMultiSig.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../common/MultiSig.sol"; - -contract ReserveSpenderMultiSig is MultiSig { - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public MultiSig(test) {} -} diff --git a/contracts/legacy/StableToken.sol b/contracts/legacy/StableToken.sol deleted file mode 100644 index 882b2992..00000000 --- a/contracts/legacy/StableToken.sol +++ /dev/null @@ -1,589 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; - -import "./interfaces/IStableToken.sol"; -import "../interfaces/ICeloToken.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/CalledByVm.sol"; -import "../common/Initializable.sol"; -import "../common/FixidityLib.sol"; -import "../common/Freezable.sol"; -import "../common/UsingRegistry.sol"; -import "../common/UsingPrecompiles.sol"; - -/** - * @title An ERC20 compliant token with adjustable supply. - */ -// solhint-disable-next-line max-line-length -contract StableToken is - ICeloVersionedContract, - Ownable, - Initializable, - UsingRegistry, - UsingPrecompiles, - Freezable, - CalledByVm, - IStableToken, - IERC20, - ICeloToken -{ - using FixidityLib for FixidityLib.Fraction; - using SafeMath for uint256; - - event InflationFactorUpdated(uint256 factor, uint256 lastUpdated); - - event InflationParametersUpdated(uint256 rate, uint256 updatePeriod, uint256 lastUpdated); - - event Transfer(address indexed from, address indexed to, uint256 value); - - event TransferComment(string comment); - - bytes32 private constant GRANDA_MENTO_REGISTRY_ID = keccak256(abi.encodePacked("GrandaMento")); - bytes32 private constant BROKER_REGISTRY_ID = keccak256(abi.encodePacked("Broker")); - - string internal name_; - string internal symbol_; - uint8 internal decimals_; - - // Stored as units. Value can be found using unitsToValue(). - mapping(address => uint256) internal balances; - uint256 internal totalSupply_; - - // Stored as values. Units can be found using valueToUnits(). - mapping(address => mapping(address => uint256)) internal allowed; - - // STABILITY FEE PARAMETERS - - // The `rate` is how much the `factor` is adjusted by per `updatePeriod`. - // The `factor` describes units/value of StableToken, and is greater than or equal to 1. - // The `updatePeriod` governs how often the `factor` is updated. - // `factorLastUpdated` indicates when the inflation factor was last updated. - struct InflationState { - FixidityLib.Fraction rate; - FixidityLib.Fraction factor; - uint256 updatePeriod; - uint256 factorLastUpdated; - } - - // solhint-disable-next-line state-visibility - InflationState inflationState; - - // The registry ID of the exchange contract with permission to mint and burn this token. - // Unique per StableToken instance. - // solhint-disable-next-line state-visibility - bytes32 exchangeRegistryId; - - /** - * @notice Recomputes and updates inflation factor if more than `updatePeriod` - * has passed since last update. - */ - modifier updateInflationFactor() { - FixidityLib.Fraction memory updatedInflationFactor; - uint256 lastUpdated; - - (updatedInflationFactor, lastUpdated) = getUpdatedInflationFactor(); - - if (lastUpdated != inflationState.factorLastUpdated) { - inflationState.factor = updatedInflationFactor; - inflationState.factorLastUpdated = lastUpdated; - emit InflationFactorUpdated(inflationState.factor.unwrap(), inflationState.factorLastUpdated); - } - _; - } - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 1, 0); - } - - /** - * @notice Sets initialized == true on implementation contracts - * @param test Set to true to skip implementation initialization - */ - constructor(bool test) public Initializable(test) {} - - /** - * @param _name The name of the stable token (English) - * @param _symbol A short symbol identifying the token (e.g. "cUSD") - * @param _decimals Tokens are divisible to this many decimal places. - * @param registryAddress Address of the Registry contract. - * @param inflationRate Weekly inflation rate. - * @param inflationFactorUpdatePeriod How often the inflation factor is updated, in seconds. - * @param initialBalanceAddresses Array of addresses with an initial balance. - * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. - * @param exchangeIdentifier String identifier of exchange in registry (for specific fiat pairs) - */ - function initialize( - string calldata _name, - string calldata _symbol, - uint8 _decimals, - address registryAddress, - uint256 inflationRate, - uint256 inflationFactorUpdatePeriod, - address[] calldata initialBalanceAddresses, - uint256[] calldata initialBalanceValues, - string calldata exchangeIdentifier - ) external initializer { - require(inflationRate != 0, "Must provide a non-zero inflation rate"); - require(inflationFactorUpdatePeriod > 0, "inflationFactorUpdatePeriod must be > 0"); - - _transferOwnership(msg.sender); - - totalSupply_ = 0; - name_ = _name; - symbol_ = _symbol; - decimals_ = _decimals; - - inflationState.rate = FixidityLib.wrap(inflationRate); - inflationState.factor = FixidityLib.fixed1(); - inflationState.updatePeriod = inflationFactorUpdatePeriod; - // solhint-disable-next-line not-rely-on-time - inflationState.factorLastUpdated = now; - - require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); - for (uint256 i = 0; i < initialBalanceAddresses.length; i = i.add(1)) { - _mint(initialBalanceAddresses[i], initialBalanceValues[i]); - } - setRegistry(registryAddress); - exchangeRegistryId = keccak256(abi.encodePacked(exchangeIdentifier)); - } - - /** - * @notice Updates Inflation Parameters. - * @param rate New rate. - * @param updatePeriod How often inflationFactor is updated. - */ - function setInflationParameters(uint256 rate, uint256 updatePeriod) external onlyOwner updateInflationFactor { - require(rate != 0, "Must provide a non-zero inflation rate."); - require(updatePeriod > 0, "updatePeriod must be > 0"); - inflationState.rate = FixidityLib.wrap(rate); - inflationState.updatePeriod = updatePeriod; - - emit InflationParametersUpdated( - rate, - updatePeriod, - // solhint-disable-next-line not-rely-on-time - now - ); - } - - /** - * @notice Increase the allowance of another user. - * @param spender The address which is being approved to spend StableToken. - * @param value The increment of the amount of StableToken approved to the spender. - * @return True if the transaction succeeds. - */ - function increaseAllowance(address spender, uint256 value) external updateInflationFactor returns (bool) { - require(spender != address(0), "reserved address 0x0 cannot have allowance"); - uint256 oldValue = allowed[msg.sender][spender]; - uint256 newValue = oldValue.add(value); - allowed[msg.sender][spender] = newValue; - emit Approval(msg.sender, spender, newValue); - return true; - } - - /** - * @notice Decrease the allowance of another user. - * @param spender The address which is being approved to spend StableToken. - * @param value The decrement of the amount of StableToken approved to the spender. - * @return True if the transaction succeeds. - */ - function decreaseAllowance(address spender, uint256 value) external updateInflationFactor returns (bool) { - uint256 oldValue = allowed[msg.sender][spender]; - uint256 newValue = oldValue.sub(value); - allowed[msg.sender][spender] = newValue; - emit Approval(msg.sender, spender, newValue); - return true; - } - - /** - * @notice Approve a user to transfer StableToken on behalf of another user. - * @param spender The address which is being approved to spend StableToken. - * @param value The amount of StableToken approved to the spender. - * @return True if the transaction succeeds. - */ - function approve(address spender, uint256 value) external updateInflationFactor returns (bool) { - require(spender != address(0), "reserved address 0x0 cannot have allowance"); - allowed[msg.sender][spender] = value; - emit Approval(msg.sender, spender, value); - return true; - } - - /** - * @notice Mints new StableToken and gives it to 'to'. - * @param to The account for which to mint tokens. - * @param value The amount of StableToken to mint. - */ - function mint(address to, uint256 value) external updateInflationFactor returns (bool) { - require( - msg.sender == registry.getAddressFor(BROKER_REGISTRY_ID) || - msg.sender == registry.getAddressFor(getExchangeRegistryId()) || - msg.sender == registry.getAddressFor(VALIDATORS_REGISTRY_ID) || - msg.sender == registry.getAddressFor(GRANDA_MENTO_REGISTRY_ID), - "Sender not authorized to mint" - ); - return _mint(to, value); - } - - /** - * @notice Mints new StableToken and gives it to 'to'. - * @param to The account for which to mint tokens. - * @param value The amount of StableToken to mint. - */ - function _mint(address to, uint256 value) private returns (bool) { - require(to != address(0), "0 is a reserved address"); - if (value == 0) { - return true; - } - - uint256 units = _valueToUnits(inflationState.factor, value); - totalSupply_ = totalSupply_.add(units); - balances[to] = balances[to].add(units); - emit Transfer(address(0), to, value); - return true; - } - - /** - * @notice Transfer token for a specified address - * @param to The address to transfer to. - * @param value The amount to be transferred. - * @param comment The transfer comment. - * @return True if the transaction succeeds. - */ - function transferWithComment( - address to, - uint256 value, - string calldata comment - ) external updateInflationFactor onlyWhenNotFrozen returns (bool) { - bool succeeded = transfer(to, value); - emit TransferComment(comment); - return succeeded; - } - - /** - * @notice Burns StableToken from the balance of msg.sender. - * @param value The amount of StableToken to burn. - */ - function burn(uint256 value) external updateInflationFactor returns (bool) { - require( - msg.sender == registry.getAddressFor(BROKER_REGISTRY_ID) || - msg.sender == registry.getAddressFor(getExchangeRegistryId()) || - msg.sender == registry.getAddressFor(GRANDA_MENTO_REGISTRY_ID), - "Sender not authorized to burn" - ); - uint256 units = _valueToUnits(inflationState.factor, value); - require(units <= balances[msg.sender], "value exceeded balance of sender"); - totalSupply_ = totalSupply_.sub(units); - balances[msg.sender] = balances[msg.sender].sub(units); - emit Transfer(msg.sender, address(0), units); - return true; - } - - /** - * @notice Transfers StableToken from one address to another on behalf of a user. - * @param from The address to transfer StableToken from. - * @param to The address to transfer StableToken to. - * @param value The amount of StableToken to transfer. - * @return True if the transaction succeeds. - */ - function transferFrom( - address from, - address to, - uint256 value - ) external updateInflationFactor onlyWhenNotFrozen returns (bool) { - uint256 units = _valueToUnits(inflationState.factor, value); - require(to != address(0), "transfer attempted to reserved address 0x0"); - require(units <= balances[from], "transfer value exceeded balance of sender"); - require(value <= allowed[from][msg.sender], "transfer value exceeded sender's allowance for recipient"); - - balances[to] = balances[to].add(units); - balances[from] = balances[from].sub(units); - allowed[from][msg.sender] = allowed[from][msg.sender].sub(value); - emit Transfer(from, to, value); - return true; - } - - /** - * @return The name of the stable token. - */ - function name() external view returns (string memory) { - return name_; - } - - /** - * @return The symbol of the stable token. - */ - function symbol() external view returns (string memory) { - return symbol_; - } - - /** - * @return The number of decimal places to which StableToken is divisible. - */ - function decimals() external view returns (uint8) { - return decimals_; - } - - /** - * @notice Gets the amount of owner's StableToken allowed to be spent by spender. - * @param accountOwner The owner of the StableToken. - * @param spender The spender of the StableToken. - * @return The amount of StableToken owner is allowing spender to spend. - */ - function allowance(address accountOwner, address spender) external view returns (uint256) { - return allowed[accountOwner][spender]; - } - - /** - * @notice Gets the balance of the specified address using the presently stored inflation factor. - * @param accountOwner The address to query the balance of. - * @return The balance of the specified address. - */ - function balanceOf(address accountOwner) external view returns (uint256) { - return unitsToValue(balances[accountOwner]); - } - - /** - * @return The total value of StableToken in existence - * @dev Though totalSupply_ is stored in units, this returns value. - */ - function totalSupply() external view returns (uint256) { - return unitsToValue(totalSupply_); - } - - /** - * @notice gets inflation parameters. - * @return rate - * @return factor - * @return updatePeriod - * @return factorLastUpdated - */ - function getInflationParameters() - external - view - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return ( - inflationState.rate.unwrap(), - inflationState.factor.unwrap(), - inflationState.updatePeriod, - inflationState.factorLastUpdated - ); - } - - /** - * @notice Returns the units for a given value given the current inflation factor. - * @param value The value to convert to units. - * @return The units corresponding to `value` given the current inflation factor. - * @dev We don't compute the updated inflationFactor here because - * we assume any function calling this will have updated the inflation factor. - */ - function valueToUnits(uint256 value) external view returns (uint256) { - FixidityLib.Fraction memory updatedInflationFactor; - - (updatedInflationFactor, ) = getUpdatedInflationFactor(); - return _valueToUnits(updatedInflationFactor, value); - } - - /** - * @notice Returns the exchange id in the registry of the corresponding fiat pair exchange. - * @dev When this storage is uninitialized, it falls back to the default EXCHANGE_REGISTRY_ID. - * exchangeRegistryId was introduced after the initial release of cUSD's StableToken, - * so exchangeRegistryId will be uninitialized for that contract. If cUSD's StableToken - * exchangeRegistryId were to be correctly initialized, this function could be deprecated - * in favor of using exchangeRegistryId directly. - * @return Registry id for the corresponding exchange. - */ - function getExchangeRegistryId() public view returns (bytes32) { - if (exchangeRegistryId == bytes32(0)) { - return EXCHANGE_REGISTRY_ID; - } else { - return exchangeRegistryId; - } - } - - /** - * @notice Returns the value of a given number of units given the current inflation factor. - * @param units The units to convert to value. - * @return The value corresponding to `units` given the current inflation factor. - */ - function unitsToValue(uint256 units) public view returns (uint256) { - FixidityLib.Fraction memory updatedInflationFactor; - - (updatedInflationFactor, ) = getUpdatedInflationFactor(); - - // We're ok using FixidityLib.divide here because updatedInflationFactor is - // not going to surpass maxFixedDivisor any time soon. - // Quick upper-bound estimation: if annual inflation were 5% (an order of - // magnitude more than the initial proposal of 0.5%), in 500 years, the - // inflation factor would be on the order of 10**10, which is still a safe - // divisor. - return FixidityLib.newFixed(units).divide(updatedInflationFactor).fromFixed(); - } - - /** - * @notice Returns the units for a given value given the current inflation factor. - * @param inflationFactor The current inflation factor. - * @param value The value to convert to units. - * @return The units corresponding to `value` given the current inflation factor. - * @dev We assume any function calling this will have updated the inflation factor. - */ - function _valueToUnits(FixidityLib.Fraction memory inflationFactor, uint256 value) private pure returns (uint256) { - return inflationFactor.multiply(FixidityLib.newFixed(value)).fromFixed(); - } - - /** - * @notice Computes the up-to-date inflation factor. - * @return Current inflation factor. - * @return Last time when the returned inflation factor was updated. - */ - function getUpdatedInflationFactor() private view returns (FixidityLib.Fraction memory, uint256) { - /* solhint-disable not-rely-on-time */ - if (now < inflationState.factorLastUpdated.add(inflationState.updatePeriod)) { - return (inflationState.factor, inflationState.factorLastUpdated); - } - - uint256 numerator; - uint256 denominator; - - // TODO: handle retroactive updates given decreases to updatePeriod - uint256 timesToApplyInflation = now.sub(inflationState.factorLastUpdated).div(inflationState.updatePeriod); - - (numerator, denominator) = fractionMulExp( - inflationState.factor.unwrap(), - FixidityLib.fixed1().unwrap(), - inflationState.rate.unwrap(), - FixidityLib.fixed1().unwrap(), - timesToApplyInflation, - decimals_ - ); - - // This should never happen. If something went wrong updating the - // inflation factor, keep the previous factor - if (numerator == 0 || denominator == 0) { - return (inflationState.factor, inflationState.factorLastUpdated); - } - - FixidityLib.Fraction memory currentInflationFactor = FixidityLib.wrap(numerator).divide( - FixidityLib.wrap(denominator) - ); - uint256 lastUpdated = inflationState.factorLastUpdated.add( - inflationState.updatePeriod.mul(now.sub(inflationState.factorLastUpdated)).div(inflationState.updatePeriod) - ); - return (currentInflationFactor, lastUpdated); - /* solhint-enable not-rely-on-time */ - } - - /** - * @notice Transfers `value` from `msg.sender` to `to` - * @param to The address to transfer to. - * @param value The amount to be transferred. - */ - // solhint-disable-next-line no-simple-event-func-name - function transfer(address to, uint256 value) public updateInflationFactor onlyWhenNotFrozen returns (bool) { - return _transfer(to, value); - } - - /** - * @notice Transfers StableToken from one address to another - * @param to The address to transfer StableToken to. - * @param value The amount of StableToken to be transferred. - */ - function _transfer(address to, uint256 value) internal returns (bool) { - require(to != address(0), "transfer attempted to reserved address 0x0"); - uint256 units = _valueToUnits(inflationState.factor, value); - require(balances[msg.sender] >= units, "transfer value exceeded balance of sender"); - balances[msg.sender] = balances[msg.sender].sub(units); - balances[to] = balances[to].add(units); - emit Transfer(msg.sender, to, value); - return true; - } - - /** - * @notice Reserve balance for making payments for gas in this StableToken currency. - * @param from The account to reserve balance from - * @param value The amount of balance to reserve - * @dev Note that this function is called by the protocol when paying for tx fees in this - * currency. After the tx is executed, gas is refunded to the sender and credited to the - * various tx fee recipients via a call to `creditGasFees`. Note too that the events emitted - * by `creditGasFees` reflect the *net* gas fee payments for the transaction. - */ - function debitGasFees(address from, uint256 value) external onlyVm onlyWhenNotFrozen updateInflationFactor { - uint256 units = _valueToUnits(inflationState.factor, value); - balances[from] = balances[from].sub(units); - totalSupply_ = totalSupply_.sub(units); - } - - /** - * @notice Alternative function to credit balance after making payments - * for gas in this StableToken currency. - * @param from The account to debit balance from - * @param feeRecipient Coinbase address - * @param gatewayFeeRecipient Gateway address - * @param communityFund Community fund address - * @param refund Amount to be refunded by the vm - * @param tipTxFee Coinbase fee - * @param baseTxFee Community fund fee - * @param gatewayFee Gateway fee - * @dev Note that this function is called by the protocol when paying for tx fees in this - * currency. Before the tx is executed, gas is debited from the sender via a call to - * `debitGasFees`. Note too that the events emitted by `creditGasFees` reflect the *net* gas fee - * payments for the transaction. - */ - function creditGasFees( - address from, - address feeRecipient, - address gatewayFeeRecipient, - address communityFund, - uint256 refund, - uint256 tipTxFee, - uint256 gatewayFee, - uint256 baseTxFee - ) external onlyVm onlyWhenNotFrozen { - uint256 units = _valueToUnits(inflationState.factor, refund); - balances[from] = balances[from].add(units); - - units = units.add(_creditGas(from, communityFund, baseTxFee)); - units = units.add(_creditGas(from, feeRecipient, tipTxFee)); - units = units.add(_creditGas(from, gatewayFeeRecipient, gatewayFee)); - totalSupply_ = totalSupply_.add(units); - } - - function _creditGas( - address from, - address to, - uint256 value - ) internal returns (uint256) { - if (to == address(0)) { - return 0; - } - uint256 units = _valueToUnits(inflationState.factor, value); - balances[to] = balances[to].add(units); - emit Transfer(from, to, value); - return units; - } -} diff --git a/contracts/legacy/StableTokenBRL.sol b/contracts/legacy/StableTokenBRL.sol deleted file mode 100644 index 21f1c81f..00000000 --- a/contracts/legacy/StableTokenBRL.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./StableToken.sol"; - -contract StableTokenBRL is StableToken { - /** - * @notice Sets initialized == true on implementation contracts. - * @param test Set to true to skip implementation initialization. - */ - constructor(bool test) public StableToken(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @dev This function is overloaded to maintain a distinct version from StableToken.sol. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 1, 0); - } -} diff --git a/contracts/legacy/StableTokenEUR.sol b/contracts/legacy/StableTokenEUR.sol deleted file mode 100644 index 2e575bae..00000000 --- a/contracts/legacy/StableTokenEUR.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./StableToken.sol"; - -contract StableTokenEUR is StableToken { - /** - * @notice Sets initialized == true on implementation contracts. - * @param test Set to true to skip implementation initialization. - */ - constructor(bool test) public StableToken(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @dev This function is overloaded to maintain a distinct version from StableToken.sol. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 1, 0); - } -} diff --git a/contracts/legacy/StableTokenXOF.sol b/contracts/legacy/StableTokenXOF.sol deleted file mode 100644 index 216174dc..00000000 --- a/contracts/legacy/StableTokenXOF.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "./StableToken.sol"; - -contract StableTokenXOF is StableToken { - /** - * @notice Sets initialized == true on implementation contracts. - * @param test Set to true to skip implementation initialization. - */ - constructor(bool test) public StableToken(test) {} - - /** - * @notice Returns the storage, major, minor, and patch version of the contract. - * @dev This function is overloaded to maintain a distinct version from StableToken.sol. - * @return Storage version of the contract. - * @return Major version of the contract. - * @return Minor version of the contract. - * @return Patch version of the contract. - */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { - return (1, 2, 1, 0); - } -} diff --git a/contracts/legacy/interfaces/IStableToken.sol b/contracts/legacy/interfaces/IStableToken.sol deleted file mode 100644 index 810942f7..00000000 --- a/contracts/legacy/interfaces/IStableToken.sol +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.17 <0.8.19; - -/** - * @title This interface describes the functions specific to Celo Stable Tokens, and in the - * absence of interface inheritance is intended as a companion to IERC20.sol and ICeloToken.sol. - */ -interface IStableToken { - function initialize( - string calldata, - string calldata, - uint8, - address, - uint256, - uint256, - address[] calldata, - uint256[] calldata, - string calldata - ) external; - - function mint(address, uint256) external returns (bool); - - function burn(uint256) external returns (bool); - - function setInflationParameters(uint256, uint256) external; - - function valueToUnits(uint256) external view returns (uint256); - - function unitsToValue(uint256) external view returns (uint256); - - function getInflationParameters() - external - view - returns ( - uint256, - uint256, - uint256, - uint256 - ); - - function getExchangeRegistryId() external view returns (bytes32); - - // NOTE: duplicated with IERC20.sol, remove once interface inheritance is supported. - function balanceOf(address) external view returns (uint256); - - function debitGasFees(address, uint256) external; - - function creditGasFees( - address, - address, - address, - address, - uint256, - uint256, - uint256, - uint256 - ) external; -} diff --git a/contracts/legacy/proxies/ExchangeBRLProxy.sol b/contracts/legacy/proxies/ExchangeBRLProxy.sol deleted file mode 100644 index ccceb9ee..00000000 --- a/contracts/legacy/proxies/ExchangeBRLProxy.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../../common/Proxy.sol"; - -/* solhint-disable-next-line no-empty-blocks */ -contract ExchangeBRLProxy is Proxy { - -} diff --git a/contracts/legacy/proxies/ExchangeProxy.sol b/contracts/legacy/proxies/ExchangeProxy.sol deleted file mode 100644 index dcee2a61..00000000 --- a/contracts/legacy/proxies/ExchangeProxy.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../../common/Proxy.sol"; - -/* solhint-disable-next-line no-empty-blocks */ -contract ExchangeProxy is Proxy { - -} diff --git a/contracts/legacy/proxies/GrandaMentoProxy.sol b/contracts/legacy/proxies/GrandaMentoProxy.sol deleted file mode 100644 index 233e2434..00000000 --- a/contracts/legacy/proxies/GrandaMentoProxy.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../../common/Proxy.sol"; - -/* solhint-disable-next-line no-empty-blocks */ -contract GrandaMentoProxy is Proxy { - -} diff --git a/contracts/legacy/proxies/ReserveSpenderMultiSigProxy.sol b/contracts/legacy/proxies/ReserveSpenderMultiSigProxy.sol deleted file mode 100644 index 4e81b25a..00000000 --- a/contracts/legacy/proxies/ReserveSpenderMultiSigProxy.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../../common/Proxy.sol"; - -/* solhint-disable-next-line no-empty-blocks */ -contract ReserveSpenderMultiSigProxy is Proxy { - -} diff --git a/contracts/libraries/TradingLimits.sol b/contracts/libraries/TradingLimits.sol index 316017e4..66ad2cfd 100644 --- a/contracts/libraries/TradingLimits.sol +++ b/contracts/libraries/TradingLimits.sol @@ -2,6 +2,8 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + /** * @title TradingLimits * @author Mento Team @@ -44,46 +46,12 @@ library TradingLimits { uint8 private constant LG = 4; // 0b100 LimitGlobal int48 private constant MAX_INT48 = int48(uint48(-1) / 2); - /** - * @dev The State struct contains the current state of a trading limit config. - * @param lastUpdated0 The timestamp of the last reset of netflow0. - * @param lastUpdated1 The timestamp of the last reset of netflow1. - * @param netflow0 The current netflow of the asset for limit0. - * @param netflow1 The current netflow of the asset for limit1. - * @param netflowGlobal The current netflow of the asset for limitGlobal. - */ - struct State { - uint32 lastUpdated0; - uint32 lastUpdated1; - int48 netflow0; - int48 netflow1; - int48 netflowGlobal; - } - - /** - * @dev The Config struct contains the configuration of trading limits. - * @param timestep0 The time window in seconds for limit0. - * @param timestep1 The time window in seconds for limit1. - * @param limit0 The limit0 for the asset. - * @param limit1 The limit1 for the asset. - * @param limitGlobal The global limit for the asset. - * @param flags A bitfield of flags to enable/disable the individual limits. - */ - struct Config { - uint32 timestep0; - uint32 timestep1; - int48 limit0; - int48 limit1; - int48 limitGlobal; - uint8 flags; - } - /** * @notice Validate a trading limit configuration. * @dev Reverts if the configuration is malformed. * @param self the Config struct to check. */ - function validate(Config memory self) internal pure { + function validate(ITradingLimits.Config memory self) internal pure { require(self.flags & L1 == 0 || self.flags & L0 != 0, "L1 without L0 not allowed"); require(self.flags & L0 == 0 || self.timestep0 > 0, "timestep0 can't be zero if active"); require(self.flags & L1 == 0 || self.timestep1 > 0, "timestep1 can't be zero if active"); @@ -101,7 +69,7 @@ library TradingLimits { * @param self the trading limit State to check. * @param config the trading limit Config to check against. */ - function verify(State memory self, Config memory config) internal pure { + function verify(ITradingLimits.State memory self, ITradingLimits.Config memory config) internal pure { if ((config.flags & L0) > 0 && (-1 * config.limit0 > self.netflow0 || self.netflow0 > config.limit0)) { revert("L0 Exceeded"); } @@ -125,7 +93,10 @@ library TradingLimits { * @param config the updated config to reset against. * @return the reset state. */ - function reset(State memory self, Config memory config) internal pure returns (State memory) { + function reset( + ITradingLimits.State memory self, + ITradingLimits.Config memory config + ) internal pure returns (ITradingLimits.State memory) { // Ensure the next swap will reset the trading limits windows. self.lastUpdated0 = 0; self.lastUpdated1 = 0; @@ -151,12 +122,12 @@ library TradingLimits { * @return State the updated state. */ function update( - State memory self, - Config memory config, + ITradingLimits.State memory self, + ITradingLimits.Config memory config, int256 _deltaFlow, uint8 decimals - ) internal view returns (State memory) { - int256 _deltaFlowUnits = _deltaFlow / int256((10**uint256(decimals))); + ) internal view returns (ITradingLimits.State memory) { + int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); int48 deltaFlowUnits = _deltaFlowUnits == 0 ? 1 : int48(_deltaFlowUnits); diff --git a/contracts/oracles/BreakerBox.sol b/contracts/oracles/BreakerBox.sol index 8fd836d6..78202e1e 100644 --- a/contracts/oracles/BreakerBox.sol +++ b/contracts/oracles/BreakerBox.sol @@ -8,9 +8,6 @@ import { IBreakerBox } from "../interfaces/IBreakerBox.sol"; import { IBreaker } from "../interfaces/IBreaker.sol"; import { ISortedOracles } from "../interfaces/ISortedOracles.sol"; -import { AddressLinkedList, LinkedList } from "../common/linkedlists/AddressLinkedList.sol"; -import { Initializable } from "../common/Initializable.sol"; - /** * @title BreakerBox * @notice The BreakerBox checks the criteria defined in separate breaker contracts @@ -99,6 +96,7 @@ contract BreakerBox is IBreakerBox, Ownable { */ function removeBreaker(address breaker) external onlyOwner { uint256 breakerIndex = 0; + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < breakers.length; i++) { if (breakers[i] == breaker) { breakerIndex = i; @@ -107,6 +105,7 @@ contract BreakerBox is IBreakerBox, Ownable { } require(breakers[breakerIndex] == breaker, "Breaker has not been added"); + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < rateFeedIDs.length; i++) { if (rateFeedBreakerStatus[rateFeedIDs[i]][breaker].enabled) { // slither-disable-start reentrancy-no-eth @@ -135,17 +134,13 @@ contract BreakerBox is IBreakerBox, Ownable { * @param enable Boolean indicating whether the breaker should be * enabled or disabled for the given rateFeed. */ - function toggleBreaker( - address breakerAddress, - address rateFeedID, - bool enable - ) public onlyOwner { + function toggleBreaker(address breakerAddress, address rateFeedID, bool enable) public onlyOwner { require(rateFeedStatus[rateFeedID], "Rate feed ID has not been added"); require(isBreaker(breakerAddress), "This breaker has not been added to the BreakerBox"); require(rateFeedBreakerStatus[rateFeedID][breakerAddress].enabled != enable, "Breaker is already in this state"); if (enable) { rateFeedBreakerStatus[rateFeedID][breakerAddress].enabled = enable; - // slither-isable-next-line reentrancy-events + // slither-disable-next-line reentrancy-events _checkAndSetBreakers(rateFeedID); } else { delete rateFeedBreakerStatus[rateFeedID][breakerAddress]; @@ -162,6 +157,7 @@ contract BreakerBox is IBreakerBox, Ownable { */ function calculateTradingMode(address rateFeedId) internal view returns (uint8) { uint8 tradingMode = 0; + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < breakers.length; i++) { if (rateFeedBreakerStatus[rateFeedId][breakers[i]].enabled) { tradingMode = tradingMode | rateFeedBreakerStatus[rateFeedId][breakers[i]].tradingMode; @@ -214,6 +210,7 @@ contract BreakerBox is IBreakerBox, Ownable { */ function removeRateFeed(address rateFeedID) external onlyOwner { uint256 rateFeedIndex = 0; + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < rateFeedIDs.length; i++) { if (rateFeedIDs[i] == rateFeedID) { rateFeedIndex = i; @@ -253,6 +250,7 @@ contract BreakerBox is IBreakerBox, Ownable { * @param rateFeedID The address of the rateFeed. */ function deleteBreakerStatus(address rateFeedID) internal { + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < breakers.length; i++) { if (rateFeedBreakerStatus[rateFeedID][breakers[i]].enabled) { delete rateFeedBreakerStatus[rateFeedID][breakers[i]]; @@ -274,6 +272,7 @@ contract BreakerBox is IBreakerBox, Ownable { * @notice Checks whether a breaker with the specifed address has been added. */ function isBreaker(address breaker) public view returns (bool) { + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < breakers.length; i++) { if (breakers[i] == breaker) { return true; @@ -333,6 +332,7 @@ contract BreakerBox is IBreakerBox, Ownable { */ function _checkAndSetBreakers(address rateFeedID) internal { uint8 _tradingMode = 0; + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < breakers.length; i++) { if (rateFeedBreakerStatus[rateFeedID][breakers[i]].enabled) { // slither-disable-next-line reentrancy-benign diff --git a/contracts/oracles/ChainlinkRelayerFactory.sol b/contracts/oracles/ChainlinkRelayerFactory.sol index 9e87a6cb..8867d8f6 100644 --- a/contracts/oracles/ChainlinkRelayerFactory.sol +++ b/contracts/oracles/ChainlinkRelayerFactory.sol @@ -210,6 +210,7 @@ contract ChainlinkRelayerFactory is IChainlinkRelayerFactory, OwnableUpgradeable */ function getRelayers() external view returns (address[] memory relayerAddresses) { address[] memory relayers = new address[](rateFeeds.length); + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < rateFeeds.length; i++) { relayers[i] = address(deployedRelayers[rateFeeds[i]]); } diff --git a/contracts/oracles/ChainlinkRelayerV1.sol b/contracts/oracles/ChainlinkRelayerV1.sol index 6e08cccb..5e0bf3b8 100644 --- a/contracts/oracles/ChainlinkRelayerV1.sol +++ b/contracts/oracles/ChainlinkRelayerV1.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable immutable-vars-naming pragma solidity 0.8.18; import "../interfaces/IChainlinkRelayer.sol"; @@ -13,20 +14,9 @@ import { UD60x18, ud, intoUint256 } from "prb/math/UD60x18.sol"; * See https://github.com/mento-protocol/mento-core/blob/develop/contracts/common/SortedOracles.sol */ interface ISortedOraclesMin { - function report( - address rateFeedId, - uint256 value, - address lesserKey, - address greaterKey - ) external; - - function getRates(address rateFeedId) - external - returns ( - address[] memory, - uint256[] memory, - uint256[] memory - ); + function report(address rateFeedId, uint256 value, address lesserKey, address greaterKey) external; + + function getRates(address rateFeedId) external returns (address[] memory, uint256[] memory, uint256[] memory); function medianTimestamp(address rateFeedId) external view returns (uint256); @@ -254,6 +244,7 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @param rate The rate to report. */ function reportRate(uint256 rate) internal { + // slither-disable-next-line unused-return (address[] memory oracles, uint256[] memory rates, ) = ISortedOraclesMin(sortedOracles).getRates(rateFeedId); uint256 numRates = oracles.length; @@ -282,8 +273,10 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { otherRate = rates[1]; } + // slither-disable-start uninitialized-local address lesserKey; address greaterKey; + // slither-disable-end uninitialized-local if (otherRate < rate) { lesserKey = otherOracle; @@ -302,6 +295,7 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @return timestamp uint256 timestamp of the report. */ function readChainlinkAggregator(ChainlinkAggregator memory aggCfg) internal view returns (UD60x18, uint256) { + // slither-disable-next-line unused-return,calls-loop (, int256 _price, , uint256 timestamp, ) = AggregatorV3Interface(aggCfg.aggregator).latestRoundData(); if (_price <= 0) { revert InvalidPrice(); @@ -348,7 +342,8 @@ contract ChainlinkRelayerV1 is IChainlinkRelayer { * @return The converted UD60x18 value. */ function chainlinkToUD60x18(int256 price, address aggregator) internal view returns (UD60x18) { + // slither-disable-next-line calls-loop uint256 chainlinkDecimals = uint256(AggregatorV3Interface(aggregator).decimals()); - return ud(uint256(price) * 10**(18 - chainlinkDecimals)); + return ud(uint256(price) * 10 ** (18 - chainlinkDecimals)); } } diff --git a/contracts/oracles/breakers/MedianDeltaBreaker.sol b/contracts/oracles/breakers/MedianDeltaBreaker.sol index 1f7c8095..5d4a678e 100644 --- a/contracts/oracles/breakers/MedianDeltaBreaker.sol +++ b/contracts/oracles/breakers/MedianDeltaBreaker.sol @@ -3,8 +3,8 @@ pragma solidity ^0.5.13; import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { FixidityLib } from "../../common/FixidityLib.sol"; import { IBreaker } from "../../interfaces/IBreaker.sol"; import { ISortedOracles } from "../../interfaces/ISortedOracles.sol"; @@ -98,10 +98,10 @@ contract MedianDeltaBreaker is IBreaker, WithCooldown, WithThreshold, Ownable { * @param rateFeedIDs Collection of the addresses rate feeds. * @param rateChangeThresholds Collection of the rate thresholds. */ - function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata rateChangeThresholds) - external - onlyOwner - { + function setRateChangeThresholds( + address[] calldata rateFeedIDs, + uint256[] calldata rateChangeThresholds + ) external onlyOwner { _setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } diff --git a/contracts/oracles/breakers/ValueDeltaBreaker.sol b/contracts/oracles/breakers/ValueDeltaBreaker.sol index e58143f0..e0e05acc 100644 --- a/contracts/oracles/breakers/ValueDeltaBreaker.sol +++ b/contracts/oracles/breakers/ValueDeltaBreaker.sol @@ -3,8 +3,8 @@ pragma solidity ^0.5.13; import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { FixidityLib } from "../../common/FixidityLib.sol"; import { IBreaker } from "../../interfaces/IBreaker.sol"; import { ISortedOracles } from "../../interfaces/ISortedOracles.sol"; @@ -87,10 +87,10 @@ contract ValueDeltaBreaker is IBreaker, WithCooldown, WithThreshold, Ownable { * @param rateFeedIDs Collection of the addresses rate feeds. * @param rateChangeThresholds Collection of the rate thresholds. */ - function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata rateChangeThresholds) - external - onlyOwner - { + function setRateChangeThresholds( + address[] calldata rateFeedIDs, + uint256[] calldata rateChangeThresholds + ) external onlyOwner { _setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } diff --git a/contracts/oracles/breakers/WithThreshold.sol b/contracts/oracles/breakers/WithThreshold.sol index 9b42094e..24e74e5b 100644 --- a/contracts/oracles/breakers/WithThreshold.sol +++ b/contracts/oracles/breakers/WithThreshold.sol @@ -2,7 +2,7 @@ pragma solidity ^0.5.13; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import { FixidityLib } from "../../common/FixidityLib.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; /** * @title Breaker With Thershold @@ -56,10 +56,10 @@ contract WithThreshold { uint256 fixed1 = FixidityLib.fixed1().unwrap(); uint256 maxPercent = uint256(fixed1).add(allowedThreshold); - uint256 maxValue = (referenceValue.mul(maxPercent)).div(10**24); + uint256 maxValue = (referenceValue.mul(maxPercent)).div(10 ** 24); uint256 minPercent = uint256(fixed1).sub(allowedThreshold); - uint256 minValue = (referenceValue.mul(minPercent)).div(10**24); + uint256 minValue = (referenceValue.mul(minPercent)).div(10 ** 24); return (currentValue < minValue || currentValue > maxValue); } diff --git a/contracts/swap/BiPoolManager.sol b/contracts/swap/BiPoolManager.sol index 9df652b9..bba771d0 100644 --- a/contracts/swap/BiPoolManager.sol +++ b/contracts/swap/BiPoolManager.sol @@ -5,16 +5,15 @@ pragma experimental ABIEncoderV2; import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import { IERC20Metadata } from "../common/interfaces/IERC20Metadata.sol"; +import { IERC20 } from "../interfaces/IERC20.sol"; import { IExchangeProvider } from "../interfaces/IExchangeProvider.sol"; import { IBiPoolManager } from "../interfaces/IBiPoolManager.sol"; import { IReserve } from "../interfaces/IReserve.sol"; -import { IPricingModule } from "../interfaces/IPricingModule.sol"; import { ISortedOracles } from "../interfaces/ISortedOracles.sol"; import { IBreakerBox } from "../interfaces/IBreakerBox.sol"; -import { Initializable } from "../common/Initializable.sol"; -import { FixidityLib } from "../common/FixidityLib.sol"; +import { Initializable } from "celo/contracts/common/Initializable.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; /** * @title BiPoolExchangeManager @@ -91,11 +90,7 @@ contract BiPoolManager is IExchangeProvider, IBiPoolManager, Initializable, Owna _; } - modifier verifyExchangeTokens( - address tokenIn, - address tokenOut, - PoolExchange memory exchange - ) { + modifier verifyExchangeTokens(address tokenIn, address tokenOut, PoolExchange memory exchange) { require( (tokenIn == exchange.asset0 && tokenOut == exchange.asset1) || (tokenIn == exchange.asset1 && tokenOut == exchange.asset0), @@ -257,8 +252,8 @@ contract BiPoolManager is IExchangeProvider, IBiPoolManager, Initializable, Owna // slither-disable-next-line encode-packed-collision exchangeId = keccak256( abi.encodePacked( - IERC20Metadata(exchange.asset0).symbol(), - IERC20Metadata(exchange.asset1).symbol(), + IERC20(exchange.asset0).symbol(), + IERC20(exchange.asset1).symbol(), exchange.pricingModule.name() ) ); @@ -270,14 +265,14 @@ contract BiPoolManager is IExchangeProvider, IBiPoolManager, Initializable, Owna exchange.bucket0 = bucket0; exchange.bucket1 = bucket1; - uint256 asset0Decimals = IERC20Metadata(exchange.asset0).decimals(); - uint256 asset1Decimals = IERC20Metadata(exchange.asset1).decimals(); + uint256 asset0Decimals = IERC20(exchange.asset0).decimals(); + uint256 asset1Decimals = IERC20(exchange.asset1).decimals(); require(asset0Decimals <= 18, "asset0 decimals must be <= 18"); require(asset1Decimals <= 18, "asset1 decimals must be <= 18"); - tokenPrecisionMultipliers[exchange.asset0] = 10**(18 - uint256(asset0Decimals)); - tokenPrecisionMultipliers[exchange.asset1] = 10**(18 - uint256(asset1Decimals)); + tokenPrecisionMultipliers[exchange.asset0] = 10 ** (18 - uint256(asset0Decimals)); + tokenPrecisionMultipliers[exchange.asset1] = 10 ** (18 - uint256(asset1Decimals)); exchanges[exchangeId] = exchange; // slither-disable-next-line controlled-array-length @@ -490,11 +485,9 @@ contract BiPoolManager is IExchangeProvider, IBiPoolManager, Initializable, Owna * @param exchange The exchange being updated. * @return exchangeAfter The updated exchange. */ - function updateBucketsIfNecessary(PoolExchange memory exchange) - internal - view - returns (PoolExchange memory, bool updated) - { + function updateBucketsIfNecessary( + PoolExchange memory exchange + ) internal view returns (PoolExchange memory, bool updated) { if (shouldUpdateBuckets(exchange)) { (exchange.bucket0, exchange.bucket1) = getUpdatedBuckets(exchange); updated = true; @@ -557,11 +550,9 @@ contract BiPoolManager is IExchangeProvider, IBiPoolManager, Initializable, Owna * @return rateNumerator * @return rateDenominator */ - function getOracleExchangeRate(address target) - internal - view - returns (uint256 rateNumerator, uint256 rateDenominator) - { + function getOracleExchangeRate( + address target + ) internal view returns (uint256 rateNumerator, uint256 rateDenominator) { (rateNumerator, rateDenominator) = sortedOracles.medianRate(target); require(rateDenominator > 0, "exchange rate denominator must be greater than 0"); } diff --git a/contracts/swap/BiPoolManagerProxy.sol b/contracts/swap/BiPoolManagerProxy.sol index 1b142fd9..7c43508b 100644 --- a/contracts/swap/BiPoolManagerProxy.sol +++ b/contracts/swap/BiPoolManagerProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract BiPoolManagerProxy is Proxy { - -} +contract BiPoolManagerProxy is Proxy {} diff --git a/contracts/swap/Broker.sol b/contracts/swap/Broker.sol index b1ff34c8..234172c0 100644 --- a/contracts/swap/Broker.sol +++ b/contracts/swap/Broker.sol @@ -4,27 +4,34 @@ pragma experimental ABIEncoderV2; import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import { SafeERC20 } from "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; import { IExchangeProvider } from "../interfaces/IExchangeProvider.sol"; import { IBroker } from "../interfaces/IBroker.sol"; import { IBrokerAdmin } from "../interfaces/IBrokerAdmin.sol"; import { IReserve } from "../interfaces/IReserve.sol"; -import { IERC20Metadata } from "../common/interfaces/IERC20Metadata.sol"; import { IStableTokenV2 } from "../interfaces/IStableTokenV2.sol"; +import { ITradingLimits } from "../interfaces/ITradingLimits.sol"; -import { Initializable } from "../common/Initializable.sol"; import { TradingLimits } from "../libraries/TradingLimits.sol"; -import { ReentrancyGuard } from "../common/ReentrancyGuard.sol"; +import { Initializable } from "celo/contracts/common/Initializable.sol"; +import { ReentrancyGuard } from "celo/contracts/common/libraries/ReentrancyGuard.sol"; + +interface IERC20Metadata { + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} /** * @title Broker * @notice The broker executes swaps and keeps track of spending limits per pair. */ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuard { - using TradingLimits for TradingLimits.State; - using TradingLimits for TradingLimits.Config; + using TradingLimits for ITradingLimits.State; + using TradingLimits for ITradingLimits.Config; using SafeERC20 for IERC20; using SafeMath for uint256; @@ -32,8 +39,8 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar address[] public exchangeProviders; mapping(address => bool) public isExchangeProvider; - mapping(bytes32 => TradingLimits.State) public tradingLimitsState; - mapping(bytes32 => TradingLimits.Config) public tradingLimitsConfig; + mapping(bytes32 => ITradingLimits.State) public tradingLimitsState; + mapping(bytes32 => ITradingLimits.Config) public tradingLimitsConfig; // Address of the reserve. IReserve public reserve; @@ -219,10 +226,12 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param token the token to target. * @param config the new trading limits config. */ + // TODO: Make this external with next update. + // slither-disable-next-line external-function function configureTradingLimit( bytes32 exchangeId, address token, - TradingLimits.Config memory config + ITradingLimits.Config memory config ) public onlyOwner { config.validate(); @@ -242,11 +251,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param token The asset to transfer. * @param amount The amount of `token` to be transferred. */ - function transferOut( - address payable to, - address token, - uint256 amount - ) internal { + function transferOut(address payable to, address token, uint256 amount) internal { if (reserve.isStableAsset(token)) { require(IStableTokenV2(token).mint(to, amount), "Minting of the stable asset failed"); } else if (reserve.isCollateralAsset(token)) { @@ -264,11 +269,7 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param token The asset to transfer. * @param amount The amount of `token` to be transferred. */ - function transferIn( - address payable from, - address token, - uint256 amount - ) internal { + function transferIn(address payable from, address token, uint256 amount) internal { if (reserve.isStableAsset(token)) { IERC20(token).safeTransferFrom(from, address(this), amount); require(IStableTokenV2(token).burn(amount), "Burning of the stable asset failed"); @@ -311,14 +312,10 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param deltaFlow the deltaflow of this token, negative for outflow, positive for inflow. * @param token the address of the token, used to lookup decimals. */ - function guardTradingLimit( - bytes32 tradingLimitId, - int256 deltaFlow, - address token - ) internal { - TradingLimits.Config memory tradingLimitConfig = tradingLimitsConfig[tradingLimitId]; + function guardTradingLimit(bytes32 tradingLimitId, int256 deltaFlow, address token) internal { + ITradingLimits.Config memory tradingLimitConfig = tradingLimitsConfig[tradingLimitId]; if (tradingLimitConfig.flags > 0) { - TradingLimits.State memory tradingLimitState = tradingLimitsState[tradingLimitId]; + ITradingLimits.State memory tradingLimitState = tradingLimitsState[tradingLimitId]; tradingLimitState = tradingLimitState.update(tradingLimitConfig, deltaFlow, IERC20Metadata(token).decimals()); tradingLimitState.verify(tradingLimitConfig); tradingLimitsState[tradingLimitId] = tradingLimitState; diff --git a/contracts/swap/BrokerProxy.sol b/contracts/swap/BrokerProxy.sol index f1c1bf22..0f22cb78 100644 --- a/contracts/swap/BrokerProxy.sol +++ b/contracts/swap/BrokerProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract BrokerProxy is Proxy { - -} +contract BrokerProxy is Proxy {} diff --git a/contracts/swap/ConstantProductPricingModule.sol b/contracts/swap/ConstantProductPricingModule.sol index 05c56c18..81af2a5d 100644 --- a/contracts/swap/ConstantProductPricingModule.sol +++ b/contracts/swap/ConstantProductPricingModule.sol @@ -3,9 +3,9 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IPricingModule } from "../interfaces/IPricingModule.sol"; -import { FixidityLib } from "../common/FixidityLib.sol"; /** * @title ConstantProductPricingModule diff --git a/contracts/swap/ConstantSumPricingModule.sol b/contracts/swap/ConstantSumPricingModule.sol index 06f73cbc..2708cb3c 100644 --- a/contracts/swap/ConstantSumPricingModule.sol +++ b/contracts/swap/ConstantSumPricingModule.sol @@ -3,9 +3,9 @@ pragma solidity ^0.5.13; pragma experimental ABIEncoderV2; import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IPricingModule } from "../interfaces/IPricingModule.sol"; -import { FixidityLib } from "../common/FixidityLib.sol"; /** * @title ConstantSumPricingModule diff --git a/contracts/swap/Reserve.sol b/contracts/swap/Reserve.sol index 92004388..918e938c 100644 --- a/contracts/swap/Reserve.sol +++ b/contracts/swap/Reserve.sol @@ -6,14 +6,14 @@ import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; import "openzeppelin-solidity/contracts/utils/Address.sol"; import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; -import "../interfaces/IReserve.sol"; -import "../interfaces/ISortedOracles.sol"; +import "celo/contracts/common/FixidityLib.sol"; +import "celo/contracts/common/Initializable.sol"; +import "celo/contracts/common/interfaces/ICeloVersionedContract.sol"; +import "celo/contracts/common/libraries/ReentrancyGuard.sol"; -import "../common/FixidityLib.sol"; -import "../common/Initializable.sol"; -import "../common/UsingRegistry.sol"; -import "../common/interfaces/ICeloVersionedContract.sol"; -import "../common/ReentrancyGuard.sol"; +import "contracts/common/UsingRegistry.sol"; +import "contracts/interfaces/IReserve.sol"; +import "contracts/interfaces/ISortedOracles.sol"; /** * @title Ensures price stability of StableTokens with respect to their pegs @@ -97,16 +97,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us * @return Minor version of the contract. * @return Patch version of the contract. */ - function getVersionNumber() - external - pure - returns ( - uint256, - uint256, - uint256, - uint256 - ) - { + function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) { return (2, 1, 0, 0); } @@ -270,6 +261,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us sum = sum.add(FixidityLib.wrap(weights[i])); } require(sum.equals(FixidityLib.fixed1()), "Sum of asset allocation must be 1"); + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < assetAllocationSymbols.length; i = i.add(1)) { delete assetAllocationWeights[assetAllocationSymbols[i]]; } @@ -455,11 +447,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us * @param value The amount of collateral assets to transfer. * @return Returns true if the transaction succeeds. */ - function transferCollateralAsset( - address collateralAsset, - address payable to, - uint256 value - ) external returns (bool) { + function transferCollateralAsset(address collateralAsset, address payable to, uint256 value) external returns (bool) { require(isSpender[msg.sender], "sender not allowed to transfer Reserve funds"); require(isOtherReserveAddress[to], "can only transfer to other reserve address"); require( @@ -538,11 +526,10 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us * @param value The amount of gold to transfer. * @return Returns true if the transaction succeeds. */ - function transferExchangeGold(address payable to, uint256 value) - external - isAllowedToSpendExchange(msg.sender) - returns (bool) - { + function transferExchangeGold( + address payable to, + uint256 value + ) external isAllowedToSpendExchange(msg.sender) returns (bool) { return _transferGold(to, value); } @@ -590,6 +577,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us */ function getAssetAllocationWeights() external view returns (uint256[] memory) { uint256[] memory weights = new uint256[](assetAllocationSymbols.length); + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < assetAllocationSymbols.length; i = i.add(1)) { weights[i] = assetAllocationWeights[assetAllocationSymbols[i]]; } @@ -620,6 +608,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us */ function getOtherReserveAddressesGoldBalance() public view returns (uint256) { uint256 reserveGoldBalance = 0; + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < otherReserveAddresses.length; i = i.add(1)) { reserveGoldBalance = reserveGoldBalance.add(otherReserveAddresses[i].balance); } @@ -643,6 +632,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us function getReserveAddressesCollateralAssetBalance(address collateralAsset) public view returns (uint256) { require(checkIsCollateralAsset(collateralAsset), "specified address is not a collateral asset"); uint256 reserveCollateralAssetBalance = 0; + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < otherReserveAddresses.length; i++) { // slither-disable-next-line calls-loop reserveCollateralAssetBalance = reserveCollateralAssetBalance.add( @@ -716,6 +706,7 @@ contract Reserve is IReserve, ICeloVersionedContract, Ownable, Initializable, Us uint256 stableTokensValueInGold = 0; FixidityLib.Fraction memory cgldWeight = FixidityLib.wrap(assetAllocationWeights["cGLD"]); + // slither-disable-next-line cache-array-length for (uint256 i = 0; i < _tokens.length; i = i.add(1)) { uint256 stableAmount; uint256 goldAmount; diff --git a/contracts/swap/ReserveProxy.sol b/contracts/swap/ReserveProxy.sol index 204614a2..340de0f8 100644 --- a/contracts/swap/ReserveProxy.sol +++ b/contracts/swap/ReserveProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract ReserveProxy is Proxy { - -} +contract ReserveProxy is Proxy {} diff --git a/contracts/tokens/StableTokenBRLProxy.sol b/contracts/tokens/StableTokenBRLProxy.sol index 4ebbde82..3cd8f0b4 100644 --- a/contracts/tokens/StableTokenBRLProxy.sol +++ b/contracts/tokens/StableTokenBRLProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenBRLProxy is Proxy { - -} +contract StableTokenBRLProxy is Proxy {} diff --git a/contracts/tokens/StableTokenCOPProxy.sol b/contracts/tokens/StableTokenCOPProxy.sol index 9abe6e85..b3c46af8 100644 --- a/contracts/tokens/StableTokenCOPProxy.sol +++ b/contracts/tokens/StableTokenCOPProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenCOPProxy is Proxy { - -} +contract StableTokenCOPProxy is Proxy {} diff --git a/contracts/tokens/StableTokenEURProxy.sol b/contracts/tokens/StableTokenEURProxy.sol index 53ee6ec7..acd3d560 100644 --- a/contracts/tokens/StableTokenEURProxy.sol +++ b/contracts/tokens/StableTokenEURProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenEURProxy is Proxy { - -} +contract StableTokenEURProxy is Proxy {} diff --git a/contracts/tokens/StableTokenINRProxy.sol b/contracts/tokens/StableTokenINRProxy.sol index f1e8f42a..566ccee3 100644 --- a/contracts/tokens/StableTokenINRProxy.sol +++ b/contracts/tokens/StableTokenINRProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenINRProxy is Proxy { - -} +contract StableTokenINRProxy is Proxy {} diff --git a/contracts/tokens/StableTokenKESProxy.sol b/contracts/tokens/StableTokenKESProxy.sol index 6b40487a..8410b9fe 100644 --- a/contracts/tokens/StableTokenKESProxy.sol +++ b/contracts/tokens/StableTokenKESProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenKESProxy is Proxy { - -} +contract StableTokenKESProxy is Proxy {} diff --git a/contracts/tokens/StableTokenPHPProxy.sol b/contracts/tokens/StableTokenPHPProxy.sol deleted file mode 100644 index 4f8b6a76..00000000 --- a/contracts/tokens/StableTokenPHPProxy.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../common/Proxy.sol"; - -/* solhint-disable-next-line no-empty-blocks */ -contract StableTokenPHPProxy is Proxy { - -} diff --git a/contracts/legacy/proxies/ExchangeEURProxy.sol b/contracts/tokens/StableTokenPSOProxy.sol similarity index 59% rename from contracts/legacy/proxies/ExchangeEURProxy.sol rename to contracts/tokens/StableTokenPSOProxy.sol index 0b6abf6f..705f9d59 100644 --- a/contracts/legacy/proxies/ExchangeEURProxy.sol +++ b/contracts/tokens/StableTokenPSOProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract ExchangeEURProxy is Proxy { - -} +contract StableTokenPSOProxy is Proxy {} diff --git a/contracts/tokens/StableTokenProxy.sol b/contracts/tokens/StableTokenProxy.sol index 0767929f..204b4b26 100644 --- a/contracts/tokens/StableTokenProxy.sol +++ b/contracts/tokens/StableTokenProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenProxy is Proxy { - -} +contract StableTokenProxy is Proxy {} diff --git a/contracts/tokens/StableTokenV2.sol b/contracts/tokens/StableTokenV2.sol index 4462ccc0..54f9ab5f 100644 --- a/contracts/tokens/StableTokenV2.sol +++ b/contracts/tokens/StableTokenV2.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable gas-custom-errors pragma solidity 0.8.18; import { ERC20PermitUpgradeable } from "./patched/ERC20PermitUpgradeable.sol"; import { ERC20Upgradeable } from "./patched/ERC20Upgradeable.sol"; import { IStableTokenV2 } from "../interfaces/IStableTokenV2.sol"; -import { CalledByVm } from "../common/CalledByVm.sol"; +import { CalledByVm } from "celo/contracts/common/CalledByVm.sol"; /** * @title ERC20 token with minting and burning permissioned to a broker and validators. @@ -102,11 +103,7 @@ contract StableTokenV2 is ERC20PermitUpgradeable, IStableTokenV2, CalledByVm { * @param _validators The address of the Validators contract. * @param _exchange The address of the Exchange contract. */ - function initializeV2( - address _broker, - address _validators, - address _exchange - ) external reinitializer(2) onlyOwner { + function initializeV2(address _broker, address _validators, address _exchange) external reinitializer(2) onlyOwner { _setBroker(_broker); _setValidators(_validators); _setExchange(_exchange); @@ -147,11 +144,7 @@ contract StableTokenV2 is ERC20PermitUpgradeable, IStableTokenV2, CalledByVm { * @param comment The transfer comment. * @return True if the transaction succeeds. */ - function transferWithComment( - address to, - uint256 value, - string calldata comment - ) external returns (bool) { + function transferWithComment(address to, uint256 value, string calldata comment) external returns (bool) { emit TransferComment(comment); return transfer(to, value); } @@ -227,12 +220,10 @@ contract StableTokenV2 is ERC20PermitUpgradeable, IStableTokenV2, CalledByVm { } /// @inheritdoc ERC20Upgradeable - function allowance(address owner, address spender) - public - view - override(ERC20Upgradeable, IStableTokenV2) - returns (uint256) - { + function allowance( + address owner, + address spender + ) public view override(ERC20Upgradeable, IStableTokenV2) returns (uint256) { return ERC20Upgradeable.allowance(owner, spender); } diff --git a/contracts/tokens/StableTokenXOFProxy.sol b/contracts/tokens/StableTokenXOFProxy.sol index 958813d5..fceb5de5 100644 --- a/contracts/tokens/StableTokenXOFProxy.sol +++ b/contracts/tokens/StableTokenXOFProxy.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.5.13; -import "../common/Proxy.sol"; +import "celo/contracts/common/Proxy.sol"; /* solhint-disable-next-line no-empty-blocks */ -contract StableTokenXOFProxy is Proxy { - -} +contract StableTokenXOFProxy is Proxy {} diff --git a/contracts/tokens/patched/ERC20PermitUpgradeable.sol b/contracts/tokens/patched/ERC20PermitUpgradeable.sol index 7855d70c..4f40fae7 100644 --- a/contracts/tokens/patched/ERC20PermitUpgradeable.sol +++ b/contracts/tokens/patched/ERC20PermitUpgradeable.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// solhint-disable gas-custom-errors // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/draft-ERC20Permit.sol) /* * 🔥 MentoLabs: This is a copied file from v4.8.0 of OZ-Upgradable, @@ -43,9 +44,12 @@ abstract contract ERC20PermitUpgradeable is ERC20Upgradeable, IERC20PermitUpgrad * to reserve a slot. * @custom:oz-renamed-from _PERMIT_TYPEHASH */ + // slither-disable-start constable-states // solhint-disable-next-line var-name-mixedcase bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT; + // slither-disable-end constable-states + /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. * diff --git a/contracts/tokens/patched/ERC20Upgradeable.sol b/contracts/tokens/patched/ERC20Upgradeable.sol index cb6ea645..af098d71 100644 --- a/contracts/tokens/patched/ERC20Upgradeable.sol +++ b/contracts/tokens/patched/ERC20Upgradeable.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// solhint-disable gas-custom-errors // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) /* * 🔥 MentoLabs: This is a copied file from v4.8.0 of OZ-Upgradable, which only changes @@ -175,11 +176,7 @@ contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20Me * - the caller must have allowance for ``from``'s tokens of at least * `amount`. */ - function transferFrom( - address from, - address to, - uint256 amount - ) public virtual override returns (bool) { + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { address spender = _msgSender(); _spendAllowance(from, spender, amount); _transfer(from, to, amount); @@ -243,11 +240,7 @@ contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20Me * - `to` cannot be the zero address. * - `from` must have a balance of at least `amount`. */ - function _transfer( - address from, - address to, - uint256 amount - ) internal virtual { + function _transfer(address from, address to, uint256 amount) internal virtual { require(from != address(0), "ERC20: transfer from the zero address"); require(to != address(0), "ERC20: transfer to the zero address"); @@ -333,11 +326,7 @@ contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20Me * - `owner` cannot be the zero address. * - `spender` cannot be the zero address. */ - function _approve( - address owner, - address spender, - uint256 amount - ) internal virtual { + function _approve(address owner, address spender, uint256 amount) internal virtual { require(owner != address(0), "ERC20: approve from the zero address"); require(spender != address(0), "ERC20: approve to the zero address"); @@ -353,11 +342,7 @@ contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20Me * * Might emit an {Approval} event. */ - function _spendAllowance( - address owner, - address spender, - uint256 amount - ) internal virtual { + function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { require(currentAllowance >= amount, "ERC20: insufficient allowance"); @@ -381,11 +366,7 @@ contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20Me * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual {} + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {} /** * @dev Hook that is called after any transfer of tokens. This includes @@ -401,11 +382,7 @@ contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20Me * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _afterTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual {} + function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} /** * @dev This empty reserved space is put in place to allow future versions to add new diff --git a/echidna.yaml b/echidna.yaml index e75157c0..7261ef73 100644 --- a/echidna.yaml +++ b/echidna.yaml @@ -1,13 +1,9 @@ cryticArgs: - --foundry-ignore-compile - --solc-remaps - - "openzeppelin-solidity/=lib/openzeppelin-contracts/ celo-foundry/=lib/celo-foundry/src/ forge-std/=lib/celo-foundry/lib/forge-std/src/ ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/ test/=test/ contracts/=contracts/" - - --compile-libraries - - "(AddressSortedLinkedListWithMedian,0x1f)" + - "openzeppelin-solidity/=lib/openzeppelin-contracts/ forge-std/=lib/forge-std/src/ mento-std/=lib/mento-std/src/ test/=test/ contracts/=contracts/" format: text deployBytecodes: [["0x00000000000000000000000000000000000000fc", "601d565b6000600060009150600090508082f3965096945050505050565b"]] -deployContracts: - - ["0x1f", "AddressSortedLinkedListWithMedian"] sender: ["0x10000"] codeSize: 0x7203 diff --git a/foundry.toml b/foundry.toml index 2a9ca20c..b631be6f 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,17 +3,37 @@ src = "contracts" out = "out" test = "test" libs = ["lib"] +script = "script" auto_detect_solc = true bytecode_hash = "none" fuzz_runs = 256 gas_reports = ["*"] -optimizer = true -optimizer_runs = 10_000 +optimizer = false +optimizer_runs = 200 legacy = true no_match_contract = "(ForkTest)|(GovernanceGasTest)" -via_ir = true + +allow_paths = [ + "node_modules/@celo" +] + +fs_permissions = [ + { access = "read", path = "out" }, + { access = "read-write", path = "test/fixtures" } +] + +additional_compiler_profiles = [ + { name = "via-ir-opt", via_ir = true, optimizer = true } +] + +compilation_restrictions = [ + { paths = "contracts/governance/Airgrab.sol", via_ir = true, optimizer = true }, + { paths = "contracts/tokens/StableTokenV2.sol", via_ir = true, optimizer = true }, +] [profile.ci] +via_ir=true +optimizer=true fuzz_runs = 1_000 verbosity = 3 @@ -22,6 +42,6 @@ no_match_contract = "_random" # in order to reset the no_match_contract match_contract = "ForkTest" [rpc_endpoints] -celo_mainnet="${CELO_MAINNET_RPC_URL}" +celo="${CELO_MAINNET_RPC_URL}" baklava="${BAKLAVA_RPC_URL}" alfajores="${ALFAJORES_RPC_URL}" diff --git a/lib/celo-foundry b/lib/celo-foundry deleted file mode 160000 index aba86156..00000000 --- a/lib/celo-foundry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit aba8615646acc8c3f3cc83cf13eab1795600c388 diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 00000000..1714bee7 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/lib/forge-std-next b/lib/forge-std-next deleted file mode 160000 index 73d44ec7..00000000 --- a/lib/forge-std-next +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73d44ec7d124e3831bc5f832267889ffb6f9bc3f diff --git a/lib/mento-std b/lib/mento-std new file mode 160000 index 00000000..cae988de --- /dev/null +++ b/lib/mento-std @@ -0,0 +1 @@ +Subproject commit cae988de9b03b6ab4811900c3c781ccbf8b8beaf diff --git a/package.json b/package.json index aff0a034..7170c96e 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "cz-conventional-changelog": "^3.3.0", "husky": "^8.0.0", "lint-staged": "^13.0.3", - "prettier": "^2.7.1", - "prettier-plugin-solidity": "^1.0.0-dev.22", - "solhint": "^3.3.7", - "solhint-plugin-prettier": "^0.0.5" + "prettier": "3.3.3", + "prettier-plugin-solidity": "1.4.1", + "solhint": "5.0.3", + "solhint-plugin-prettier": "0.1.0" }, "keywords": [ "mento", @@ -28,17 +28,19 @@ "private": true, "scripts": { "lint": "yarn solhint", - "lint:check": "yarn solhint:check", "postinstall": "husky install", "prettier": "prettier --config \"./.prettierrc.yml\" --write \"**/*.{json,md,sol,yml}\" --list-different", "prettier:check": "prettier --config \"./.prettierrc.yml\" --check \"**/*.{json,md,sol,yml}\"", - "solhint": "solhint --config \"./.solhint.json\" \"{contracts,test,script}/**/*.sol\" -w 0", - "solhint:check": "solhint --config \"./.solhint.json\" \"{contracts,test,script}/**/*.sol\" -w 0 -q", + "solhint": "yarn solhint:contracts && yarn solhint:tests", + "solhint:contracts": "solhint --config \"./.solhint.json\" \"contracts/**/*.sol\" -w 0", + "solhint:tests": "solhint --config \"./.solhint.test.json\" \"test/**/*.sol\" -w 0", "test": "forge test", "fork-test": "env FOUNDRY_PROFILE=fork-tests forge test", "fork-test:baklava": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Baklava", "fork-test:alfajores": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores", "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract CeloMainnet" }, - "dependencies": {} + "dependencies": { + "@celo/contracts": "^11.0.0" + } } diff --git a/remappings.txt b/remappings.txt index 863374e5..45801675 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,8 +1,10 @@ openzeppelin-solidity/=lib/openzeppelin-contracts/ -celo-foundry/=lib/celo-foundry/src/ -forge-std/=lib/celo-foundry/lib/forge-std/src/ -ds-test/=lib/celo-foundry/lib/forge-std/lib/ds-test/src/ +openzeppelin-contracts/=lib/openzeppelin-contracts-next/ +openzeppelin-contracts-next/=lib/openzeppelin-contracts-next/ test/=test/ -contracts/=contracts/ +forge-std/=lib/forge-std/src/ safe-contracts/=lib/safe-contracts/ prb/math/=lib/prb-math/src/ +celo/=node_modules/@celo/ +contracts/=contracts/ + diff --git a/slither.config.json b/slither.config.json index 233793e1..ef85c52b 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,6 @@ { - "detectors_to_exclude": "timestamp", - "filter_paths": "(contracts/(legacy/|common/))|/test/|/lib/", + "detectors_to_exclude": "timestamp,missing-zero-check,similar-names", + "exclude_informational": true, + "filter_paths": "node_modules/|test/|lib/", "fail_on": "low" } diff --git a/test/echidna/EchidnaFixidityLib.sol b/test/echidna/EchidnaFixidityLib.sol index 0beda030..358420cf 100644 --- a/test/echidna/EchidnaFixidityLib.sol +++ b/test/echidna/EchidnaFixidityLib.sol @@ -1,5 +1,6 @@ -pragma solidity ^0.5.13; -import "../../contracts/common/FixidityLib.sol"; +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; +import "celo/contracts/common/FixidityLib.sol"; // solhint-disable-next-line max-line-length //echidna ./test/echidna/EchidnaFixidityLib.sol --contract EchidnaFixidityLib --config ./echidna.yaml --test-mode assertion @@ -9,6 +10,7 @@ contract EchidnaFixidityLib { FixidityLib.Fraction memory fraction = FixidityLib.wrap(a); uint256 r = FixidityLib.unwrap(fraction); assert(r == a); + return true; } function integerFractional(uint256 a) public pure returns (bool) { @@ -16,6 +18,7 @@ contract EchidnaFixidityLib { FixidityLib.Fraction memory integer = FixidityLib.integer(fraction); FixidityLib.Fraction memory fractional = FixidityLib.fractional(fraction); assert(fraction.value == integer.value + fractional.value); + return true; } function addSubtract(uint256 a, uint256 b) public pure returns (bool) { @@ -25,5 +28,6 @@ contract EchidnaFixidityLib { FixidityLib.Fraction memory result = FixidityLib.subtract(sum, fraction2); uint256 r = FixidityLib.unwrap(result); assert(r == a); + return true; } } diff --git a/test/echidna/EchidnaStableToken.sol b/test/echidna/EchidnaStableToken.sol deleted file mode 100644 index 4408de64..00000000 --- a/test/echidna/EchidnaStableToken.sol +++ /dev/null @@ -1,68 +0,0 @@ -pragma solidity ^0.5.13; - -import { StableToken } from "contracts/legacy/StableToken.sol"; -import { Registry } from "contracts/common/Registry.sol"; - -// solhint-disable-next-line max-line-length -//echidna ./test/echidna/EchidnaStableToken.sol --contract EchidnaStableToken --config ./echidna.yaml --test-mode assertion - -contract EchidnaStableToken { - StableToken public stableToken; - Registry public registry; - - constructor() public { - registry = new Registry(true); - registry.initialize(); - registry.setAddressFor("GrandaMento", address(this)); - stableToken = new StableToken(true); - stableToken.initialize( - "Celo Dollar", - "cUSD", - 18, - address(registry), - 1e24, - 1 weeks, - new address[](0), - new uint256[](0), - "Exchange" - ); - } - - function zeroAlwaysEmptyERC20Properties() public view { - assert(stableToken.balanceOf(address(0x0)) == 0); - } - - function totalSupplyConsistantERC20Properties( - uint120 user1Amount, - uint120 user2Amount, - uint120 user3Amount - ) public { - address user1 = address(0x1234); - address user2 = address(0x5678); - address user3 = address(0x9abc); - assert( - stableToken.balanceOf(user1) + stableToken.balanceOf(user2) + stableToken.balanceOf(user3) == - stableToken.totalSupply() - ); - stableToken.mint(user1, user1Amount); - stableToken.mint(user2, user2Amount); - stableToken.mint(user3, user3Amount); - assert( - stableToken.balanceOf(user1) + stableToken.balanceOf(user2) + stableToken.balanceOf(user3) == - stableToken.totalSupply() - ); - } - - function transferToOthersERC20PropertiesTransferable() public { - address receiver = address(0x123456); - uint256 amount = 100; - stableToken.mint(msg.sender, amount); - assert(stableToken.balanceOf(msg.sender) == amount); - assert(stableToken.balanceOf(receiver) == 0); - - bool transfer = stableToken.transfer(receiver, amount); - assert(stableToken.balanceOf(msg.sender) == 0); - assert(stableToken.balanceOf(receiver) == amount); - assert(transfer); - } -} diff --git a/test/fork-tests/BaseForkTest.t.sol b/test/fork-tests/BaseForkTest.t.sol deleted file mode 100644 index ca1acdaa..00000000 --- a/test/fork-tests/BaseForkTest.t.sol +++ /dev/null @@ -1,435 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import { Test } from "celo-foundry/Test.sol"; -import { console2 } from "forge-std/console2.sol"; -import { console } from "forge-std/console.sol"; -import { PrecompileHandler } from "celo-foundry/PrecompileHandler.sol"; - -import { Arrays } from "test/utils/Arrays.sol"; -import { TokenHelpers } from "test/utils/TokenHelpers.t.sol"; -import { Chain } from "test/utils/Chain.sol"; - -import { Utils } from "./Utils.t.sol"; -import { TestAsserts } from "./TestAsserts.t.sol"; - -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; -import { IBreaker } from "contracts/interfaces/IBreaker.sol"; -import { IRegistry } from "contracts/common/interfaces/IRegistry.sol"; -import { IERC20Metadata } from "contracts/common/interfaces/IERC20Metadata.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { Proxy } from "contracts/common/Proxy.sol"; - -import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; -import { Broker } from "contracts/swap/Broker.sol"; -import { BreakerBox } from "contracts/oracles/BreakerBox.sol"; -import { SortedOracles } from "contracts/common/SortedOracles.sol"; -import { Reserve } from "contracts/swap/Reserve.sol"; -import { BiPoolManager } from "contracts/swap/BiPoolManager.sol"; -import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; -import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; -import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; - -/** - * @title BaseForkTest - * @notice Fork tests for Mento! - * This test suite tests invariantes on a fork of a live Mento environemnts. - * The philosophy is to test in accordance with how the target fork is configured, - * therfore it doesn't make assumptions about the systems, nor tries to configure - * the system to test specific scenarios. - * However, it should be exausitve in testing invariants across all tradable pairs - * in the system, therfore each test should. - */ -contract BaseForkTest is Test, TokenHelpers, TestAsserts { - using FixidityLib for FixidityLib.Fraction; - using TradingLimits for TradingLimits.State; - using TradingLimits for TradingLimits.Config; - - using Utils for Utils.Context; - using Utils for uint256; - - struct ExchangeWithProvider { - address exchangeProvider; - IExchangeProvider.Exchange exchange; - } - - address public constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; - IRegistry public registry = IRegistry(REGISTRY_ADDRESS); - - address governance; - Broker public broker; - BreakerBox public breakerBox; - SortedOracles public sortedOracles; - Reserve public reserve; - - address public trader; - - ExchangeWithProvider[] public exchanges; - mapping(address => mapping(bytes32 => ExchangeWithProvider)) public exchangeMap; - - uint8 private constant L0 = 1; // 0b001 Limit0 - uint8 private constant L1 = 2; // 0b010 Limit1 - uint8 private constant LG = 4; // 0b100 LimitGlobal - - uint256 targetChainId; - - constructor(uint256 _targetChainId) public Test() { - targetChainId = _targetChainId; - } - - function setUp() public { - Chain.fork(targetChainId); - // The precompile handler is usually initialized in the celo-foundry/Test constructor - // but it needs to be reinitalized after forking - ph = new PrecompileHandler(); - - broker = Broker(registry.getAddressForStringOrDie("Broker")); - sortedOracles = SortedOracles(registry.getAddressForStringOrDie("SortedOracles")); - governance = registry.getAddressForStringOrDie("Governance"); - breakerBox = BreakerBox(address(sortedOracles.breakerBox())); - trader = actor("trader"); - reserve = Reserve(uint160(address(broker.reserve()))); - - vm.startPrank(trader); - currentPrank = trader; - - vm.label(address(broker), "Broker"); - - // Use this by running tests like: - // env ONLY={exchangeId} yarn fork-tests:baklava - // solhint-disable-next-line avoid-low-level-calls - (bool success, bytes memory data) = address(vm).call(abi.encodeWithSignature("envBytes32(string)", "ONLY")); - bytes32 exchangeIdFilter; - if (success) { - exchangeIdFilter = abi.decode(data, (bytes32)); - } - - if (exchangeIdFilter != bytes32(0)) { - console.log("🚨 Filtering exchanges by exchangeId:"); - console.logBytes32(exchangeIdFilter); - console.log("------------------------------------------------------------------"); - } - - address[] memory exchangeProviders = broker.getExchangeProviders(); - for (uint256 i = 0; i < exchangeProviders.length; i++) { - IExchangeProvider.Exchange[] memory _exchanges = IExchangeProvider(exchangeProviders[i]).getExchanges(); - for (uint256 j = 0; j < _exchanges.length; j++) { - if (exchangeIdFilter != bytes32(0) && _exchanges[j].exchangeId != exchangeIdFilter) continue; - exchanges.push(ExchangeWithProvider(exchangeProviders[i], _exchanges[j])); - exchangeMap[exchangeProviders[i]][_exchanges[j].exchangeId] = ExchangeWithProvider( - exchangeProviders[i], - _exchanges[j] - ); - } - } - require(exchanges.length > 0, "No exchanges found"); - - // The number of collateral assets 5 is hardcoded here [CELO, AxelarUSDC, EUROC, NativeUSDC, NativeUSDT] - for (uint256 i = 0; i < 5; i++) { - address collateralAsset = reserve.collateralAssets(i); - mint(collateralAsset, address(reserve), Utils.toSubunits(25_000_000, collateralAsset)); - console.log("Minting 25mil %s to reserve", IERC20Metadata(collateralAsset).symbol()); - } - - console.log("Exchanges(%d): ", exchanges.length); - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - console.log("%d | %s | %s", i, ctx.ticker(), ctx.exchangeProvider); - console.logBytes32(ctx.exchange.exchangeId); - } - } - - function test_biPoolManagerCanNotBeReinitialized() public { - BiPoolManager biPoolManager = BiPoolManager(broker.getExchangeProviders()[0]); - - vm.expectRevert("contract already initialized"); - biPoolManager.initialize(address(broker), reserve, sortedOracles, breakerBox); - } - - function test_brokerCanNotBeReinitialized() public { - vm.expectRevert("contract already initialized"); - broker.initialize(new address[](0), address(reserve)); - } - - function test_sortedOraclesCanNotBeReinitialized() public { - vm.expectRevert("contract already initialized"); - sortedOracles.initialize(1); - } - - function test_reserveCanNotBeReinitialized() public { - vm.expectRevert("contract already initialized"); - reserve.initialize( - address(10), - 0, - 0, - 0, - 0, - new bytes32[](0), - new uint256[](0), - 0, - 0, - new address[](0), - new uint256[](0) - ); - } - - function test_stableTokensCanNotBeReinitialized() public { - IStableTokenV2 stableToken = IStableTokenV2(registry.getAddressForStringOrDie("StableToken")); - IStableTokenV2 stableTokenEUR = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenEUR")); - IStableTokenV2 stableTokenBRL = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenBRL")); - IStableTokenV2 stableTokenXOF = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenXOF")); - IStableTokenV2 stableTokenKES = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenKES")); - - vm.expectRevert("Initializable: contract is already initialized"); - stableToken.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); - - vm.expectRevert("Initializable: contract is already initialized"); - stableTokenEUR.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); - - vm.expectRevert("Initializable: contract is already initialized"); - stableTokenBRL.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); - - vm.expectRevert("Initializable: contract is already initialized"); - stableTokenXOF.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); - - vm.expectRevert("Initializable: contract is already initialized"); - stableTokenKES.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); - } - - function test_swapsHappenInBothDirections() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - // asset0 -> asset1 - assert_swapIn(ctx, exchange.assets[0], exchange.assets[1], Utils.toSubunits(1000, exchange.assets[0])); - // asset1 -> asset0 - assert_swapIn(ctx, exchange.assets[1], exchange.assets[0], Utils.toSubunits(1000, exchange.assets[1])); - } - } - - function test_tradingLimitsAreConfigured() public view { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - bytes32 asset0Bytes32 = bytes32(uint256(uint160(exchange.assets[0]))); - bytes32 limitIdForAsset0 = exchange.exchangeId ^ asset0Bytes32; - bytes32 asset1Bytes32 = bytes32(uint256(uint160(exchange.assets[1]))); - bytes32 limitIdForAsset1 = exchange.exchangeId ^ asset1Bytes32; - - bool asset0LimitConfigured = ctx.isLimitConfigured(limitIdForAsset0); - bool asset1LimitConfigured = ctx.isLimitConfigured(limitIdForAsset1); - - require(asset0LimitConfigured || asset1LimitConfigured, "Limit not configured"); - } - } - - function test_tradingLimitsAreEnforced_0to1_L0() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], L0); - } - } - - function test_tradingLimitsAreEnforced_0to1_L1() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], L1); - } - } - - function test_tradingLimitsAreEnforced_0to1_LG() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], LG); - } - } - - function test_tradingLimitsAreEnforced_1to0_L0() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], L0); - } - } - - function test_tradingLimitsAreEnforced_1to0_L1() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], L1); - } - } - - function test_tradingLimitsAreEnforced_1to0_LG() public { - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], LG); - } - } - - function test_circuitBreaker_rateFeedsAreProtected() public view { - address[] memory breakers = breakerBox.getBreakers(); - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); - bool found = false; - for (uint256 j = 0; j < breakers.length && !found; j++) { - found = breakerBox.isBreakerEnabled(breakers[j], rateFeedID); - } - require(found, "No breaker found for rateFeedID"); - } - } - - function test_circuitBreaker_breaks() public { - address[] memory breakers = breakerBox.getBreakers(); - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); - for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { - assert_breakerBreaks(ctx, breakers[j], j); - // we recover this breaker so that it doesn't affect other exchanges in this test, - // since the rateFeed for this exchange could be a dependency for other rateFeeds - assert_breakerRecovers(ctx, breakers[j], j); - } - } - } - } - - function test_circuitBreaker_recovers() public { - address[] memory breakers = breakerBox.getBreakers(); - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); - for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { - assert_breakerRecovers(ctx, breakers[j], j); - } - } - } - } - - function test_circuitBreaker_haltsTrading() public { - address[] memory breakers = breakerBox.getBreakers(); - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { - assert_breakerBreaks(ctx, breakers[j], j); - - assert_swapInFails( - ctx, - exchange.assets[0], - exchange.assets[1], - Utils.toSubunits(1000, exchange.assets[0]), - "Trading is suspended for this reference rate" - ); - assert_swapInFails( - ctx, - exchange.assets[1], - exchange.assets[0], - Utils.toSubunits(1000, exchange.assets[1]), - "Trading is suspended for this reference rate" - ); - - assert_swapOutFails( - ctx, - exchange.assets[0], - exchange.assets[1], - Utils.toSubunits(1000, exchange.assets[1]), - "Trading is suspended for this reference rate" - ); - assert_swapOutFails( - ctx, - exchange.assets[1], - exchange.assets[0], - Utils.toSubunits(1000, exchange.assets[0]), - "Trading is suspended for this reference rate" - ); - - // we recover this breaker so that it doesn't affect other exchanges in this test, - // since the rateFeed for this exchange could be a dependency for other rateFeeds - assert_breakerRecovers(ctx, breakers[j], j); - } - } - } - } - - mapping(address => uint256) depsCount; - - function test_rateFeedDependencies_haltsDependantTrading() public { - // Hardcoded number of dependencies for each ratefeed - depsCount[registry.getAddressForStringOrDie("StableToken")] = 0; - depsCount[registry.getAddressForStringOrDie("StableTokenEUR")] = 0; - depsCount[registry.getAddressForStringOrDie("StableTokenBRL")] = 0; - depsCount[registry.getAddressForStringOrDie("StableTokenXOF")] = 2; - depsCount[0xA1A8003936862E7a15092A91898D69fa8bCE290c] = 0; // USDC/USD - depsCount[0x206B25Ea01E188Ee243131aFdE526bA6E131a016] = 1; // USDC/EUR - depsCount[0x25F21A1f97607Edf6852339fad709728cffb9a9d] = 1; // USDC/BRL - depsCount[0x26076B9702885d475ac8c3dB3Bd9F250Dc5A318B] = 0; // EUROC/EUR - - address[] memory breakers = breakerBox.getBreakers(); - - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - address[] memory dependencies = new address[](depsCount[ctx.getReferenceRateFeedID()]); - for (uint256 d = 0; d < dependencies.length; d++) { - dependencies[d] = ctx.breakerBox.rateFeedDependencies(ctx.getReferenceRateFeedID(), d); - } - if (dependencies.length == 0) { - continue; - } - - Utils.logPool(ctx); - address rateFeedID = ctx.getReferenceRateFeedID(); - console.log("\t exchangeIndex: %d | rateFeedId: %s | %s dependencies", i, rateFeedID, dependencies.length); - - for (uint256 k = 0; k < dependencies.length; k++) { - Utils.Context memory dependencyContext = Utils.getContextForRateFeedID(address(this), dependencies[k]); - - for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], dependencies[k])) { - assert_breakerBreaks(dependencyContext, breakers[j], j); - - assert_swapInFails( - ctx, - ctx.exchange.assets[0], - ctx.exchange.assets[1], - Utils.toSubunits(1000, ctx.exchange.assets[0]), - "Trading is suspended for this reference rate" - ); - - assert_breakerRecovers(dependencyContext, breakers[j], j); - } - } - } - } - } -} diff --git a/test/fork-tests/EnvForkTest.t.sol b/test/fork-tests/EnvForkTest.t.sol deleted file mode 100644 index f2d6c70e..00000000 --- a/test/fork-tests/EnvForkTest.t.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import { BaseForkTest } from "./BaseForkTest.t.sol"; - -contract BaklavaForkTest is BaseForkTest(62320) {} - -contract AlfajoresForkTest is BaseForkTest(44787) {} - -contract CeloMainnetForkTest is BaseForkTest(42220) {} diff --git a/test/fork-tests/TokenUpgrade.t.sol b/test/fork-tests/TokenUpgrade.t.sol deleted file mode 100644 index 68e35172..00000000 --- a/test/fork-tests/TokenUpgrade.t.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; - -import { Test } from "forge-std/Test.sol"; -import { console } from "forge-std/console.sol"; - -import { IRegistry } from "contracts/common/interfaces/IRegistry.sol"; -import { IProxy as ILegacyProxy } from "contracts/common/interfaces/IProxy.sol"; -import { StableTokenV2 } from "contracts/tokens/StableTokenV2.sol"; - -contract TokenUpgradeForkTest is Test { - address public constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; - IRegistry public registry = IRegistry(REGISTRY_ADDRESS); - - // solhint-disable-next-line func-name-mixedcase - function test_upgrade() public { - uint256 forkId = vm.createFork("celo_mainnet"); - vm.selectFork(forkId); - - address stableToken = registry.getAddressForString("StableToken"); - ILegacyProxy stableTokenProxy = ILegacyProxy(stableToken); - console.log(ILegacyProxy(stableToken)._getImplementation()); - console.log(ILegacyProxy(stableToken)._getOwner()); - vm.startPrank(ILegacyProxy(stableToken)._getOwner()); - address mentoERC20Impl = address(new StableTokenV2(false)); - stableTokenProxy._setImplementation(mentoERC20Impl); - - StableTokenV2 cusd = StableTokenV2(stableToken); - cusd.initializeV2( - registry.getAddressForString("Broker"), - registry.getAddressForString("Validators"), - registry.getAddressForString("Exchange") - ); - - address governance = registry.getAddressForString("Governance"); - cusd.balanceOf(governance); - - changePrank(governance); - cusd.transfer(address(this), 1 ether); - cusd.balanceOf(address(this)); - } -} diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol new file mode 100644 index 00000000..804440c6 --- /dev/null +++ b/test/fork/BaseForkTest.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count +pragma solidity ^0.8; + +import { Test } from "mento-std/Test.sol"; +import { CELO_REGISTRY_ADDRESS } from "mento-std/Constants.sol"; +import { console } from "forge-std/console.sol"; + +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; + +import { Utils } from "./Utils.sol"; +import { TestAsserts } from "./TestAsserts.sol"; + +import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; +import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; + +interface IMint { + function mint(address, uint256) external; +} + +/** + * @title BaseForkTest + * @notice Fork tests for Mento! + * This test suite tests invariantes on a fork of a live Mento environemnts. + * The philosophy is to test in accordance with how the target fork is configured, + * therfore it doesn't make assumptions about the systems, nor tries to configure + * the system to test specific scenarios. + * However, it should be exausitve in testing invariants across all tradable pairs + * in the system, therfore each test should. + */ +contract BaseForkTest is Test, TestAsserts { + using FixidityLib for FixidityLib.Fraction; + + using Utils for Utils.Context; + using Utils for uint256; + + struct ExchangeWithProvider { + address exchangeProvider; + IExchangeProvider.Exchange exchange; + } + + IRegistry public registry = IRegistry(CELO_REGISTRY_ADDRESS); + + address governance; + IBroker public broker; + IBreakerBox public breakerBox; + ISortedOracles public sortedOracles; + IReserve public reserve; + ITradingLimitsHarness public tradingLimits; + + address public trader; + + ExchangeWithProvider[] public exchanges; + mapping(address => mapping(bytes32 => ExchangeWithProvider)) public exchangeMap; + + uint8 public constant L0 = 1; // 0b001 Limit0 + uint8 public constant L1 = 2; // 0b010 Limit1 + uint8 public constant LG = 4; // 0b100 LimitGlobal + + uint256 targetChainId; + + constructor(uint256 _targetChainId) Test() { + targetChainId = _targetChainId; + } + + function lookup(string memory key) public returns (address) { + address addr = registry.getAddressForStringOrDie(key); + if (addr != address(0)) { + vm.label(addr, key); + } + return addr; + } + + function setUp() public virtual { + fork(targetChainId); + // The precompile handler needs to be reinitialized after forking. + __CeloPrecompiles_init(); + + tradingLimits = ITradingLimitsHarness(deployCode("TradingLimitsHarness")); + broker = IBroker(lookup("Broker")); + sortedOracles = ISortedOracles(lookup("SortedOracles")); + governance = lookup("Governance"); + breakerBox = IBreakerBox(address(sortedOracles.breakerBox())); + vm.label(address(breakerBox), "BreakerBox"); + trader = makeAddr("trader"); + reserve = IReserve(broker.reserve()); + + vm.startPrank(trader); + + address[] memory exchangeProviders = broker.getExchangeProviders(); + for (uint256 i = 0; i < exchangeProviders.length; i++) { + vm.label(exchangeProviders[i], "ExchangeProvider"); + IExchangeProvider.Exchange[] memory _exchanges = IExchangeProvider(exchangeProviders[i]).getExchanges(); + for (uint256 j = 0; j < _exchanges.length; j++) { + exchanges.push(ExchangeWithProvider(exchangeProviders[i], _exchanges[j])); + exchangeMap[exchangeProviders[i]][_exchanges[j].exchangeId] = ExchangeWithProvider( + exchangeProviders[i], + _exchanges[j] + ); + } + } + require(exchanges.length > 0, "No exchanges found"); + + // The number of collateral assets 5 is hardcoded here [CELO, AxelarUSDC, EUROC, NativeUSDC, NativeUSDT] + for (uint256 i = 0; i < 5; i++) { + address collateralAsset = reserve.collateralAssets(i); + vm.label(collateralAsset, IERC20(collateralAsset).symbol()); + _deal(collateralAsset, address(reserve), Utils.toSubunits(25_000_000, collateralAsset), true); + console.log("Minting 25mil %s to reserve", IERC20(collateralAsset).symbol()); + } + + console.log("Exchanges(%d): ", exchanges.length); + for (uint256 i = 0; i < exchanges.length; i++) { + Utils.Context memory ctx = Utils.newContext(address(this), i); + console.log("%d | %s | %s", i, ctx.ticker(), ctx.exchangeProvider); + console.logBytes32(ctx.exchange.exchangeId); + } + } + + function _deal(address asset, address to, uint256 amount, bool updateSupply) public { + if (asset == lookup("GoldToken")) { + vm.startPrank(address(0)); + IMint(asset).mint(to, amount); + vm.startPrank(trader); + return; + } + + deal(asset, to, amount, updateSupply); + } +} diff --git a/test/fork/ChainForkTest.sol b/test/fork/ChainForkTest.sol new file mode 100644 index 00000000..0b9b802a --- /dev/null +++ b/test/fork/ChainForkTest.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count +pragma solidity ^0.8; + +import "./BaseForkTest.sol"; + +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; + +contract ChainForkTest is BaseForkTest { + using FixidityLib for FixidityLib.Fraction; + + using Utils for Utils.Context; + using Utils for uint256; + + uint256 expectedExchangesCount; + + constructor(uint256 _chainId, uint256 _expectedExchangesCount) BaseForkTest(_chainId) { + expectedExchangesCount = _expectedExchangesCount; + } + + function test_biPoolManagerCanNotBeReinitialized() public { + IBiPoolManager biPoolManager = IBiPoolManager(broker.getExchangeProviders()[0]); + + vm.expectRevert("contract already initialized"); + biPoolManager.initialize(address(broker), reserve, sortedOracles, breakerBox); + } + + function test_brokerCanNotBeReinitialized() public { + vm.expectRevert("contract already initialized"); + broker.initialize(new address[](0), address(reserve)); + } + + function test_sortedOraclesCanNotBeReinitialized() public { + vm.expectRevert("contract already initialized"); + sortedOracles.initialize(1); + } + + function test_reserveCanNotBeReinitialized() public { + vm.expectRevert("contract already initialized"); + reserve.initialize( + address(10), + 0, + 0, + 0, + 0, + new bytes32[](0), + new uint256[](0), + 0, + 0, + new address[](0), + new uint256[](0) + ); + } + + function test_testsAreConfigured() public view { + assertEq(expectedExchangesCount, exchanges.length); + } + + function test_stableTokensCanNotBeReinitialized() public { + IStableTokenV2 stableToken = IStableTokenV2(registry.getAddressForStringOrDie("StableToken")); + IStableTokenV2 stableTokenEUR = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenEUR")); + IStableTokenV2 stableTokenBRL = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenBRL")); + IStableTokenV2 stableTokenXOF = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenXOF")); + IStableTokenV2 stableTokenKES = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenKES")); + + vm.expectRevert("Initializable: contract is already initialized"); + stableToken.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); + + vm.expectRevert("Initializable: contract is already initialized"); + stableTokenEUR.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); + + vm.expectRevert("Initializable: contract is already initialized"); + stableTokenBRL.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); + + vm.expectRevert("Initializable: contract is already initialized"); + stableTokenXOF.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); + + vm.expectRevert("Initializable: contract is already initialized"); + stableTokenKES.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); + } +} diff --git a/test/fork/EnvForkTest.t.sol b/test/fork/EnvForkTest.t.sol new file mode 100644 index 00000000..e90d0e07 --- /dev/null +++ b/test/fork/EnvForkTest.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count +pragma solidity ^0.8; + +import { ChainForkTest } from "./ChainForkTest.sol"; +import { ExchangeForkTest } from "./ExchangeForkTest.sol"; +import { CELO_ID, BAKLAVA_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; + +contract BaklavaChainForkTest is ChainForkTest(BAKLAVA_ID, 14) {} + +contract BaklavaExchangeForkTest0 is ExchangeForkTest(BAKLAVA_ID, 0) {} + +contract BaklavaExchangeForkTest1 is ExchangeForkTest(BAKLAVA_ID, 1) {} + +contract BaklavaExchangeForkTest2 is ExchangeForkTest(BAKLAVA_ID, 2) {} + +contract BaklavaExchangeForkTest3 is ExchangeForkTest(BAKLAVA_ID, 3) {} + +contract BaklavaExchangeForkTest4 is ExchangeForkTest(BAKLAVA_ID, 4) {} + +contract BaklavaExchangeForkTest5 is ExchangeForkTest(BAKLAVA_ID, 5) {} + +contract BaklavaExchangeForkTest6 is ExchangeForkTest(BAKLAVA_ID, 6) {} + +contract BaklavaExchangeForkTest7 is ExchangeForkTest(BAKLAVA_ID, 7) {} + +contract BaklavaExchangeForkTest8 is ExchangeForkTest(BAKLAVA_ID, 8) {} + +contract BaklavaExchangeForkTest9 is ExchangeForkTest(BAKLAVA_ID, 9) {} + +contract BaklavaExchangeForkTest10 is ExchangeForkTest(BAKLAVA_ID, 10) {} + +contract BaklavaExchangeForkTest11 is ExchangeForkTest(BAKLAVA_ID, 11) {} + +contract BaklavaExchangeForkTest12 is ExchangeForkTest(BAKLAVA_ID, 12) {} + +contract BaklavaExchangeForkTest13 is ExchangeForkTest(BAKLAVA_ID, 13) {} + +contract AlfajoresChainForkTest is ChainForkTest(ALFAJORES_ID, 14) {} + +contract AlfajoresExchangeForkTest0 is ExchangeForkTest(ALFAJORES_ID, 0) {} + +contract AlfajoresExchangeForkTest1 is ExchangeForkTest(ALFAJORES_ID, 1) {} + +contract AlfajoresExchangeForkTest2 is ExchangeForkTest(ALFAJORES_ID, 2) {} + +contract AlfajoresExchangeForkTest3 is ExchangeForkTest(ALFAJORES_ID, 3) {} + +contract AlfajoresExchangeForkTest4 is ExchangeForkTest(ALFAJORES_ID, 4) {} + +contract AlfajoresExchangeForkTest5 is ExchangeForkTest(ALFAJORES_ID, 5) {} + +contract AlfajoresExchangeForkTest6 is ExchangeForkTest(ALFAJORES_ID, 6) {} + +contract AlfajoresExchangeForkTest7 is ExchangeForkTest(ALFAJORES_ID, 7) {} + +contract AlfajoresExchangeForkTest8 is ExchangeForkTest(ALFAJORES_ID, 8) {} + +contract AlfajoresExchangeForkTest9 is ExchangeForkTest(ALFAJORES_ID, 9) {} + +contract AlfajoresExchangeForkTest10 is ExchangeForkTest(ALFAJORES_ID, 10) {} + +contract AlfajoresExchangeForkTest11 is ExchangeForkTest(ALFAJORES_ID, 11) {} + +contract AlfajoresExchangeForkTest12 is ExchangeForkTest(ALFAJORES_ID, 12) {} + +contract AlfajoresExchangeForkTest13 is ExchangeForkTest(ALFAJORES_ID, 13) {} + +contract CeloChainForkTest is ChainForkTest(CELO_ID, 14) {} + +contract CeloExchangeForkTest0 is ExchangeForkTest(CELO_ID, 0) {} + +contract CeloExchangeForkTest1 is ExchangeForkTest(CELO_ID, 1) {} + +contract CeloExchangeForkTest2 is ExchangeForkTest(CELO_ID, 2) {} + +contract CeloExchangeForkTest3 is ExchangeForkTest(CELO_ID, 3) {} + +contract CeloExchangeForkTest4 is ExchangeForkTest(CELO_ID, 4) {} + +contract CeloExchangeForkTest5 is ExchangeForkTest(CELO_ID, 5) {} + +contract CeloExchangeForkTest6 is ExchangeForkTest(CELO_ID, 6) {} + +contract CeloExchangeForkTest7 is ExchangeForkTest(CELO_ID, 7) {} + +contract CeloExchangeForkTest8 is ExchangeForkTest(CELO_ID, 8) {} + +contract CeloExchangeForkTest9 is ExchangeForkTest(CELO_ID, 9) {} + +contract CeloExchangeForkTest10 is ExchangeForkTest(CELO_ID, 10) {} + +contract CeloExchangeForkTest11 is ExchangeForkTest(CELO_ID, 11) {} + +contract CeloExchangeForkTest12 is ExchangeForkTest(CELO_ID, 12) {} + +contract CeloExchangeForkTest13 is ExchangeForkTest(CELO_ID, 13) {} diff --git a/test/fork/ExchangeForkTest.sol b/test/fork/ExchangeForkTest.sol new file mode 100644 index 00000000..0f61edd9 --- /dev/null +++ b/test/fork/ExchangeForkTest.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count +pragma solidity ^0.8; + +import "./BaseForkTest.sol"; + +contract ExchangeForkTest is BaseForkTest { + using FixidityLib for FixidityLib.Fraction; + + using Utils for Utils.Context; + using Utils for uint256; + + uint256 exchangeIndex; + + constructor(uint256 _chainId, uint256 _exchangeIndex) BaseForkTest(_chainId) { + exchangeIndex = _exchangeIndex; + } + + Utils.Context ctx; + + function setUp() public override { + super.setUp(); + ctx = Utils.newContext(address(this), exchangeIndex); + } + + function test_swapsHappenInBothDirections() public { + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + // asset0 -> asset1 + assert_swapIn(ctx, exchange.assets[0], exchange.assets[1], Utils.toSubunits(1000, exchange.assets[0])); + // asset1 -> asset0 + assert_swapIn(ctx, exchange.assets[1], exchange.assets[0], Utils.toSubunits(1000, exchange.assets[1])); + } + + function test_tradingLimitsAreConfigured() public view { + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + bytes32 asset0Bytes32 = bytes32(uint256(uint160(exchange.assets[0]))); + bytes32 limitIdForAsset0 = exchange.exchangeId ^ asset0Bytes32; + bytes32 asset1Bytes32 = bytes32(uint256(uint160(exchange.assets[1]))); + bytes32 limitIdForAsset1 = exchange.exchangeId ^ asset1Bytes32; + + bool asset0LimitConfigured = ctx.isLimitConfigured(limitIdForAsset0); + bool asset1LimitConfigured = ctx.isLimitConfigured(limitIdForAsset1); + + require(asset0LimitConfigured || asset1LimitConfigured, "Limit not configured"); + } + + function test_tradingLimitsAreEnforced_0to1_L0() public { + ctx.logHeader(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], L0); + } + + function test_tradingLimitsAreEnforced_0to1_L1() public { + ctx.logHeader(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], L1); + } + + function test_tradingLimitsAreEnforced_0to1_LG() public { + ctx.logHeader(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], LG); + } + + function test_tradingLimitsAreEnforced_1to0_L0() public { + ctx.logHeader(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], L0); + } + + function test_tradingLimitsAreEnforced_1to0_L1() public { + ctx.logHeader(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], L1); + } + + function test_tradingLimitsAreEnforced_1to0_LG() public { + ctx.logHeader(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], LG); + } + + function test_circuitBreaker_rateFeedsAreProtected() public view { + address[] memory breakers = breakerBox.getBreakers(); + ctx.logHeader(); + address rateFeedID = ctx.getReferenceRateFeedID(); + bool found = false; + for (uint256 j = 0; j < breakers.length && !found; j++) { + found = breakerBox.isBreakerEnabled(breakers[j], rateFeedID); + } + require(found, "No breaker found for rateFeedID"); + } + + function test_circuitBreaker_breaks() public { + address[] memory breakers = breakerBox.getBreakers(); + ctx.logHeader(); + address rateFeedID = ctx.getReferenceRateFeedID(); + for (uint256 j = 0; j < breakers.length; j++) { + if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { + assert_breakerBreaks(ctx, breakers[j], j); + // we recover this breaker so that it doesn't affect other exchanges in this test, + // since the rateFeed for this exchange could be a dependency for other rateFeeds + assert_breakerRecovers(ctx, breakers[j], j); + } + } + } + + function test_circuitBreaker_recovers() public { + address[] memory breakers = breakerBox.getBreakers(); + ctx.logHeader(); + address rateFeedID = ctx.getReferenceRateFeedID(); + for (uint256 j = 0; j < breakers.length; j++) { + if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { + assert_breakerRecovers(ctx, breakers[j], j); + } + } + } + + function test_circuitBreaker_haltsTrading() public { + address[] memory breakers = breakerBox.getBreakers(); + ctx.logHeader(); + address rateFeedID = ctx.getReferenceRateFeedID(); + IExchangeProvider.Exchange memory exchange = ctx.exchange; + + for (uint256 j = 0; j < breakers.length; j++) { + if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { + assert_breakerBreaks(ctx, breakers[j], j); + + assert_swapInFails( + ctx, + exchange.assets[0], + exchange.assets[1], + Utils.toSubunits(1000, exchange.assets[0]), + "Trading is suspended for this reference rate" + ); + assert_swapInFails( + ctx, + exchange.assets[1], + exchange.assets[0], + Utils.toSubunits(1000, exchange.assets[1]), + "Trading is suspended for this reference rate" + ); + + assert_swapOutFails( + ctx, + exchange.assets[0], + exchange.assets[1], + Utils.toSubunits(1000, exchange.assets[1]), + "Trading is suspended for this reference rate" + ); + assert_swapOutFails( + ctx, + exchange.assets[1], + exchange.assets[0], + Utils.toSubunits(1000, exchange.assets[0]), + "Trading is suspended for this reference rate" + ); + + // we recover this breaker so that it doesn't affect other exchanges in this test, + // since the rateFeed for this exchange could be a dependency for other rateFeeds + assert_breakerRecovers(ctx, breakers[j], j); + } + } + } + + mapping(address => uint256) depsCount; + + function test_rateFeedDependencies_haltsDependantTrading() public { + // Hardcoded number of dependencies for each ratefeed + depsCount[registry.getAddressForStringOrDie("StableToken")] = 0; + depsCount[registry.getAddressForStringOrDie("StableTokenEUR")] = 0; + depsCount[registry.getAddressForStringOrDie("StableTokenBRL")] = 0; + depsCount[registry.getAddressForStringOrDie("StableTokenXOF")] = 2; + depsCount[0xA1A8003936862E7a15092A91898D69fa8bCE290c] = 0; // USDC/USD + depsCount[0x206B25Ea01E188Ee243131aFdE526bA6E131a016] = 1; // USDC/EUR + depsCount[0x25F21A1f97607Edf6852339fad709728cffb9a9d] = 1; // USDC/BRL + depsCount[0x26076B9702885d475ac8c3dB3Bd9F250Dc5A318B] = 0; // EUROC/EUR + + address[] memory breakers = breakerBox.getBreakers(); + + address[] memory dependencies = new address[](depsCount[ctx.getReferenceRateFeedID()]); + for (uint256 d = 0; d < dependencies.length; d++) { + dependencies[d] = ctx.breakerBox.rateFeedDependencies(ctx.getReferenceRateFeedID(), d); + } + if (dependencies.length == 0) { + return; + } + + Utils.logPool(ctx); + address rateFeedID = ctx.getReferenceRateFeedID(); + console.log( + "\t exchangeIndex: %d | rateFeedId: %s | %s dependencies", + exchangeIndex, + rateFeedID, + dependencies.length + ); + + for (uint256 k = 0; k < dependencies.length; k++) { + Utils.Context memory dependencyContext = Utils.getContextForRateFeedID(address(this), dependencies[k]); + + for (uint256 j = 0; j < breakers.length; j++) { + if (breakerBox.isBreakerEnabled(breakers[j], dependencies[k])) { + assert_breakerBreaks(dependencyContext, breakers[j], j); + + assert_swapInFails( + ctx, + ctx.exchange.assets[0], + ctx.exchange.assets[1], + Utils.toSubunits(1000, ctx.exchange.assets[0]), + "Trading is suspended for this reference rate" + ); + + assert_breakerRecovers(dependencyContext, breakers[j], j); + } + } + } + } +} diff --git a/test/fork-tests/TestAsserts.t.sol b/test/fork/TestAsserts.sol similarity index 73% rename from test/fork-tests/TestAsserts.t.sol rename to test/fork/TestAsserts.sol index 7187bf3c..4d786b94 100644 --- a/test/fork-tests/TestAsserts.t.sol +++ b/test/fork/TestAsserts.sol @@ -1,34 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import { Test } from "celo-foundry/Test.sol"; -import { console2 } from "forge-std/console2.sol"; +import { Test } from "mento-std/Test.sol"; import { console } from "forge-std/console.sol"; -import { Utils } from "./Utils.t.sol"; +import { Utils } from "./Utils.sol"; -import { IERC20Metadata } from "contracts/common/interfaces/IERC20Metadata.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { IBreaker } from "contracts/interfaces/IBreaker.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { BiPoolManager } from "contracts/swap/BiPoolManager.sol"; -import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; -import { WithCooldown } from "contracts/oracles/breakers/WithCooldown.sol"; -import { MedianDeltaBreaker } from "contracts/oracles/breakers/MedianDeltaBreaker.sol"; -import { ValueDeltaBreaker } from "contracts/oracles/breakers/ValueDeltaBreaker.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; contract TestAsserts is Test { using Utils for Utils.Context; - using Utils for TradingLimits.Config; - using Utils for TradingLimits.State; + using Utils for ITradingLimits.Config; + using Utils for ITradingLimits.State; using Utils for uint8; using Utils for uint256; - using SafeMath for uint256; - using TradingLimits for TradingLimits.State; - using TradingLimits for TradingLimits.Config; using FixidityLib for FixidityLib.Fraction; uint8 private constant L0 = 1; // 0b001 Limit0 @@ -40,12 +31,7 @@ contract TestAsserts is Test { // ========================= Swap Asserts ========================= // - function assert_swapIn( - Utils.Context memory ctx, - address from, - address to, - uint256 sellAmount - ) internal { + function assert_swapIn(Utils.Context memory ctx, address from, address to, uint256 sellAmount) internal { FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(from); FixidityLib.Fraction memory amountIn = sellAmount.toUnitsFixed(from); FixidityLib.Fraction memory amountOut = ctx.swapIn(from, to, sellAmount).toUnitsFixed(to); @@ -54,12 +40,7 @@ contract TestAsserts is Test { assertApproxEqAbs(amountOut.unwrap(), expectedAmountOut.unwrap(), pc10.multiply(expectedAmountOut).unwrap()); } - function assert_swapOut( - Utils.Context memory ctx, - address from, - address to, - uint256 buyAmount - ) internal { + function assert_swapOut(Utils.Context memory ctx, address from, address to, uint256 buyAmount) internal { FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(from); FixidityLib.Fraction memory amountOut = buyAmount.toUnitsFixed(to); FixidityLib.Fraction memory amountIn = ctx.swapOut(from, to, buyAmount).toUnitsFixed(from); @@ -76,8 +57,8 @@ contract TestAsserts is Test { string memory revertReason ) internal { ctx.addReportsIfNeeded(); - ctx.t.mint(from, ctx.trader, sellAmount); - IERC20Metadata(from).approve(address(ctx.broker), sellAmount); + ctx.t._deal(from, ctx.trader, sellAmount, true); + IERC20(from).approve(address(ctx.broker), sellAmount); uint256 minAmountOut = ctx.broker.getAmountOut(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount); vm.expectRevert(bytes(revertReason)); ctx.broker.swapIn(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount, minAmountOut); @@ -92,24 +73,19 @@ contract TestAsserts is Test { ) internal { ctx.addReportsIfNeeded(); uint256 maxAmountIn = ctx.broker.getAmountIn(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount); - ctx.t.mint(from, ctx.trader, maxAmountIn); - IERC20Metadata(from).approve(address(ctx.broker), maxAmountIn); + ctx.t._deal(from, ctx.trader, maxAmountIn, true); + IERC20(from).approve(address(ctx.broker), maxAmountIn); vm.expectRevert(bytes(revertReason)); ctx.broker.swapOut(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount, maxAmountIn); } // ========================= Trading Limit Asserts ========================= // - function assert_swapOverLimitFails( - Utils.Context memory ctx, - address from, - address to, - uint8 limit - ) internal { - TradingLimits.Config memory fromLimitConfig = ctx.tradingLimitsConfig(from); - TradingLimits.Config memory toLimitConfig = ctx.tradingLimitsConfig(to); + function assert_swapOverLimitFails(Utils.Context memory ctx, address from, address to, uint8 limit) internal { + ITradingLimits.Config memory fromLimitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.Config memory toLimitConfig = ctx.tradingLimitsConfig(to); console.log( - string(abi.encodePacked("Swapping ", IERC20Metadata(from).symbol(), " -> ", IERC20Metadata(to).symbol())), + string(abi.encodePacked("Swapping ", IERC20(from).symbol(), " -> ", IERC20(to).symbol())), "with limit", limit.limitString() ); @@ -151,10 +127,10 @@ contract TestAsserts is Test { revert("Invalid limit"); } - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - TradingLimits.State memory limitState = ctx.tradingLimitsState(from); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.State memory limitState = ctx.tradingLimitsState(from); - uint256 inflowRequiredUnits = uint256(limitConfig.getLimit(limit) - limitState.getNetflow(limit)) + 1; + uint256 inflowRequiredUnits = uint256(int256(limitConfig.getLimit(limit)) - limitState.getNetflow(limit)) + 1; console.log("Inflow required to pass limit: ", inflowRequiredUnits); assert_swapInFails(ctx, from, to, inflowRequiredUnits.toSubunits(from), limit.revertReason()); } @@ -185,19 +161,15 @@ contract TestAsserts is Test { revert("Invalid limit"); } - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - TradingLimits.State memory limitState = ctx.tradingLimitsState(to); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + ITradingLimits.State memory limitState = ctx.tradingLimitsState(to); - uint256 outflowRequiredUnits = uint256(limitConfig.getLimit(limit) + limitState.getNetflow(limit)) + 1; + uint256 outflowRequiredUnits = uint256(int256(limitConfig.getLimit(limit)) + limitState.getNetflow(limit)) + 1; console.log("Outflow required: ", outflowRequiredUnits); assert_swapOutFails(ctx, from, to, outflowRequiredUnits.toSubunits(to), limit.revertReason()); } - function swapUntilL0_onInflow( - Utils.Context memory ctx, - address from, - address to - ) internal { + function swapUntilL0_onInflow(Utils.Context memory ctx, address from, address to) internal { /* * L0[from] -> to * This function will do valid swaps until just before L0 is hit @@ -205,14 +177,14 @@ contract TestAsserts is Test { * of the limit because `from` flows into the reserve. */ - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - console.log("🏷️ [%d] Swap until L0=%d on inflow", block.timestamp, uint256(limitConfig.limit0)); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + console.log(unicode"🏷️ [%d] Swap until L0=%d on inflow", block.timestamp, uint256(int256(limitConfig.limit0))); uint256 maxPossible; uint256 maxPossibleUntilLimit; do { int48 maxPossibleUntilLimitUnits = ctx.maxPossibleInflow(from); require(maxPossibleUntilLimitUnits >= 0, "max possible trade amount is negative"); - maxPossibleUntilLimit = uint256(maxPossibleUntilLimitUnits).toSubunits(from); + maxPossibleUntilLimit = uint256(int256(maxPossibleUntilLimitUnits)).toSubunits(from); maxPossible = ctx.maxSwapIn(maxPossibleUntilLimit, from, to); if (maxPossible > 0) { @@ -222,20 +194,16 @@ contract TestAsserts is Test { ctx.logNetflows(from); } - function swapUntilL1_onInflow( - Utils.Context memory ctx, - address from, - address to - ) internal { + function swapUntilL1_onInflow(Utils.Context memory ctx, address from, address to) internal { /* * L1[from] -> to * This function will do valid swaps until just before L1 is hit * during inflow on `from`, therfore we check the positive end * of the limit because `from` flows into the reserve. */ - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - TradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); - console.log("🏷️ [%d] Swap until L1=%d on inflow", block.timestamp, uint256(limitConfig.limit1)); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); + console.log(unicode"🏷️ [%d] Swap until L1=%d on inflow", block.timestamp, uint256(int256(limitConfig.limit1))); int48 maxPerSwap = limitConfig.limit0; while (limitState.netflow1 + maxPerSwap <= limitConfig.limit1) { skip(limitConfig.timestep0 + 1); @@ -249,20 +217,16 @@ contract TestAsserts is Test { ensureRateActive(ctx); } - function swapUntilLG_onInflow( - Utils.Context memory ctx, - address from, - address to - ) internal { + function swapUntilLG_onInflow(Utils.Context memory ctx, address from, address to) internal { /* * L1[from] -> to * This function will do valid swaps until just before LG is hit * during inflow on `from`, therfore we check the positive end * of the limit because `from` flows into the reserve. */ - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - TradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); - console.log("🏷️ [%d] Swap until LG=%d on inflow", block.timestamp, uint256(limitConfig.limitGlobal)); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); + console.log(unicode"🏷️ [%d] Swap until LG=%d on inflow", block.timestamp, uint256(int256(limitConfig.limitGlobal))); if (limitConfig.isLimitEnabled(L1)) { int48 maxPerSwap = limitConfig.limit0; @@ -285,11 +249,7 @@ contract TestAsserts is Test { } } - function swapUntilL0_onOutflow( - Utils.Context memory ctx, - address from, - address to - ) public { + function swapUntilL0_onOutflow(Utils.Context memory ctx, address from, address to) public { /* * from -> L0[to] * This function will do valid swaps until just before L0 is hit @@ -297,14 +257,14 @@ contract TestAsserts is Test { * of the limit because `to` flows out of the reserve. */ - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - console.log("🏷️ [%d] Swap until L0=%d on outflow", block.timestamp, uint256(limitConfig.limit0)); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + console.log(unicode"🏷️ [%d] Swap until L0=%d on outflow", block.timestamp, uint256(int256(limitConfig.limit0))); uint256 maxPossible; uint256 maxPossibleUntilLimit; do { int48 maxPossibleUntilLimitUnits = ctx.maxPossibleOutflow(to); require(maxPossibleUntilLimitUnits >= 0, "max possible trade amount is negative"); - maxPossibleUntilLimit = uint256(maxPossibleUntilLimitUnits).toSubunits(to); + maxPossibleUntilLimit = uint256(int256(maxPossibleUntilLimitUnits)).toSubunits(to); maxPossible = ctx.maxSwapOut(maxPossibleUntilLimit, to); if (maxPossible > 0) { @@ -314,21 +274,17 @@ contract TestAsserts is Test { ctx.logNetflows(to); } - function swapUntilL1_onOutflow( - Utils.Context memory ctx, - address from, - address to - ) public { + function swapUntilL1_onOutflow(Utils.Context memory ctx, address from, address to) public { /* * from -> L1[to] * This function will do valid swaps until just before L1 is hit * during outflow on `to`, therfore we check the negative end * of the limit because `to` flows out of the reserve. */ - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - TradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); - console.log("🏷️ [%d] Swap until L1=%d on outflow", block.timestamp, uint256(limitConfig.limit1)); + console.log(unicode"🏷️ [%d] Swap until L1=%d on outflow", block.timestamp, uint48(limitConfig.limit1)); int48 maxPerSwap = limitConfig.limit0; while (limitState.netflow1 - maxPerSwap >= -1 * limitConfig.limit1) { @@ -345,20 +301,16 @@ contract TestAsserts is Test { skip(limitConfig.timestep0 + 1); } - function swapUntilLG_onOutflow( - Utils.Context memory ctx, - address from, - address to - ) public { + function swapUntilLG_onOutflow(Utils.Context memory ctx, address from, address to) public { /* * from -> LG[to] * This function will do valid swaps until just before LG is hit * during outflow on `to`, therfore we check the negative end * of the limit because `to` flows out of the reserve. */ - TradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - TradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); - console.log("🏷️ [%d] Swap until LG=%d on outflow", block.timestamp, uint256(limitConfig.limitGlobal)); + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); + console.log(unicode"🏷️ [%d] Swap until LG=%d on outflow", block.timestamp, uint48(limitConfig.limitGlobal)); if (limitConfig.isLimitEnabled(L1)) { int48 maxPerSwap = limitConfig.limit0; @@ -385,11 +337,7 @@ contract TestAsserts is Test { // ========================= Circuit Breaker Asserts ========================= // - function assert_breakerBreaks( - Utils.Context memory ctx, - address breaker, - uint256 breakerIndex - ) public { + function assert_breakerBreaks(Utils.Context memory ctx, address breaker, uint256 breakerIndex) public { // XXX: There is currently no straightforward way to determine what type of a breaker // we are dealing with, so we will use the deployment setup that we currently chose, // where the medianDeltaBreaker gets deployed first and the valueDeltaBreaker second. @@ -410,11 +358,11 @@ contract TestAsserts is Test { uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 // trigger breaker by setting new median to ema - (threshold + 0.001% buffer) - uint256 currentEMA = MedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); + uint256 currentEMA = IMedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); // small buffer because of rounding errors - uint256 maxPercent = fixed1.add(rateChangeThreshold.add(thresholdBuffer)); - uint256 newMedian = currentEMA.mul(maxPercent).div(fixed1); + uint256 maxPercent = fixed1 + rateChangeThreshold + thresholdBuffer; + uint256 newMedian = (currentEMA * maxPercent) / fixed1; console.log("Current Median: ", currentMedian); console.log("Current EMA: ", currentEMA); @@ -426,11 +374,11 @@ contract TestAsserts is Test { uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 // trigger breaker by setting new median to ema + (threshold + 0.001% buffer) - uint256 currentEMA = MedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); + uint256 currentEMA = IMedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); // small buffer because of rounding errors - uint256 maxPercent = fixed1.sub(rateChangeThreshold.add(thresholdBuffer)); - uint256 newMedian = currentEMA.mul(maxPercent).div(fixed1); + uint256 maxPercent = fixed1 - (rateChangeThreshold + thresholdBuffer); + uint256 newMedian = (currentEMA * maxPercent) / fixed1; console.log("Current Median: ", currentMedian); console.log("Current EMA: ", currentEMA); @@ -444,8 +392,8 @@ contract TestAsserts is Test { // trigger breaker by setting new median to reference value + threshold + 1 uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(_breaker); - uint256 maxPercent = fixed1.add(rateChangeThreshold); - uint256 newMedian = referenceValue.mul(maxPercent).div(fixed1); + uint256 maxPercent = fixed1 + rateChangeThreshold; + uint256 newMedian = (referenceValue * maxPercent) / fixed1; newMedian = newMedian + 1; console.log("Current Median: ", currentMedian); @@ -460,8 +408,8 @@ contract TestAsserts is Test { // trigger breaker by setting new median to reference value - threshold - 1 uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(_breaker); - uint256 maxPercent = fixed1.sub(rateChangeThreshold); - uint256 newMedian = referenceValue.mul(maxPercent).div(fixed1); + uint256 maxPercent = fixed1 - rateChangeThreshold; + uint256 newMedian = (referenceValue * maxPercent) / fixed1; newMedian = newMedian - 1; console.log("Current Median: ", currentMedian); @@ -470,11 +418,7 @@ contract TestAsserts is Test { assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); } - function assert_breakerRecovers( - Utils.Context memory ctx, - address breaker, - uint256 breakerIndex - ) public { + function assert_breakerRecovers(Utils.Context memory ctx, address breaker, uint256 breakerIndex) public { // XXX: There is currently no straightforward way to determine what type of a breaker // we are dealing with, so we will use the deployment setup that we currently chose, // where the medianDeltaBreaker gets deployed first and the valueDeltaBreaker second. @@ -491,38 +435,40 @@ contract TestAsserts is Test { function assert_medianDeltaBreakerRecovers(Utils.Context memory ctx, address _breaker) internal { uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 + IMedianDeltaBreaker breaker = IMedianDeltaBreaker(_breaker); // trigger breaker by setting new median to ema + threshold + 0.001% - uint256 currentEMA = MedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); + uint256 currentEMA = breaker.medianRatesEMA(ctx.getReferenceRateFeedID()); uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); - uint256 maxPercent = fixed1.add(rateChangeThreshold.add(thresholdBuffer)); - uint256 newMedian = currentEMA.mul(maxPercent).div(fixed1); + uint256 maxPercent = fixed1 + rateChangeThreshold + thresholdBuffer; + uint256 newMedian = (currentEMA * maxPercent) / fixed1; console.log("Current Median: ", currentMedian); console.log("New Median: ", newMedian); assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); // wait for cool down and reset by setting new median to ema - uint256 cooldown = WithCooldown(_breaker).getCooldown(ctx.getReferenceRateFeedID()); + uint256 cooldown = breaker.getCooldown(ctx.getReferenceRateFeedID()); if (cooldown == 0) { changePrank(ctx.breakerBox.owner()); ctx.breakerBox.setRateFeedTradingMode(ctx.getReferenceRateFeedID(), 0); } else { skip(cooldown); - currentEMA = MedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); + currentEMA = breaker.medianRatesEMA(ctx.getReferenceRateFeedID()); assert_breakerRecovers_withNewMedian(ctx, currentEMA); } } function assert_valueDeltaBreakerRecovers(Utils.Context memory ctx, address _breaker) internal { uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 + IValueDeltaBreaker breaker = IValueDeltaBreaker(_breaker); // trigger breaker by setting new median to reference value + threshold + 1 uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(_breaker); - uint256 maxPercent = fixed1.add(rateChangeThreshold); - uint256 newMedian = referenceValue.mul(maxPercent).div(fixed1); + uint256 maxPercent = fixed1 + rateChangeThreshold; + uint256 newMedian = (referenceValue * maxPercent) / fixed1; newMedian = newMedian + 1; console.log("Current Median: ", currentMedian); @@ -531,7 +477,7 @@ contract TestAsserts is Test { assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); // wait for cool down and reset by setting new median to refernece value - uint256 cooldown = WithCooldown(_breaker).getCooldown(ctx.getReferenceRateFeedID()); + uint256 cooldown = breaker.getCooldown(ctx.getReferenceRateFeedID()); if (cooldown == 0) { changePrank(ctx.breakerBox.owner()); ctx.breakerBox.setRateFeedTradingMode(ctx.getReferenceRateFeedID(), 0); @@ -570,7 +516,7 @@ contract TestAsserts is Test { // Always do a small update in order to make sure // the breakers are warm. (uint256 currentRate, ) = ctx.sortedOracles.medianRate(rateFeedID); - newMedian = currentRate.add(currentRate.div(100_000_000)); // a small increase + newMedian = currentRate + (currentRate / 100_000_000); // a small increase ctx.updateOracleMedianRate(newMedian); uint8 tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); uint256 attempts = 0; @@ -585,10 +531,10 @@ contract TestAsserts is Test { uint256 breakerIndex; for (uint256 i = 0; i < _breakers.length; i++) { if (ctx.breakerBox.isBreakerEnabled(_breakers[i], rateFeedID)) { - (uint8 _tradingMode, , ) = ctx.breakerBox.rateFeedBreakerStatus(rateFeedID, _breakers[i]); - if (_tradingMode != 0) { + IBreakerBox.BreakerStatus memory status = ctx.breakerBox.rateFeedBreakerStatus(rateFeedID, _breakers[i]); + if (status.tradingMode != 0) { breakerIndex = i; - cooldown = WithCooldown(_breakers[i]).getCooldown(rateFeedID); + cooldown = IValueDeltaBreaker(_breakers[i]).getCooldown(rateFeedID); break; } } @@ -605,16 +551,15 @@ contract TestAsserts is Test { } } - function newMedianToResetBreaker(Utils.Context memory ctx, uint256 breakerIndex) - internal - view - returns (uint256 newMedian) - { + function newMedianToResetBreaker( + Utils.Context memory ctx, + uint256 breakerIndex + ) internal view returns (uint256 newMedian) { address[] memory _breakers = ctx.breakerBox.getBreakers(); bool isMedianDeltaBreaker = breakerIndex == 0; bool isValueDeltaBreaker = breakerIndex == 1; if (isMedianDeltaBreaker) { - uint256 currentEMA = MedianDeltaBreaker(_breakers[breakerIndex]).medianRatesEMA(ctx.getReferenceRateFeedID()); + uint256 currentEMA = IMedianDeltaBreaker(_breakers[breakerIndex]).medianRatesEMA(ctx.getReferenceRateFeedID()); return currentEMA; } else if (isValueDeltaBreaker) { return ctx.getValueDeltaBreakerReferenceValue(_breakers[breakerIndex]); diff --git a/test/fork/TokenUpgrade.t.sol b/test/fork/TokenUpgrade.t.sol new file mode 100644 index 00000000..36e2bdba --- /dev/null +++ b/test/fork/TokenUpgrade.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { Test } from "mento-std/Test.sol"; +import { CELO_ID } from "mento-std/Constants.sol"; + +import { WithRegistry } from "../utils/WithRegistry.sol"; + +import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; +import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; + +contract TokenUpgradeForkTest is Test, WithRegistry { + // solhint-disable-next-line func-name-mixedcase + function test_upgrade() public { + fork(CELO_ID, 22856317); + + address stableToken = registry.getAddressForString("StableToken"); + ICeloProxy stableTokenProxy = ICeloProxy(stableToken); + console.log(ICeloProxy(stableToken)._getImplementation()); + console.log(ICeloProxy(stableToken)._getOwner()); + vm.startPrank(ICeloProxy(stableToken)._getOwner()); + address mentoERC20Impl = deployCode("StableTokenV2", abi.encode(false)); + stableTokenProxy._setImplementation(mentoERC20Impl); + + IStableTokenV2 cusd = IStableTokenV2(stableToken); + cusd.initializeV2( + registry.getAddressForString("Broker"), + registry.getAddressForString("Validators"), + registry.getAddressForString("Exchange") + ); + + address governance = registry.getAddressForString("Governance"); + cusd.balanceOf(governance); + + changePrank(governance); + cusd.transfer(address(this), 1 ether); + cusd.balanceOf(address(this)); + } +} diff --git a/test/fork-tests/Utils.t.sol b/test/fork/Utils.sol similarity index 65% rename from test/fork-tests/Utils.t.sol rename to test/fork/Utils.sol index 9943be8c..737ff544 100644 --- a/test/fork-tests/Utils.t.sol +++ b/test/fork/Utils.sol @@ -1,64 +1,47 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; +pragma solidity ^0.8; pragma experimental ABIEncoderV2; -import { BaseForkTest } from "./BaseForkTest.t.sol"; -import { console2 } from "forge-std/console2.sol"; import { console } from "forge-std/console.sol"; import { Vm } from "forge-std/Vm.sol"; +import { VM_ADDRESS } from "mento-std/Constants.sol"; -import { IERC20Metadata } from "contracts/common/interfaces/IERC20Metadata.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; - -import { Broker } from "contracts/swap/Broker.sol"; -import { BiPoolManager } from "contracts/swap/BiPoolManager.sol"; -import { BreakerBox } from "contracts/oracles/BreakerBox.sol"; -import { SortedOracles } from "contracts/common/SortedOracles.sol"; -import { MedianDeltaBreaker } from "contracts/oracles/breakers/MedianDeltaBreaker.sol"; -import { ValueDeltaBreaker } from "contracts/oracles/breakers/ValueDeltaBreaker.sol"; -import { WithThreshold } from "contracts/oracles/breakers/WithThreshold.sol"; - -/** - * @title IBrokerWithCasts - * @notice Interface for Broker with tuple -> struct casting - * @dev This is used to access the internal trading limits state and - * config as structs as opposed to tuples. - */ -interface IBrokerWithCasts { - function tradingLimitsState(bytes32 id) external view returns (TradingLimits.State memory); - - function tradingLimitsConfig(bytes32 id) external view returns (TradingLimits.Config memory); -} +import { BaseForkTest } from "./BaseForkTest.sol"; + +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; +import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; +import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; library Utils { - using SafeMath for uint256; using FixidityLib for FixidityLib.Fraction; - using TradingLimits for TradingLimits.State; uint8 private constant L0 = 1; // 0b001 Limit0 uint8 private constant L1 = 2; // 0b010 Limit1 uint8 private constant LG = 4; // 0b100 LimitGlobal - address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); - Vm public constant vm = Vm(VM_ADDRESS); struct Context { BaseForkTest t; - Broker broker; - IBrokerWithCasts brokerWithCasts; - SortedOracles sortedOracles; - BreakerBox breakerBox; + IBroker broker; + ISortedOracles sortedOracles; + IBreakerBox breakerBox; address exchangeProvider; bytes32 exchangeId; address rateFeedID; IExchangeProvider.Exchange exchange; address trader; + ITradingLimitsHarness tradingLimits; } function newContext(address _t, uint256 index) public view returns (Context memory ctx) { @@ -68,14 +51,14 @@ library Utils { ctx = Context( t, t.broker(), - IBrokerWithCasts(address(t.broker())), t.sortedOracles(), t.breakerBox(), exchangeProvider, exchange.exchangeId, address(0), exchange, - t.trader() + t.trader(), + t.tradingLimits() ); } @@ -85,21 +68,21 @@ library Utils { ctx = Context( t, t.broker(), - IBrokerWithCasts(address(t.broker())), t.sortedOracles(), t.breakerBox(), address(0), bytes32(0), rateFeed, IExchangeProvider.Exchange(0, new address[](0)), - t.trader() + t.trader(), + t.tradingLimits() ); } function getContextForRateFeedID(address _t, address rateFeedID) public view returns (Context memory) { BaseForkTest t = BaseForkTest(_t); (address biPoolManagerAddr, ) = t.exchanges(0); - uint256 nOfExchanges = BiPoolManager(biPoolManagerAddr).getExchanges().length; + uint256 nOfExchanges = IBiPoolManager(biPoolManagerAddr).getExchanges().length; for (uint256 i = 0; i < nOfExchanges; i++) { Context memory ctx = newContext(_t, i); if (getReferenceRateFeedID(ctx) == rateFeedID) { @@ -111,69 +94,53 @@ library Utils { // ========================= Swaps ========================= - function swapIn( - Context memory ctx, - address from, - address to, - uint256 sellAmount - ) public returns (uint256) { - ctx.t.mint(from, ctx.trader, sellAmount); + function swapIn(Context memory ctx, address from, address to, uint256 sellAmount) public returns (uint256) { + ctx.t._deal(from, ctx.trader, sellAmount, true); changePrank(ctx.trader); - IERC20Metadata(from).approve(address(ctx.broker), sellAmount); + IERC20(from).approve(address(ctx.broker), sellAmount); addReportsIfNeeded(ctx); uint256 minAmountOut = ctx.broker.getAmountOut(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount); console.log( - string(abi.encodePacked("🤝 swapIn(", toSymbol(from), "->", toSymbol(to), ", amountIn: %d, minAmountOut:%d)")), + string( + abi.encodePacked(unicode"🤝 swapIn(", toSymbol(from), "->", toSymbol(to), ", amountIn: %d, minAmountOut:%d)") + ), toUnits(sellAmount, from), toUnits(minAmountOut, to) ); return ctx.broker.swapIn(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount, minAmountOut); } - function swapOut( - Context memory ctx, - address from, - address to, - uint256 buyAmount - ) public returns (uint256) { + function swapOut(Context memory ctx, address from, address to, uint256 buyAmount) public returns (uint256) { addReportsIfNeeded(ctx); uint256 maxAmountIn = ctx.broker.getAmountIn(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount); - ctx.t.mint(from, ctx.trader, maxAmountIn); + ctx.t._deal(from, ctx.trader, maxAmountIn, true); changePrank(ctx.trader); - IERC20Metadata(from).approve(address(ctx.broker), maxAmountIn); + IERC20(from).approve(address(ctx.broker), maxAmountIn); console.log( - string(abi.encodePacked("🤝 swapOut(", toSymbol(from), "->", toSymbol(to), ",amountOut: %d, maxAmountIn: %d)")), + string( + abi.encodePacked(unicode"🤝 swapOut(", toSymbol(from), "->", toSymbol(to), ",amountOut: %d, maxAmountIn: %d)") + ), toUnits(buyAmount, to), toUnits(maxAmountIn, from) ); return ctx.broker.swapOut(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount, maxAmountIn); } - function shouldUpdateBuckets(Context memory ctx) - internal - view - returns ( - bool, - bool, - bool, - bool, - bool - ) - { - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); + function shouldUpdateBuckets(Context memory ctx) internal view returns (bool, bool, bool, bool, bool) { + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); (bool isReportExpired, ) = ctx.sortedOracles.isOldestReportExpired(exchange.config.referenceRateFeedID); // solhint-disable-next-line not-rely-on-time - bool timePassed = now >= exchange.lastBucketUpdate.add(exchange.config.referenceRateResetFrequency); + bool timePassed = block.timestamp >= exchange.lastBucketUpdate + exchange.config.referenceRateResetFrequency; bool enoughReports = (ctx.sortedOracles.numRates(exchange.config.referenceRateFeedID) >= exchange.config.minimumReports); // solhint-disable-next-line not-rely-on-time bool medianReportRecent = ctx.sortedOracles.medianTimestamp(exchange.config.referenceRateFeedID) > - now.sub(exchange.config.referenceRateResetFrequency); + block.timestamp - exchange.config.referenceRateResetFrequency; return ( timePassed, @@ -185,27 +152,27 @@ library Utils { } function getUpdatedBuckets(Context memory ctx) internal view returns (uint256 bucket0, uint256 bucket1) { - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); bucket0 = exchange.config.stablePoolResetSize; uint256 exchangeRateNumerator; uint256 exchangeRateDenominator; (exchangeRateNumerator, exchangeRateDenominator) = getReferenceRate(ctx); - bucket1 = exchangeRateDenominator.mul(bucket0).div(exchangeRateNumerator); + bucket1 = (exchangeRateDenominator * bucket0) / exchangeRateNumerator; } function addReportsIfNeeded(Context memory ctx) internal { // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); (bool timePassed, bool enoughReports, bool medianReportRecent, bool isReportExpired, ) = shouldUpdateBuckets(ctx); // logPool(ctx); if (timePassed && (!medianReportRecent || isReportExpired || !enoughReports)) { (uint256 newMedian, ) = ctx.sortedOracles.medianRate(pool.config.referenceRateFeedID); (timePassed, enoughReports, medianReportRecent, isReportExpired, ) = shouldUpdateBuckets(ctx); - updateOracleMedianRate(ctx, newMedian.mul(1_000_001).div(1_000_000)); + updateOracleMedianRate(ctx, (newMedian * 1_000_001) / 1_000_000); // logPool(ctx); return; @@ -219,36 +186,32 @@ library Utils { address to ) internal view returns (uint256 maxPossible) { // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); uint256 toBucket = (pool.asset0 == to ? pool.bucket0 : pool.bucket1) - 1; (, , , , bool shouldUpdate) = shouldUpdateBuckets(ctx); if (shouldUpdate) { (uint256 bucket0, uint256 bucket1) = getUpdatedBuckets(ctx); toBucket = (pool.asset0 == to ? bucket0 : bucket1) - 1; } - toBucket = toBucket.div(biPoolManager.tokenPrecisionMultipliers(to)); + toBucket = toBucket / biPoolManager.tokenPrecisionMultipliers(to); maxPossible = ctx.broker.getAmountIn(ctx.exchangeProvider, ctx.exchangeId, from, to, toBucket); if (maxPossible > desired) { maxPossible = desired; } } - function maxSwapOut( - Context memory ctx, - uint256 desired, - address to - ) internal view returns (uint256 maxPossible) { + function maxSwapOut(Context memory ctx, uint256 desired, address to) internal view returns (uint256 maxPossible) { // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); uint256 maxPossible_ = (pool.asset0 == to ? pool.bucket0 : pool.bucket1) - 1; (, , , , bool shouldUpdate) = shouldUpdateBuckets(ctx); if (shouldUpdate) { (uint256 bucket0, uint256 bucket1) = getUpdatedBuckets(ctx); maxPossible_ = (pool.asset0 == to ? bucket0 : bucket1) - 1; } - maxPossible = maxPossible_.div(biPoolManager.tokenPrecisionMultipliers(to)); + maxPossible = maxPossible / biPoolManager.tokenPrecisionMultipliers(to); if (maxPossible > desired) { maxPossible = desired; } @@ -256,11 +219,10 @@ library Utils { // ========================= Sorted Oracles ========================= - function getReferenceRateFraction(Context memory ctx, address baseAsset) - internal - view - returns (FixidityLib.Fraction memory) - { + function getReferenceRateFraction( + Context memory ctx, + address baseAsset + ) internal view returns (FixidityLib.Fraction memory) { (uint256 numerator, uint256 denominator) = getReferenceRate(ctx); address asset0 = ctx.exchange.assets[0]; if (baseAsset == asset0) { @@ -282,19 +244,19 @@ library Utils { return ctx.rateFeedID; } // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); return pool.config.referenceRateFeedID; } function getValueDeltaBreakerReferenceValue(Context memory ctx, address _breaker) internal view returns (uint256) { - ValueDeltaBreaker breaker = ValueDeltaBreaker(_breaker); + IValueDeltaBreaker breaker = IValueDeltaBreaker(_breaker); address rateFeedID = getReferenceRateFeedID(ctx); return breaker.referenceValues(rateFeedID); } function getBreakerRateChangeThreshold(Context memory ctx, address _breaker) internal view returns (uint256) { - MedianDeltaBreaker breaker = MedianDeltaBreaker(_breaker); + IMedianDeltaBreaker breaker = IMedianDeltaBreaker(_breaker); address rateFeedID = getReferenceRateFeedID(ctx); uint256 rateChangeThreshold = breaker.defaultRateChangeThreshold(); @@ -309,7 +271,7 @@ library Utils { address rateFeedID = getReferenceRateFeedID(ctx); address[] memory oracles = ctx.sortedOracles.getOracles(rateFeedID); require(oracles.length > 0, "No oracles for rateFeedID"); - console.log("🔮 Updating oracles to new median: ", newMedian); + console.log(unicode"🔮 Updating oracles to new median: ", newMedian); for (uint256 i = 0; i < oracles.length; i++) { skip(5); address oracle = oracles[i]; @@ -332,56 +294,55 @@ library Utils { // ========================= Trading Limits ========================= function isLimitConfigured(Context memory ctx, bytes32 limitId) public view returns (bool) { - TradingLimits.Config memory limitConfig = ctx.brokerWithCasts.tradingLimitsConfig(limitId); + ITradingLimits.Config memory limitConfig = ctx.broker.tradingLimitsConfig(limitId); return limitConfig.flags > uint8(0); } - function tradingLimitsConfig(Context memory ctx, bytes32 limitId) public view returns (TradingLimits.Config memory) { - return ctx.brokerWithCasts.tradingLimitsConfig(limitId); + function tradingLimitsConfig(Context memory ctx, bytes32 limitId) public view returns (ITradingLimits.Config memory) { + return ctx.broker.tradingLimitsConfig(limitId); } - function tradingLimitsState(Context memory ctx, bytes32 limitId) public view returns (TradingLimits.State memory) { - return ctx.brokerWithCasts.tradingLimitsState(limitId); + function tradingLimitsState(Context memory ctx, bytes32 limitId) public view returns (ITradingLimits.State memory) { + return ctx.broker.tradingLimitsState(limitId); } - function tradingLimitsConfig(Context memory ctx, address asset) public view returns (TradingLimits.Config memory) { + function tradingLimitsConfig(Context memory ctx, address asset) public view returns (ITradingLimits.Config memory) { bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.brokerWithCasts.tradingLimitsConfig(ctx.exchangeId ^ assetBytes32); + return ctx.broker.tradingLimitsConfig(ctx.exchangeId ^ assetBytes32); } - function tradingLimitsState(Context memory ctx, address asset) public view returns (TradingLimits.State memory) { + function tradingLimitsState(Context memory ctx, address asset) public view returns (ITradingLimits.State memory) { bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.brokerWithCasts.tradingLimitsState(ctx.exchangeId ^ assetBytes32); + return ctx.broker.tradingLimitsState(ctx.exchangeId ^ assetBytes32); } - function refreshedTradingLimitsState(Context memory ctx, address asset) - public - view - returns (TradingLimits.State memory) - { - TradingLimits.Config memory config = tradingLimitsConfig(ctx, asset); + function refreshedTradingLimitsState( + Context memory ctx, + address asset + ) public view returns (ITradingLimits.State memory) { + ITradingLimits.Config memory config = tradingLimitsConfig(ctx, asset); // Netflow might be outdated because of a skip(...) call and doing // an update(0) would reset the netflow if enough time has passed. - return tradingLimitsState(ctx, asset).update(config, 0, 0); + return ctx.tradingLimits.update(tradingLimitsState(ctx, asset), config, 0, 0); } - function isLimitEnabled(TradingLimits.Config memory config, uint8 limit) internal pure returns (bool) { + function isLimitEnabled(ITradingLimits.Config memory config, uint8 limit) internal pure returns (bool) { return (config.flags & limit) > 0; } - function getLimit(TradingLimits.Config memory config, uint8 limit) internal pure returns (int48) { + function getLimit(ITradingLimits.Config memory config, uint8 limit) internal pure returns (uint256) { if (limit == L0) { - return config.limit0; + return uint256(int256(config.limit0)); } else if (limit == L1) { - return config.limit1; + return uint256(int256(config.limit1)); } else if (limit == LG) { - return config.limitGlobal; + return uint256(int256(config.limitGlobal)); } else { revert("invalid limit"); } } - function getNetflow(TradingLimits.State memory state, uint8 limit) internal pure returns (int48) { + function getNetflow(ITradingLimits.State memory state, uint8 limit) internal pure returns (int256) { if (limit == L0) { return state.netflow0; } else if (limit == L1) { @@ -418,8 +379,8 @@ library Utils { } function maxPossibleInflow(Context memory ctx, address from) internal view returns (int48) { - TradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, from); - TradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, from); + ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, from); + ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, from); int48 maxInflowL0 = limitConfig.limit0 - limitState.netflow0; int48 maxInflowL1 = limitConfig.limit1 - limitState.netflow1; int48 maxInflowLG = limitConfig.limitGlobal - limitState.netflowGlobal; @@ -438,8 +399,8 @@ library Utils { } function maxPossibleOutflow(Context memory ctx, address to) internal view returns (int48) { - TradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, to); - TradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, to); + ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, to); + ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, to); int48 maxOutflowL0 = limitConfig.limit0 + limitState.netflow0 - 1; int48 maxOutflowL1 = limitConfig.limit1 + limitState.netflow1 - 1; int48 maxOutflowLG = limitConfig.limitGlobal + limitState.netflowGlobal - 1; @@ -460,38 +421,32 @@ library Utils { // ========================= Misc ========================= function toSubunits(uint256 units, address token) internal view returns (uint256) { - uint256 tokenBase = 10**uint256(IERC20Metadata(token).decimals()); - return units.mul(tokenBase); + uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); + return units * tokenBase; } function toUnits(uint256 subunits, address token) internal view returns (uint256) { - uint256 tokenBase = 10**uint256(IERC20Metadata(token).decimals()); - return subunits.div(tokenBase); + uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); + return subunits / tokenBase; } function toUnitsFixed(uint256 subunits, address token) internal view returns (FixidityLib.Fraction memory) { - uint256 tokenBase = 10**uint256(IERC20Metadata(token).decimals()); + uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); return FixidityLib.newFixedFraction(subunits, tokenBase); } function toSymbol(address token) internal view returns (string memory) { - return IERC20Metadata(token).symbol(); + return IERC20(token).symbol(); } function ticker(Context memory ctx) internal view returns (string memory) { return - string( - abi.encodePacked( - IERC20Metadata(ctx.exchange.assets[0]).symbol(), - "/", - IERC20Metadata(ctx.exchange.assets[1]).symbol() - ) - ); + string(abi.encodePacked(IERC20(ctx.exchange.assets[0]).symbol(), "/", IERC20(ctx.exchange.assets[1]).symbol())); } function logHeader(Context memory ctx) internal view { console.log("========================================"); - console.log("🔦 Testing pair:", ticker(ctx)); + console.log(unicode"🔦 Testing pair:", ticker(ctx)); console.log("========================================"); } @@ -499,24 +454,20 @@ library Utils { return a > b ? b : a; } - function min( - int48 a, - int48 b, - int48 c - ) internal pure returns (int48) { + function min(int48 a, int48 b, int48 c) internal pure returns (int48) { return min(a, min(b, c)); } function logPool(Context memory ctx) internal view { if (ctx.exchangeId == 0) { - console.log("🎱 RateFeed: %s", ctx.rateFeedID); + console.log(unicode"🎱 RateFeed: %s", ctx.rateFeedID); return; } - BiPoolManager biPoolManager = BiPoolManager(ctx.exchangeProvider); - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); + IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); (bool timePassed, bool enoughReports, bool medianReportRecent, bool isReportExpired, ) = shouldUpdateBuckets(ctx); - console.log("🎱 Pool: %s", ticker(ctx)); + console.log(unicode"🎱 Pool: %s", ticker(ctx)); console.log( "\t timePassed: %s | enoughReports: %s", timePassed ? "true" : "false", @@ -536,21 +487,21 @@ library Utils { } function logNetflows(Context memory ctx, address target) internal view { - TradingLimits.State memory limitState = tradingLimitsState(ctx, target); + ITradingLimits.State memory limitState = tradingLimitsState(ctx, target); console.log( "\t netflow0: %s%d", limitState.netflow0 < 0 ? "-" : "", - uint256(limitState.netflow0 < 0 ? limitState.netflow0 * -1 : limitState.netflow0) + uint256(int256(limitState.netflow0 < 0 ? limitState.netflow0 * -1 : limitState.netflow0)) ); console.log( "\t netflow1: %s%d", limitState.netflow1 < 0 ? "-" : "", - uint256(limitState.netflow1 < 0 ? limitState.netflow1 * -1 : limitState.netflow1) + uint256(int256(limitState.netflow1 < 0 ? limitState.netflow1 * -1 : limitState.netflow1)) ); console.log( "\t netflowGlobal: %s%d", limitState.netflowGlobal < 0 ? "-" : "", - uint256(limitState.netflowGlobal < 0 ? limitState.netflowGlobal * -1 : limitState.netflowGlobal) + uint256(int256(limitState.netflowGlobal < 0 ? limitState.netflowGlobal * -1 : limitState.netflowGlobal)) ); } diff --git a/test/governance/Locking/Locking.fuzz.t.sol b/test/governance/Locking/Locking.fuzz.t.sol deleted file mode 100644 index f950e3aa..00000000 --- a/test/governance/Locking/Locking.fuzz.t.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.18; -// solhint-disable state-visibility - -import { TestERC20 } from "../../utils/TestERC20.sol"; -import { TestLocking } from "../../utils/TestLocking.sol"; -import { Vm } from "forge-std/Vm.sol"; -import { DSTest } from "ds-test/test.sol"; - -/** - * @notice https://github.com/rarible/locking-contracts/tree/4f189a96b3e85602dedfbaf69d9a1f5056d835eb - */ -contract FuzzTestLocking is TestLocking, DSTest { - Vm internal immutable vm = Vm(HEVM_ADDRESS); - - TestERC20 public testERC20; - - address user0; - address user1; - - function setUp() public { - user0 = address(100); - vm.deal(user0, 100 ether); - user1 = address(200); - vm.deal(user1, 100 ether); - testERC20 = new TestERC20(); - this.__Locking_init(testERC20, 0, 1, 3); - - this.incrementBlock(this.WEEK() + 1); - } - - function testLockAmount(uint96 amount) public { - vm.assume(amount < 2**95); - vm.assume(amount > 1e18); - prepareTokens(user0, amount); - lockTokens(user0, user0, uint96(amount), 100, 100); - } - - function testLockSlope(uint32 slope) public { - vm.assume(slope >= minSlopePeriod); - vm.assume(slope <= MAX_SLOPE_PERIOD); - - uint96 amount = 100 * (10**18); - - prepareTokens(user0, amount); - lockTokens(user0, user0, uint96(amount), slope, 100); - } - - function testLockCliff(uint32 cliff) public { - vm.assume(cliff >= minCliffPeriod); - vm.assume(cliff <= MAX_CLIFF_PERIOD); - - uint96 amount = 100 * (10**18); - - prepareTokens(user0, amount); - lockTokens(user0, user0, uint96(amount), 100, cliff); - } - - function prepareTokens(address user, uint256 amount) public { - testERC20.mint(user, amount); - vm.prank(user); - testERC20.approve(address(this), amount); - assertEq(testERC20.balanceOf(user), amount); - assertEq(testERC20.allowance(user, address(this)), amount); - } - - function lockTokens( - address user, - address delegate, - uint96 amount, - uint32 slopePeriod, - uint32 cliff - ) public { - vm.prank(user); - this.lock(user, delegate, amount, slopePeriod, cliff); - - assertEq(this.locked(user), amount); - } -} diff --git a/test/governance/IntegrationTests/GovernanceIntegration.gas.t.sol b/test/integration/governance/GovernanceIntegration.gas.t.sol similarity index 96% rename from test/governance/IntegrationTests/GovernanceIntegration.gas.t.sol rename to test/integration/governance/GovernanceIntegration.gas.t.sol index f7df40f5..86035df7 100644 --- a/test/governance/IntegrationTests/GovernanceIntegration.gas.t.sol +++ b/test/integration/governance/GovernanceIntegration.gas.t.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length, max-states-count -import { TestSetup } from "../TestSetup.sol"; -import { Vm } from "forge-std-next/Vm.sol"; -import { VmExtension } from "test/utils/VmExtension.sol"; +import { addresses, uints } from "mento-std/Array.sol"; +import { Vm } from "forge-std/Vm.sol"; import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; import { GovernanceFactory } from "contracts/governance/GovernanceFactory.sol"; @@ -14,12 +13,13 @@ import { Emission } from "contracts/governance/Emission.sol"; import { Locking } from "contracts/governance/locking/Locking.sol"; import { TimelockController } from "contracts/governance/TimelockController.sol"; +import { VmExtension } from "test/utils/VmExtension.sol"; +import { GovernanceTest } from "test/unit/governance/GovernanceTest.sol"; import { Proposals } from "./Proposals.sol"; -import { Arrays } from "test/utils/Arrays.sol"; import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; -contract GovernanceGasTest is TestSetup { +contract GovernanceGasTest is GovernanceTest { using VmExtension for Vm; GovernanceFactory public factory; @@ -75,8 +75,8 @@ contract GovernanceGasTest is TestSetup { .MentoTokenAllocationParams({ airgrabAllocation: 50, mentoTreasuryAllocation: 100, - additionalAllocationRecipients: Arrays.addresses(address(mentoLabsMultisig)), - additionalAllocationAmounts: Arrays.uints(200) + additionalAllocationRecipients: addresses(address(mentoLabsMultisig)), + additionalAllocationAmounts: uints(200) }); vm.prank(owner); diff --git a/test/governance/IntegrationTests/GovernanceIntegration.t.sol b/test/integration/governance/GovernanceIntegration.t.sol similarity index 95% rename from test/governance/IntegrationTests/GovernanceIntegration.t.sol rename to test/integration/governance/GovernanceIntegration.t.sol index 52d44068..adaede52 100644 --- a/test/governance/IntegrationTests/GovernanceIntegration.t.sol +++ b/test/integration/governance/GovernanceIntegration.t.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length, max-states-count -import { TestSetup } from "../TestSetup.sol"; -import { Vm } from "forge-std-next/Vm.sol"; -import { VmExtension } from "test/utils/VmExtension.sol"; +import { addresses, uints, bytes32s } from "mento-std/Array.sol"; +import { Vm } from "forge-std/Vm.sol"; import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; import { GovernanceFactory } from "contracts/governance/GovernanceFactory.sol"; @@ -15,17 +14,17 @@ import { Locking } from "contracts/governance/locking/Locking.sol"; import { TimelockController } from "contracts/governance/TimelockController.sol"; import { Proposals } from "./Proposals.sol"; -import { Arrays } from "test/utils/Arrays.sol"; -import { TestLocking } from "test/utils/TestLocking.sol"; +import { VmExtension } from "test/utils/VmExtension.sol"; +import { GovernanceTest } from "test/unit/governance/GovernanceTest.sol"; +import { LockingHarness } from "test/utils/harnesses/LockingHarness.sol"; import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; import { GnosisSafe } from "safe-contracts/contracts/GnosisSafe.sol"; import { GnosisSafeProxyFactory } from "safe-contracts/contracts/proxies/GnosisSafeProxyFactory.sol"; -import { Enum } from "safe-contracts/contracts/common/Enum.sol"; import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -contract GovernanceIntegrationTest is TestSetup { +contract GovernanceIntegrationTest is GovernanceTest { using VmExtension for Vm; GovernanceFactory public factory; @@ -58,10 +57,10 @@ contract GovernanceIntegrationTest is TestSetup { bytes32 public merkleRoot = 0x945d83ced94efc822fed712b4c4694b4e1129607ec5bbd2ab971bb08dca4d809; address public claimer0 = 0x547a9687D36e51DA064eE7C6ac82590E344C4a0e; uint96 public claimer0Amount = 100e18; - bytes32[] public claimer0Proof = Arrays.bytes32s(0xf213211627972cf2d02a11f800ed3f60110c1d11d04ec1ea8cb1366611efdaa3); + bytes32[] public claimer0Proof = bytes32s(0xf213211627972cf2d02a11f800ed3f60110c1d11d04ec1ea8cb1366611efdaa3); address public claimer1 = 0x6B70014D9c0BF1F53695a743Fe17996f132e9482; uint96 public claimer1Amount = 20_000e18; - bytes32[] public claimer1Proof = Arrays.bytes32s(0x0294d3fc355e136dd6fea7f5c2934dd7cb67c2b4607110780e5fbb23d65d7ac4); + bytes32[] public claimer1Proof = bytes32s(0x0294d3fc355e136dd6fea7f5c2934dd7cb67c2b4607110780e5fbb23d65d7ac4); string public constant EXPECTED_CREDENTIAL = "level:plus+liveness;citizenship_not:;residency_not:cd,cu,gb,ir,kp,ml,mm,ss,sy,us,ye"; @@ -127,8 +126,8 @@ contract GovernanceIntegrationTest is TestSetup { .MentoTokenAllocationParams({ airgrabAllocation: 50, mentoTreasuryAllocation: 100, - additionalAllocationRecipients: Arrays.addresses(address(mentoLabsMultisig)), - additionalAllocationAmounts: Arrays.uints(200) + additionalAllocationRecipients: addresses(address(mentoLabsMultisig)), + additionalAllocationAmounts: uints(200) }); vm.prank(owner); @@ -156,11 +155,11 @@ contract GovernanceIntegrationTest is TestSetup { mentoToken.approve(address(locking), type(uint256).max); } - function test_factory_shouldCreateAndSetupContracts() public { - assertEq(mentoToken.balanceOf(address(mentoLabsMultisig)), 200_000_000 * 10**18); - assertEq(mentoToken.balanceOf(address(airgrab)), 50_000_000 * 10**18); - assertEq(mentoToken.balanceOf(governanceTimelockAddress), 100_000_000 * 10**18); - assertEq(mentoToken.emissionSupply(), 650_000_000 * 10**18); + function test_factory_shouldCreateAndSetupContracts() public view { + assertEq(mentoToken.balanceOf(address(mentoLabsMultisig)), 200_000_000 * 10 ** 18); + assertEq(mentoToken.balanceOf(address(airgrab)), 50_000_000 * 10 ** 18); + assertEq(mentoToken.balanceOf(governanceTimelockAddress), 100_000_000 * 10 ** 18); + assertEq(mentoToken.emissionSupply(), 650_000_000 * 10 ** 18); assertEq(mentoToken.emission(), address(emission)); assertEq(mentoToken.symbol(), "MENTO"); assertEq(mentoToken.name(), "Mento Token"); @@ -168,7 +167,7 @@ contract GovernanceIntegrationTest is TestSetup { assertEq(emission.emissionStartTime(), block.timestamp); assertEq(address(emission.mentoToken()), address(mentoToken)); assertEq(emission.emissionTarget(), address(governanceTimelockAddress)); - assertEq(emission.emissionSupply(), 650_000_000 * 10**18); + assertEq(emission.emissionSupply(), 650_000_000 * 10 ** 18); assertEq(emission.owner(), governanceTimelockAddress); assertEq(airgrab.root(), merkleRoot); @@ -542,7 +541,7 @@ contract GovernanceIntegrationTest is TestSetup { function test_governor_propose_whenExecutedForImplementationUpgrade_shouldUpgradeTheContracts() public s_governance { // create new implementations - TestLocking newLockingContract = new TestLocking(); + LockingHarness newLockingContract = new LockingHarness(); TimelockController newGovernanceTimelockContract = new TimelockController(); MentoGovernor newGovernorContract = new MentoGovernor(); Emission newEmissionContract = new Emission(false); @@ -592,7 +591,7 @@ contract GovernanceIntegrationTest is TestSetup { // the old implementation has no such method vm.expectRevert(); - TestLocking(address(locking)).setEpochShift(1); + LockingHarness(address(locking)).setEpochShift(1); mentoGovernor.execute(targets, values, calldatas, keccak256(bytes(description))); @@ -605,6 +604,6 @@ contract GovernanceIntegrationTest is TestSetup { assertEq(address(proxyAdmin.getProxyImplementation(emissionProxy)), address(newEmissionContract)); // new implementation has the method and governance upgraded the contract - TestLocking(address(locking)).setEpochShift(1); + LockingHarness(address(locking)).setEpochShift(1); } } diff --git a/test/governance/IntegrationTests/LockingIntegration.fuzz.t.sol b/test/integration/governance/LockingIntegration.fuzz.t.sol similarity index 90% rename from test/governance/IntegrationTests/LockingIntegration.fuzz.t.sol rename to test/integration/governance/LockingIntegration.fuzz.t.sol index 5bc7ecf2..360e7d53 100644 --- a/test/governance/IntegrationTests/LockingIntegration.fuzz.t.sol +++ b/test/integration/governance/LockingIntegration.fuzz.t.sol @@ -2,22 +2,22 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length, max-states-count -import { TestSetup } from "../TestSetup.sol"; -import { Vm } from "forge-std-next/Vm.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { addresses, uints } from "mento-std/Array.sol"; + import { VmExtension } from "test/utils/VmExtension.sol"; -import { Arrays } from "test/utils/Arrays.sol"; +import { GovernanceTest } from "test/unit/governance/GovernanceTest.sol"; import { GovernanceFactory } from "contracts/governance/GovernanceFactory.sol"; import { MentoToken } from "contracts/governance/MentoToken.sol"; import { Locking } from "contracts/governance/locking/Locking.sol"; import { TimelockController } from "contracts/governance/TimelockController.sol"; -import { console } from "forge-std-next/console.sol"; /** * @title Fuzz Testing for Locking Integration * @dev Fuzz tests to ensure the locking mechanism integrates correctly with the governance system, providing the expected voting power based on token lock amount and duration. */ -contract FuzzLockingIntegrationTest is TestSetup { +contract FuzzLockingIntegrationTest is GovernanceTest { using VmExtension for Vm; GovernanceFactory public factory; @@ -44,8 +44,8 @@ contract FuzzLockingIntegrationTest is TestSetup { .MentoTokenAllocationParams({ airgrabAllocation: 50, mentoTreasuryAllocation: 100, - additionalAllocationRecipients: Arrays.addresses(address(mentoLabsMultisig)), - additionalAllocationAmounts: Arrays.uints(200) + additionalAllocationRecipients: addresses(address(mentoLabsMultisig)), + additionalAllocationAmounts: uints(200) }); vm.prank(celoGovernance); @@ -152,11 +152,7 @@ contract FuzzLockingIntegrationTest is TestSetup { /** * @dev Calculates the expected voting power based on lock amount, slope, and cliff. */ - function calculateVotes( - uint96 amount, - uint32 slope, - uint32 cliff - ) public pure returns (uint96) { + function calculateVotes(uint96 amount, uint32 slope, uint32 cliff) public pure returns (uint96) { uint96 cliffSide = (uint96(cliff) * 1e8) / 103; uint96 slopeSide = (uint96(slope) * 1e8) / 104; uint96 multiplier = cliffSide + slopeSide; diff --git a/test/governance/IntegrationTests/Proposals.sol b/test/integration/governance/Proposals.sol similarity index 84% rename from test/governance/IntegrationTests/Proposals.sol rename to test/integration/governance/Proposals.sol index 1e86ecfb..d0b8b859 100644 --- a/test/governance/IntegrationTests/Proposals.sol +++ b/test/integration/governance/Proposals.sol @@ -2,16 +2,16 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length +import { uints, addresses, bytesList } from "mento-std/Array.sol"; + +import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; +import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; import { TimelockController } from "contracts/governance/TimelockController.sol"; import { Locking } from "contracts/governance/locking/Locking.sol"; import { Emission } from "contracts/governance/Emission.sol"; -import { Arrays } from "../../utils/Arrays.sol"; - -import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; -import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - library Proposals { function _proposeChangeEmissionTarget( MentoGovernor mentoGovernor, @@ -27,9 +27,9 @@ library Proposals { string memory description ) { - targets = Arrays.addresses(address(emission)); - values = Arrays.uints(0); - calldatas = Arrays.bytess(abi.encodeWithSelector(emission.setEmissionTarget.selector, newTarget)); + targets = addresses(address(emission)); + values = uints(0); + calldatas = bytesList(abi.encodeWithSelector(emission.setEmissionTarget.selector, newTarget)); description = "Change emission target"; proposalId = mentoGovernor.propose(targets, values, calldatas, description); @@ -56,7 +56,7 @@ library Proposals { string memory description ) { - targets = Arrays.addresses( + targets = addresses( address(mentoGovernor), address(mentoGovernor), address(mentoGovernor), @@ -65,8 +65,8 @@ library Proposals { address(locking), address(locking) ); - values = Arrays.uints(0, 0, 0, 0, 0, 0, 0); - calldatas = Arrays.bytess( + values = uints(0, 0, 0, 0, 0, 0, 0); + calldatas = bytesList( abi.encodeWithSelector(mentoGovernor.setVotingDelay.selector, votingDelay), abi.encodeWithSelector(mentoGovernor.setVotingPeriod.selector, votingPeriod), abi.encodeWithSelector(mentoGovernor.setProposalThreshold.selector, threshold), @@ -101,9 +101,9 @@ library Proposals { string memory description ) { - targets = Arrays.addresses(address(proxyAdmin), address(proxyAdmin), address(proxyAdmin), address(proxyAdmin)); - values = Arrays.uints(0, 0, 0, 0); - calldatas = Arrays.bytess( + targets = addresses(address(proxyAdmin), address(proxyAdmin), address(proxyAdmin), address(proxyAdmin)); + values = uints(0, 0, 0, 0); + calldatas = bytesList( abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy0, newImpl0), abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy1, newImpl1), abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy2, newImpl2), @@ -128,9 +128,9 @@ library Proposals { string memory description ) { - targets = Arrays.addresses(address(mentoLabsTreasury)); - values = Arrays.uints(0); - calldatas = Arrays.bytess(abi.encodeWithSelector(mentoLabsTreasury.cancel.selector, id)); + targets = addresses(address(mentoLabsTreasury)); + values = uints(0); + calldatas = bytesList(abi.encodeWithSelector(mentoLabsTreasury.cancel.selector, id)); description = "Cancel queued tx"; proposalId = mentoGovernor.propose(targets, values, calldatas, description); diff --git a/test/gas/GasComparison.md b/test/integration/protocol/BrokerGas.md similarity index 100% rename from test/gas/GasComparison.md rename to test/integration/protocol/BrokerGas.md diff --git a/test/gas/BrokerGas.t.sol b/test/integration/protocol/BrokerGas.t.sol similarity index 61% rename from test/gas/BrokerGas.t.sol rename to test/integration/protocol/BrokerGas.t.sol index 4ac00256..00a16f8a 100644 --- a/test/gas/BrokerGas.t.sol +++ b/test/integration/protocol/BrokerGas.t.sol @@ -1,70 +1,60 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { ProtocolTest } from "./ProtocolTest.sol"; -import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; -import { TokenHelpers } from "../utils/TokenHelpers.t.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; // forge test --match-contract BrokerGasTest -vvv -contract BrokerGasTest is IntegrationTest, TokenHelpers { - address trader; +contract BrokerGasTest is ProtocolTest { + address trader = makeAddr("trader"); - function setUp() public { - IntegrationTest.setUp(); + function setUp() public override { + super.setUp(); - trader = actor("trader"); + deal(address(cUSDToken), trader, 10 ** 22, true); // Mint 10k to trader + deal(address(cEURToken), trader, 10 ** 22, true); // Mint 10k to trader - mint(cUSDToken, trader, 10**22); // Mint 10k to trader - mint(cEURToken, trader, 10**22); // Mint 10k to trader + deal(address(celoToken), trader, 1000 * 10 ** 18); // Gift 10k to trader - deal(address(celoToken), trader, 1000 * 10**18); // Gift 10k to trader - - deal(address(celoToken), address(reserve), 10**24); // Gift 1Mil to reserve - deal(address(usdcToken), address(reserve), 10**24); // Gift 1Mil to reserve + deal(address(celoToken), address(reserve), 10 ** 24); // Gift 1Mil to reserve + deal(address(usdcToken), address(reserve), 10 ** 24); // Gift 1Mil to reserve } /** * @notice Test helper function to do swap in */ - function doSwapIn( - bytes32 poolId, - uint256 amountIn, - address tokenIn, - address tokenOut - ) public { + function doSwapIn(bytes32 poolId, uint256 amountIn, address tokenIn, address tokenOut) public { // Get exchange provider from broker address[] memory exchangeProviders = broker.getExchangeProviders(); assertEq(exchangeProviders.length, 1); changePrank(trader); IERC20(tokenIn).approve(address(broker), amountIn); - broker.swapIn(address(exchangeProviders[0]), poolId, tokenIn, tokenOut, 1000 * 10**18, 0); + broker.swapIn(address(exchangeProviders[0]), poolId, tokenIn, tokenOut, 1000 * 10 ** 18, 0); } function test_gas_swapIn_cUSDToBridgedUSDC() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut)); } function test_gas_swapIn_cEURToBridgedUSDC() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cEURToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cEUR_bridgedUSDC_ID; doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut)); } function test_gas_swapIn_cEURTocUSD() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cEURToken)); IERC20 tokenOut = IERC20(address(cUSDToken)); bytes32 poolId = pair_cUSD_cEUR_ID; @@ -73,7 +63,7 @@ contract BrokerGasTest is IntegrationTest, TokenHelpers { } function test_gas_swapIn_cUSDTocEUR() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cUSDToken)); IERC20 tokenOut = IERC20(address(cEURToken)); bytes32 poolId = pair_cUSD_cEUR_ID; @@ -82,8 +72,8 @@ contract BrokerGasTest is IntegrationTest, TokenHelpers { } function test_gas_swapIn_CELOTocEUR() public { - uint256 amountIn = 1000 * 10**18; // 1k - IERC20 tokenIn = celoToken; + uint256 amountIn = 1000 * 10 ** 18; // 1k + IERC20 tokenIn = IERC20(address(celoToken)); IERC20 tokenOut = IERC20(address(cEURToken)); bytes32 poolId = pair_cEUR_CELO_ID; @@ -91,8 +81,8 @@ contract BrokerGasTest is IntegrationTest, TokenHelpers { } function test_gas_swapIn_CELOTocUSD() public { - uint256 amountIn = 1000 * 10**18; // 1k - IERC20 tokenIn = celoToken; + uint256 amountIn = 1000 * 10 ** 18; // 1k + IERC20 tokenIn = IERC20(address(celoToken)); IERC20 tokenOut = IERC20(address(cUSDToken)); bytes32 poolId = pair_cUSD_CELO_ID; @@ -100,18 +90,18 @@ contract BrokerGasTest is IntegrationTest, TokenHelpers { } function test_gas_swapIn_CUSDToCelo() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = celoToken; + IERC20 tokenOut = IERC20(address(celoToken)); bytes32 poolId = pair_cUSD_CELO_ID; doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut)); } function test_gas_swapIn_CEURToCelo() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cEURToken)); - IERC20 tokenOut = celoToken; + IERC20 tokenOut = IERC20(address(celoToken)); bytes32 poolId = pair_cEUR_CELO_ID; doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut)); diff --git a/test/integration/BrokerIntegration.t.sol b/test/integration/protocol/BrokerIntegration.t.sol similarity index 86% rename from test/integration/BrokerIntegration.t.sol rename to test/integration/protocol/BrokerIntegration.t.sol index d7f01867..7668b3b1 100644 --- a/test/integration/BrokerIntegration.t.sol +++ b/test/integration/protocol/BrokerIntegration.t.sol @@ -1,38 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { ProtocolTest } from "./ProtocolTest.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; -import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; -import { TokenHelpers } from "../utils/TokenHelpers.t.sol"; - -import { Broker } from "contracts/swap/Broker.sol"; -import { IReserve } from "contracts/interfaces/IReserve.sol"; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; -import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; // forge test --match-contract BrokerIntegration -vvv -contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { +contract BrokerIntegrationTest is ProtocolTest { + using FixidityLib for FixidityLib.Fraction; + address trader; - function setUp() public { - IntegrationTest.setUp(); + function setUp() public override { + super.setUp(); - trader = actor("trader"); + trader = makeAddr("trader"); - mint(cUSDToken, trader, 10**22); // Mint 10k to trader - mint(cEURToken, trader, 10**22); // Mint 10k to trader + deal(address(cUSDToken), trader, 10 ** 22, true); // Mint 10k to trader + deal(address(cEURToken), trader, 10 ** 22, true); // Mint 10k to trader - deal(address(celoToken), trader, 1000 * 10**18); // Gift 10k to trader + deal(address(celoToken), trader, 1000 * 10 ** 18, true); // Gift 10k to trader - deal(address(celoToken), address(reserve), 10**(6 + 18)); // Gift 1Mil Celo to reserve - deal(address(usdcToken), address(reserve), 10**(6 + 6)); // Gift 1Mil USDC to reserve + deal(address(celoToken), address(reserve), 10 ** (6 + 18), true); // Gift 1Mil Celo to reserve + deal(address(usdcToken), address(reserve), 10 ** (6 + 6), true); // Gift 1Mil USDC to reserve } /** @@ -76,7 +71,7 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { broker.swapIn(address(biPoolManager), pair_cUSD_bridgedUSDC_ID, address(cUSDToken), address(usdcToken), 1, 0); } - function test_getExchangeProviders_shouldReturnProviderWithCorrectExchanges() public { + function test_getExchangeProviders_shouldReturnProviderWithCorrectExchanges() public view { address[] memory exchangeProviders = broker.getExchangeProviders(); assertEq(exchangeProviders.length, 1); @@ -101,9 +96,9 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { } function test_swapIn_cUSDToBridgedUSDC() public { - uint256 amountIn = 1000 * 10**18; // 1k (18 decimals) + uint256 amountIn = 1000 * 10 ** 18; // 1k (18 decimals) IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; // Get amounts before swap @@ -114,7 +109,7 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { // Execute swap (uint256 expectedOut, uint256 actualOut) = doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut)); - assertEq(actualOut, 995 * 10**6); // 0.9995k (6 decimals) + assertEq(actualOut, 995 * 10 ** 6); // 0.9995k (6 decimals) // Get amounts after swap uint256 traderTokenInAfter = tokenIn.balanceOf(trader); @@ -135,16 +130,16 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { } function test_swapIn_cEURToBridgedUSDC() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cEURToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cEUR_bridgedUSDC_ID; // Get amounts before swap uint256 traderTokenInBefore = tokenIn.balanceOf(trader); uint256 traderTokenOutBefore = tokenOut.balanceOf(trader); uint256 reserveCollateralBalanceBefore = tokenOut.balanceOf(address(reserve)); - uint256 StableAssetSupplyBefore = tokenIn.totalSupply(); + uint256 stableAssetSupplyBefore = tokenIn.totalSupply(); // Execute swap (uint256 expectedOut, uint256 actualOut) = doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut)); @@ -153,7 +148,7 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { uint256 traderTokenInAfter = tokenIn.balanceOf(trader); uint256 traderTokenOutAfter = tokenOut.balanceOf(trader); uint256 reserveCollateralBalanceAfter = tokenOut.balanceOf(address(reserve)); - uint256 StableAssetSupplyAfter = tokenIn.totalSupply(); + uint256 stableAssetSupplyAfter = tokenIn.totalSupply(); // getAmountOut == swapOut assertEq(expectedOut, actualOut); @@ -164,11 +159,11 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { // Reserve collateral asset balance decreased assertEq(reserveCollateralBalanceBefore - expectedOut, reserveCollateralBalanceAfter); // Stable asset supply decrease from burn - assertEq(StableAssetSupplyBefore - amountIn, StableAssetSupplyAfter); + assertEq(stableAssetSupplyBefore - amountIn, stableAssetSupplyAfter); } function test_swapIn_cEURTocUSD() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cEURToken)); IERC20 tokenOut = IERC20(address(cUSDToken)); bytes32 poolId = pair_cUSD_cEUR_ID; @@ -203,7 +198,7 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { } function test_swapIn_cUSDTocEUR() public { - uint256 amountIn = 1000 * 10**18; // 1k + uint256 amountIn = 1000 * 10 ** 18; // 1k IERC20 tokenIn = IERC20(address(cUSDToken)); IERC20 tokenOut = IERC20(address(cEURToken)); bytes32 poolId = pair_cUSD_cEUR_ID; @@ -238,8 +233,8 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { } function test_swapIn_CELOTocEUR() public { - uint256 amountIn = 1000 * 10**18; // 1k - IERC20 tokenIn = celoToken; + uint256 amountIn = 1000 * 10 ** 18; // 1k + IERC20 tokenIn = IERC20(address(celoToken)); IERC20 tokenOut = IERC20(address(cEURToken)); bytes32 poolId = pair_cEUR_CELO_ID; @@ -273,8 +268,8 @@ contract BrokerIntegrationTest is IntegrationTest, TokenHelpers { } function test_swapIn_CELOTocUSD() public { - uint256 amountIn = 1000 * 10**18; // 1k - IERC20 tokenIn = celoToken; + uint256 amountIn = 1000 * 10 ** 18; // 1k + IERC20 tokenIn = IERC20(address(celoToken)); IERC20 tokenOut = IERC20(address(cUSDToken)); bytes32 poolId = pair_cUSD_CELO_ID; diff --git a/test/integration/ChainlinkRelayerIntegration.t.sol b/test/integration/protocol/ChainlinkRelayerIntegration.t.sol similarity index 69% rename from test/integration/ChainlinkRelayerIntegration.t.sol rename to test/integration/protocol/ChainlinkRelayerIntegration.t.sol index 2d722ad4..6996e95d 100644 --- a/test/integration/ChainlinkRelayerIntegration.t.sol +++ b/test/integration/protocol/ChainlinkRelayerIntegration.t.sol @@ -1,38 +1,38 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Ownable } from "openzeppelin-contracts/ownership/Ownable.sol"; +import { ProtocolTest } from "./ProtocolTest.sol"; +import { MockAggregatorV3 } from "test/utils/mocks/MockAggregatorV3.sol"; -import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; -import { MockAggregatorV3 } from "../mocks/MockAggregatorV3.sol"; +import { IOwnable } from "contracts/interfaces/IOwnable.sol"; -import { IChainlinkRelayerFactory } from "contracts/interfaces/IChainlinkRelayerFactory.sol"; -import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; -import { IProxyAdmin } from "contracts/interfaces/IProxyAdmin.sol"; -import { ITransparentProxy } from "contracts/interfaces/ITransparentProxy.sol"; +import "contracts/interfaces/IChainlinkRelayerFactory.sol"; +import "contracts/interfaces/ITransparentProxy.sol"; +import "contracts/oracles/ChainlinkRelayerFactoryProxyAdmin.sol"; +import "contracts/oracles/ChainlinkRelayerFactoryProxy.sol"; +import "contracts/oracles/ChainlinkRelayerFactory.sol"; +import "contracts/oracles/ChainlinkRelayerV1.sol"; +// solhint-disable-next-line max-line-length +import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -contract ChainlinkRelayerIntegration is IntegrationTest { - address owner = actor("owner"); +contract ChainlinkRelayerIntegration is ProtocolTest { + address owner = makeAddr("owner"); - IChainlinkRelayerFactory relayerFactoryImplementation; + ChainlinkRelayerFactory relayerFactoryImplementation; + ChainlinkRelayerFactoryProxyAdmin proxyAdmin; + ITransparentUpgradeableProxy proxy; IChainlinkRelayerFactory relayerFactory; - IProxyAdmin proxyAdmin; - ITransparentProxy proxy; - function setUp() public { - IntegrationTest.setUp(); + function setUp() public virtual override { + super.setUp(); - proxyAdmin = IProxyAdmin(factory.createContract("ChainlinkRelayerFactoryProxyAdmin", "")); - relayerFactoryImplementation = IChainlinkRelayerFactory( - factory.createContract("ChainlinkRelayerFactory", abi.encode(true)) - ); - proxy = ITransparentProxy( - factory.createContract( - "ChainlinkRelayerFactoryProxy", - abi.encode( + proxyAdmin = new ChainlinkRelayerFactoryProxyAdmin(); + relayerFactoryImplementation = new ChainlinkRelayerFactory(true); + proxy = ITransparentUpgradeableProxy( + address( + new ChainlinkRelayerFactoryProxy( address(relayerFactoryImplementation), address(proxyAdmin), abi.encodeWithSignature("initialize(address,address)", address(sortedOracles), owner) @@ -40,21 +40,20 @@ contract ChainlinkRelayerIntegration is IntegrationTest { ) ); relayerFactory = IChainlinkRelayerFactory(address(proxy)); - vm.startPrank(address(factory)); - Ownable(address(proxyAdmin)).transferOwnership(owner); - Ownable(address(relayerFactory)).transferOwnership(owner); - vm.stopPrank(); + + IOwnable(address(proxyAdmin)).transferOwnership(owner); + IOwnable(address(relayerFactory)).transferOwnership(owner); } } contract ChainlinkRelayerIntegration_ProxySetup is ChainlinkRelayerIntegration { - function test_proxyOwnedByAdmin() public { - address admin = proxyAdmin.getProxyAdmin(address(proxy)); + function test_proxyOwnedByAdmin() public view { + address admin = proxyAdmin.getProxyAdmin(proxy); assertEq(admin, address(proxyAdmin)); } - function test_adminOwnedByOwner() public { - address realOwner = Ownable(address(proxyAdmin)).owner(); + function test_adminOwnedByOwner() public view { + address realOwner = IOwnable(address(proxyAdmin)).owner(); assertEq(realOwner, owner); } @@ -70,13 +69,13 @@ contract ChainlinkRelayerIntegration_ProxySetup is ChainlinkRelayerIntegration { proxy.implementation(); } - function test_implementationOwnedByOwner() public { - address realOwner = Ownable(address(relayerFactory)).owner(); + function test_implementationOwnedByOwner() public view { + address realOwner = IOwnable(address(relayerFactory)).owner(); assertEq(realOwner, owner); } - function test_implementationSetCorrectly() public { - address implementation = proxyAdmin.getProxyImplementation(address(proxy)); + function test_implementationSetCorrectly() public view { + address implementation = proxyAdmin.getProxyImplementation(proxy); assertEq(implementation, address(relayerFactoryImplementation)); } @@ -93,7 +92,7 @@ contract ChainlinkRelayerIntegration_ReportAfterRedeploy is ChainlinkRelayerInte MockAggregatorV3 chainlinkAggregator0; MockAggregatorV3 chainlinkAggregator1; - function setUp() public { + function setUp() public override { super.setUp(); chainlinkAggregator0 = new MockAggregatorV3(8); @@ -111,7 +110,6 @@ contract ChainlinkRelayerIntegration_ReportAfterRedeploy is ChainlinkRelayerInte relayerFactory.deployRelayer(rateFeedId, "cUSD/FOO", 0, aggregatorList0) ); - vm.prank(deployer); sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer0)); chainlinkAggregator0.setRoundData(1000000, block.timestamp); @@ -124,7 +122,6 @@ contract ChainlinkRelayerIntegration_ReportAfterRedeploy is ChainlinkRelayerInte relayerFactory.redeployRelayer(rateFeedId, "cUSD/FOO", 0, aggregatorList1) ); - vm.prank(deployer); sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer1)); chainlinkAggregator1.setRoundData(1000000, block.timestamp); @@ -146,7 +143,7 @@ contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelay MockAggregatorV3 chainlinkAggregator; IChainlinkRelayer chainlinkRelayer; - function setUp() public { + function setUp() public override { super.setUp(); setUpRelayer(); @@ -160,7 +157,6 @@ contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelay vm.prank(owner); chainlinkRelayer = IChainlinkRelayer(relayerFactory.deployRelayer(rateFeedId, "CELO/USD", 0, aggregators)); - vm.prank(deployer); sortedOracles.addOracle(rateFeedId, address(chainlinkRelayer)); } @@ -170,30 +166,26 @@ contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelay } function setUpBreakerBox() public { - vm.startPrank(deployer); breakerBox.addRateFeed(rateFeedId); breakerBox.toggleBreaker(address(valueDeltaBreaker), rateFeedId, true); - vm.stopPrank(); } function setUpBreaker() public { address[] memory rateFeeds = new address[](1); rateFeeds[0] = rateFeedId; uint256[] memory thresholds = new uint256[](1); - thresholds[0] = 10**23; // 10% + thresholds[0] = 10 ** 23; // 10% uint256[] memory cooldownTimes = new uint256[](1); cooldownTimes[0] = 1 minutes; uint256[] memory referenceValues = new uint256[](1); - referenceValues[0] = 10**24; + referenceValues[0] = 10 ** 24; - vm.startPrank(deployer); valueDeltaBreaker.setRateChangeThresholds(rateFeeds, thresholds); valueDeltaBreaker.setCooldownTimes(rateFeeds, cooldownTimes); valueDeltaBreaker.setReferenceValues(rateFeeds, referenceValues); - vm.stopPrank(); } - function test_initiallyNoPrice() public { + function test_initiallyNoPrice() public view { (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); assertEq(price, 0); @@ -202,56 +194,60 @@ contract ChainlinkRelayerIntegration_CircuitBreakerInteraction is ChainlinkRelay } function test_passesPriceFromAggregatorToSortedOracles() public { - chainlinkAggregator.setRoundData(10**8, block.timestamp - 1); + chainlinkAggregator.setRoundData(10 ** 8, block.timestamp - 1); chainlinkRelayer.relay(); (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); - assertEq(price, 10**24); - assertEq(denominator, 10**24); + assertEq(price, 10 ** 24); + assertEq(denominator, 10 ** 24); assertEq(uint256(tradingMode), 0); } function test_whenPriceBeyondThresholdIsRelayed_breakerShouldTrigger() public { - chainlinkAggregator.setRoundData(12 * 10**7, block.timestamp - 1); + chainlinkAggregator.setRoundData(12 * 10 ** 7, block.timestamp - 1); chainlinkRelayer.relay(); (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); - assertEq(price, 12 * 10**23); - assertEq(denominator, 10**24); + assertEq(price, 12 * 10 ** 23); + assertEq(denominator, 10 ** 24); assertEq(uint256(tradingMode), 3); } function test_whenPriceBeyondThresholdIsRelayedThenRecovers_breakerShouldTriggerThenRecover() public { - chainlinkAggregator.setRoundData(12 * 10**7, block.timestamp - 1); + vm.warp(100000); + uint256 t0 = block.timestamp; + chainlinkAggregator.setRoundData(12 * 10 ** 7, t0 - 1); chainlinkRelayer.relay(); uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); assertEq(uint256(tradingMode), 3); - vm.warp(now + 1 minutes + 1); + vm.warp(t0 + 1 minutes + 1); - chainlinkAggregator.setRoundData(105 * 10**6, block.timestamp - 1); + chainlinkAggregator.setRoundData(105 * 10 ** 6, t0 + 1 minutes + 1); chainlinkRelayer.relay(); (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); - assertEq(price, 105 * 10**22); - assertEq(denominator, 10**24); + assertEq(price, 105 * 10 ** 22); + assertEq(denominator, 10 ** 24); assertEq(uint256(tradingMode), 0); } function test_whenPriceBeyondThresholdIsRelayedAndCooldownIsntReached_breakerShouldTriggerAndNotRecover() public { - chainlinkAggregator.setRoundData(12 * 10**7, block.timestamp - 1); + vm.warp(100000); + uint256 t0 = block.timestamp; + chainlinkAggregator.setRoundData(12 * 10 ** 7, t0 - 1); chainlinkRelayer.relay(); uint8 tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); assertEq(uint256(tradingMode), 3); - vm.warp(now + 1 minutes - 1); + vm.warp(t0 + 1 minutes - 1); - chainlinkAggregator.setRoundData(105 * 10**6, block.timestamp - 1); + chainlinkAggregator.setRoundData(105 * 10 ** 6, t0 + 1 minutes - 1); chainlinkRelayer.relay(); (uint256 price, uint256 denominator) = sortedOracles.medianRate(rateFeedId); tradingMode = breakerBox.getRateFeedTradingMode(rateFeedId); - assertEq(price, 105 * 10**22); - assertEq(denominator, 10**24); + assertEq(price, 105 * 10 ** 22); + assertEq(denominator, 10 ** 24); assertEq(uint256(tradingMode), 3); } } diff --git a/test/integration/CircuitBreakerIntegration.t.sol b/test/integration/protocol/CircuitBreakerIntegration.t.sol similarity index 78% rename from test/integration/CircuitBreakerIntegration.t.sol rename to test/integration/protocol/CircuitBreakerIntegration.t.sol index 2d1d0195..b2c18f23 100644 --- a/test/integration/CircuitBreakerIntegration.t.sol +++ b/test/integration/protocol/CircuitBreakerIntegration.t.sol @@ -1,34 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { ProtocolTest } from "./ProtocolTest.sol"; -import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; -import { TokenHelpers } from "../utils/TokenHelpers.t.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { BreakerBox } from "contracts/oracles/BreakerBox.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { - using SafeMath for uint256; +contract CircuitBreakerIntegration is ProtocolTest { using FixidityLib for FixidityLib.Fraction; uint256 fixed1 = FixidityLib.fixed1().unwrap(); - address trader; - - function setUp() public { - IntegrationTest.setUp(); + address trader = makeAddr("trader"); - trader = actor("trader"); + function setUp() public override { + super.setUp(); - mint(cUSDToken, trader, 10**22); // Mint 10k to trader - mint(cEURToken, trader, 10**22); // Mint 10k to trader + deal(address(cUSDToken), trader, 10 ** 22, true); // Mint 10k to trader + deal(address(cEURToken), trader, 10 ** 22, true); // Mint 10k to trader deal(address(celoToken), address(reserve), 1e24); // Gift 1Mil Celo to reserve deal(address(usdcToken), address(reserve), 1e24); // Gift 1Mil USDC to reserve } @@ -36,23 +28,19 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { /** * @notice Test helper function to do swap in */ - function doSwapIn( - bytes32 poolId, - address tokenIn, - address tokenOut, - bool shouldBreak - ) public { - uint256 amountIn = 10**18; + function doSwapIn(bytes32 poolId, address tokenIn, address tokenOut, bool shouldBreak) public { + uint256 amountIn = 10 ** 18; address[] memory exchangeProviders = broker.getExchangeProviders(); assertEq(exchangeProviders.length, 1); - changePrank(trader); + vm.prank(trader); IERC20(tokenIn).approve(address(broker), amountIn); if (shouldBreak) { vm.expectRevert("Trading is suspended for this reference rate"); } // Execute swap + vm.prank(trader); broker.swapIn(exchangeProviders[0], poolId, tokenIn, tokenOut, amountIn, 0); } @@ -68,7 +56,7 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { assertEq(uint256(tradingMode), 3); // 3 = trading halted // Cool down breaker and set new median that doesnt exceed threshold - vm.warp(now + 5 minutes); + vm.warp(block.timestamp + 5 minutes); setMedianRate(cUSD_CELO_referenceRateFeedID, 5e23); // Check trading mode @@ -92,7 +80,7 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { assertEq(uint256(tradingMode), 3); // 3 = trading halted // Cool down breaker and set new median that doesnt exceed threshold - vm.warp(now + 5 minutes); + vm.warp(block.timestamp + 5 minutes); newMedian = newMedian + (5e23 - 5e23 * 0.151) * 0.14; setMedianRate(cUSD_CELO_referenceRateFeedID, newMedian); @@ -113,37 +101,37 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { // Check trading modes & ensure only value delta breaker tripped uint8 rateFeedTradingMode = breakerBox.getRateFeedTradingMode(cUSD_bridgedUSDC_referenceRateFeedID); - (uint8 valueDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(valueDeltaBreaker) ); - (uint8 medianDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory medianBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(medianDeltaBreaker) ); assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDeltaTradingMode), 3); // 3 = trading halted - assertEq(uint256(medianDeltaTradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreakerStatus.tradingMode), 3); // 3 = trading halted + assertEq(uint256(medianBreakerStatus.tradingMode), 0); // 0 = bidirectional trading setMedianRate(cUSD_bridgedUSDC_referenceRateFeedID, 1e24 + 1e24 * 0.11); // Cool down breaker and set new median that doesnt exceed threshold - vm.warp(now + 1 seconds); + vm.warp(block.timestamp + 1 seconds); setMedianRate(cUSD_bridgedUSDC_referenceRateFeedID, 1e24); // Check trading modes rateFeedTradingMode = breakerBox.getRateFeedTradingMode(cUSD_bridgedUSDC_referenceRateFeedID); - (valueDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(valueDeltaBreaker) ); - (medianDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + medianBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(medianDeltaBreaker) ); assertEq(uint256(rateFeedTradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(valueDeltaTradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(medianDeltaTradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreakerStatus.tradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(medianBreakerStatus.tradingMode), 0); // 0 = bidirectional trading // Try swap with shouldBreak false -> trading is bidirectional again doSwapIn(pair_cUSD_bridgedUSDC_ID, address(cUSDToken), address(usdcToken), false); @@ -158,35 +146,35 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { // Check trading modes & ensure only value delta breaker tripped uint8 rateFeedTradingMode = breakerBox.getRateFeedTradingMode(cUSD_bridgedUSDC_referenceRateFeedID); - (uint8 valueDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(valueDeltaBreaker) ); - (uint8 medianDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory medianBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(medianDeltaBreaker) ); - assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDeltaTradingMode), 3); // 3 = trading halted - assertEq(uint256(medianDeltaTradingMode), 0); // 0 = bidirectional trading + assertEq(rateFeedTradingMode, 3); // 3 = trading halted + assertEq(valueBreakerStatus.tradingMode, 3); // 3 = trading halted + assertEq(medianBreakerStatus.tradingMode, 0); // 0 = bidirectional trading // Cool down breaker and set new median that doesnt exceed threshold - vm.warp(now + 1 seconds); + vm.warp(block.timestamp + 1 seconds); setMedianRate(cUSD_bridgedUSDC_referenceRateFeedID, 1e24); // Check trading modes rateFeedTradingMode = breakerBox.getRateFeedTradingMode(cUSD_bridgedUSDC_referenceRateFeedID); - (valueDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(valueDeltaBreaker) ); - (medianDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + medianBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(medianDeltaBreaker) ); assertEq(uint256(rateFeedTradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(valueDeltaTradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(medianDeltaTradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreakerStatus.tradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(medianBreakerStatus.tradingMode), 0); // 0 = bidirectional trading // Try swap with shouldBreak false -> trading is bidirectional again doSwapIn(pair_cUSD_bridgedUSDC_ID, address(cUSDToken), address(usdcToken), false); @@ -204,7 +192,7 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { assertEq(uint256(tradingMode), 3); // 3 = trading halted // Cool down breaker - vm.warp(now + 5 minutes); + vm.warp(block.timestamp + 5 minutes); setMedianRate(cUSD_CELO_referenceRateFeedID, 5e23 * 0.95); // Check trading mode @@ -224,35 +212,35 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { // Check trading modes uint8 rateFeedTradingMode = breakerBox.getRateFeedTradingMode(cUSD_bridgedUSDC_referenceRateFeedID); - (uint8 valueDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(valueDeltaBreaker) ); - (uint8 medianDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory medianBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(medianDeltaBreaker) ); - assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDeltaTradingMode), 3); // 3 = trading halted - assertEq(uint256(medianDeltaTradingMode), 0); // 0 = bidirectional trading + assertEq(rateFeedTradingMode, 3); // 3 = trading halted + assertEq(valueBreakerStatus.tradingMode, 3); // 3 = trading halted + assertEq(medianBreakerStatus.tradingMode, 0); // 0 = bidirectional trading // Cool down breaker - vm.warp(now + 1 seconds); + vm.warp(block.timestamp + 1 seconds); setMedianRate(cUSD_bridgedUSDC_referenceRateFeedID, 1e24 + 1e24 * 0.11); // Check trading modes rateFeedTradingMode = breakerBox.getRateFeedTradingMode(cUSD_bridgedUSDC_referenceRateFeedID); - (valueDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(valueDeltaBreaker) ); - (medianDeltaTradingMode, , ) = breakerBox.rateFeedBreakerStatus( + medianBreakerStatus = breakerBox.rateFeedBreakerStatus( cUSD_bridgedUSDC_referenceRateFeedID, address(medianDeltaBreaker) ); - assertEq(uint256(rateFeedTradingMode), 3); // 0 = bidirectional trading - assertEq(uint256(valueDeltaTradingMode), 3); // 0 = bidirectional trading - assertEq(uint256(medianDeltaTradingMode), 0); // 0 = bidirectional trading + assertEq(rateFeedTradingMode, 3); // 3 = trading halted + assertEq(valueBreakerStatus.tradingMode, 3); // 3 = trading halted + assertEq(medianBreakerStatus.tradingMode, 0); // 0 = bidirectional trading // Try swap with shouldBreak true -> median still exceeds threshold doSwapIn(pair_cUSD_bridgedUSDC_ID, address(cUSDToken), address(usdcToken), true); @@ -270,7 +258,7 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted // Cool down breaker - vm.warp(now + 5 seconds); + vm.warp(block.timestamp + 5 seconds); setMedianRate(cUSD_cEUR_referenceRateFeedID, 11e23 + 11e23 * 0.01); // Try swap with shouldBreak true @@ -293,7 +281,7 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted // Cool down breaker - vm.warp(now + 5 seconds); + vm.warp(block.timestamp + 5 seconds); setMedianRate(cEUR_CELO_referenceRateFeedID, 1e24 + 1e24 * 0.1); // Try swap with shouldBreak true @@ -320,7 +308,7 @@ contract CircuitBreakerIntegration is IntegrationTest, TokenHelpers { doSwapIn(pair_cEUR_bridgedUSDC_ID, address(cEURToken), address(usdcToken), true); // Cool down breaker and set new median that doesnt exceed threshold - vm.warp(now + 1 seconds); + vm.warp(block.timestamp + 1 seconds); setMedianRate(cUSD_bridgedUSDC_referenceRateFeedID, 1e24); // Check that trading modes are set correctly diff --git a/test/integration/ConstantSumIntegration.t.sol b/test/integration/protocol/ConstantSumIntegration.t.sol similarity index 74% rename from test/integration/ConstantSumIntegration.t.sol rename to test/integration/protocol/ConstantSumIntegration.t.sol index 9177cd95..2556b5ea 100644 --- a/test/integration/ConstantSumIntegration.t.sol +++ b/test/integration/protocol/ConstantSumIntegration.t.sol @@ -1,35 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { ProtocolTest } from "./ProtocolTest.sol"; -import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; -import { TokenHelpers } from "../utils/TokenHelpers.t.sol"; - -import { Broker } from "contracts/swap/Broker.sol"; -import { IReserve } from "contracts/interfaces/IReserve.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; -import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; - -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { - using SafeMath for uint256; - - address trader; - function setUp() public { - IntegrationTest.setUp(); +contract ConstantSumIntegrationTest is ProtocolTest { + address trader = makeAddr("trader"); - trader = actor("trader"); + function setUp() public override { + super.setUp(); - mint(cUSDToken, trader, 10**22); // Mint 10k to trader - deal(address(usdcToken), address(reserve), 10**(6 + 6)); // Gift 1Mil USDC to reserve + deal(address(cUSDToken), trader, 10 ** 22, true); // Mint 10k to trader + deal(address(usdcToken), address(reserve), 10 ** (6 + 6), true); // Gift 1Mil USDC to reserve } /** @@ -50,20 +35,21 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { } expectedOut = broker.getAmountOut(exchangeProviders[0], poolId, tokenIn, tokenOut, amountIn); - changePrank(trader); + vm.prank(trader); IERC20(tokenIn).approve(address(broker), amountIn); // Execute swap if (shouldRevert) { vm.expectRevert("no valid median"); } + vm.prank(trader); actualOut = broker.swapIn(address(exchangeProviders[0]), poolId, tokenIn, tokenOut, amountIn, 0); } function test_swap_whenConstantSum_pricesShouldStayTheSameInBetweenBucketUpdates() public { - uint256 amountIn = 5000 * 10**18; // 5k cUSD + uint256 amountIn = 5000 * 10 ** 18; // 5k cUSD IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; // Buckets before @@ -73,7 +59,7 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { // Execute swap cUSD -> USDC (, uint256 actualOut) = doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut), false); - assertEq(actualOut, 5000 * 0.995 * 10**6); // 4975(6 decimals) + assertEq(actualOut, 5000 * 0.995 * 10 ** 6); // 4975(6 decimals) IBiPoolManager.PoolExchange memory exchangeAfter1 = biPoolManager.getPoolExchange(poolId); assertEq(exchangeBefore.bucket0, exchangeAfter1.bucket0); @@ -88,9 +74,9 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { assertEq(exchangeBefore.bucket1, exchangeAfter2.bucket1); // Execute swap USDC -> cUSD - amountIn = 5000 * 10**6; // 5k USDC + amountIn = 5000 * 10 ** 6; // 5k USDC (, uint256 actualOut3) = doSwapIn(poolId, amountIn, address(tokenOut), address(tokenIn), false); - assertEq(actualOut3, 5000 * 0.995 * 10**18); // 4975(18 decimals) + assertEq(actualOut3, 5000 * 0.995 * 10 ** 18); // 4975(18 decimals) IBiPoolManager.PoolExchange memory exchangeAfter3 = biPoolManager.getPoolExchange(poolId); assertEq(exchangeBefore.bucket0, exchangeAfter3.bucket0); @@ -98,9 +84,9 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { } function test_swap_whenConstantSum_pricesShouldChangeWhenBucketRatioChanges() public { - uint256 amountIn = 5000 * 10**18; // 5k cUSD + uint256 amountIn = 5000 * 10 ** 18; // 5k cUSD IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; IBiPoolManager.PoolExchange memory exchangeBefore = biPoolManager.getPoolExchange(poolId); @@ -109,9 +95,9 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { // Execute swap cUSD -> USDC (, uint256 actualOut) = doSwapIn(poolId, amountIn, address(tokenIn), address(tokenOut), false); - assertEq(actualOut, 5000 * 0.995 * 10**6); // 4975(6 decimals) + assertEq(actualOut, 5000 * 0.995 * 10 ** 6); // 4975(6 decimals) - vm.warp(now + exchangeBefore.config.referenceRateResetFrequency); // time travel enable bucket update + vm.warp(block.timestamp + exchangeBefore.config.referenceRateResetFrequency); // time travel enable bucket update setMedianRate(cUSD_bridgedUSDC_referenceRateFeedID, 1e24 * 1.1); // new valid Median that wont trigger breaker 0.13 // Execute swap cUSD -> USDC @@ -124,9 +110,9 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { } function test_swap_whenConstantSumAndOldestReportExpired_shouldRevert() public { - uint256 amountIn = 5000 * 10**18; // 5k cUSD + uint256 amountIn = 5000 * 10 ** 18; // 5k cUSD IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; // Oldest report is not expired @@ -135,7 +121,7 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { assertEq(false, expired); // Expire report - vm.warp(now + tokenExpiry); + vm.warp(block.timestamp + tokenExpiry); (expired, ) = sortedOracles.isOldestReportExpired(cUSD_bridgedUSDC_referenceRateFeedID); assertEq(true, expired); @@ -144,22 +130,22 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { } function test_swap_whenConstantSumAndMedianExpired_shouldRevert() public { - uint256 amountIn = 5000 * 10**18; // 5k cUSD + uint256 amountIn = 5000 * 10 ** 18; // 5k cUSD IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(poolId); // Median is recent enough bool medianReportRecent = sortedOracles.medianTimestamp(cUSD_bridgedUSDC_referenceRateFeedID) > - now.sub(exchange.config.referenceRateResetFrequency); + block.timestamp - exchange.config.referenceRateResetFrequency; assertEq(true, medianReportRecent); // Expire median - vm.warp(now + exchange.config.referenceRateResetFrequency); + vm.warp(block.timestamp + exchange.config.referenceRateResetFrequency); medianReportRecent = sortedOracles.medianTimestamp(cUSD_bridgedUSDC_referenceRateFeedID) > - now.sub(exchange.config.referenceRateResetFrequency); + block.timestamp - exchange.config.referenceRateResetFrequency; assertEq(false, medianReportRecent); // Execute swap cUSD -> USDC with shouldRevert true @@ -167,9 +153,9 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { } function test_swap_whenConstantSumAndNotEnoughReports_shouldRevert() public { - uint256 amountIn = 5000 * 10**18; // 5k cUSD + uint256 amountIn = 5000 * 10 ** 18; // 5k cUSD IERC20 tokenIn = IERC20(address(cUSDToken)); - IERC20 tokenOut = usdcToken; + IERC20 tokenOut = IERC20(address(usdcToken)); bytes32 poolId = pair_cUSD_bridgedUSDC_ID; IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(poolId); @@ -179,7 +165,6 @@ contract ConstantSumIntegrationTest is IntegrationTest, TokenHelpers { // remove reports by removing oracles while (numReports >= exchange.config.minimumReports) { - changePrank(deployer); address oracle = sortedOracles.oracles(cUSD_bridgedUSDC_referenceRateFeedID, 0); sortedOracles.removeOracle(cUSD_bridgedUSDC_referenceRateFeedID, oracle, 0); numReports = sortedOracles.numRates(cUSD_bridgedUSDC_referenceRateFeedID); diff --git a/test/utils/IntegrationTest.t.sol b/test/integration/protocol/ProtocolTest.sol similarity index 72% rename from test/utils/IntegrationTest.t.sol rename to test/integration/protocol/ProtocolTest.sol index f36415e1..0e18faee 100644 --- a/test/utils/IntegrationTest.t.sol +++ b/test/integration/protocol/ProtocolTest.sol @@ -1,44 +1,33 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { console } from "forge-std/console.sol"; -import { Factory } from "./Factory.sol"; +import { Test } from "mento-std/Test.sol"; +import { bytes32s, addresses, uints } from "mento-std/Array.sol"; +import { CELO_REGISTRY_ADDRESS } from "mento-std/Constants.sol"; -import { MockSortedOracles } from "../mocks/MockSortedOracles.sol"; -import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { IFreezer } from "celo/contracts/common/interfaces/IFreezer.sol"; + +import { TestERC20 } from "test/utils/mocks/TestERC20.sol"; +import { USDC } from "test/utils/mocks/USDC.sol"; +import { WithRegistry } from "test/utils/WithRegistry.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { Freezer } from "contracts/common/Freezer.sol"; -import { AddressSortedLinkedListWithMedian } from "contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol"; -import { SortedLinkedListWithMedian } from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; - -import { BiPoolManager } from "contracts/swap/BiPoolManager.sol"; -import { Broker } from "contracts/swap/Broker.sol"; -import { ConstantProductPricingModule } from "contracts/swap/ConstantProductPricingModule.sol"; -import { ConstantSumPricingModule } from "contracts/swap/ConstantSumPricingModule.sol"; -import { Reserve } from "contracts/swap/Reserve.sol"; -import { SortedOracles } from "contracts/common/SortedOracles.sol"; -import { BreakerBox } from "contracts/oracles/BreakerBox.sol"; -import { MedianDeltaBreaker } from "contracts/oracles/breakers/MedianDeltaBreaker.sol"; -import { ValueDeltaBreaker } from "contracts/oracles/breakers/ValueDeltaBreaker.sol"; -import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; - -import { Arrays } from "./Arrays.sol"; -import { Token } from "./Token.sol"; -import { BaseTest } from "./BaseTest.t.sol"; - -contract IntegrationTest is BaseTest { +contract ProtocolTest is Test, WithRegistry { using FixidityLib for FixidityLib.Fraction; - using AddressSortedLinkedListWithMedian for SortedLinkedListWithMedian.List; - using TradingLimits for TradingLimits.Config; uint256 constant tobinTaxStalenessThreshold = 600; uint256 constant dailySpendingRatio = 1000000000000000000000000; @@ -50,24 +39,24 @@ contract IntegrationTest is BaseTest { mapping(address => uint256) oracleCounts; - Broker broker; - BiPoolManager biPoolManager; - Reserve reserve; + IBroker broker; + IBiPoolManager biPoolManager; + IReserve reserve; IPricingModule constantProduct; IPricingModule constantSum; - SortedOracles sortedOracles; - BreakerBox breakerBox; - MedianDeltaBreaker medianDeltaBreaker; - ValueDeltaBreaker valueDeltaBreaker; + ISortedOracles sortedOracles; + IBreakerBox breakerBox; + IMedianDeltaBreaker medianDeltaBreaker; + IValueDeltaBreaker valueDeltaBreaker; - Token celoToken; - Token usdcToken; - Token eurocToken; + TestERC20 celoToken; + TestERC20 usdcToken; + TestERC20 eurocToken; IStableTokenV2 cUSDToken; IStableTokenV2 cEURToken; IStableTokenV2 eXOFToken; - Freezer freezer; + IFreezer freezer; address cUSD_CELO_referenceRateFeedID; address cEUR_CELO_referenceRateFeedID; @@ -84,10 +73,9 @@ contract IntegrationTest is BaseTest { bytes32 pair_cUSD_cEUR_ID; bytes32 pair_eXOF_bridgedEUROC_ID; - function setUp() public { + function setUp() public virtual { vm.warp(60 * 60 * 24 * 10); // Start at a non-zero timestamp. - vm.startPrank(deployer); - broker = new Broker(true); + broker = IBroker(deployCode("Broker", abi.encode(true))); setUp_assets(); setUp_reserve(); @@ -101,19 +89,19 @@ contract IntegrationTest is BaseTest { function setUp_assets() internal { /* ===== Deploy collateral and stable assets ===== */ - celoToken = new Token("Celo", "cGLD", 18); - usdcToken = new Token("bridgedUSDC", "bridgedUSDC", 6); - eurocToken = new Token("bridgedEUROC", "bridgedEUROC", 6); + celoToken = new TestERC20("Celo", "cGLD"); + usdcToken = new USDC("bridgedUSDC", "bridgedUSDC"); + eurocToken = new USDC("bridgedEUROC", "bridgedEUROC"); address[] memory initialAddresses = new address[](0); uint256[] memory initialBalances = new uint256[](0); - cUSDToken = IStableTokenV2(factory.createContract("StableTokenV2", abi.encode(false))); + cUSDToken = IStableTokenV2(deployCode("StableTokenV2", abi.encode(false))); cUSDToken.initialize( "cUSD", "cUSD", 18, - REGISTRY_ADDRESS, + CELO_REGISTRY_ADDRESS, FixidityLib.unwrap(FixidityLib.fixed1()), 60 * 60 * 24 * 7, initialAddresses, @@ -122,12 +110,12 @@ contract IntegrationTest is BaseTest { ); cUSDToken.initializeV2(address(broker), address(0x0), address(0x0)); - cEURToken = IStableTokenV2(factory.createContract("StableTokenV2", abi.encode(false))); + cEURToken = IStableTokenV2(deployCode("StableTokenV2", abi.encode(false))); cEURToken.initialize( "cEUR", "cEUR", 18, - REGISTRY_ADDRESS, + CELO_REGISTRY_ADDRESS, FixidityLib.unwrap(FixidityLib.fixed1()), 60 * 60 * 24 * 7, initialAddresses, @@ -136,12 +124,12 @@ contract IntegrationTest is BaseTest { ); cEURToken.initializeV2(address(broker), address(0x0), address(0x0)); - eXOFToken = IStableTokenV2(factory.createContract("StableTokenV2", abi.encode(false))); + eXOFToken = IStableTokenV2(deployCode("StableTokenV2", abi.encode(false))); eXOFToken.initialize( "eXOF", "eXOF", 18, - REGISTRY_ADDRESS, + CELO_REGISTRY_ADDRESS, FixidityLib.unwrap(FixidityLib.fixed1()), 60 * 60 * 24 * 7, initialAddresses, @@ -156,9 +144,7 @@ contract IntegrationTest is BaseTest { } function setUp_reserve() internal { - changePrank(deployer); /* ===== Deploy reserve ===== */ - bytes32[] memory initialAssetAllocationSymbols = new bytes32[](3); uint256[] memory initialAssetAllocationWeights = new uint256[](3); initialAssetAllocationSymbols[0] = bytes32("cGLD"); @@ -176,9 +162,9 @@ contract IntegrationTest is BaseTest { assetDailySpendingRatios[1] = 100000000000000000000000; assets[2] = address(eurocToken); assetDailySpendingRatios[2] = 100000000000000000000000; - reserve = new Reserve(true); + reserve = IReserve(deployCode("Reserve", abi.encode(true))); reserve.initialize( - REGISTRY_ADDRESS, + CELO_REGISTRY_ADDRESS, tobinTaxStalenessThreshold, dailySpendingRatio, 0, @@ -197,10 +183,9 @@ contract IntegrationTest is BaseTest { } function setUp_sortedOracles() internal { - changePrank(deployer); /* ===== Deploy SortedOracles ===== */ - sortedOracles = new SortedOracles(true); + sortedOracles = ISortedOracles(deployCode("SortedOracles", abi.encode(true))); sortedOracles.initialize(60 * 10); cUSD_CELO_referenceRateFeedID = address(cUSDToken); @@ -254,9 +239,8 @@ contract IntegrationTest is BaseTest { if (values[i] >= rate) greaterKey = keys[i]; } - changePrank(oracleAddy); + vm.prank(oracleAddy); sortedOracles.report(rateFeedID, rate, lesserKey, greaterKey); - changePrank(deployer); } } @@ -266,7 +250,7 @@ contract IntegrationTest is BaseTest { function setUp_breakers() internal { /* ========== Deploy Breaker Box =============== */ - address[] memory rateFeedIDs = Arrays.addresses( + address[] memory rateFeedIDs = addresses( cUSD_CELO_referenceRateFeedID, cEUR_CELO_referenceRateFeedID, cUSD_bridgedUSDC_referenceRateFeedID, @@ -276,19 +260,19 @@ contract IntegrationTest is BaseTest { eXOF_bridgedEUROC_referenceRateFeedID ); - breakerBox = new BreakerBox(rateFeedIDs, ISortedOracles(address(sortedOracles))); + breakerBox = IBreakerBox(deployCode("BreakerBox", abi.encode(rateFeedIDs, ISortedOracles(address(sortedOracles))))); sortedOracles.setBreakerBox(breakerBox); // set rate feed dependencies - address[] memory cEUR_bridgedUSDC_dependencies = Arrays.addresses(cUSD_bridgedUSDC_referenceRateFeedID); + address[] memory cEUR_bridgedUSDC_dependencies = addresses(cUSD_bridgedUSDC_referenceRateFeedID); breakerBox.setRateFeedDependencies(cEUR_bridgedUSDC_referenceRateFeedID, cEUR_bridgedUSDC_dependencies); - address[] memory eXOF_bridgedEUROC_dependencies = Arrays.addresses(bridgedEUROC_EUR_referenceRateFeedID); + address[] memory eXOF_bridgedEUROC_dependencies = addresses(bridgedEUROC_EUR_referenceRateFeedID); breakerBox.setRateFeedDependencies(eXOF_bridgedEUROC_referenceRateFeedID, eXOF_bridgedEUROC_dependencies); /* ========== Deploy Median Delta Breaker =============== */ - address[] memory medianDeltaBreakerRateFeedIDs = Arrays.addresses( + address[] memory medianDeltaBreakerRateFeedIDs = addresses( cUSD_CELO_referenceRateFeedID, cEUR_CELO_referenceRateFeedID, cUSD_bridgedUSDC_referenceRateFeedID, @@ -296,14 +280,14 @@ contract IntegrationTest is BaseTest { cUSD_cEUR_referenceRateFeedID ); - uint256[] memory medianDeltaBreakerRateChangeThresholds = Arrays.uints( - 0.15 * 10**24, - 0.14 * 10**24, - 0.13 * 10**24, - 0.12 * 10**24, - 0.11 * 10**24 + uint256[] memory medianDeltaBreakerRateChangeThresholds = uints( + 0.15 * 10 ** 24, + 0.14 * 10 ** 24, + 0.13 * 10 ** 24, + 0.12 * 10 ** 24, + 0.11 * 10 ** 24 ); - uint256[] memory medianDeltaBreakerCooldownTimes = Arrays.uints( + uint256[] memory medianDeltaBreakerCooldownTimes = uints( 5 minutes, 0 minutes, // non recoverable median delta breaker 5 minutes, @@ -311,17 +295,22 @@ contract IntegrationTest is BaseTest { 5 minutes ); - uint256 medianDeltaBreakerDefaultThreshold = 0.15 * 10**24; // 15% + uint256 medianDeltaBreakerDefaultThreshold = 0.15 * 10 ** 24; // 15% uint256 medianDeltaBreakerDefaultCooldown = 0 seconds; - medianDeltaBreaker = new MedianDeltaBreaker( - medianDeltaBreakerDefaultCooldown, - medianDeltaBreakerDefaultThreshold, - ISortedOracles(address(sortedOracles)), - address(breakerBox), - medianDeltaBreakerRateFeedIDs, - medianDeltaBreakerRateChangeThresholds, - medianDeltaBreakerCooldownTimes + medianDeltaBreaker = IMedianDeltaBreaker( + deployCode( + "MedianDeltaBreaker", + abi.encode( + medianDeltaBreakerDefaultCooldown, + medianDeltaBreakerDefaultThreshold, + ISortedOracles(address(sortedOracles)), + address(breakerBox), + medianDeltaBreakerRateFeedIDs, + medianDeltaBreakerRateChangeThresholds, + medianDeltaBreakerCooldownTimes + ) + ) ); breakerBox.addBreaker(address(medianDeltaBreaker), 3); @@ -335,34 +324,39 @@ contract IntegrationTest is BaseTest { /* ============= Value Delta Breaker =============== */ - address[] memory valueDeltaBreakerRateFeedIDs = Arrays.addresses( + address[] memory valueDeltaBreakerRateFeedIDs = addresses( cUSD_bridgedUSDC_referenceRateFeedID, eXOF_bridgedEUROC_referenceRateFeedID, bridgedEUROC_EUR_referenceRateFeedID, cUSD_cEUR_referenceRateFeedID ); - uint256[] memory valueDeltaBreakerRateChangeThresholds = Arrays.uints( - 0.1 * 10**24, - 0.15 * 10**24, - 0.05 * 10**24, - 0.05 * 10**24 + uint256[] memory valueDeltaBreakerRateChangeThresholds = uints( + 0.1 * 10 ** 24, + 0.15 * 10 ** 24, + 0.05 * 10 ** 24, + 0.05 * 10 ** 24 ); - uint256[] memory valueDeltaBreakerCooldownTimes = Arrays.uints(1 seconds, 1 seconds, 1 seconds, 0 seconds); + uint256[] memory valueDeltaBreakerCooldownTimes = uints(1 seconds, 1 seconds, 1 seconds, 0 seconds); - uint256 valueDeltaBreakerDefaultThreshold = 0.1 * 10**24; + uint256 valueDeltaBreakerDefaultThreshold = 0.1 * 10 ** 24; uint256 valueDeltaBreakerDefaultCooldown = 0 seconds; - valueDeltaBreaker = new ValueDeltaBreaker( - valueDeltaBreakerDefaultCooldown, - valueDeltaBreakerDefaultThreshold, - ISortedOracles(address(sortedOracles)), - valueDeltaBreakerRateFeedIDs, - valueDeltaBreakerRateChangeThresholds, - valueDeltaBreakerCooldownTimes + valueDeltaBreaker = IValueDeltaBreaker( + deployCode( + "ValueDeltaBreaker", + abi.encode( + valueDeltaBreakerDefaultCooldown, + valueDeltaBreakerDefaultThreshold, + ISortedOracles(address(sortedOracles)), + valueDeltaBreakerRateFeedIDs, + valueDeltaBreakerRateChangeThresholds, + valueDeltaBreakerCooldownTimes + ) + ) ); // set reference value - uint256[] memory valueDeltaBreakerReferenceValues = Arrays.uints(1e24, 656 * 10**24, 1e24, 1.1 * 10**24); + uint256[] memory valueDeltaBreakerReferenceValues = uints(1e24, 656 * 10 ** 24, 1e24, 1.1 * 10 ** 24); valueDeltaBreaker.setReferenceValues(valueDeltaBreakerRateFeedIDs, valueDeltaBreakerReferenceValues); // add value delta breaker and enable for rate feeds @@ -376,16 +370,16 @@ contract IntegrationTest is BaseTest { function setUp_broker() internal { /* ===== Deploy BiPoolManager & Broker ===== */ - constantProduct = new ConstantProductPricingModule(); - constantSum = new ConstantSumPricingModule(); - biPoolManager = new BiPoolManager(true); + constantProduct = IPricingModule(deployCode("ConstantProductPricingModule")); + constantSum = IPricingModule(deployCode("ConstantSumPricingModule")); + biPoolManager = IBiPoolManager(deployCode("BiPoolManager", abi.encode(true))); - bytes32[] memory pricingModuleIdentifiers = Arrays.bytes32s( + bytes32[] memory pricingModuleIdentifiers = bytes32s( keccak256(abi.encodePacked(constantProduct.name())), keccak256(abi.encodePacked(constantSum.name())) ); - address[] memory pricingModules = Arrays.addresses(address(constantProduct), address(constantSum)); + address[] memory pricingModules = addresses(address(constantProduct), address(constantSum)); biPoolManager.initialize( address(broker), @@ -403,11 +397,11 @@ contract IntegrationTest is BaseTest { /* ====== Create pairs for all asset combinations ======= */ - BiPoolManager.PoolExchange memory pair_cUSD_CELO; + IBiPoolManager.PoolExchange memory pair_cUSD_CELO; pair_cUSD_CELO.asset0 = address(cUSDToken); pair_cUSD_CELO.asset1 = address(celoToken); pair_cUSD_CELO.pricingModule = constantProduct; - pair_cUSD_CELO.lastBucketUpdate = now; + pair_cUSD_CELO.lastBucketUpdate = block.timestamp; pair_cUSD_CELO.config.spread = FixidityLib.newFixedFraction(5, 100); pair_cUSD_CELO.config.referenceRateResetFrequency = 60 * 5; pair_cUSD_CELO.config.minimumReports = 5; @@ -416,11 +410,11 @@ contract IntegrationTest is BaseTest { pair_cUSD_CELO_ID = biPoolManager.createExchange(pair_cUSD_CELO); - BiPoolManager.PoolExchange memory pair_cEUR_CELO; + IBiPoolManager.PoolExchange memory pair_cEUR_CELO; pair_cEUR_CELO.asset0 = address(cEURToken); pair_cEUR_CELO.asset1 = address(celoToken); pair_cEUR_CELO.pricingModule = constantProduct; - pair_cEUR_CELO.lastBucketUpdate = now; + pair_cEUR_CELO.lastBucketUpdate = block.timestamp; pair_cEUR_CELO.config.spread = FixidityLib.newFixedFraction(5, 100); pair_cEUR_CELO.config.referenceRateResetFrequency = 60 * 5; pair_cEUR_CELO.config.minimumReports = 5; @@ -429,11 +423,11 @@ contract IntegrationTest is BaseTest { pair_cEUR_CELO_ID = biPoolManager.createExchange(pair_cEUR_CELO); - BiPoolManager.PoolExchange memory pair_cUSD_bridgedUSDC; + IBiPoolManager.PoolExchange memory pair_cUSD_bridgedUSDC; pair_cUSD_bridgedUSDC.asset0 = address(cUSDToken); pair_cUSD_bridgedUSDC.asset1 = address(usdcToken); pair_cUSD_bridgedUSDC.pricingModule = constantSum; - pair_cUSD_bridgedUSDC.lastBucketUpdate = now; + pair_cUSD_bridgedUSDC.lastBucketUpdate = block.timestamp; pair_cUSD_bridgedUSDC.config.spread = FixidityLib.newFixedFraction(5, 1000); pair_cUSD_bridgedUSDC.config.referenceRateResetFrequency = 60 * 5; pair_cUSD_bridgedUSDC.config.minimumReports = 5; @@ -442,11 +436,11 @@ contract IntegrationTest is BaseTest { pair_cUSD_bridgedUSDC_ID = biPoolManager.createExchange(pair_cUSD_bridgedUSDC); - BiPoolManager.PoolExchange memory pair_cEUR_bridgedUSDC; + IBiPoolManager.PoolExchange memory pair_cEUR_bridgedUSDC; pair_cEUR_bridgedUSDC.asset0 = address(cEURToken); pair_cEUR_bridgedUSDC.asset1 = address(usdcToken); pair_cEUR_bridgedUSDC.pricingModule = constantSum; - pair_cEUR_bridgedUSDC.lastBucketUpdate = now; + pair_cEUR_bridgedUSDC.lastBucketUpdate = block.timestamp; pair_cEUR_bridgedUSDC.config.spread = FixidityLib.newFixedFraction(5, 100); pair_cEUR_bridgedUSDC.config.referenceRateResetFrequency = 60 * 5; pair_cEUR_bridgedUSDC.config.minimumReports = 5; @@ -455,11 +449,11 @@ contract IntegrationTest is BaseTest { pair_cEUR_bridgedUSDC_ID = biPoolManager.createExchange(pair_cEUR_bridgedUSDC); - BiPoolManager.PoolExchange memory pair_cUSD_cEUR; + IBiPoolManager.PoolExchange memory pair_cUSD_cEUR; pair_cUSD_cEUR.asset0 = address(cUSDToken); pair_cUSD_cEUR.asset1 = address(cEURToken); pair_cUSD_cEUR.pricingModule = constantProduct; - pair_cUSD_cEUR.lastBucketUpdate = now; + pair_cUSD_cEUR.lastBucketUpdate = block.timestamp; pair_cUSD_cEUR.config.spread = FixidityLib.newFixedFraction(5, 100); pair_cUSD_cEUR.config.referenceRateResetFrequency = 60 * 5; pair_cUSD_cEUR.config.minimumReports = 5; @@ -468,11 +462,11 @@ contract IntegrationTest is BaseTest { pair_cUSD_cEUR_ID = biPoolManager.createExchange(pair_cUSD_cEUR); - BiPoolManager.PoolExchange memory pair_eXOF_bridgedEUROC; + IBiPoolManager.PoolExchange memory pair_eXOF_bridgedEUROC; pair_eXOF_bridgedEUROC.asset0 = address(eXOFToken); pair_eXOF_bridgedEUROC.asset1 = address(eurocToken); pair_eXOF_bridgedEUROC.pricingModule = constantSum; - pair_eXOF_bridgedEUROC.lastBucketUpdate = now; + pair_eXOF_bridgedEUROC.lastBucketUpdate = block.timestamp; pair_eXOF_bridgedEUROC.config.spread = FixidityLib.newFixedFraction(5, 1000); pair_eXOF_bridgedEUROC.config.referenceRateResetFrequency = 60 * 5; pair_eXOF_bridgedEUROC.config.minimumReports = 5; @@ -485,13 +479,13 @@ contract IntegrationTest is BaseTest { function setUp_freezer() internal { /* ========== Deploy Freezer =============== */ - freezer = new Freezer(true); + freezer = IFreezer(deployCode("Freezer", abi.encode(true))); registry.setAddressFor("Freezer", address(freezer)); } function setUp_tradingLimits() internal { /* ========== Config Trading Limits =============== */ - TradingLimits.Config memory config = configL0L1LG(100, 10000, 1000, 100000, 1000000); + ITradingLimits.Config memory config = configL0L1LG(100, 10000, 1000, 100000, 1000000); broker.configureTradingLimit(pair_cUSD_CELO_ID, address(cUSDToken), config); broker.configureTradingLimit(pair_cEUR_CELO_ID, address(cEURToken), config); broker.configureTradingLimit(pair_cUSD_bridgedUSDC_ID, address(usdcToken), config); @@ -506,7 +500,7 @@ contract IntegrationTest is BaseTest { uint32 timestep1, int48 limit1, int48 limitGlobal - ) internal pure returns (TradingLimits.Config memory config) { + ) internal pure returns (ITradingLimits.Config memory config) { config.timestep0 = timestep0; config.limit0 = limit0; config.timestep1 = timestep1; diff --git a/test/integration/eXOFIntegration.t.sol b/test/integration/protocol/eXOFIntegration.t.sol similarity index 71% rename from test/integration/eXOFIntegration.t.sol rename to test/integration/protocol/eXOFIntegration.t.sol index 408cb24d..a38f8a31 100644 --- a/test/integration/eXOFIntegration.t.sol +++ b/test/integration/protocol/eXOFIntegration.t.sol @@ -1,61 +1,56 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; -import { ValueDeltaBreaker } from "contracts/oracles/breakers/ValueDeltaBreaker.sol"; +import { addresses, uints } from "mento-std/Array.sol"; -import { IntegrationTest } from "../utils/IntegrationTest.t.sol"; -import { TokenHelpers } from "../utils/TokenHelpers.t.sol"; -import { Arrays } from "../utils/Arrays.sol"; +import { ProtocolTest } from "./ProtocolTest.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; -import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; -contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { - using SafeMath for uint256; - address trader; - ValueDeltaBreaker valueDeltaBreaker2; +contract EXOFIntegrationTest is ProtocolTest { + address trader = makeAddr("trader"); + IValueDeltaBreaker valueDeltaBreaker2; bytes32 pair_eXOF_EUROC_ID; - function setUp() public { - IntegrationTest.setUp(); + function setUp() public override { + super.setUp(); - trader = actor("trader"); - - mint(eXOFToken, trader, 10**22); // Mint 10k to trader - deal(address(eurocToken), trader, 10**(6 + 4)); // Gift 10K EUROC to Trader - deal(address(eurocToken), address(reserve), 10**(6 + 6)); // Gift 1Mil EUROC to reserve + deal(address(eXOFToken), trader, 10 ** 22, true); // Mint 10k to trader + deal(address(eurocToken), trader, 10 ** (6 + 4), true); // Gift 10K EUROC to Trader + deal(address(eurocToken), address(reserve), 10 ** (6 + 6), true); // Gift 1Mil EUROC to reserve // set up second ValueDeltaBreaker used for eXOF - uint256 valueDeltaBreakerDefaultThreshold = 0.1 * 10**24; + uint256 valueDeltaBreakerDefaultThreshold = 0.1 * 10 ** 24; uint256 valueDeltaBreakerDefaultCooldown = 0 seconds; - address[] memory rateFeed = Arrays.addresses(eXOF_bridgedEUROC_referenceRateFeedID); + address[] memory rateFeed = addresses(eXOF_bridgedEUROC_referenceRateFeedID); - uint256[] memory rateChangeThreshold = Arrays.uints(0.2 * 10**24); // 20% -> potential rebase + uint256[] memory rateChangeThreshold = uints(0.2 * 10 ** 24); // 20% -> potential rebase - uint256[] memory cooldownTime = Arrays.uints(0 seconds); // 0 seconds cooldown -> non-recoverable + uint256[] memory cooldownTime = uints(0 seconds); // 0 seconds cooldown -> non-recoverable - valueDeltaBreaker2 = new ValueDeltaBreaker( - valueDeltaBreakerDefaultCooldown, - valueDeltaBreakerDefaultThreshold, - ISortedOracles(address(sortedOracles)), - rateFeed, - rateChangeThreshold, - cooldownTime + valueDeltaBreaker2 = IValueDeltaBreaker( + deployCode( + "ValueDeltaBreaker", + abi.encode( + valueDeltaBreakerDefaultCooldown, + valueDeltaBreakerDefaultThreshold, + sortedOracles, + rateFeed, + rateChangeThreshold, + cooldownTime + ) + ) ); - uint256[] memory referenceValues = Arrays.uints(656 * 1e24); // 1 eXOF ≈ 0.001524 EUROC + uint256[] memory referenceValues = uints(656 * 1e24); // 1 eXOF ≈ 0.001524 EUROC valueDeltaBreaker2.setReferenceValues(rateFeed, referenceValues); - vm.startPrank(deployer); breakerBox.addBreaker(address(valueDeltaBreaker2), 3); breakerBox.toggleBreaker(address(valueDeltaBreaker2), eXOF_bridgedEUROC_referenceRateFeedID, true); } @@ -83,49 +78,38 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { } broker.getAmountOut(exchangeProviders[0], poolId, tokenIn, tokenOut, amountIn); - changePrank(trader); + vm.prank(trader); IERC20(tokenIn).approve(address(broker), amountIn); if (swapInReverts) { vm.expectRevert(bytes(swapInRevertMessage)); } + vm.prank(trader); broker.swapIn(address(exchangeProviders[0]), poolId, tokenIn, tokenOut, amountIn, 0); } /** * @notice Test helper function to do a succesful swap */ - function assert_swapIn_successful( - uint256 amountIn, - address tokenIn, - address tokenOut - ) public { + function assert_swapIn_successful(uint256 amountIn, address tokenIn, address tokenOut) public { assert_swapIn(tokenIn, tokenOut, amountIn, false, "", false, ""); } /** * @notice Test helper function to do a swap that reverts on circuit breaker */ - function assert_swapIn_tradingSuspended( - uint256 amountIn, - address tokenIn, - address tokenOut - ) public { + function assert_swapIn_tradingSuspended(uint256 amountIn, address tokenIn, address tokenOut) public { assert_swapIn(tokenIn, tokenOut, amountIn, false, "", true, "Trading is suspended for this reference rate"); } /** * @notice Test helper function to do a swap that reverts on invalid median */ - function assert_swapIn_noValidMedian( - uint256 amountIn, - address tokenIn, - address tokenOut - ) public { + function assert_swapIn_noValidMedian(uint256 amountIn, address tokenIn, address tokenOut) public { assert_swapIn(tokenIn, tokenOut, amountIn, true, "no valid median", true, "no valid median"); } - function test_setUp_isCorrect() public { + function test_setUp_isCorrect() public view { assertTrue(breakerBox.isBreakerEnabled(address(valueDeltaBreaker), eXOF_bridgedEUROC_referenceRateFeedID)); assertTrue(breakerBox.isBreakerEnabled(address(valueDeltaBreaker2), eXOF_bridgedEUROC_referenceRateFeedID)); assertEq( @@ -169,20 +153,20 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { // Check breakers: verify that only recoverable breaker has triggered uint8 rateFeedTradingMode = breakerBox.getRateFeedTradingMode(eXOF_bridgedEUROC_referenceRateFeedID); - (uint8 valueDelta1TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreakerStatus = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker) ); - (uint8 valueDelta2TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreaker2Status = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker2) ); assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDelta1TradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDelta2TradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreakerStatus.tradingMode), 3); // 3 = trading halted + assertEq(uint256(valueBreaker2Status.tradingMode), 0); // 0 = bidirectional trading // New median that is below recoverable breaker threshold: 15% - vm.warp(now + 1 seconds); + vm.warp(block.timestamp + 1 seconds); setMedianRate(eXOF_bridgedEUROC_referenceRateFeedID, 1e24 * 656 - (1e24 * 656 * 0.14)); // Try succesful swap -> trading should be possible again @@ -190,17 +174,17 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { // Check breakers: verify that recoverable breaker has recovered rateFeedTradingMode = breakerBox.getRateFeedTradingMode(eXOF_bridgedEUROC_referenceRateFeedID); - (valueDelta1TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreakerStatus = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker) ); - (valueDelta2TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreaker2Status = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker2) ); assertEq(uint256(rateFeedTradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(valueDelta1TradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(valueDelta2TradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreakerStatus.tradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreaker2Status.tradingMode), 0); // 0 = bidirectional trading } function test_eXOFPool_whenMedianExceedsNonRecoverableBreaker_shouldBreakAndNeverRecover() public { @@ -212,20 +196,20 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { // Check breakers: verify that non recoverable breaker has triggered uint8 rateFeedTradingMode = breakerBox.getRateFeedTradingMode(eXOF_bridgedEUROC_referenceRateFeedID); - (uint8 valueDelta1TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreakerStatus = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker) ); - (uint8 valueDelta2TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + IBreakerBox.BreakerStatus memory valueBreaker2Status = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker2) ); assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDelta1TradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDelta2TradingMode), 3); // 3 = trading halted + assertEq(uint256(valueBreakerStatus.tradingMode), 3); // 3 = trading halted + assertEq(uint256(valueBreaker2Status.tradingMode), 3); // 3 = trading halted // New median that is below non recoverable breaker threshold: 20% - vm.warp(now + 5 minutes); + vm.warp(block.timestamp + 5 minutes); setMedianRate(eXOF_bridgedEUROC_referenceRateFeedID, 1e24 * 656); // Try swap that should still revert @@ -233,17 +217,17 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { // Check breakers: verify that recoverable breaker has recovered and non recoverable breaker hasn't rateFeedTradingMode = breakerBox.getRateFeedTradingMode(eXOF_bridgedEUROC_referenceRateFeedID); - (valueDelta1TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreakerStatus = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker) ); - (valueDelta2TradingMode, , ) = breakerBox.rateFeedBreakerStatus( + valueBreaker2Status = breakerBox.rateFeedBreakerStatus( eXOF_bridgedEUROC_referenceRateFeedID, address(valueDeltaBreaker2) ); - assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted - assertEq(uint256(valueDelta1TradingMode), 0); // 0 = bidirectional trading - assertEq(uint256(valueDelta2TradingMode), 3); // 3 = trading halted + assertEq(uint256(rateFeedTradingMode), 3); // 3 = trading halted + assertEq(uint256(valueBreakerStatus.tradingMode), 0); // 0 = bidirectional trading + assertEq(uint256(valueBreaker2Status.tradingMode), 3); // 3 = trading halted } function test_eXOFPool_whenEUROCDepegs_shouldHaltTrading() public { @@ -262,7 +246,7 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { assertEq(uint256(dependencyTradingMode), 3); // 3 = trading halted // New median that is below EUROC/EUR breaker threshold: 5% - vm.warp(now + 5 seconds); + vm.warp(block.timestamp + 5 seconds); setMedianRate(bridgedEUROC_EUR_referenceRateFeedID, 1e24 * 1.04); // Try succesful swap -> trading should be possible again @@ -285,7 +269,7 @@ contract EXOFIntegrationTest is IntegrationTest, TokenHelpers { assert_swapIn_successful(1e6, address(eurocToken), address(eXOFToken)); // time jump that expires reports - vm.warp(now + 5 minutes); + vm.warp(block.timestamp + 5 minutes); // Try swap that should revert assert_swapIn_noValidMedian(1e6, address(eurocToken), address(eXOFToken)); diff --git a/test/legacy/Exchange.t.sol b/test/legacy/Exchange.t.sol deleted file mode 100644 index 65213d12..00000000 --- a/test/legacy/Exchange.t.sol +++ /dev/null @@ -1,725 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import "../utils/TokenHelpers.t.sol"; -import "../utils/BaseTest.t.sol"; - -import "../mocks/MockReserve.sol"; -import "../mocks/MockSortedOracles.sol"; - -import "contracts/legacy/Exchange.sol"; -import "contracts/common/FixidityLib.sol"; -import "contracts/common/Freezer.sol"; -import "contracts/common/GoldToken.sol"; -import "contracts/interfaces/IStableTokenV2.sol"; - -contract ExchangeTest is BaseTest, TokenHelpers { - using SafeMath for uint256; - using FixidityLib for FixidityLib.Fraction; - - // Declare exchange events for matching - event Exchanged(address indexed exchanger, uint256 sellAmount, uint256 buyAmount, bool soldCelo); - event UpdateFrequencySet(uint256 updateFrequency); - event MinimumReportsSet(uint256 minimumReports); - event StableTokenSet(address indexed stable); - event SpreadSet(uint256 spread); - event ReserveFractionSet(uint256 reserveFraction); - event BucketsUpdated(uint256 celoBucket, uint256 stableBucket); - event Approval(address indexed owner, address indexed spender, uint256 value); - - address deployer; - address rando; - - Exchange exchange; - Freezer freezer; - IStableTokenV2 stableToken; - GoldToken celoToken; - MockReserve reserve; - MockSortedOracles sortedOracles; - - uint256 constant referenceRateResetFrequency = 60 * 60; - uint256 constant initialReserveBalance = 10000000000000000000000; - FixidityLib.Fraction reserveFraction = FixidityLib.newFixedFraction(5, 100); - uint256 initialCeloBucket = FixidityLib.newFixed(initialReserveBalance).multiply(reserveFraction).fromFixed(); - uint256 constant celoAmountForRate = 1000000000000000000000000; - uint256 constant stableAmountForRate = 2000000000000000000000000; - uint256 initialStableBucket = initialCeloBucket * 2; - FixidityLib.Fraction spread = FixidityLib.newFixedFraction(3, 1000); - - function setUp() public { - deployer = address(factory); - rando = actor("rando"); - // Go somwehre in the future - vm.warp(60 * 60 * 24 * 7 * 100); - vm.startPrank(deployer); - currentPrank = deployer; - freezer = new Freezer(true); - celoToken = new GoldToken(true); - reserve = new MockReserve(); - exchange = new Exchange(true); - stableToken = IStableTokenV2(factory.createContract("StableTokenV2", abi.encode(false))); - sortedOracles = new MockSortedOracles(); - - registry.setAddressFor("Freezer", address(freezer)); - registry.setAddressFor("GoldToken", address(celoToken)); - registry.setAddressFor("Reserve", address(reserve)); - registry.setAddressFor("StableToken", address(stableToken)); - registry.setAddressFor("GrandaMento", address(0x1)); - registry.setAddressFor("Exchange", address(exchange)); - registry.setAddressFor("SortedOracles", address(sortedOracles)); - - reserve.setGoldToken(address(celoToken)); - celoToken.initialize(address(registry)); - - mint(celoToken, address(reserve), initialReserveBalance); - - address[] memory initialAddresses = new address[](0); - uint256[] memory initialBalances = new uint256[](0); - - stableToken.initialize( - "Celo Dollar", - "cUSD", - 18, - address(registry), - FixidityLib.unwrap(FixidityLib.fixed1()), - 60 * 60 * 24 * 7, - initialAddresses, - initialBalances, - "Exchange" - ); - stableToken.initializeV2(address(0), address(0), address(exchange)); - - sortedOracles.setMedianRate(address(stableToken), stableAmountForRate); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - sortedOracles.setNumRates(address(stableToken), 2); - - exchange.initialize( - address(registry), - "StableToken", - FixidityLib.unwrap(spread), - FixidityLib.unwrap(reserveFraction), - referenceRateResetFrequency, - 2 - ); - } - - function getBuyTokenAmount( - uint256 sellAmount, - uint256 sellSupply, - uint256 buySupply - ) public view returns (uint256) { - return getBuyTokenAmount(sellAmount, sellSupply, buySupply, spread); - } - - function getBuyTokenAmount( - uint256 sellAmount, - uint256 sellSupply, - uint256 buySupply, - FixidityLib.Fraction memory spread_ - ) public pure returns (uint256) { - FixidityLib.Fraction memory reducedSellAmount = FixidityLib.newFixed(sellAmount).multiply( - FixidityLib.fixed1().subtract(spread_) - ); - FixidityLib.Fraction memory numerator = reducedSellAmount.multiply(FixidityLib.newFixed(buySupply)); - FixidityLib.Fraction memory denominator = FixidityLib.newFixed(sellSupply).add(reducedSellAmount); - return numerator.unwrap().div(denominator.unwrap()); - } - - function getSellTokenAmount( - uint256 buyAmount, - uint256 sellSupply, - uint256 buySupply - ) public view returns (uint256) { - return getSellTokenAmount(buyAmount, sellSupply, buySupply, spread); - } - - function getSellTokenAmount( - uint256 buyAmount, - uint256 sellSupply, - uint256 buySupply, - FixidityLib.Fraction memory spread_ - ) public pure returns (uint256) { - FixidityLib.Fraction memory numerator = FixidityLib.newFixed(buyAmount.mul(sellSupply)); - FixidityLib.Fraction memory denominator = FixidityLib.newFixed(buySupply.sub(buyAmount)).multiply( - FixidityLib.fixed1().subtract(spread_) - ); - return numerator.unwrap().div(denominator.unwrap()); - } -} - -contract Exchange_initializeAndSetters is ExchangeTest { - function test_initialize_shouldHaveSetOwner() public view { - assert(exchange.owner() == deployer); - } - - function test_initialize_setsStableTokenIdentifier() public view { - bytes32 identifier = exchange.stableTokenRegistryId(); - assert(identifier == keccak256("StableToken")); - } - - function test_initialize_canOnlyBeCalledOnce() public { - vm.expectRevert("contract already initialized"); - exchange.initialize( - address(registry), - "StableToken", - FixidityLib.unwrap(FixidityLib.newFixedFraction(3, 1000)), - FixidityLib.unwrap(FixidityLib.newFixedFraction(5, 100)), - 60 * 60, - 2 - ); - } - - function test_activateStable_setsTheStableStorageAddress() public { - assert(exchange.stable() == address(0)); - vm.expectEmit(true, true, true, true, address(exchange)); - emit StableTokenSet(address(stableToken)); - exchange.activateStable(); - assert(exchange.stable() == address(stableToken)); - } - - function test_activateStable_canOnlyBeCalledByOwner() public { - vm.expectRevert("Ownable: caller is not the owner"); - changePrank(rando); - exchange.activateStable(); - } - - function test_activateStable_canOnlyBeCalledOnce() public { - exchange.activateStable(); - vm.expectRevert("StableToken address already activated"); - exchange.activateStable(); - } - - function test_setUpdateFrequency_setsTheValueAndEmits() public { - vm.expectEmit(true, true, true, true, address(exchange)); - emit UpdateFrequencySet(60 * 3); - exchange.setUpdateFrequency(60 * 3); - assert(exchange.updateFrequency() == 60 * 3); - } - - function test_setUpdateFrequency_isOnlyCallableByOwner() public { - changePrank(rando); - vm.expectRevert("Ownable: caller is not the owner"); - exchange.setUpdateFrequency(60 * 4); - } - - function test_setMinimumReports_setsTheValueAndEmits() public { - vm.expectEmit(true, true, true, true, address(exchange)); - emit MinimumReportsSet(10); - exchange.setMinimumReports(10); - assert(exchange.minimumReports() == 10); - } - - function test_setMinimumReports_isOnlyCallableByOwner() public { - changePrank(rando); - vm.expectRevert("Ownable: caller is not the owner"); - exchange.setMinimumReports(10); - } - - function test_setStableToken_setsTheValueAndEmits() public { - vm.expectEmit(true, true, true, true, address(exchange)); - emit StableTokenSet(address(11)); - exchange.setStableToken(address(11)); - assert(exchange.stable() == address(11)); - } - - function test_setStableToken_isOnlyCallableByOwner() public { - changePrank(rando); - vm.expectRevert("Ownable: caller is not the owner"); - exchange.setStableToken(address(11)); - } - - function test_setSpread_setsTheValueAndEmits() public { - uint256 newSpread = FixidityLib.unwrap(FixidityLib.newFixedFraction(5, 100)); - vm.expectEmit(true, true, true, true, address(exchange)); - emit SpreadSet(newSpread); - exchange.setSpread(newSpread); - assert(exchange.spread() == newSpread); - } - - function test_setSpread_isOnlyCallableByOwner() public { - changePrank(rando); - vm.expectRevert("Ownable: caller is not the owner"); - exchange.setSpread(0); - } - - function test_setReserveFraction_setsTheValueAndEmits() public { - uint256 newReserveFraction = FixidityLib.unwrap(FixidityLib.newFixedFraction(5, 100)); - vm.expectEmit(true, true, true, true, address(exchange)); - emit ReserveFractionSet(newReserveFraction); - exchange.setReserveFraction(newReserveFraction); - assert(exchange.reserveFraction() == newReserveFraction); - } - - function test_setReserveFraction_cantBeOne() public { - uint256 newReserveFraction = FixidityLib.unwrap(FixidityLib.fixed1()); - vm.expectRevert("reserve fraction must be smaller than 1"); - exchange.setReserveFraction(newReserveFraction); - } - - function test_setReserveFraction_isOnlyCallableByOwner() public { - changePrank(rando); - vm.expectRevert("Ownable: caller is not the owner"); - exchange.setReserveFraction(0); - } -} - -contract ExchangeTest_stableActivated is ExchangeTest { - function setUp() public { - super.setUp(); - exchange.activateStable(); - } -} - -contract ExchangeTest_buyAndSellValues is ExchangeTest_stableActivated { - function test_getBuyAndSellBuckets_returnsTheCorrectAmountOfTokens() public view { - (uint256 buyBucketSize, uint256 sellBucketSize) = exchange.getBuyAndSellBuckets(true); - assert(buyBucketSize == initialStableBucket); - assert(sellBucketSize == initialCeloBucket); - } - - function test_getBuyAndSellBuckets_afterReserveChange_isTheSameIfNotStale() public { - mint(celoToken, address(reserve), initialReserveBalance); - - (uint256 buyBucketSize, uint256 sellBucketSize) = exchange.getBuyAndSellBuckets(true); - assert(buyBucketSize == initialStableBucket); - assert(sellBucketSize == initialCeloBucket); - } - - function test_getBuyAndSellBuckets_afterReserveChange_updatesIfTimeHasPassed() public { - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - - (uint256 buyBucketSize, uint256 sellBucketSize) = exchange.getBuyAndSellBuckets(true); - assert(buyBucketSize == 2 * initialStableBucket); - assert(sellBucketSize == 2 * initialCeloBucket); - } - - function test_getBuyAndSellBuckets_afterOracelUpdate_isTheSameIfNotStale() public { - sortedOracles.setMedianRate(address(stableToken), celoAmountForRate.mul(4)); - (uint256 buyBucketSize, uint256 sellBucketSize) = exchange.getBuyAndSellBuckets(true); - assert(buyBucketSize == initialStableBucket); - assert(sellBucketSize == initialCeloBucket); - } - - function test_getBuyAndSellBuckets_afterOracelUpdate_updatesIfTimeHasPassed() public { - sortedOracles.setMedianRate(address(stableToken), celoAmountForRate.mul(4)); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - - (uint256 buyBucketSize, uint256 sellBucketSize) = exchange.getBuyAndSellBuckets(true); - assert(buyBucketSize == initialStableBucket * 2); - assert(sellBucketSize == initialCeloBucket); - } - - function test_getBuyTokenAmount_returnsCorrectNumberOfTokens(uint256 amount) public { - vm.assume(amount < initialCeloBucket); - uint256 buyAmount = exchange.getBuyTokenAmount(amount, true); - uint256 expectedBuyAmount = getBuyTokenAmount(amount, initialCeloBucket, initialStableBucket); - assertEq(buyAmount, expectedBuyAmount); - } - - function test_getSellTokenAmount_returnsCorrectNumberOfTokens(uint256 amount) public { - vm.assume(amount < initialCeloBucket); - uint256 sellAmount = exchange.getSellTokenAmount(amount, true); - uint256 expectedSellAmount = getSellTokenAmount(amount, initialCeloBucket, initialStableBucket); - assertEq(sellAmount, expectedSellAmount); - } -} - -contract ExchangeTest_sell is ExchangeTest_stableActivated { - address seller; - uint256 constant sellerCeloBalance = 100000000000000000000; - uint256 constant sellerStableBalance = 100000000000000000000; - - function setUp() public { - super.setUp(); - seller = vm.addr(2); - vm.label(seller, "Seller"); - mint(celoToken, seller, sellerCeloBalance); - mint(stableToken, seller, sellerStableBalance); - } - - // This function will be overriden to test both `sell` and `exchange` functions - function sell( - uint256 amount, - uint256 minBuyAmount, - bool sellCelo - ) internal returns (uint256) { - return exchange.sell(amount, minBuyAmount, sellCelo); - } - - function approveExchange(uint256 amount, bool sellCelo) internal { - changePrank(seller); - if (sellCelo) { - vm.expectEmit(true, true, true, true, address(celoToken)); - emit Approval(seller, address(exchange), amount); - celoToken.approve(address(exchange), amount); - } else { - vm.expectEmit(true, true, true, true, address(stableToken)); - emit Approval(seller, address(exchange), amount); - stableToken.approve(address(exchange), amount); - } - } - - function approveAndSell(uint256 amount, bool sellCelo) internal returns (uint256 expected, uint256 received) { - approveExchange(amount, sellCelo); - if (sellCelo) { - expected = getBuyTokenAmount(amount, initialCeloBucket, initialStableBucket); - } else { - expected = getBuyTokenAmount(amount, initialStableBucket, initialCeloBucket); - } - vm.expectEmit(true, true, true, true, address(exchange)); - emit Exchanged(seller, amount, expected, sellCelo); - received = sell(amount, expected, sellCelo); - } - - function approveAndSell( - uint256 amount, - bool sellCelo, - uint256 updatedCeloBucket, - uint256 updatedStableBucket - ) internal returns (uint256 expected, uint256 received) { - approveExchange(amount, sellCelo); - if (sellCelo) { - expected = getBuyTokenAmount(amount, initialCeloBucket, initialStableBucket); - } else { - expected = getBuyTokenAmount(amount, initialStableBucket, initialCeloBucket); - } - vm.expectEmit(true, true, true, true, address(exchange)); - emit BucketsUpdated(updatedCeloBucket, updatedStableBucket); - emit Exchanged(seller, expected, amount, sellCelo); - received = sell(amount, expected, sellCelo); - } - - function test_sellCelo_executesTrade(uint256 amount) public { - vm.assume(amount <= sellerCeloBalance && amount > 10); - uint256 stableSupply = stableToken.totalSupply(); - uint256 expected = getBuyTokenAmount(amount, initialCeloBucket, initialStableBucket); - - approveAndSell(amount, true); - - assertEq(stableToken.balanceOf(seller), sellerStableBalance + expected); - assertEq(celoToken.balanceOf(seller), sellerCeloBalance - amount); - assertEq(celoToken.allowance(seller, address(exchange)), 0); - assertEq(celoToken.balanceOf(address(reserve)), initialReserveBalance + amount); - assertEq(stableToken.totalSupply(), stableSupply + expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket - expected); - assertEq(tradableCelo, initialCeloBucket + amount); - } - - function test_sellCelo_revertsIfApprovalIsWrong(uint256 amount) public { - vm.assume(amount < sellerCeloBalance); - approveExchange(amount, true); - uint256 expectedStableAmount = getBuyTokenAmount(amount, initialCeloBucket, initialStableBucket); - vm.expectRevert("transfer value exceeded sender's allowance for recipient"); - sell(amount + 1, expectedStableAmount, true); - } - - function test_sellCelo_revertsIfMinBuyAmountUnsatisfied(uint256 amount) public { - vm.assume(amount <= sellerCeloBalance); - approveExchange(amount, true); - uint256 expectedStableAmount = getBuyTokenAmount(amount, initialCeloBucket, initialStableBucket); - - vm.expectRevert("Calculated buyAmount was less than specified minBuyAmount"); - sell(amount, expectedStableAmount + 1, true); - } - - function test_sellCelo_whenBucketsStaleAndReportFresh_updatesBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - - uint256 updatedCeloBucket = initialCeloBucket.mul(2); - uint256 updatedStableBucket = updatedCeloBucket.mul(stableAmountForRate).div(celoAmountForRate); - (uint256 expected, ) = approveAndSell(amount, true, updatedCeloBucket, updatedStableBucket); - - assertEq(stableToken.balanceOf(seller), sellerStableBalance + expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, updatedStableBucket - expected); - assertEq(tradableCelo, updatedCeloBucket + amount); - } - - function test_sellCelo_whenBucketsStaleAndReportStale_doesNotUpdateBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setOldestReportExpired(address(stableToken)); - - (uint256 expected, ) = approveAndSell(amount, true); - assertEq(stableToken.balanceOf(seller), sellerStableBalance + expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket - expected); - assertEq(tradableCelo, initialCeloBucket + amount); - } - - function test_sellStable_executesTrade(uint256 amount) public { - vm.assume(amount < sellerStableBalance && amount > 10); - uint256 stableTokenSupplyBefore = stableToken.totalSupply(); - uint256 expected = getBuyTokenAmount(amount, initialStableBucket, initialCeloBucket); - - approveAndSell(amount, false); - - assertEq(stableToken.balanceOf(seller), sellerStableBalance - amount); - assertEq(celoToken.balanceOf(seller), sellerCeloBalance + expected); - assertEq(stableToken.allowance(seller, address(exchange)), 0); - assertEq(celoToken.balanceOf(address(reserve)), initialReserveBalance - expected); - assertEq(stableToken.totalSupply(), stableTokenSupplyBefore - amount); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket + amount); - assertEq(tradableCelo, initialCeloBucket - expected); - } - - function test_sellStable_revertWithoutApproval(uint256 amount) public { - vm.assume(amount < sellerStableBalance); - uint256 expectedCelo = getBuyTokenAmount(amount, initialStableBucket, initialCeloBucket); - changePrank(seller); - approveExchange(amount, false); - vm.expectRevert("ERC20: insufficient allowance"); - sell(amount + 1, expectedCelo, false); - } - - function test_sellStable_revertIfMinBuyAmountUnsatisfied(uint256 amount) public { - vm.assume(amount < sellerStableBalance); - uint256 expectedCelo = getBuyTokenAmount(amount, initialStableBucket, initialCeloBucket); - changePrank(seller); - approveExchange(amount, false); - vm.expectRevert("Calculated buyAmount was less than specified minBuyAmount"); - sell(amount, expectedCelo + 1, false); - } - - function test_sellStable_whenBucketsStaleAndReportFresh_updatesBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - - uint256 updatedCeloBucket = initialCeloBucket.mul(2); - uint256 updatedStableBucket = updatedCeloBucket.mul(stableAmountForRate).div(celoAmountForRate); - (uint256 expected, ) = approveAndSell(amount, false, updatedCeloBucket, updatedStableBucket); - - assertEq(celoToken.balanceOf(seller), sellerCeloBalance + expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, updatedStableBucket + amount); - assertEq(tradableCelo, updatedCeloBucket - expected); - } - - function test_sellStable_whenBucketsStaleAndReportStale_doesNotUpdateBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setOldestReportExpired(address(stableToken)); - - (uint256 expected, ) = approveAndSell(amount, false); - assertEq(celoToken.balanceOf(seller), sellerCeloBalance + expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket + amount); - assertEq(tradableCelo, initialCeloBucket - expected); - } - - function test_whenContractIsFrozen_reverts() public { - freezer.freeze(address(exchange)); - vm.expectRevert("can't call when contract is frozen"); - sell(1000, 0, false); - } -} - -contract ExchangeTest_exchange is ExchangeTest_sell { - function sell( - uint256 amount, - uint256 minBuyAmount, - bool sellCelo - ) internal returns (uint256) { - changePrank(seller); - return exchange.exchange(amount, minBuyAmount, sellCelo); - } -} - -contract ExchangeTest_buy is ExchangeTest_stableActivated { - address buyer; - uint256 constant buyerCeloBalance = 100000000000000000000; - uint256 constant buyerStableBalance = 100000000000000000000; - - function setUp() public { - super.setUp(); - buyer = vm.addr(2); - vm.label(buyer, "buyer"); - mint(celoToken, buyer, buyerCeloBalance); - mint(stableToken, buyer, buyerStableBalance); - } - - function approveExchange(uint256 amount, bool buyCelo) internal returns (uint256 expected) { - changePrank(buyer); - if (buyCelo) { - expected = getSellTokenAmount(amount, initialStableBucket, initialCeloBucket); - vm.expectEmit(true, true, true, true, address(stableToken)); - emit Approval(buyer, address(exchange), expected); - stableToken.approve(address(exchange), expected); - } else { - expected = getSellTokenAmount(amount, initialCeloBucket, initialStableBucket); - vm.expectEmit(true, true, true, true, address(celoToken)); - emit Approval(buyer, address(exchange), expected); - celoToken.approve(address(exchange), expected); - } - } - - function approveAndBuy(uint256 amount, bool buyCelo) internal returns (uint256 expected, uint256 received) { - expected = approveExchange(amount, buyCelo); - vm.expectEmit(true, true, true, true, address(exchange)); - emit Exchanged(buyer, expected, amount, !buyCelo); - received = exchange.buy(amount, expected, buyCelo); - } - - function approveAndBuy( - uint256 amount, - bool buyCelo, - uint256 updatedCeloBucket, - uint256 updateStableBucket - ) internal returns (uint256 expected, uint256 received) { - expected = approveExchange(amount, buyCelo); - vm.expectEmit(true, true, true, true, address(exchange)); - emit BucketsUpdated(updatedCeloBucket, updateStableBucket); - emit Exchanged(buyer, expected, amount, !buyCelo); - received = exchange.buy(amount, expected, buyCelo); - } - - function test_buyCelo_executesTrade(uint256 amount) public { - vm.assume(amount < buyerCeloBalance); - uint256 expected = getSellTokenAmount(amount, initialStableBucket, initialCeloBucket); - vm.assume(expected < buyerStableBalance); - uint256 stableSupply = stableToken.totalSupply(); - - approveAndBuy(amount, true); - - assertEq(stableToken.balanceOf(buyer), buyerStableBalance - expected); - assertEq(celoToken.balanceOf(buyer), buyerCeloBalance + amount); - assertEq(stableToken.allowance(buyer, address(exchange)), 0); - assertEq(celoToken.balanceOf(address(reserve)), initialReserveBalance - amount); - assertEq(stableToken.totalSupply(), stableSupply - expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket + expected); - assertEq(tradableCelo, initialCeloBucket - amount); - } - - function test_buyCelo_revertsIfApprovalIsWrong(uint256 amount) public { - vm.assume(amount > 10 && amount < buyerCeloBalance); - uint256 expected = getSellTokenAmount(amount, initialStableBucket, initialCeloBucket); - vm.assume(expected < buyerStableBalance); - approveExchange(amount - 10, true); - vm.expectRevert("ERC20: insufficient allowance"); - exchange.buy(amount, expected, true); - } - - function test_buyCelo_revertsIfMaxSellAmountUnsatisfied(uint256 amount) public { - vm.assume(amount <= buyerCeloBalance); - uint256 expected = approveExchange(amount, true); - vm.expectRevert("Calculated sellAmount was greater than specified maxSellAmount"); - exchange.buy(amount + 1, expected, true); - } - - function test_buyCelo_whenBucketsStaleAndReportFresh_updatesBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - - uint256 updatedCeloBucket = initialCeloBucket.mul(2); - uint256 updatedStableBucket = updatedCeloBucket.mul(stableAmountForRate).div(celoAmountForRate); - (uint256 expected, ) = approveAndBuy(amount, true, updatedCeloBucket, updatedStableBucket); - - assertEq(celoToken.balanceOf(buyer), buyerCeloBalance + amount); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, updatedStableBucket + expected); - assertEq(tradableCelo, updatedCeloBucket - amount); - } - - function test_buyCelo_whenBucketsStaleAndReportStale_doesNotUpdateBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setOldestReportExpired(address(stableToken)); - - (uint256 expected, ) = approveAndBuy(amount, true); - assertEq(celoToken.balanceOf(buyer), buyerCeloBalance + amount); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket + expected); - assertEq(tradableCelo, initialCeloBucket - amount); - } - - function test_buyStable_executesTrade(uint256 amount) public { - vm.assume(amount < buyerStableBalance && amount > 0); - uint256 stableTokenSupplyBefore = stableToken.totalSupply(); - uint256 expected = getSellTokenAmount(amount, initialCeloBucket, initialStableBucket); - - approveAndBuy(amount, false); - - assertEq(stableToken.balanceOf(buyer), buyerStableBalance + amount); - assertEq(celoToken.balanceOf(buyer), buyerCeloBalance - expected); - assertEq(celoToken.allowance(buyer, address(exchange)), 0); - assertEq(celoToken.balanceOf(address(reserve)), initialReserveBalance + expected); - assertEq(stableToken.totalSupply(), stableTokenSupplyBefore + amount); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket - amount); - assertEq(tradableCelo, initialCeloBucket + expected); - } - - function test_buyStable_revertWithoutApproval(uint256 amount) public { - vm.assume(amount < buyerStableBalance && amount > 10); - uint256 expected = getSellTokenAmount(amount, initialCeloBucket, initialStableBucket); - vm.assume(expected < buyerCeloBalance); - approveExchange(amount - 10, false); - vm.expectRevert("transfer value exceeded sender's allowance for recipient"); - exchange.buy(amount, expected, false); - } - - function test_buyStable_revertIfMinBuyAmountUnsatisfied(uint256 amount) public { - vm.assume(amount < buyerStableBalance && amount > 0); - uint256 expected = getSellTokenAmount(amount, initialCeloBucket, initialStableBucket); - vm.assume(expected > 0); - approveExchange(amount, false); - vm.expectRevert("Calculated sellAmount was greater than specified maxSellAmount"); - exchange.buy(amount + 10, expected, false); - } - - function test_buyStable_whenBucketsStaleAndReportFresh_updatesBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setMedianTimestampToNow(address(stableToken)); - - uint256 updatedCeloBucket = initialCeloBucket.mul(2); - uint256 updatedStableBucket = updatedCeloBucket.mul(stableAmountForRate).div(celoAmountForRate); - (uint256 expected, ) = approveAndBuy(amount, false, updatedCeloBucket, updatedStableBucket); - - assertEq(celoToken.balanceOf(buyer), buyerCeloBalance - expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, updatedStableBucket - amount); - assertEq(tradableCelo, updatedCeloBucket + expected); - } - - function test_buyStable_whenBucketsStaleAndReportStale_doesNotUpdateBuckets() public { - uint256 amount = 1000; - mint(celoToken, address(reserve), initialReserveBalance); - vm.warp(block.timestamp + referenceRateResetFrequency); - sortedOracles.setOldestReportExpired(address(stableToken)); - - (uint256 expected, ) = approveAndBuy(amount, false); - assertEq(celoToken.balanceOf(buyer), buyerCeloBalance - expected); - (uint256 mintableStable, uint256 tradableCelo) = exchange.getBuyAndSellBuckets(true); - assertEq(mintableStable, initialStableBucket - amount); - assertEq(tradableCelo, initialCeloBucket + expected); - } - - function test_whenContractIsFrozen_reverts() public { - freezer.freeze(address(exchange)); - vm.expectRevert("can't call when contract is frozen"); - exchange.buy(1000, 0, false); - } -} diff --git a/test/legacy/StableToken.t.sol b/test/legacy/StableToken.t.sol deleted file mode 100644 index 03d5ae72..00000000 --- a/test/legacy/StableToken.t.sol +++ /dev/null @@ -1,758 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; - -import { Test, console2 as console } from "celo-foundry/Test.sol"; - -import { WithRegistry } from "../utils/WithRegistry.t.sol"; - -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; -import { UsingPrecompiles } from "contracts/common/UsingPrecompiles.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; - -import { StableToken } from "contracts/legacy/StableToken.sol"; -import { Freezer } from "contracts/common/Freezer.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { BaseTest } from "../utils/BaseTest.t.sol"; - -contract StableTokenTest is BaseTest, UsingPrecompiles { - using SafeMath for uint256; - using FixidityLib for FixidityLib.Fraction; - - event InflationFactorUpdated(uint256 factor, uint256 lastUpdated); - event InflationParametersUpdated(uint256 rate, uint256 updatePeriod, uint256 lastUpdated); - event Transfer(address indexed from, address indexed to, uint256 value); - event TransferComment(string comment); - event Approval(address indexed owner, address indexed spender, uint256 value); - - // Dependencies - Freezer freezer; - StableToken testee; - - // Actors - address notDeployer; - - // Global variables - uint256 initTime; - uint256 inflationRate = 0.995 * 10**24; // 0.5% weekly deflation - uint256 inflationRate2 = 1.005 * 10**24; // 0.5% weekly inflation - - function setUp() public { - notDeployer = actor("notDeployer"); - vm.startPrank(deployer); - - freezer = new Freezer(true); - testee = new StableToken(true); - - registry.setAddressFor("Freezer", address(freezer)); - - address[] memory initialAddresses = new address[](0); - uint256[] memory initialBalances = new uint256[](0); - testee.initialize( - "Celo Dollar", - "cUSD", - 18, - address(registry), - 1e24, - 1 weeks, - initialAddresses, - initialBalances, - "Exchange" - ); - - initTime = block.timestamp; - } - - function setUpInflation(uint256 _inflationRate) public { - testee.setInflationParameters(_inflationRate, 1 weeks); - skip(1 weeks); - vm.roll(block.number + 1); - } - - function mockFractionMul0995() public { - ph.mockReturn( - FRACTION_MUL, - keccak256( - abi.encodePacked(uint256(1e24), uint256(1e24), uint256(995e21), uint256(1e24), uint256(1), uint256(18)) - ), - abi.encode(uint256(0.995 * 10**18), uint256(1e18)) - ); - } - - function mockFractionMul0995Twice() public { - ph.mockReturn( - FRACTION_MUL, - keccak256( - abi.encodePacked(uint256(1e24), uint256(1e24), uint256(995e21), uint256(1e24), uint256(2), uint256(18)) - ), - abi.encode(uint256(0.990025 * 10**18), uint256(1e18)) - ); - } - - function mockFractionMul1005() public { - ph.mockReturn( - FRACTION_MUL, - keccak256( - abi.encodePacked(uint256(1e24), uint256(1e24), uint256(1.005 * 10**24), uint256(1e24), uint256(1), uint256(18)) - ), - abi.encode(uint256(1.005 * 10**18), uint256(1e18)) - ); - } - - function mockFractionMul1005Twice() public { - ph.mockReturn( - FRACTION_MUL, - keccak256( - abi.encodePacked(uint256(1e24), uint256(1e24), uint256(1.005 * 10**24), uint256(1e24), uint256(2), uint256(18)) - ), - abi.encode(uint256(1.010025 * 10**18), uint256(1e18)) - ); - } -} - -contract StableTokenTest_initilizerAndSetters is StableTokenTest { - function test_initialize_shouldSetName() public { - assertEq(testee.name(), "Celo Dollar"); - } - - function test_initialize_shouldSetSymbol() public { - assertEq(testee.symbol(), "cUSD"); - } - - function test_initialize_shouldSetOwner() public view { - assert(testee.owner() == deployer); - } - - function test_initialize_shouldSetDecimals() public view { - assert(testee.decimals() == 18); - } - - function test_initialize_shouldSetRegistry() public view { - assert(address(testee.registry()) == address(registry)); - } - - function test_initialize_shouldSetInflationRateParams() public { - (uint256 rate, uint256 factor, uint256 updatePeriod, uint256 lastUpdated) = testee.getInflationParameters(); - - assertEq(rate, FixidityLib.unwrap(FixidityLib.fixed1())); - assertEq(factor, FixidityLib.unwrap(FixidityLib.fixed1())); - assertEq(updatePeriod, 1 weeks); - assertEq(lastUpdated, initTime); - } - - function test_initialize_whenCalledAgain_shouldRevert() public { - address[] memory addresses = new address[](0); - uint256[] memory balances = new uint256[](0); - vm.expectRevert("contract already initialized"); - testee.initialize( - "Celo Dollar", - "cUSD", - 18, - address(registry), - FixidityLib.unwrap(FixidityLib.fixed1()), - 1 weeks, - addresses, - balances, - "Exchange" - ); - } - - function test_setRegistry_shouldSetRegistryAddress() public { - address newRegistry = actor("newRegistry"); - testee.setRegistry(newRegistry); - assertEq(address(testee.registry()), newRegistry); - } - - function test_setRegistry_whenNotCalledByOwner_shouldRevert() public { - changePrank(notDeployer); - address newRegistry = actor("newRegistry"); - vm.expectRevert("Ownable: caller is not the owner"); - testee.setRegistry(newRegistry); - } -} - -contract StableTokenTest_mint is StableTokenTest { - address exchange; - address validators; - address grandaMento; - address broker; - - uint256 mintAmount = 100 * 10**18; - - function setUp() public { - super.setUp(); - - exchange = actor("exchange"); - validators = actor("validators"); - grandaMento = actor("grandaMento"); - broker = actor("broker"); - - registry.setAddressFor("Exchange", exchange); - registry.setAddressFor("Validators", validators); - registry.setAddressFor("GrandaMento", grandaMento); - registry.setAddressFor("Broker", broker); - } - - function mintAndAssert(address to, uint256 value) public { - changePrank(to); - testee.mint(to, value); - assertEq(testee.balanceOf(to), value); - assertEq(testee.totalSupply(), value); - } - - function test_mint_whenCalledByExchange_shouldMintTokens() public { - mintAndAssert(exchange, mintAmount); - } - - function test_mint_whenCalledByValidators_shouldMintTokens() public { - mintAndAssert(validators, mintAmount); - } - - function test_mint_whenCalledByGrandaMento_shouldMintTokens() public { - mintAndAssert(grandaMento, mintAmount); - } - - function test_mint_whenCalledByBroker_shouldMintTokens() public { - mintAndAssert(broker, mintAmount); - } - - function test_mint_whenValueIsZero_shouldAllowMint() public { - mintAndAssert(validators, 0); - } - - function test_mint_whenSenderIsNotAuthorized_shouldRevert() public { - changePrank(notDeployer); - vm.expectRevert("Sender not authorized to mint"); - testee.mint(notDeployer, 10000); - } - - function test_mint_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - mockFractionMul0995(); - - changePrank(deployer); - setUpInflation(inflationRate); - - changePrank(exchange); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.mint(exchange, mintAmount); - } -} - -contract StableTokenTest_transferWithComment is StableTokenTest { - address sender; - address receiver; - string comment; - uint256 mintAmount = 100 * 10**18; - - function setUp() public { - super.setUp(); - - sender = actor("sender"); - receiver = actor("receiver"); - comment = "pineapples absolutely do not belong on pizza"; - - registry.setAddressFor("Exchange", sender); - registry.setAddressFor("Validators", sender); - registry.setAddressFor("GrandaMento", sender); - - changePrank(sender); - testee.mint(sender, mintAmount); - } - - function test_transferWithComment_whenToAddressIsNull_shouldRevert() public { - vm.expectRevert("transfer attempted to reserved address 0x0"); - testee.transferWithComment(address(0), 1, comment); - } - - function test_transferWithComment_whenValueGreaterThanBalance_shouldRevert() public { - uint256 value = IERC20(testee).balanceOf(sender) + 1; - vm.expectRevert("transfer value exceeded balance of sender"); - testee.transferWithComment(receiver, value, comment); - } - - function test_transferWithComment_shouldTransferBalance() public { - uint256 senderBalanceBefore = IERC20(testee).balanceOf(sender); - uint256 receiverBalanceBefore = IERC20(testee).balanceOf(receiver); - - vm.expectEmit(true, true, true, true); - emit Transfer(sender, receiver, 5); - - vm.expectEmit(true, true, true, true); - emit TransferComment(comment); - - testee.transferWithComment(receiver, 5, comment); - - uint256 senderBalanceAfter = IERC20(testee).balanceOf(sender); - uint256 receiverBalanceAfter = IERC20(testee).balanceOf(receiver); - - assertEq(senderBalanceAfter, senderBalanceBefore - 5); - assertEq(receiverBalanceAfter, receiverBalanceBefore + 5); - } - - function test_transferWithComment_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - mockFractionMul0995(); - changePrank(deployer); - setUpInflation(inflationRate); - - changePrank(sender); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.transferWithComment(receiver, 5, comment); - } - - function test_transferWithComment_whenContractIsFrozen_shouldRevert() public { - changePrank(deployer); - freezer.freeze(address(testee)); - vm.expectRevert("can't call when contract is frozen"); - testee.transferWithComment(receiver, 5, comment); - } -} - -contract StableTokenTest_setInflationParameters is StableTokenTest { - uint256 newUpdatePeriod = 1 weeks + 5; - - function test_setInflationParameters_shouldUpdateParameters() public { - uint256 newInflationRate = 2.1428571429 * 10**24; - - changePrank(deployer); - testee.setInflationParameters(newInflationRate, newUpdatePeriod); - (uint256 rate, , uint256 updatePeriod, uint256 lastUpdated) = testee.getInflationParameters(); - - assertEq(rate, newInflationRate); - assertEq(updatePeriod, newUpdatePeriod); - assertEq(lastUpdated, initTime); - } - - function test_setInflationParameters_shouldEmitEvent() public { - newUpdatePeriod = 1 weeks + 5; - - changePrank(deployer); - vm.expectEmit(true, true, true, true); - emit InflationParametersUpdated(inflationRate, newUpdatePeriod, now); - testee.setInflationParameters(inflationRate, newUpdatePeriod); - } - - function test_setInflationParameters_whenInflationFactorIsOutdated_shouldUpdateFactor() public { - mockFractionMul0995(); - - changePrank(deployer); - - setUpInflation(inflationRate); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.setInflationParameters(2, newUpdatePeriod); - } - - function test_setInflationParameters_whenRateIsZero_shouldRevert() public { - changePrank(deployer); - vm.expectRevert("Must provide a non-zero inflation rate."); - testee.setInflationParameters(0, 1 weeks); - } -} - -contract StableTokenTest_balanceOf is StableTokenTest { - uint256 mintAmount = 1000; - address sender; - - function setUp() public { - super.setUp(); - - sender = actor("sender"); - - registry.setAddressFor("Exchange", sender); - - changePrank(sender); - testee.mint(sender, mintAmount); - } - - function test_balanceOf_whenNoInflation_shouldFetchCorrectBalance() public { - uint256 balance = testee.balanceOf(sender); - assertEq(balance, mintAmount); - vm.roll(block.number + 1); - uint256 newBalance = testee.balanceOf(sender); - assertEq(newBalance, balance); - } - - function test_balanceOf_withInflation_shouldFetchCorrectBalance() public { - changePrank(deployer); - setUpInflation(inflationRate); - mockFractionMul0995(); - uint256 adjustedBalance = testee.balanceOf(sender); - assertEq(adjustedBalance, 1005); - } - - function test_balanceOf_withInflation2_shouldFetchCorrectBalance() public { - changePrank(deployer); - setUpInflation(inflationRate2); - mockFractionMul1005(); - uint256 adjustedBalance = testee.balanceOf(sender); - assertEq(adjustedBalance, 995); - } -} - -contract StableTokenTest_valueToUnits is StableTokenTest { - function test_valueToUnits_value1000AfterInflation_shouldCorrespondToRoughly995() public { - changePrank(deployer); - setUpInflation(inflationRate); - mockFractionMul0995(); - uint256 value = testee.valueToUnits(1000); - assertEq(value, 995); - } - - function test_valueToUnits_value1000AfterInflationTwice_shouldCorrespondToRoughly990() public { - changePrank(deployer); - setUpInflation(inflationRate); - mockFractionMul0995Twice(); - skip(1 weeks); - uint256 value = testee.valueToUnits(1000); - assertEq(value, 990); - } - - function test_valueToUnits_value1000AfterInflation2_shouldCorrespondToRoughly1005() public { - changePrank(deployer); - setUpInflation(inflationRate2); - mockFractionMul1005(); - uint256 value = testee.valueToUnits(1000); - assertEq(value, 1005); - } - - function test_valueToUnits_value1000AfterInflation2Twice_shouldCorrespondToRoughly1010() public { - changePrank(deployer); - setUpInflation(inflationRate2); - mockFractionMul1005Twice(); - skip(1 weeks); - uint256 value = testee.valueToUnits(1000); - assertEq(value, 1010); - } -} - -contract StableTokenTest_unitsToValue is StableTokenTest { - function test_unitsToValue_1000UnitsAfterInflation_shouldCorrespondToRoughly1005() public { - changePrank(deployer); - setUpInflation(inflationRate); - mockFractionMul0995(); - uint256 value = testee.unitsToValue(1000); - assertEq(value, 1005); - } - - function test_unitsToValue_1000UnitsAfterInflationTwice_shouldCorrespondToRoughly1010() public { - changePrank(deployer); - setUpInflation(inflationRate); - mockFractionMul0995Twice(); - skip(1 weeks); - uint256 value = testee.unitsToValue(1000); - assertEq(value, 1010); - } - - function test_unitsToValue_1000UnitsAfterInflation2_shouldCorrespondToRoughly995() public { - changePrank(deployer); - setUpInflation(inflationRate2); - mockFractionMul1005(); - uint256 value = testee.unitsToValue(1000); - assertEq(value, 995); - } - - function test_unitsToValue_1000UnitsAfterInflation2Twice_shouldCorrespondToRoughly990() public { - changePrank(deployer); - setUpInflation(inflationRate2); - mockFractionMul1005Twice(); - skip(1 weeks); - uint256 value = testee.unitsToValue(1000); - assertEq(value, 990); - } -} - -contract StableTokenTest_burn is StableTokenTest { - address exchange; - address grandaMento; - address broker; - - uint256 mintAmount = 10; - uint256 burnAmount = 5; - - function setUp() public { - super.setUp(); - - exchange = actor("exchange"); - grandaMento = actor("grandaMento"); - broker = actor("broker"); - - registry.setAddressFor("Exchange", exchange); - registry.setAddressFor("GrandaMento", grandaMento); - registry.setAddressFor("Broker", broker); - } - - function burnAndAssert(address to, uint256 value) public { - changePrank(to); - vm.expectEmit(true, true, true, true); - emit Transfer(to, address(0), testee.valueToUnits(value)); - testee.burn(value); - assertEq(testee.balanceOf(to), value); - assertEq(testee.totalSupply(), value); - } - - function test_burn_whenCalledByExchange_shouldBurnTokens() public { - changePrank(exchange); - testee.mint(exchange, mintAmount); - burnAndAssert(exchange, burnAmount); - } - - function test_burn_whenCalledByGrandaMento_shouldBurnTokens() public { - changePrank(grandaMento); - testee.mint(grandaMento, mintAmount); - burnAndAssert(grandaMento, burnAmount); - } - - function test_burn_whenCalledByBroker_shouldBurnTokens() public { - changePrank(broker); - testee.mint(broker, mintAmount); - burnAndAssert(broker, burnAmount); - } - - function test_burn_whenValueExceedsBalance_shouldRevert() public { - changePrank(grandaMento); - testee.mint(grandaMento, mintAmount); - vm.expectRevert("value exceeded balance of sender"); - testee.burn(11); - } - - function test_burn_whenCalledByUnothorizedSender_shouldRevert() public { - changePrank(notDeployer); - vm.expectRevert("Sender not authorized to burn"); - testee.burn(burnAmount); - } - - function test_burn_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - mockFractionMul0995(); - changePrank(deployer); - setUpInflation(inflationRate); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - changePrank(exchange); - testee.mint(exchange, mintAmount); - testee.burn(burnAmount); - } -} - -contract StableTokenTest_getExchangeRegistryId is StableTokenTest { - function test_getExchangeRegistryId_shouldMatchInitializedValue() public { - StableToken stableToken2 = new StableToken(true); - stableToken2.initialize( - "Celo Dollar", - "cUSD", - 18, - address(registry), - 1e24, - 1 weeks, - new address[](0), - new uint256[](0), - "ExchangeEUR" - ); - bytes32 fetchedId = stableToken2.getExchangeRegistryId(); - assertEq(fetchedId, keccak256("ExchangeEUR")); - } - - function test_whenUnitialized_shouldFallbackToDefault() public { - StableToken stableToken2 = new StableToken(true); - bytes32 fetchedId = stableToken2.getExchangeRegistryId(); - assertEq(fetchedId, keccak256("Exchange")); - } -} - -contract StableTokenTest_erc20Functions is StableTokenTest { - address sender = actor("sender"); - address receiver = actor("receiver"); - uint256 transferAmount = 1; - uint256 amountToMint = 10; - - function setUp() public { - super.setUp(); - - registry.setAddressFor("Exchange", sender); - changePrank(sender); - testee.mint(sender, amountToMint); - } - - function assertBalance(address spenderAddress, uint256 balance) public { - assertEq(testee.balanceOf(spenderAddress), balance); - } - - function test_allowance_shouldReturnAllowance() public { - testee.approve(receiver, transferAmount); - assertEq(testee.allowance(sender, receiver), transferAmount); - } - - function test_approve_whenSpenderIsNotZeroAddress_shouldUpdateAndEmit() public { - vm.expectEmit(true, true, true, true); - emit Approval(sender, receiver, transferAmount); - bool res = testee.approve(receiver, transferAmount); - assertEq(res, true); - assertEq(testee.allowance(sender, receiver), transferAmount); - } - - function test_approve_whenSpenderIsZeroAddress_shouldRevert() public { - vm.expectRevert("reserved address 0x0 cannot have allowance"); - testee.approve(address(0), transferAmount); - } - - function test_approve_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - mockFractionMul0995(); - - changePrank(deployer); - setUpInflation(inflationRate); - - changePrank(sender); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.approve(receiver, transferAmount); - } - - function test_increaseAllowance_whenSpenderIsNotZeroAddress_shouldUpdateAndEmit() public { - vm.expectEmit(true, true, true, true); - emit Approval(sender, receiver, 2); - testee.increaseAllowance(receiver, 2); - assertEq(testee.allowance(sender, receiver), 2); - assertEq(testee.allowance(sender, sender), 0); - } - - function test_increaseAllowance_whenSpenderIsZeroAddress_shouldRevert() public { - vm.expectRevert("reserved address 0x0 cannot have allowance"); - testee.increaseAllowance(address(0), transferAmount); - } - - function test_increaseAllowance_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - mockFractionMul0995(); - - changePrank(deployer); - setUpInflation(inflationRate); - changePrank(sender); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.increaseAllowance(receiver, 2); - } - - function test_decreaseAllowance_whenSpenderIsNotZeroAddress_shouldUpdateAndEmit() public { - testee.approve(receiver, 2); - vm.expectEmit(true, true, true, true); - emit Approval(sender, receiver, transferAmount); - bool res = testee.decreaseAllowance(receiver, transferAmount); - assertEq(res, true); - assertEq(testee.allowance(sender, receiver), transferAmount); - } - - function test_decreaseAllowance_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - testee.approve(receiver, 2); - - mockFractionMul0995(); - - changePrank(deployer); - testee.approve(receiver, 2); - setUpInflation(inflationRate); - changePrank(sender); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.decreaseAllowance(receiver, transferAmount); - } - - function test_transfer_whenReceiverIsNotZeroAddress_shouldTransferAndEmit() public { - uint256 senderStartBalance = testee.balanceOf(sender); - uint256 receiverStartBalance = testee.balanceOf(receiver); - - vm.expectEmit(true, true, true, true); - emit Transfer(sender, receiver, transferAmount); - bool res = testee.transfer(receiver, transferAmount); - assertEq(res, true); - assertBalance(sender, senderStartBalance.sub(transferAmount)); - assertBalance(receiver, receiverStartBalance.add(transferAmount)); - } - - function test_transfer_whenReceiverIsZeroAddress_shouldRevert() public { - vm.expectRevert("transfer attempted to reserved address 0x0"); - testee.transfer(address(0), transferAmount); - } - - function test_transfer_whenItExceedsSenderBalance_shouldRevert() public { - vm.expectRevert("transfer value exceeded balance of sender"); - testee.transfer(receiver, amountToMint + 1); - } - - function test_transfer_whenContractIsFrozen_shouldRevert() public { - changePrank(deployer); - freezer.freeze(address(testee)); - vm.expectRevert("can't call when contract is frozen"); - testee.transfer(receiver, amountToMint + 1); - } - - function test_transfer_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - mockFractionMul0995(); - - changePrank(deployer); - setUpInflation(inflationRate); - - changePrank(sender); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.transfer(receiver, transferAmount); - } - - function test_transferFrom_whenSenderIsExchange_shouldTransferAndEmit() public { - testee.approve(sender, transferAmount); - - uint256 exchangeStartBalance = testee.balanceOf(sender); - uint256 receiverStartBalance = testee.balanceOf(receiver); - - vm.expectEmit(true, true, true, true); - emit Transfer(sender, receiver, transferAmount); - bool res = testee.transferFrom(sender, receiver, transferAmount); - assertEq(res, true); - assertBalance(sender, exchangeStartBalance.sub(transferAmount)); - assertBalance(receiver, receiverStartBalance.add(transferAmount)); - } - - function test_transferFrom_whenReceiverIsZeroAddress_shouldRevert() public { - vm.expectRevert("transfer attempted to reserved address 0x0"); - testee.transferFrom(sender, address(0), transferAmount); - } - - function test_transferFrom_whenItExceedsSenderBalance_shouldRevert() public { - vm.expectRevert("transfer value exceeded balance of sender"); - testee.transferFrom(sender, receiver, amountToMint + 1); - } - - function test_transferFrom_whenItExceedsSpenderAllowence_shoulrRevert() public { - vm.expectRevert("transfer value exceeded sender's allowance for recipient"); - testee.transferFrom(sender, receiver, transferAmount); - } - - function test_transferFrom_whenContractIsFrozen_shouldRevert() public { - changePrank(deployer); - freezer.freeze(address(testee)); - vm.expectRevert("can't call when contract is frozen"); - testee.transferFrom(sender, receiver, transferAmount); - } - - function test_transferFrom_whenInflationFactorIsOutdated_shouldUpdateAndEmit() public { - testee.approve(sender, transferAmount); - - mockFractionMul0995(); - - changePrank(deployer); - setUpInflation(inflationRate); - - changePrank(sender); - - vm.expectEmit(true, true, true, true); - emit InflationFactorUpdated(inflationRate, 1 weeks + initTime); - testee.transferFrom(sender, receiver, transferAmount); - } -} diff --git a/test/mocks/MockBreakerBox.sol b/test/mocks/MockBreakerBox.sol deleted file mode 100644 index 411c4dfa..00000000 --- a/test/mocks/MockBreakerBox.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; - -contract MockBreakerBox is IBreakerBox { - uint256 public tradingMode; - - function setTradingMode(uint256 _tradingMode) external { - tradingMode = _tradingMode; - } - - function getBreakers() external view returns (address[] memory) { - return new address[](0); - } - - function isBreaker(address) external view returns (bool) { - return true; - } - - function getRateFeedTradingMode(address) external view returns (uint8) { - return 0; - } - - function checkAndSetBreakers(address) external {} -} diff --git a/test/mocks/MockLocking.sol b/test/mocks/MockLocking.sol deleted file mode 100644 index 49d59cc8..00000000 --- a/test/mocks/MockLocking.sol +++ /dev/null @@ -1,12 +0,0 @@ -pragma solidity ^0.8.0; - -import "../../contracts/governance/locking/Locking.sol"; - -contract MockLocking { - function initiateData( - uint256 idLock, - LibBrokenLine.Line memory line, - address locker, - address delegate - ) external {} -} diff --git a/test/mocks/MockStableToken.sol b/test/mocks/MockStableToken.sol deleted file mode 100644 index 54c6c096..00000000 --- a/test/mocks/MockStableToken.sol +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; -// solhint-disable no-unused-vars, const-name-snakecase - -import "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import "contracts/common/FixidityLib.sol"; - -/** - * @title A mock StableToken for testing. - */ -contract MockStableToken { - using FixidityLib for FixidityLib.Fraction; - using SafeMath for uint256; - - uint8 public constant decimals = 18; - uint256 public _totalSupply; - FixidityLib.Fraction public inflationFactor; - - // Stored as units. Value can be found using unitsToValue(). - mapping(address => uint256) public balances; - - constructor() public { - setInflationFactor(FixidityLib.fixed1().unwrap()); - } - - function setInflationFactor(uint256 newInflationFactor) public { - inflationFactor = FixidityLib.wrap(newInflationFactor); - } - - function setTotalSupply(uint256 value) external { - _totalSupply = value; - } - - function mint(address to, uint256 value) external returns (bool) { - require(to != address(0), "0 is a reserved address"); - balances[to] = balances[to].add(valueToUnits(value)); - _totalSupply = _totalSupply.add(value); - return true; - } - - function burn(uint256 value) external returns (bool) { - balances[msg.sender] = balances[msg.sender].sub(valueToUnits(value)); - _totalSupply = _totalSupply.sub(value); - return true; - } - - function totalSupply() external view returns (uint256) { - return _totalSupply; - } - - function transfer(address to, uint256 value) external returns (bool) { - return _transfer(msg.sender, to, value); - } - - function transferFrom( - address from, - address to, - uint256 value - ) external returns (bool) { - return _transfer(from, to, value); - } - - function _transfer( - address from, - address to, - uint256 value - ) internal returns (bool) { - uint256 balanceValue = balanceOf(from); - if (balanceValue < value) { - return false; - } - uint256 units = valueToUnits(value); - balances[from] = balances[from].sub(units); - balances[to] = balances[to].add(units); - return true; - } - - function balanceOf(address account) public view returns (uint256) { - return unitsToValue(balances[account]); - } - - function unitsToValue(uint256 units) public view returns (uint256) { - return FixidityLib.newFixed(units).divide(inflationFactor).fromFixed(); - } - - function valueToUnits(uint256 value) public view returns (uint256) { - return inflationFactor.multiply(FixidityLib.newFixed(value)).fromFixed(); - } - - function getExchangeRegistryId() public view returns (bytes32) {} -} diff --git a/test/oracles/SortedOracles.t.sol b/test/oracles/SortedOracles.t.sol deleted file mode 100644 index 4254fd6e..00000000 --- a/test/oracles/SortedOracles.t.sol +++ /dev/null @@ -1,594 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; - -import { Test, console2 as console } from "celo-foundry/Test.sol"; - -import { MockBreakerBox } from "../mocks/MockBreakerBox.sol"; - -import { SortedLinkedListWithMedian } from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; - -import { SortedOracles } from "contracts/common/SortedOracles.sol"; - -contract SortedOraclesTest is Test { - // Declare SortedOracles events for matching - event ReportExpirySet(uint256 reportExpiry); - event TokenReportExpirySet(address token, uint256 reportExpiry); - event OracleAdded(address indexed token, address indexed oracleAddress); - event OracleRemoved(address indexed token, address indexed oracleAddress); - event OracleReportRemoved(address indexed token, address indexed oracle); - event MedianUpdated(address indexed token, uint256 value); - event OracleReported(address indexed token, address indexed oracle, uint256 timestamp, uint256 value); - event BreakerBoxUpdated(address indexed newBreakerBox); - - SortedOracles sortedOracles; - address owner; - address notOwner; - address rando; - address token; - uint256 aReportExpiry = 3600; - uint256 fixed1 = FixidityLib.unwrap(FixidityLib.fixed1()); - - address oracle; - - bytes32 constant MOCK_EXCHANGE_ID = keccak256(abi.encodePacked("mockExchange")); - - MockBreakerBox mockBreakerBox; - - function setUp() public { - sortedOracles = new SortedOracles(true); - sortedOracles.initialize(aReportExpiry); - - owner = address(this); - notOwner = address(10); - rando = address(2); - token = address(3); - oracle = address(4); - - mockBreakerBox = new MockBreakerBox(); - sortedOracles.setBreakerBox(IBreakerBox(mockBreakerBox)); - vm.startPrank(owner); - currentPrank = owner; - } - - /** - * @notice Test helper function. Submits n Reports for a token from n different Oracles. - */ - - function submitNReports(uint256 n) public { - sortedOracles.addOracle(token, oracle); - changePrank(oracle); - sortedOracles.report(token, fixed1 * 10, address(0), address(0)); - for (uint256 i = 5; i < 5 + n - 1; i++) { - address anotherOracle = address(i); - changePrank(owner); - sortedOracles.addOracle(token, anotherOracle); - changePrank(address(i)); - sortedOracles.report(token, fixed1 * 10, oracle, address(0)); - } - changePrank(owner); - } -} - -/** - * @notice Tests - */ -contract SortedOracles_initialize is SortedOraclesTest { - function test_initialize_shouldHaveSetTheOwner() public { - assertEq(sortedOracles.owner(), owner); - } - - function test_initialize_shouldHaveSetReportExpiryToAReportExpiry() public { - assertEq(sortedOracles.reportExpirySeconds(), aReportExpiry); - } - - function test_initialize_whenCalledAgain_shouldRevert() public { - vm.expectRevert("contract already initialized"); - sortedOracles.initialize(aReportExpiry); - } -} - -contract SortedOracles_setReportExpiry is SortedOraclesTest { - function test_setReportExpiry_shouldUpdateReportExpiry() public { - sortedOracles.setReportExpiry(aReportExpiry + 1); - assertEq(sortedOracles.reportExpirySeconds(), aReportExpiry + 1); - } - - function test_setReportExpiry_shouldEmitEvent() public { - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit ReportExpirySet(aReportExpiry + 1); - sortedOracles.setReportExpiry(aReportExpiry + 1); - } - - function test_setReportExpiry_whenCalledByNonOwner_shouldRevert() public { - vm.expectRevert("Ownable: caller is not the owner"); - changePrank(rando); - sortedOracles.setReportExpiry(aReportExpiry + 1); - } -} - -contract SortedOracles_setTokenReportExpiry is SortedOraclesTest { - uint256 aNewReportExpiry = aReportExpiry + 1; - - function test_setTokenReportExpiry_shouldUpdateTokenReportExpiry() public { - sortedOracles.setTokenReportExpiry(token, aNewReportExpiry); - assertEq(sortedOracles.tokenReportExpirySeconds(token), aNewReportExpiry); - } - - function test_setTokenReportExpiry_shouldEmitTokenReportExpirySetEvent() public { - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit TokenReportExpirySet(token, aNewReportExpiry); - sortedOracles.setTokenReportExpiry(token, aNewReportExpiry); - } - - function test_setTokenReportExpiry_whenCalledByNonOwner_shouldRevert() public { - vm.expectRevert("Ownable: caller is not the owner"); - changePrank(rando); - sortedOracles.setTokenReportExpiry(token, aNewReportExpiry); - } -} - -contract SortedOracles_getTokenReportExpiry is SortedOraclesTest { - function test_getTokenReportExpirySeconds_whenNoTokenLevelExpiryIsSet_shouldReturnContractLevel() public { - assertEq(sortedOracles.getTokenReportExpirySeconds(token), aReportExpiry); - } - - function test_getTokenReportExpirySeconds_whenTokenLevelExpiryIsSet_shouldReturnTokenLevel() public { - sortedOracles.setTokenReportExpiry(token, aReportExpiry + 1); - assertEq(sortedOracles.getTokenReportExpirySeconds(token), aReportExpiry + 1); - } -} - -contract SortedOracles_addOracles is SortedOraclesTest { - function test_addOracle_shouldAddAnOracle() public { - sortedOracles.addOracle(token, oracle); - assertTrue(sortedOracles.isOracle(token, oracle)); - } - - function test_addOracle_shouldEmitEvent() public { - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit OracleAdded(token, oracle); - sortedOracles.addOracle(token, oracle); - } - - function test_addOracle_whenTokenIsTheNullAddress_shouldRevert() public { - vm.expectRevert("token addr was null or oracle addr was null or oracle addr is already an oracle for token addr"); - sortedOracles.addOracle(address(0), oracle); - } - - function test_addOracle_whenOracleIsTheNullAddress_shouldRevert() public { - vm.expectRevert("token addr was null or oracle addr was null or oracle addr is already an oracle for token addr"); - sortedOracles.addOracle(token, address(0)); - } - - function test_addOracle_whenOracleHasBeenAdded_shouldRevert() public { - sortedOracles.addOracle(token, oracle); - vm.expectRevert("token addr was null or oracle addr was null or oracle addr is already an oracle for token addr"); - sortedOracles.addOracle(token, oracle); - } - - function test_addOracle_whenCalledByNonOwner_shouldRevert() public { - vm.expectRevert("Ownable: caller is not the owner"); - changePrank(rando); - sortedOracles.addOracle(token, oracle); - } -} - -contract SortedOracles_breakerBox is SortedOraclesTest { - function test_setBreakerBox_whenCalledByNonOwner_shouldRevert() public { - changePrank(notOwner); - vm.expectRevert("Ownable: caller is not the owner"); - sortedOracles.setBreakerBox(MockBreakerBox(address(0))); - } - - function test_setBreakerBox_whenGivenAddressIsNull_shouldRevert() public { - vm.expectRevert("BreakerBox address must be set"); - sortedOracles.setBreakerBox(MockBreakerBox(address(0))); - } - - function test_setBreakerBox_shouldUpdateAndEmit() public { - sortedOracles = new SortedOracles(true); - assertEq(address(sortedOracles.breakerBox()), address(0)); - vm.expectEmit(true, true, true, true); - emit BreakerBoxUpdated(address(mockBreakerBox)); - - sortedOracles.setBreakerBox(mockBreakerBox); - assertEq(address(sortedOracles.breakerBox()), address(mockBreakerBox)); - } -} - -contract SortedOracles_RemoveOracles is SortedOraclesTest { - function test_removeOracle_shouldRemoveAnOracle() public { - sortedOracles.addOracle(token, oracle); - sortedOracles.removeOracle(token, oracle, 0); - assertFalse(sortedOracles.isOracle(token, oracle)); - } - - function test_removeOracle_whenMoreThanOneReportExists_shouldDecreaseNumberOfRates() public { - submitNReports(2); - sortedOracles.removeOracle(token, oracle, 0); - assertEq(sortedOracles.numRates(token), 1); - } - - function test_removeOracle_whenMoreThanOneReportExists_shouldDecreaseNumberOfTimestamps() public { - submitNReports(2); - sortedOracles.removeOracle(token, oracle, 0); - assertEq(sortedOracles.numTimestamps(token), 1); - } - - function test_removeOracle_whenMoreThanOneReportExists_shouldEmitOracleRemovedOracleReportRemovedMedianUpdatedEvent() - public - { - submitNReports(1); - sortedOracles.addOracle(token, address(6)); - changePrank(address(6)); - sortedOracles.report(token, fixed1 * 12, oracle, address(0)); - - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit OracleReportRemoved(token, address(6)); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit MedianUpdated(token, fixed1 * 10); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit OracleRemoved(token, address(6)); - - changePrank(owner); - sortedOracles.removeOracle(token, address(6), 1); - } - - function test_removeOracle_whenOneReportExists_shouldNotDecreaseNumberOfRates() public { - submitNReports(1); - sortedOracles.removeOracle(token, oracle, 0); - assertEq(sortedOracles.numRates(token), 1); - } - - function test_removeOracle_whenOneReportExists_shouldNotResetTheMedianRate() public { - submitNReports(1); - (uint256 numeratorBefore, ) = sortedOracles.medianRate(token); - sortedOracles.removeOracle(token, oracle, 0); - (uint256 numeratorAfter, ) = sortedOracles.medianRate(token); - assertEq(numeratorBefore, numeratorAfter); - } - - function test_removeOracle_whenOneReportExists_shouldNotDecreaseNumberOfTimestamps() public { - submitNReports(1); - sortedOracles.removeOracle(token, oracle, 0); - assertEq(sortedOracles.numTimestamps(token), 1); - } - - function test_removeOracle_whenOneReportExists_shouldNotResetTheMedianTimestamp() public { - submitNReports(1); - uint256 medianTimestampBefore = sortedOracles.medianTimestamp(token); - sortedOracles.removeOracle(token, oracle, 0); - uint256 medianTimestampAfter = sortedOracles.medianTimestamp(token); - assertEq(medianTimestampBefore, medianTimestampAfter); - } - - function testFail_removeOracle_whenOneReportExists_shouldNotEmitTheOracleReportedAndMedianUpdatedEvent() public { - // testFail feals impricise here. - // TODO: Better way of testing this case :) - submitNReports(1); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit OracleReportRemoved(token, oracle); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit MedianUpdated(token, 0); - sortedOracles.removeOracle(token, oracle, 0); - } - - function test_removeOracle_whenOneReportExists_shouldEmitTheOracleRemovedEvent() public { - submitNReports(1); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit OracleRemoved(token, oracle); - sortedOracles.removeOracle(token, oracle, 0); - } - - function test_removeOracle_whenIndexIsWrong_shouldRevert() public { - submitNReports(1); - vm.expectRevert("token addr null or oracle addr null or index of token oracle not mapped to oracle addr"); - sortedOracles.removeOracle(token, oracle, 1); - } - - function test_removeOracle_whenAddressIsWrong_shouldRevert() public { - submitNReports(1); - vm.expectRevert("token addr null or oracle addr null or index of token oracle not mapped to oracle addr"); - sortedOracles.removeOracle(token, address(15), 0); - } - - function test_removeOracle_whenCalledByNonOwner_shouldRevert() public { - submitNReports(1); - vm.expectRevert("Ownable: caller is not the owner"); - changePrank(rando); - sortedOracles.removeOracle(token, address(15), 0); - } -} - -contract SortedOracles_removeExpiredReports is SortedOraclesTest { - function test_removeExpiredReports_whenNoReportExists_shouldRevert() public { - sortedOracles.addOracle(token, oracle); - vm.expectRevert("token addr null or trying to remove too many reports"); - sortedOracles.removeExpiredReports(token, 1); - } - - function test_removeExpiredReports_whenOnlyOneReportExists_shouldRevert() public { - sortedOracles.addOracle(token, oracle); - changePrank(oracle); - sortedOracles.report(token, fixed1, address(0), address(0)); - vm.expectRevert("token addr null or trying to remove too many reports"); - sortedOracles.removeExpiredReports(token, 1); - } - - function test_removeExpiredReports_whenOldestReportIsNotExpired_shouldDoNothing() public { - submitNReports(5); - sortedOracles.removeExpiredReports(token, 3); - assertEq(sortedOracles.numTimestamps(token), 5); - } - - function test_removeExpiredReports_whenLessThanNReportsAreExpired_shouldRemoveAllExpiredAndStop() public { - //first 5 expired reports - submitNReports(5); - skip(aReportExpiry); - //two reports that aren't expired - sortedOracles.addOracle(token, address(10)); - changePrank(address(10)); - sortedOracles.report(token, fixed1 * 10, oracle, address(0)); - changePrank(owner); - sortedOracles.addOracle(token, address(11)); - changePrank(address(11)); - sortedOracles.report(token, fixed1 * 10, oracle, address(0)); - - changePrank(owner); - sortedOracles.removeExpiredReports(token, 6); - assertEq(sortedOracles.numTimestamps(token), 2); - } - - function test_removeExpiredReports_whenNLargerThanNumberOfTimestamps_shouldRevert() public { - submitNReports(5); - vm.expectRevert("token addr null or trying to remove too many reports"); - sortedOracles.removeExpiredReports(token, 7); - } - - function test_removeExpiredReports_whenNReportsAreExpired_shouldRemoveNReports() public { - submitNReports(6); - skip(aReportExpiry); - sortedOracles.removeExpiredReports(token, 5); - assertEq(sortedOracles.numTimestamps(token), 1); - } - - function test_removeExpiredReports_whenMoreThanOneReportExistsAndMedianUpdated_shouldCallCheckAndSetBreakers() - public - { - submitNReports(2); - sortedOracles.addOracle(token, address(6)); - changePrank(address(6)); - - vm.warp(now + aReportExpiry); - sortedOracles.report(token, fixed1 * 12, oracle, address(0)); - - vm.expectEmit(false, false, false, false, address(sortedOracles)); - emit OracleReportRemoved(token, rando); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit MedianUpdated(token, fixed1 * 12); - vm.expectCall(address(mockBreakerBox), abi.encodeWithSelector(mockBreakerBox.checkAndSetBreakers.selector)); - - sortedOracles.removeExpiredReports(token, 2); - } -} - -contract SortedOracles_isOldestReportExpired is SortedOraclesTest { - function test_isOldestReportExpired_whenNoReportsExist_shouldReturnTrue() public { - //added this skip because foundry starts at a block time of 0 - //without the skip isOldestReortExpired would return false when no reports exist - skip(aReportExpiry); - sortedOracles.addOracle(token, oracle); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertTrue(isReportExpired); - } - - function test_isOldestReportExpired_whenReportIsExpired_shouldReturnTrue() public { - submitNReports(1); - skip(aReportExpiry); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertTrue(isReportExpired); - } - - function test_isOldestReportExpired_whenReportIsntExpired_shouldReturnFalse() public { - submitNReports(1); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertFalse(isReportExpired); - } - - function test_isOldestReportExpired_whenTokenSpecificExpiryIsntExceeded_shoulReturnFalse() public { - submitNReports(1); - sortedOracles.setTokenReportExpiry(token, aReportExpiry * 2); - //neither general nor specific Expiry expired - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertFalse(isReportExpired); - //general Expiry expired but not specific - skip(aReportExpiry); - (isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertFalse(isReportExpired); - } - - function test_isOldestReportExpired_whenSpecificTokenExpiryIsExceeded_shouldReturnTrue() public { - submitNReports(1); - sortedOracles.setTokenReportExpiry(token, aReportExpiry * 2); - skip(aReportExpiry * 2); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertTrue(isReportExpired); - } - - function test_isOldestReportExpired_whenSpecificExpiryIsLowerButNotExpired_shouldReturnFalse() public { - submitNReports(1); - sortedOracles.setTokenReportExpiry(token, (aReportExpiry * 1) / 2); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertFalse(isReportExpired); - } - - function test_isOldestReportExpired_whenSpecificExpiryIsLowerAndGeneralExpiryIsExceeded_shouldReturnTrue() public { - submitNReports(1); - sortedOracles.setTokenReportExpiry(token, (aReportExpiry * 1) / 2); - skip(aReportExpiry); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertTrue(isReportExpired); - } - - function test_isOldestReportExpired_whenSpecificExpiryIsLowerAndSpecificExpiryIsExceeded_shouldReturnTrue() public { - submitNReports(1); - sortedOracles.setTokenReportExpiry(token, (aReportExpiry * 1) / 2); - skip((aReportExpiry * 1) / 2); - (bool isReportExpired, ) = sortedOracles.isOldestReportExpired(token); - assertTrue(isReportExpired); - } -} - -contract SortedOracles_report is SortedOraclesTest { - address oracleB = actor("oracleB"); - address oracleC = actor("oracleC"); - - function test_report_shouldIncreaseTheNumberOfRates() public { - assertEq(sortedOracles.numRates(token), 0); - submitNReports(1); - assertEq(sortedOracles.numRates(token), 1); - } - - function test_report_shouldSetTheMedianRate() public { - sortedOracles.addOracle(token, oracle); - changePrank(oracle); - sortedOracles.report(token, fixed1 * 10, address(0), address(0)); - (uint256 numerator, uint256 denominator) = sortedOracles.medianRate(token); - assertEq(numerator, fixed1 * 10); - assertEq(denominator, fixed1); - } - - function test_report_shouldIncreaseTheNumberOfTimestamps() public { - assertEq(sortedOracles.numTimestamps(token), 0); - submitNReports(1); - assertEq(sortedOracles.numTimestamps(token), 1); - } - - function test_report_shouldSetTheMedianTimestamp() public { - submitNReports(1); - assertEq(block.timestamp, sortedOracles.medianTimestamp(token)); - } - - function test_report_shouldEmitTheOracleReportedAndMedianUpdatedEvent() public { - sortedOracles.addOracle(token, oracle); - vm.expectEmit(true, true, true, true, address(sortedOracles)); - emit OracleReported(token, oracle, block.timestamp, fixed1 * 10); - emit MedianUpdated(token, fixed1 * 10); - changePrank(oracle); - sortedOracles.report(token, fixed1 * 10, address(0), address(0)); - } - - function test_report_whenCalledByNonOracle_shouldRevert() public { - changePrank(rando); - vm.expectRevert("sender was not an oracle for token addr"); - sortedOracles.report(token, fixed1, address(0), address(0)); - } - - function test_report_whenOneReportBySameOracleExists_shouldResetMedianRate() public { - submitNReports(1); - (uint256 numerator, uint256 denominator) = sortedOracles.medianRate(token); - assertEq(numerator, fixed1 * 10); - assertEq(denominator, fixed1); - - changePrank(oracle); - sortedOracles.report(token, fixed1 * 20, address(0), address(0)); - (numerator, denominator) = sortedOracles.medianRate(token); - assertEq(numerator, fixed1 * 20); - assertEq(denominator, fixed1); - } - - function test_report_whenOneReportBySameOracleExists_shouldNotChangeNumberOfTotalReports() public { - submitNReports(1); - uint256 initialNumberOfReports = sortedOracles.numRates(token); - changePrank(oracle); - sortedOracles.report(token, fixed1 * 20, address(0), address(0)); - assertEq(initialNumberOfReports, sortedOracles.numRates(token)); - } - - function test_report_whenMultipleReportsExistTheMostRecent_shouldUpdateListOfRatesCorrectly() public { - address anotherOracle = address(5); - uint256 oracleValue1 = fixed1; - uint256 oracleValue2 = fixed1 * 2; - uint256 oracleValue3 = fixed1 * 3; - sortedOracles.addOracle(token, anotherOracle); - sortedOracles.addOracle(token, oracle); - - changePrank(anotherOracle); - sortedOracles.report(token, oracleValue1, address(0), address(0)); - changePrank(oracle); - sortedOracles.report(token, oracleValue2, anotherOracle, address(0)); - - //confirm correct setUp - changePrank(owner); - (address[] memory oracles, uint256[] memory rates, ) = sortedOracles.getRates(token); - assertEq(oracle, oracles[0]); - assertEq(oracleValue2, rates[0]); - assertEq(anotherOracle, oracles[1]); - assertEq(oracleValue1, rates[1]); - - changePrank(oracle); - sortedOracles.report(token, oracleValue3, anotherOracle, address(0)); - - changePrank(owner); - (oracles, rates, ) = sortedOracles.getRates(token); - assertEq(oracle, oracles[0]); - assertEq(oracleValue3, rates[0]); - assertEq(anotherOracle, oracles[1]); - assertEq(oracleValue1, rates[1]); - } - - function test_report_whenMultipleReportsExistTheMostRecent_shouldUpdateTimestampsCorrectly() public { - address anotherOracle = address(5); - uint256 oracleValue1 = fixed1; - uint256 oracleValue2 = fixed1 * 2; - uint256 oracleValue3 = fixed1 * 3; - sortedOracles.addOracle(token, anotherOracle); - sortedOracles.addOracle(token, oracle); - - changePrank(anotherOracle); - uint256 timestamp0 = block.timestamp; - sortedOracles.report(token, oracleValue1, address(0), address(0)); - skip(5); - - uint256 timestamp1 = block.timestamp; - changePrank(oracle); - sortedOracles.report(token, oracleValue2, anotherOracle, address(0)); - skip(5); - - //confirm correct setUp - changePrank(owner); - (address[] memory oracles, uint256[] memory timestamps, ) = sortedOracles.getTimestamps(token); - assertEq(oracle, oracles[0]); - assertEq(timestamp1, timestamps[0]); - assertEq(anotherOracle, oracles[1]); - assertEq(timestamp0, timestamps[1]); - - changePrank(oracle); - uint256 timestamp3 = block.timestamp; - sortedOracles.report(token, oracleValue3, anotherOracle, address(0)); - - changePrank(owner); - (oracles, timestamps, ) = sortedOracles.getTimestamps(token); - assertEq(oracle, oracles[0]); - assertEq(timestamp3, timestamps[0]); - - assertEq(anotherOracle, oracles[1]); - assertEq(timestamp0, timestamps[1]); - } - - function test_report_shouldCallBreakerBoxWithRateFeedID() public { - // token is a legacy reference of rateFeedID - sortedOracles.addOracle(token, oracle); - sortedOracles.setBreakerBox(mockBreakerBox); - - vm.expectCall(address(mockBreakerBox), abi.encodeWithSelector(mockBreakerBox.checkAndSetBreakers.selector, token)); - - changePrank(oracle); - - sortedOracles.report(token, 9999, address(0), address(0)); - } -} diff --git a/test/oracles/breakers/WithThreshold.t.sol b/test/oracles/breakers/WithThreshold.t.sol deleted file mode 100644 index f9cfacf5..00000000 --- a/test/oracles/breakers/WithThreshold.t.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; - -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { WithThreshold } from "contracts/oracles/breakers/WithThreshold.sol"; - -contract WithThresholdTest is WithThreshold, Test { - function test_setDefaultRateChangeThreshold() public { - uint256 testThreshold = 1e20; - vm.expectEmit(true, true, true, true); - emit DefaultRateChangeThresholdUpdated(testThreshold); - _setDefaultRateChangeThreshold(testThreshold); - assertEq(defaultRateChangeThreshold.unwrap(), testThreshold); - } - - function test_setRateChangeThresholds_withZeroAddress_reverts() public { - address[] memory rateFeedIDs = new address[](1); - uint256[] memory thresholds = new uint256[](1); - - vm.expectRevert("rate feed invalid"); - _setRateChangeThresholds(rateFeedIDs, thresholds); - } - - function test_setRateChangeThresholds_withMismatchingArrays_reverts() public { - address[] memory rateFeedIDs = new address[](2); - uint256[] memory thresholds = new uint256[](1); - - vm.expectRevert("array length missmatch"); - _setRateChangeThresholds(rateFeedIDs, thresholds); - } - - function test_setRateChangeThresholds_emitsEvents() public { - address[] memory rateFeedIDs = new address[](2); - uint256[] memory thresholds = new uint256[](2); - - rateFeedIDs[0] = address(1111); - rateFeedIDs[1] = address(2222); - thresholds[0] = 1e20; - thresholds[1] = 2e20; - - vm.expectEmit(true, true, true, true); - emit RateChangeThresholdUpdated(rateFeedIDs[0], thresholds[0]); - vm.expectEmit(true, true, true, true); - emit RateChangeThresholdUpdated(rateFeedIDs[1], thresholds[1]); - - _setRateChangeThresholds(rateFeedIDs, thresholds); - - assertEq(rateChangeThreshold[rateFeedIDs[0]].unwrap(), thresholds[0]); - assertEq(rateChangeThreshold[rateFeedIDs[1]].unwrap(), thresholds[1]); - } -} - -contract WithThresholdTest_exceedsThreshold is WithThresholdTest { - uint256 constant _1PC = 0.01 * 1e24; // 1% - uint256 constant _10PC = 0.1 * 1e24; // 10% - uint256 constant _20PC = 0.2 * 1e24; // 20% - - address rateFeedID0 = actor("rateFeedID0-10%"); - address rateFeedID1 = actor("rateFeedID2-1%"); - address rateFeedID2 = actor("rateFeedID3-default-20%"); - - function setUp() public { - uint256[] memory ts = new uint256[](2); - ts[0] = _10PC; - ts[1] = _1PC; - address[] memory rateFeedIDs = new address[](2); - rateFeedIDs[0] = rateFeedID0; - rateFeedIDs[1] = rateFeedID1; - - _setDefaultRateChangeThreshold(_20PC); - _setRateChangeThresholds(rateFeedIDs, ts); - } - - function test_exceedsThreshold_withDefault_whenWithin_isFalse() public { - assertEq(exceedsThreshold(1e24, 1.1 * 1e24, rateFeedID2), false); - assertEq(exceedsThreshold(1e24, 0.9 * 1e24, rateFeedID2), false); - } - - function test_exceedsThreshold_withDefault_whenNotWithin_isTrue() public { - assertEq(exceedsThreshold(1e24, 1.3 * 1e24, rateFeedID2), true); - assertEq(exceedsThreshold(1e24, 0.7 * 1e24, rateFeedID2), true); - } - - function test_exceedsThreshold_withOverride_whenWithin_isTrue() public { - assertEq(exceedsThreshold(1e24, 1.1 * 1e24, rateFeedID1), true); - assertEq(exceedsThreshold(1e24, 0.9 * 1e24, rateFeedID1), true); - assertEq(exceedsThreshold(1e24, 1.11 * 1e24, rateFeedID0), true); - assertEq(exceedsThreshold(1e24, 0.89 * 1e24, rateFeedID0), true); - } - - function test_exceedsThreshold_withOverride_whenNotWithin_isFalse() public { - assertEq(exceedsThreshold(1e24, 1.01 * 1e24, rateFeedID1), false); - assertEq(exceedsThreshold(1e24, 1.01 * 1e24, rateFeedID0), false); - } -} diff --git a/test/swap/ConstantProductPricingModule.t.sol b/test/swap/ConstantProductPricingModule.t.sol deleted file mode 100644 index defc7485..00000000 --- a/test/swap/ConstantProductPricingModule.t.sol +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import { console2 as console } from "forge-std/console2.sol"; -import { stdStorage } from "forge-std/Test.sol"; -import { BaseTest } from "../utils/BaseTest.t.sol"; -import { MockSortedOracles } from "../mocks/MockSortedOracles.sol"; - -import { Exchange } from "contracts/legacy/Exchange.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; -import { ConstantProductPricingModule } from "contracts/swap/ConstantProductPricingModule.sol"; - -contract LegacyExchangeWrapper { - address constant registryAddress = 0x000000000000000000000000000000000000ce10; - using stdStorage for stdStorage.StdStorage; - stdStorage.StdStorage internal stdstore; - - Exchange exchange; - - bytes4 constant STABLE_BUCKET_SIG = bytes4(keccak256("stableBucket()")); - bytes4 constant GOLD_BUCKET_SIG = bytes4(keccak256("goldBucket()")); - - constructor() public { - exchange = new Exchange(true); - exchange.initialize(registryAddress, "StableToken", 5 * 1e23, 5 * 1e23, 60 * 60, 2); - } - - function getAmountOut( - uint256 tokenInBucketSize, - uint256 tokenOutBucketSize, - uint256 spread, - uint256 amountIn - ) external returns (uint256 amountOut) { - setupExchange(tokenInBucketSize, tokenOutBucketSize, spread); - return exchange.getBuyTokenAmount(amountIn, false); - } - - function getAmountIn( - uint256 tokenInBucketSize, - uint256 tokenOutBucketSize, - uint256 spread, - uint256 amountOut - ) external returns (uint256 amountIn) { - setupExchange(tokenInBucketSize, tokenOutBucketSize, spread); - return exchange.getSellTokenAmount(amountOut, false); - } - - function setupExchange( - uint256 stableBucket, - uint256 goldBucket, - uint256 spread - ) internal { - exchange.setSpread(spread); - stdstore.target(address(exchange)).sig(STABLE_BUCKET_SIG).checked_write(stableBucket); - stdstore.target(address(exchange)).sig(GOLD_BUCKET_SIG).checked_write(goldBucket); - } -} - -contract ConstantProductPricingModuleTest is BaseTest { - IPricingModule constantProduct; - LegacyExchangeWrapper legacyExchange; - MockSortedOracles sortedOracles; - - uint256 pc1 = 1 * 1e22; - uint256 pc5 = 5 * 1e22; - uint256 pc10 = 1e23; - - function setUp() public { - vm.warp(24 * 60 * 60); - vm.startPrank(deployer); - sortedOracles = new MockSortedOracles(); - sortedOracles.setNumRates(address(0), 10); - - registry.setAddressFor("SortedOracles", address(sortedOracles)); - constantProduct = new ConstantProductPricingModule(); - legacyExchange = new LegacyExchangeWrapper(); - vm.stopPrank(); - } - - function test_getAmountOut_compareWithLegacyExchange_t1() public { - uint256 expectedAmountOut = legacyExchange.getAmountOut(1e24, 2e24, pc1, 1e23); - uint256 newAmountOut = constantProduct.getAmountOut(1e24, 2e24, pc1, 1e23); - - assertEq(newAmountOut, expectedAmountOut); - } - - function test_getAmountOut_compareWithLegacyExchange_t2() public { - uint256 expectedAmountOut = legacyExchange.getAmountOut(11e24, 23e24, pc5, 3e23); - uint256 newAmountOut = constantProduct.getAmountOut(11e24, 23e24, pc5, 3e23); - - assertEq(newAmountOut, expectedAmountOut); - } - - function test_getAmountIn_compareWithLegacyExchange_t1() public { - uint256 expectedAmountIn = legacyExchange.getAmountIn(1e24, 2e24, pc1, 1e23); - uint256 newAmountIn = constantProduct.getAmountIn(1e24, 2e24, pc1, 1e23); - - assertEq(newAmountIn, expectedAmountIn); - } - - function test_getAmountIn_compareWithLegacyExchange_t2() public { - uint256 expectedAmountIn = legacyExchange.getAmountIn(11e24, 23e24, pc5, 3e23); - uint256 newAmountIn = constantProduct.getAmountIn(11e24, 23e24, pc5, 3e23); - - assertEq(newAmountIn, expectedAmountIn); - } -} diff --git a/test/tokens/StableTokenV1V2GasPayment.t.sol b/test/tokens/StableTokenV1V2GasPayment.t.sol deleted file mode 100644 index b727a0d8..00000000 --- a/test/tokens/StableTokenV1V2GasPayment.t.sol +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility -// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.8.0; -pragma experimental ABIEncoderV2; - -import { Arrays } from "../utils/Arrays.sol"; -import { BaseTest } from "../utils/BaseTest.next.sol"; - -import { StableTokenV2 } from "contracts/tokens/StableTokenV2.sol"; - -import { IStableToken } from "contracts/legacy/interfaces/IStableToken.sol"; -import { IFreezer } from "contracts/common/interfaces/IFreezer.sol"; - -contract StableTokenV1V2GasPaymentTest is BaseTest { - StableTokenV2 tokenV2; - IStableToken tokenV1; - IFreezer freezer; - - address holder0 = address(0x2001); - address holder1 = address(0x2002); - address feeRecipient = address(0x2003); - address gatewayFeeReceipient = address(0x2004); - address communityFund = address(0x2005); - - function setUp() public { - tokenV2 = new StableTokenV2(false); - freezer = IFreezer(factory.createContract("Freezer", abi.encode(true))); - tokenV1 = IStableToken(factory.createContract("StableToken", abi.encode(true))); - - tokenV2.initialize( - "cUSD", - "cUSD", - 0, - REGISTRY_ADDRESS, - 0, - 0, - Arrays.addresses(holder0, holder1), - Arrays.uints(1000, 1000), - "" - ); - tokenV1.initialize( - "cUSD", - "cUSD", - 0, - REGISTRY_ADDRESS, - 1e24, - 1 weeks, - Arrays.addresses(holder0, holder1), - Arrays.uints(1000, 1000), - "" - ); - - vm.startPrank(deployer); - registry.setAddressFor("Freezer", address(freezer)); - } - - function test_debitGasFees_whenCalledByVM_shouldBurnAmount() public { - uint256 balanceV1Before = tokenV1.balanceOf(holder0); - uint256 balanceV2Before = tokenV2.balanceOf(holder0); - assertEq(balanceV1Before, 1000); - assertEq(balanceV2Before, 1000); - - vm.startPrank(address(0)); - tokenV1.debitGasFees(holder0, 100); - tokenV2.debitGasFees(holder0, 100); - - uint256 balanceV1After = tokenV1.balanceOf(holder0); - uint256 balanceV2After = tokenV2.balanceOf(holder0); - assertEq(balanceV1After, 900); - assertEq(balanceV2After, 900); - } - - function test_creditGasFees_whenCalledByVM_shouldCreditFees() public { - uint256 amount = 100; - uint256 refund = 20; - uint256 tipTxFee = 30; - uint256 gatewayFee = 10; - uint256 baseTxFee = 40; - - uint256 balanceV1Before = tokenV1.balanceOf(holder0); - uint256 balanceV2Before = tokenV2.balanceOf(holder0); - assertEq(balanceV1Before, 1000); - assertEq(balanceV2Before, 1000); - - vm.startPrank(address(0)); - tokenV1.debitGasFees(holder0, amount); - tokenV1.creditGasFees( - holder0, - feeRecipient, - gatewayFeeReceipient, - communityFund, - refund, - tipTxFee, - gatewayFee, - baseTxFee - ); - - uint256 balanceV1After = tokenV1.balanceOf(holder0); - assertEq(balanceV1After, balanceV1Before - amount + refund); - assertEq(tokenV1.balanceOf(feeRecipient), tipTxFee); - assertEq(tokenV1.balanceOf(gatewayFeeReceipient), gatewayFee); - assertEq(tokenV1.balanceOf(communityFund), baseTxFee); - - tokenV2.debitGasFees(holder0, amount); - tokenV2.creditGasFees( - holder0, - feeRecipient, - gatewayFeeReceipient, - communityFund, - refund, - tipTxFee, - gatewayFee, - baseTxFee - ); - - uint256 balanceV2After = tokenV2.balanceOf(holder0); - assertEq(balanceV2After, balanceV2Before - amount + refund); - assertEq(tokenV2.balanceOf(feeRecipient), tipTxFee); - assertEq(tokenV2.balanceOf(gatewayFeeReceipient), gatewayFee); - assertEq(tokenV2.balanceOf(communityFund), baseTxFee); - } -} diff --git a/test/governance/Airgrab.t.sol b/test/unit/governance/Airgrab.t.sol similarity index 97% rename from test/governance/Airgrab.t.sol rename to test/unit/governance/Airgrab.t.sol index 1292f79d..4f693627 100644 --- a/test/governance/Airgrab.t.sol +++ b/test/unit/governance/Airgrab.t.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, state-visibility, max-states-count, var-name-mixedcase -import { Test } from "forge-std-next/Test.sol"; -import { Arrays } from "test/utils/Arrays.sol"; +import { Test } from "mento-std/Test.sol"; +import { bytes32s } from "mento-std/Array.sol"; import { ECDSA } from "openzeppelin-contracts-next/contracts/utils/cryptography/ECDSA.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; @@ -50,10 +50,10 @@ contract AirgrabTest is Test { address public invalidClaimer = makeAddr("InvalidClaimer"); address public claimer0 = 0x547a9687D36e51DA064eE7C6ac82590E344C4a0e; uint96 public claimer0Amount = 100000000000000000000; - bytes32[] public claimer0Proof = Arrays.bytes32s(0xf213211627972cf2d02a11f800ed3f60110c1d11d04ec1ea8cb1366611efdaa3); + bytes32[] public claimer0Proof = bytes32s(0xf213211627972cf2d02a11f800ed3f60110c1d11d04ec1ea8cb1366611efdaa3); address public claimer1 = 0x6B70014D9c0BF1F53695a743Fe17996f132e9482; uint96 public claimer1Amount = 20000000000000000000000; - bytes32[] public claimer1Proof = Arrays.bytes32s(0x0294d3fc355e136dd6fea7f5c2934dd7cb67c2b4607110780e5fbb23d65d7ac4); + bytes32[] public claimer1Proof = bytes32s(0x0294d3fc355e136dd6fea7f5c2934dd7cb67c2b4607110780e5fbb23d65d7ac4); uint256 public endTimestamp; diff --git a/test/governance/Emission.t.sol b/test/unit/governance/Emission.t.sol similarity index 94% rename from test/governance/Emission.t.sol rename to test/unit/governance/Emission.t.sol index dc09cca2..97589b41 100644 --- a/test/governance/Emission.t.sol +++ b/test/unit/governance/Emission.t.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase -import { TestSetup } from "./TestSetup.sol"; +import { GovernanceTest } from "./GovernanceTest.sol"; import { Emission } from "contracts/governance/Emission.sol"; -import { MockMentoToken } from "../mocks/MockMentoToken.sol"; +import { MockMentoToken } from "test/utils/mocks/MockMentoToken.sol"; -contract EmissionTest is TestSetup { +contract EmissionTest is GovernanceTest { Emission public emission; MockMentoToken public mentoToken; @@ -27,19 +27,19 @@ contract EmissionTest is TestSetup { emission.initialize(address(mentoToken), emissionTarget, EMISSION_SUPPLY); } - function test_initialize_shouldSetOwner() public { + function test_initialize_shouldSetOwner() public view { assertEq(emission.owner(), owner); } - function test_initialize_shouldSetStartTime() public { + function test_initialize_shouldSetStartTime() public view { assertEq(emission.emissionStartTime(), 1); } - function test_initialize_shouldSetEmissionToken() public { + function test_initialize_shouldSetEmissionToken() public view { assertEq(address(emission.mentoToken()), address(mentoToken)); } - function test_initialize_shouldSetEmissionTarget() public { + function test_initialize_shouldSetEmissionTarget() public view { assertEq(emission.emissionTarget(), emissionTarget); } diff --git a/test/governance/GovernanceFactory.t.sol b/test/unit/governance/GovernanceFactory.t.sol similarity index 93% rename from test/governance/GovernanceFactory.t.sol rename to test/unit/governance/GovernanceFactory.t.sol index 808573ab..3163e6fa 100644 --- a/test/governance/GovernanceFactory.t.sol +++ b/test/unit/governance/GovernanceFactory.t.sol @@ -2,17 +2,20 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length -import { TestSetup } from "./TestSetup.sol"; -import { Arrays } from "test/utils/Arrays.sol"; -import { TestLocking } from "../utils/TestLocking.sol"; +import { GovernanceTest } from "./GovernanceTest.sol"; +import { addresses, uints } from "mento-std/Array.sol"; + import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import { LockingHarness } from "test/utils/harnesses/LockingHarness.sol"; +import { GovernanceFactoryHarness } from "test/utils/harnesses/GovernanceFactoryHarness.t.sol"; + import { GovernanceFactory } from "contracts/governance/GovernanceFactory.sol"; -import { GovernanceFactoryHarness } from "./GovernanceFactoryHarness.t.sol"; import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; import { TimelockController } from "contracts/governance/TimelockController.sol"; -contract GovernanceFactoryTest is TestSetup { +contract GovernanceFactoryTest is GovernanceTest { GovernanceFactoryHarness public factory; address public mentoLabsMultiSig = makeAddr("MentoLabsVestingMultisig"); @@ -38,8 +41,8 @@ contract GovernanceFactoryTest is TestSetup { .MentoTokenAllocationParams({ airgrabAllocation: 50, mentoTreasuryAllocation: 100, - additionalAllocationRecipients: Arrays.addresses(mentoLabsMultiSig), - additionalAllocationAmounts: Arrays.uints(200) + additionalAllocationRecipients: addresses(mentoLabsMultiSig), + additionalAllocationAmounts: uints(200) }); factory.createGovernance(watchdogMultiSig, airgrabMerkleRoot, fractalSigner, allocationParams); @@ -97,17 +100,13 @@ contract GovernanceFactoryTest is TestSetup { } function test_createGovernance_whenAdditionalAllocationRecipients_shouldCombineRecipients() public i_setUp { - uint256 supply = 1_000_000_000 * 10**18; + uint256 supply = 1_000_000_000 * 10 ** 18; GovernanceFactory.MentoTokenAllocationParams memory allocationParams = GovernanceFactory .MentoTokenAllocationParams({ airgrabAllocation: 50, mentoTreasuryAllocation: 100, - additionalAllocationRecipients: Arrays.addresses( - makeAddr("Recipient1"), - makeAddr("Recipient2"), - mentoLabsMultiSig - ), - additionalAllocationAmounts: Arrays.uints(55, 50, 80) + additionalAllocationRecipients: addresses(makeAddr("Recipient1"), makeAddr("Recipient2"), mentoLabsMultiSig), + additionalAllocationAmounts: uints(55, 50, 80) }); vm.prank(owner); @@ -167,7 +166,7 @@ contract GovernanceFactoryTest is TestSetup { assertEq(initialImpl, precalculatedAddress, "Factory: lockingProxy should have an implementation"); // deploy and upgrade to new implementation - TestLocking newImplContract = new TestLocking(); + LockingHarness newImplContract = new LockingHarness(); vm.prank(address(factory.governanceTimelock())); proxyAdmin.upgrade(proxy, address(newImplContract)); diff --git a/test/governance/TestSetup.sol b/test/unit/governance/GovernanceTest.sol similarity index 88% rename from test/governance/TestSetup.sol rename to test/unit/governance/GovernanceTest.sol index 19159778..3916f1f4 100644 --- a/test/governance/TestSetup.sol +++ b/test/unit/governance/GovernanceTest.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; -import { Test } from "forge-std-next/Test.sol"; +import { Test } from "mento-std/Test.sol"; -contract TestSetup is Test { +contract GovernanceTest is Test { address public owner = makeAddr("owner"); address public alice = makeAddr("alice"); address public bob = makeAddr("bob"); diff --git a/test/governance/Locking/LibBrokenLine.t.sol b/test/unit/governance/Locking/LibBrokenLine.t.sol similarity index 99% rename from test/governance/Locking/LibBrokenLine.t.sol rename to test/unit/governance/Locking/LibBrokenLine.t.sol index fb0f4450..d18e5a2c 100644 --- a/test/governance/Locking/LibBrokenLine.t.sol +++ b/test/unit/governance/Locking/LibBrokenLine.t.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, contract-name-camelcase -import { TestSetup } from "../TestSetup.sol"; +import { GovernanceTest } from "../GovernanceTest.sol"; import { LibBrokenLine } from "contracts/governance/locking/libs/LibBrokenLine.sol"; -contract LibBrokenLine_Test is TestSetup { +contract LibBrokenLine_Test is GovernanceTest { LibBrokenLine.BrokenLine public brokenLine; - function assertLineEq(LibBrokenLine.Line memory a, LibBrokenLine.Line memory b) internal { + function assertLineEq(LibBrokenLine.Line memory a, LibBrokenLine.Line memory b) internal pure { assertEq(a.start, b.start); assertEq(a.bias, b.bias); assertEq(a.slope, b.slope); diff --git a/test/unit/governance/Locking/Locking.fuzz.t.sol b/test/unit/governance/Locking/Locking.fuzz.t.sol new file mode 100644 index 00000000..f48e0475 --- /dev/null +++ b/test/unit/governance/Locking/Locking.fuzz.t.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.18; +// solhint-disable state-visibility + +import { Test } from "mento-std/Test.sol"; +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; + +import { TestERC20 } from "test/utils/mocks/TestERC20.sol"; +import { LockingHarness } from "test/utils/harnesses/LockingHarness.sol"; + +/** + * @notice https://github.com/rarible/locking-contracts/tree/4f189a96b3e85602dedfbaf69d9a1f5056d835eb + */ +contract FuzzTestLocking is Test { + TestERC20 public testERC20; + + LockingHarness locking; + + address user0; + address user1; + + function setUp() public { + user0 = address(100); + vm.deal(user0, 100 ether); + user1 = address(200); + vm.deal(user1, 100 ether); + testERC20 = new TestERC20("Test", "TST"); + + locking = new LockingHarness(); + locking.__Locking_init(IERC20Upgradeable(address(testERC20)), 0, 1, 3); + locking.incrementBlock(locking.WEEK() + 1); + } + + function testFuzz_lockAmount(uint96 amount) public { + vm.assume(amount < 2 ** 95); + vm.assume(amount > 1e18); + prepareTokens(user0, amount); + lockTokens(user0, user0, uint96(amount), 100, 100); + } + + function testFuzz_lockSlope(uint32 slope) public { + vm.assume(slope >= locking.minSlopePeriod()); + vm.assume(slope <= locking.getMaxSlopePeriod()); + + uint96 amount = 100 * (10 ** 18); + + prepareTokens(user0, amount); + lockTokens(user0, user0, uint96(amount), slope, 100); + } + + function testFuzz_lockCliff(uint32 cliff) public { + vm.assume(cliff >= locking.minCliffPeriod()); + vm.assume(cliff <= locking.getMaxCliffPeriod()); + + uint96 amount = 100 * (10 ** 18); + + prepareTokens(user0, amount); + lockTokens(user0, user0, uint96(amount), 100, cliff); + } + + function prepareTokens(address user, uint256 amount) public { + testERC20.mint(user, amount); + vm.prank(user); + testERC20.approve(address(locking), amount); + assertEq(testERC20.balanceOf(user), amount); + assertEq(testERC20.allowance(user, address(locking)), amount); + } + + function lockTokens(address user, address delegate, uint96 amount, uint32 slopePeriod, uint32 cliff) public { + vm.prank(user); + locking.lock(user, delegate, amount, slopePeriod, cliff); + + assertEq(locking.locked(user), amount); + } +} diff --git a/test/governance/Locking/Base.t.sol b/test/unit/governance/Locking/LockingTest.sol similarity index 71% rename from test/governance/Locking/Base.t.sol rename to test/unit/governance/Locking/LockingTest.sol index 4d48b214..45f7b6b7 100644 --- a/test/governance/Locking/Base.t.sol +++ b/test/unit/governance/Locking/LockingTest.sol @@ -2,20 +2,20 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, contract-name-camelcase -import { TestSetup } from "../TestSetup.sol"; -import { TestLocking } from "../../utils/TestLocking.sol"; -import { MockMentoToken } from "../../mocks/MockMentoToken.sol"; +import { GovernanceTest } from "../GovernanceTest.sol"; +import { LockingHarness } from "test/utils/harnesses/LockingHarness.sol"; +import { MockMentoToken } from "test/utils/mocks/MockMentoToken.sol"; import { IERC20Upgradeable } from "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; -contract Locking_Test is TestSetup { - TestLocking public locking; +contract LockingTest is GovernanceTest { + LockingHarness public locking; MockMentoToken public mentoToken; uint32 public weekInBlocks; function setUp() public virtual { mentoToken = new MockMentoToken(); - locking = new TestLocking(); + locking = new LockingHarness(); vm.prank(owner); locking.__Locking_init(IERC20Upgradeable(address(mentoToken)), 0, 0, 0); diff --git a/test/governance/Locking/delegateTo.t.sol b/test/unit/governance/Locking/delegateTo.t.sol similarity index 98% rename from test/governance/Locking/delegateTo.t.sol rename to test/unit/governance/Locking/delegateTo.t.sol index 055dc6a3..041e123d 100644 --- a/test/governance/Locking/delegateTo.t.sol +++ b/test/unit/governance/Locking/delegateTo.t.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, contract-name-camelcase -import { Locking_Test } from "./Base.t.sol"; +import { LockingTest } from "./LockingTest.sol"; -contract DelegateTo_Locking_Test is Locking_Test { +contract DelegateTo_LockingTest is LockingTest { uint256 public lockId; function test_delegateTo_whenDelegateZero_shouldRevert() public { diff --git a/test/governance/Locking/locking.t.sol b/test/unit/governance/Locking/locking.t.sol similarity index 97% rename from test/governance/Locking/locking.t.sol rename to test/unit/governance/Locking/locking.t.sol index 33d4ad6d..589b4b02 100644 --- a/test/governance/Locking/locking.t.sol +++ b/test/unit/governance/Locking/locking.t.sol @@ -2,11 +2,10 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, contract-name-camelcase -import { Locking_Test } from "./Base.t.sol"; -import { MockLocking } from "../../mocks/MockLocking.sol"; +import { LockingTest } from "./LockingTest.sol"; -contract Lock_Locking_Test is Locking_Test { - function test_init_shouldSetState() public { +contract Lock_LockingTest is LockingTest { + function test_init_shouldSetState() public view { assertEq(address(locking.token()), address(mentoToken)); assertEq(locking.startingPointWeek(), 0); @@ -169,7 +168,7 @@ contract Lock_Locking_Test is Locking_Test { assertEq(locking.balanceOf(alice), 0); } - function test_getLock_shouldReturnCorrectValues() public { + function test_getLock_shouldReturnCorrectValues() public view { uint96 amount = 60000e18; uint32 slopePeriod = 30; uint32 cliff = 30; diff --git a/test/governance/Locking/relock.t.sol b/test/unit/governance/Locking/relock.t.sol similarity index 99% rename from test/governance/Locking/relock.t.sol rename to test/unit/governance/Locking/relock.t.sol index db0128bb..1676a3e8 100644 --- a/test/governance/Locking/relock.t.sol +++ b/test/unit/governance/Locking/relock.t.sol @@ -2,9 +2,9 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, contract-name-camelcase -import { Locking_Test } from "./Base.t.sol"; +import { LockingTest } from "./LockingTest.sol"; -contract Relock_Locking_Test is Locking_Test { +contract Relock_LockingTest is LockingTest { uint256 public lockId; function test_relock_whenDelegateZero_shouldRevert() public { diff --git a/test/governance/MentoGovernor.t.sol b/test/unit/governance/MentoGovernor.t.sol similarity index 97% rename from test/governance/MentoGovernor.t.sol rename to test/unit/governance/MentoGovernor.t.sol index 8f6fa08a..f15fe69e 100644 --- a/test/governance/MentoGovernor.t.sol +++ b/test/unit/governance/MentoGovernor.t.sol @@ -2,14 +2,17 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length -import { TestSetup } from "./TestSetup.sol"; +import { IVotesUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/governance/extensions/GovernorVotesUpgradeable.sol"; + +import { GovernanceTest } from "./GovernanceTest.sol"; + import { TimelockController } from "contracts/governance/TimelockController.sol"; import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; -import { MockOwnable } from "../mocks/MockOwnable.sol"; -import { MockVeMento } from "../mocks/MockVeMento.sol"; -import { IVotesUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/governance/extensions/GovernorVotesUpgradeable.sol"; -contract MentoGovernorTest is TestSetup { +import { MockOwnable } from "test/utils/mocks/MockOwnable.sol"; +import { MockVeMento } from "test/utils/mocks/MockVeMento.sol"; + +contract MentoGovernorTest is GovernanceTest { TimelockController public timelockController; MentoGovernor public mentoGovernor; @@ -58,7 +61,7 @@ contract MentoGovernorTest is TestSetup { vm.stopPrank(); } - function test_init_shouldSetStateCorrectly() public { + function test_init_shouldSetStateCorrectly() public view { assertEq(mentoGovernor.votingDelay(), BLOCKS_DAY); assertEq(mentoGovernor.votingPeriod(), BLOCKS_WEEK); assertEq(mentoGovernor.proposalThreshold(), 1_000e18); @@ -66,7 +69,7 @@ contract MentoGovernorTest is TestSetup { assertEq(timelockController.getMinDelay(), 1 days); } - function test_hasRole_shouldReturnCorrectRoles() public { + function test_hasRole_shouldReturnCorrectRoles() public view { bytes32 proposerRole = timelockController.PROPOSER_ROLE(); bytes32 executorRole = timelockController.EXECUTOR_ROLE(); bytes32 adminRole = timelockController.TIMELOCK_ADMIN_ROLE(); diff --git a/test/governance/MentoToken.t.sol b/test/unit/governance/MentoToken.t.sol similarity index 92% rename from test/governance/MentoToken.t.sol rename to test/unit/governance/MentoToken.t.sol index deaddb9a..9af173b3 100644 --- a/test/governance/MentoToken.t.sol +++ b/test/unit/governance/MentoToken.t.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase -import { TestSetup } from "./TestSetup.sol"; +import { uints, addresses } from "mento-std/Array.sol"; +import { GovernanceTest } from "./GovernanceTest.sol"; import { MentoToken } from "contracts/governance/MentoToken.sol"; -import { Arrays } from "test/utils/Arrays.sol"; -contract MentoTokenTest is TestSetup { +contract MentoTokenTest is GovernanceTest { event Paused(address account); MentoToken public mentoToken; @@ -18,9 +18,9 @@ contract MentoTokenTest is TestSetup { address public emission = makeAddr("emission"); address public locking = makeAddr("locking"); - uint256[] public allocationAmounts = Arrays.uints(80, 120, 50, 100); + uint256[] public allocationAmounts = uints(80, 120, 50, 100); address[] public allocationRecipients = - Arrays.addresses(mentoLabsMultiSig, mentoLabsTreasuryTimelock, airgrab, governanceTimelock); + addresses(mentoLabsMultiSig, mentoLabsTreasuryTimelock, airgrab, governanceTimelock); modifier notPaused() { mentoToken.unpause(); @@ -43,13 +43,13 @@ contract MentoTokenTest is TestSetup { function test_constructor_whenAllocationRecipientsAndAmountsLengthMismatch_shouldRevert() public { vm.expectRevert("MentoToken: recipients and amounts length mismatch"); - mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50), emission, locking); + mentoToken = new MentoToken(allocationRecipients, uints(80, 120, 50), emission, locking); } function test_constructor_whenAllocationRecipientIsZero_shouldRevert() public { vm.expectRevert("MentoToken: allocation recipient is zero address"); mentoToken = new MentoToken( - Arrays.addresses(mentoLabsMultiSig, mentoLabsTreasuryTimelock, airgrab, address(0)), + addresses(mentoLabsMultiSig, mentoLabsTreasuryTimelock, airgrab, address(0)), allocationAmounts, emission, locking @@ -58,26 +58,26 @@ contract MentoTokenTest is TestSetup { function test_constructor_whenTotalAllocationExceeds1000_shouldRevert() public { vm.expectRevert("MentoToken: total allocation exceeds 100%"); - mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50, 1000), emission, locking); + mentoToken = new MentoToken(allocationRecipients, uints(80, 120, 50, 1000), emission, locking); } function test_constructor_shouldPauseTheContract() public { vm.expectEmit(true, true, true, true); emit Paused(address(this)); - mentoToken = new MentoToken(allocationRecipients, Arrays.uints(80, 120, 50, 100), emission, locking); + mentoToken = new MentoToken(allocationRecipients, uints(80, 120, 50, 100), emission, locking); assertEq(mentoToken.paused(), true); } /// @dev Test the state initialization post-construction of the MentoToken contract. - function test_constructor_shouldSetCorrectState() public { + function test_constructor_shouldSetCorrectState() public view { assertEq(mentoToken.emission(), emission); assertEq(mentoToken.emissionSupply(), EMISSION_SUPPLY); assertEq(mentoToken.emittedAmount(), 0); } /// @dev Test the correct token amounts are minted to respective contracts during initialization. - function test_constructor_shouldMintCorrectAmounts() public { + function test_constructor_shouldMintCorrectAmounts() public view { uint256 mentoLabsMultiSigSupply = mentoToken.balanceOf(mentoLabsMultiSig); uint256 mentoLabsTreasurySupply = mentoToken.balanceOf(mentoLabsTreasuryTimelock); uint256 airgrabSupply = mentoToken.balanceOf(airgrab); diff --git a/test/libraries/TradingLimits.t.sol b/test/unit/libraries/TradingLimits.t.sol similarity index 60% rename from test/libraries/TradingLimits.t.sol rename to test/unit/libraries/TradingLimits.t.sol index b9528d40..136b2bc2 100644 --- a/test/libraries/TradingLimits.t.sol +++ b/test/unit/libraries/TradingLimits.t.sol @@ -1,39 +1,37 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test } from "celo-foundry/Test.sol"; -import { console } from "forge-std/console.sol"; -import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; +import { Test } from "mento-std/Test.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; // forge test --match-contract TradingLimits -vvv contract TradingLimitsTest is Test { - using TradingLimits for TradingLimits.State; - using TradingLimits for TradingLimits.Config; - uint8 private constant L0 = 1; // 0b001 uint8 private constant L1 = 2; // 0b010 uint8 private constant LG = 4; // 0b100 - TradingLimits.State private state; + ITradingLimits.State private state; + ITradingLimitsHarness private harness; - function configEmpty() internal pure returns (TradingLimits.Config memory config) {} + function configEmpty() internal pure returns (ITradingLimits.Config memory config) {} - function configL0(uint32 timestep0, int48 limit0) internal pure returns (TradingLimits.Config memory config) { + function configL0(uint32 timestep0, int48 limit0) internal pure returns (ITradingLimits.Config memory config) { config.timestep0 = timestep0; config.limit0 = limit0; config.flags = L0; } - function configL1(uint32 timestep1, int48 limit1) internal pure returns (TradingLimits.Config memory config) { + function configL1(uint32 timestep1, int48 limit1) internal pure returns (ITradingLimits.Config memory config) { config.timestep1 = timestep1; config.limit1 = limit1; config.flags = L1; } - function configLG(int48 limitGlobal) internal pure returns (TradingLimits.Config memory config) { + function configLG(int48 limitGlobal) internal pure returns (ITradingLimits.Config memory config) { config.limitGlobal = limitGlobal; config.flags = LG; } @@ -43,7 +41,7 @@ contract TradingLimitsTest is Test { int48 limit0, uint32 timestep1, int48 limit1 - ) internal pure returns (TradingLimits.Config memory config) { + ) internal pure returns (ITradingLimits.Config memory config) { config.timestep0 = timestep0; config.limit0 = limit0; config.timestep1 = timestep1; @@ -57,7 +55,7 @@ contract TradingLimitsTest is Test { uint32 timestep1, int48 limit1, int48 limitGlobal - ) internal pure returns (TradingLimits.Config memory config) { + ) internal pure returns (ITradingLimits.Config memory config) { config.timestep0 = timestep0; config.limit0 = limit0; config.timestep1 = timestep1; @@ -70,7 +68,7 @@ contract TradingLimitsTest is Test { uint32 timestep1, int48 limit1, int48 limitGlobal - ) internal pure returns (TradingLimits.Config memory config) { + ) internal pure returns (ITradingLimits.Config memory config) { config.timestep1 = timestep1; config.limit1 = limit1; config.limitGlobal = limitGlobal; @@ -81,82 +79,86 @@ contract TradingLimitsTest is Test { uint32 timestep0, int48 limit0, int48 limitGlobal - ) internal pure returns (TradingLimits.Config memory config) { + ) internal pure returns (ITradingLimits.Config memory config) { config.timestep0 = timestep0; config.limit0 = limit0; config.limitGlobal = limitGlobal; config.flags = L0 | LG; } + function setUp() public { + harness = ITradingLimitsHarness(deployCode("TradingLimitsHarness")); + } + /* ==================== Config#validate ==================== */ - function test_validate_withL0_isValid() public pure { - TradingLimits.Config memory config = configL0(100, 1000); - config.validate(); + function test_validate_withL0_isValid() public view { + ITradingLimits.Config memory config = configL0(100, 1000); + harness.validate(config); } function test_validate_withL0_withoutTimestep_isNotValid() public { - TradingLimits.Config memory config = configL0(0, 1000); + ITradingLimits.Config memory config = configL0(0, 1000); vm.expectRevert(bytes("timestep0 can't be zero if active")); - config.validate(); + harness.validate(config); } function test_validate_withL0_withoutLimit0_isNotValid() public { - TradingLimits.Config memory config = configL0(100, 0); + ITradingLimits.Config memory config = configL0(100, 0); vm.expectRevert(bytes("limit0 can't be zero if active")); - config.validate(); + harness.validate(config); } - function test_validate_withL0L1_isValid() public pure { - TradingLimits.Config memory config = configL0L1(100, 1000, 1000, 10000); - config.validate(); + function test_validate_withL0L1_isValid() public view { + ITradingLimits.Config memory config = configL0L1(100, 1000, 1000, 10000); + harness.validate(config); } function test_validate_withL1_withoutLimit1_isNotValid() public { - TradingLimits.Config memory config = configL0L1(100, 1000, 1000, 0); + ITradingLimits.Config memory config = configL0L1(100, 1000, 1000, 0); vm.expectRevert(bytes("limit1 can't be zero if active")); - config.validate(); + harness.validate(config); } function test_validate_withL0L1_withoutTimestape_isNotValid() public { - TradingLimits.Config memory config = configL0L1(0, 1000, 1000, 10000); + ITradingLimits.Config memory config = configL0L1(0, 1000, 1000, 10000); vm.expectRevert(bytes("timestep0 can't be zero if active")); - config.validate(); + harness.validate(config); } function test_validate_withL0L1_withLimit0LargerLimit1_isNotValid() public { - TradingLimits.Config memory config = configL0L1(10000, 10000, 1000, 1000); + ITradingLimits.Config memory config = configL0L1(10000, 10000, 1000, 1000); vm.expectRevert(bytes("limit1 must be greater than limit0")); - config.validate(); + harness.validate(config); } function test_validate_withLG_withoutLimitGlobal_isNotValid() public { - TradingLimits.Config memory config = configL0L1LG(100, 1000, 1000, 10000, 0); + ITradingLimits.Config memory config = configL0L1LG(100, 1000, 1000, 10000, 0); vm.expectRevert(bytes("limitGlobal can't be zero if active")); - config.validate(); + harness.validate(config); } function test_validate_withL0LG_withLimit0LargerLimitGlobal_isNotValid() public { - TradingLimits.Config memory config = configL0LG(10000, 10000, 1000); + ITradingLimits.Config memory config = configL0LG(10000, 10000, 1000); vm.expectRevert(bytes("limitGlobal must be greater than limit0")); - config.validate(); + harness.validate(config); } function test_validate_withL1LG_withLimit1LargerLimitGlobal_isNotValid() public { - TradingLimits.Config memory config = configL0L1LG(100, 1000, 10000, 10000, 1000); + ITradingLimits.Config memory config = configL0L1LG(100, 1000, 10000, 10000, 1000); vm.expectRevert(bytes("limitGlobal must be greater than limit1")); - config.validate(); + harness.validate(config); } - function test_validate_withL0L1LG_isValid() public pure { - TradingLimits.Config memory config = configL0L1LG(100, 1000, 1000, 10000, 100000); - config.validate(); + function test_validate_withL0L1LG_isValid() public view { + ITradingLimits.Config memory config = configL0L1LG(100, 1000, 1000, 10000, 100000); + harness.validate(config); } function test_configure_withL1LG_isNotValid() public { - TradingLimits.Config memory config = configL1LG(1000, 10000, 100000); + ITradingLimits.Config memory config = configL1LG(1000, 10000, 100000); vm.expectRevert(bytes("L1 without L0 not allowed")); - config.validate(); + harness.validate(config); } /* ==================== State#reset ==================== */ @@ -164,7 +166,7 @@ contract TradingLimitsTest is Test { function test_reset_clearsCheckpoints() public { state.lastUpdated0 = 123412412; state.lastUpdated1 = 123124412; - state = state.reset(configL0(500, 1000)); + state = harness.reset(state, configL0(500, 1000)); assertEq(uint256(state.lastUpdated0), 0); assertEq(uint256(state.lastUpdated1), 0); @@ -173,133 +175,133 @@ contract TradingLimitsTest is Test { function test_reset_resetsNetflowsOnDisabled() public { state.netflow1 = 12312; state.netflowGlobal = 12312; - state = state.reset(configL0(500, 1000)); + state = harness.reset(state, configL0(500, 1000)); - assertEq(uint256(state.netflow1), 0); - assertEq(uint256(state.netflowGlobal), 0); + assertEq(state.netflow1, 0); + assertEq(state.netflowGlobal, 0); } function test_reset_keepsNetflowsOnEnabled() public { state.netflow0 = 12312; state.netflow1 = 12312; state.netflowGlobal = 12312; - state = state.reset(configL0LG(500, 1000, 100000)); + state = harness.reset(state, configL0LG(500, 1000, 100000)); - assertEq(uint256(state.netflow0), 12312); - assertEq(uint256(state.netflow1), 0); - assertEq(uint256(state.netflowGlobal), 12312); + assertEq(state.netflow0, 12312); + assertEq(state.netflow1, 0); + assertEq(state.netflowGlobal, 12312); } /* ==================== State#verify ==================== */ function test_verify_withNothingOn() public view { - TradingLimits.Config memory config; - state.verify(config); + ITradingLimits.Config memory config; + harness.verify(state, config); } function test_verify_withL0_butNotMet() public { state.netflow0 = 500; - state.verify(configL0(500, 1230)); + harness.verify(state, configL0(500, 1230)); } function test_verify_withL0_andMetPositively() public { state.netflow0 = 1231; vm.expectRevert(bytes("L0 Exceeded")); - state.verify(configL0(500, 1230)); + harness.verify(state, configL0(500, 1230)); } function test_verify_withL0_andMetNegatively() public { state.netflow0 = -1231; vm.expectRevert(bytes("L0 Exceeded")); - state.verify(configL0(500, 1230)); + harness.verify(state, configL0(500, 1230)); } function test_verify_withL0L1_butNoneMet() public { state.netflow1 = 500; - state.verify(configL0L1(50, 100, 500, 1230)); + harness.verify(state, configL0L1(50, 100, 500, 1230)); } function test_verify_withL0L1_andL1MetPositively() public { state.netflow1 = 1231; vm.expectRevert(bytes("L1 Exceeded")); - state.verify(configL0L1(50, 100, 500, 1230)); + harness.verify(state, configL0L1(50, 100, 500, 1230)); } function test_verify_withL0L1_andL1MetNegatively() public { state.netflow1 = -1231; vm.expectRevert(bytes("L1 Exceeded")); - state.verify(configL0L1(50, 100, 500, 1230)); + harness.verify(state, configL0L1(50, 100, 500, 1230)); } function test_verify_withLG_butNoneMet() public { state.netflowGlobal = 500; - state.verify(configLG(1230)); + harness.verify(state, configLG(1230)); } function test_verify_withLG_andMetPositively() public { state.netflowGlobal = 1231; vm.expectRevert(bytes("LG Exceeded")); - state.verify(configLG(1230)); + harness.verify(state, configLG(1230)); } function test_verify_withLG_andMetNegatively() public { state.netflowGlobal = -1231; vm.expectRevert(bytes("LG Exceeded")); - state.verify(configLG(1230)); + harness.verify(state, configLG(1230)); } /* ==================== State#update ==================== */ function test_update_withNoLimit_doesNotUpdate() public { - state = state.update(configEmpty(), 100 * 1e18, 18); + state = harness.update(state, configEmpty(), 100 * 1e18, 18); assertEq(state.netflow0, 0); assertEq(state.netflow1, 0); assertEq(state.netflowGlobal, 0); } function test_update_withL0_updatesActive() public { - state = state.update(configL0(500, 1000), 100 * 1e18, 18); + state = harness.update(state, configL0(500, 1000), 100 * 1e18, 18); assertEq(state.netflow0, 100); assertEq(state.netflowGlobal, 0); } function test_update_withL0L1_updatesActive() public { - state = state.update(configL0L1(500, 1000, 5000, 500000), 100 * 1e18, 18); + state = harness.update(state, configL0L1(500, 1000, 5000, 500000), 100 * 1e18, 18); assertEq(state.netflow0, 100); assertEq(state.netflow1, 100); assertEq(state.netflowGlobal, 0); } function test_update_withL0LG_updatesActive() public { - state = state.update(configL0LG(500, 1000, 500000), 100 * 1e18, 18); + state = harness.update(state, configL0LG(500, 1000, 500000), 100 * 1e18, 18); assertEq(state.netflow0, 100); assertEq(state.netflow1, 0); assertEq(state.netflowGlobal, 100); } function test_update_withLG_updatesActive() public { - state = state.update(configLG(500000), 100 * 1e18, 18); + state = harness.update(state, configLG(500000), 100 * 1e18, 18); assertEq(state.netflow0, 0); assertEq(state.netflow1, 0); assertEq(state.netflowGlobal, 100); } function test_update_withSubUnitAmounts_updatesAs1() public { - state = state.update(configLG(500000), 1e6, 18); + state = harness.update(state, configLG(500000), 1e6, 18); assertEq(state.netflowGlobal, 1); } function test_update_withTooLargeAmount_reverts() public { vm.expectRevert(bytes("dFlow too large")); - state = state.update(configLG(500000), 3 * 10e32, 18); + state = harness.update(state, configLG(500000), 3 * 10e32, 18); } function test_update_withOverflowOnAdd_reverts() public { - TradingLimits.Config memory config = configLG(int48(2**47)); - int256 maxFlow = int256(uint48(-1) / 2); + ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); + int256 maxFlow = int256(uint256(type(uint48).max / 2)); - state = state.update(config, (maxFlow - 1000) * 1e18, 18); + state = harness.update(state, config, (maxFlow - 1000) * 1e18, 18); vm.expectRevert(bytes("int48 addition overflow")); - state = state.update(config, 1002 * 10e18, 18); + state = harness.update(state, config, 1002 * 10e18, 18); } } diff --git a/test/oracles/BreakerBox.t.sol b/test/unit/oracles/BreakerBox.t.sol similarity index 77% rename from test/oracles/BreakerBox.t.sol rename to test/unit/oracles/BreakerBox.t.sol index 122fe776..6d91841e 100644 --- a/test/oracles/BreakerBox.t.sol +++ b/test/unit/oracles/BreakerBox.t.sol @@ -1,20 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; +pragma solidity ^0.8.26; -import { console2 as console } from "celo-foundry/Test.sol"; +import { Test } from "mento-std/Test.sol"; -import { BaseTest } from "../utils/BaseTest.t.sol"; -import { MockBreaker } from "../mocks/MockBreaker.sol"; -import { MockSortedOracles } from "../mocks/MockSortedOracles.sol"; +import { MockBreaker } from "test/utils/mocks/MockBreaker.sol"; +import { MockSortedOracles } from "test/utils/mocks/MockSortedOracles.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; -import { BreakerBox } from "contracts/oracles/BreakerBox.sol"; - -contract BreakerBoxTest is BaseTest { +contract BreakerBoxTest is Test { address rateFeedID1; address rateFeedID2; address rateFeedID3; @@ -24,13 +21,12 @@ contract BreakerBoxTest is BaseTest { MockBreaker mockBreaker2; MockBreaker mockBreaker3; MockBreaker mockBreaker4; - BreakerBox breakerBox; + IBreakerBox breakerBox; MockSortedOracles sortedOracles; event BreakerAdded(address indexed breaker); event BreakerRemoved(address indexed breaker); event BreakerTripped(address indexed breaker, address indexed rateFeedID); - event TradingModeUpdated(address indexed rateFeedID, uint256 tradingMode); event ResetSuccessful(address indexed rateFeedID, address indexed breaker); event ResetAttemptCriteriaFail(address indexed rateFeedID, address indexed breaker); event ResetAttemptNotCool(address indexed rateFeedID, address indexed breaker); @@ -39,28 +35,28 @@ contract BreakerBoxTest is BaseTest { event RateFeedRemoved(address indexed rateFeedID); event SortedOraclesUpdated(address indexed newSortedOracles); event BreakerStatusUpdated(address breaker, address rateFeedID, bool status); + event TradingModeUpdated(address indexed rateFeedID, uint256 tradingMode); function setUp() public { - rateFeedID1 = actor("rateFeedID1"); - rateFeedID2 = actor("rateFeedID2"); - rateFeedID3 = actor("rateFeedID3"); - notDeployer = actor("notDeployer"); + rateFeedID1 = makeAddr("rateFeedID1"); + rateFeedID2 = makeAddr("rateFeedID2"); + rateFeedID3 = makeAddr("rateFeedID3"); + notDeployer = makeAddr("notDeployer"); address[] memory testRateFeedIDs = new address[](2); testRateFeedIDs[0] = rateFeedID1; testRateFeedIDs[1] = rateFeedID2; - vm.startPrank(deployer); mockBreaker1 = new MockBreaker(0, false, false); mockBreaker2 = new MockBreaker(0, false, false); mockBreaker3 = new MockBreaker(0, false, false); mockBreaker4 = new MockBreaker(0, false, false); sortedOracles = new MockSortedOracles(); - sortedOracles.addOracle(rateFeedID1, actor("oracleClient1")); - sortedOracles.addOracle(rateFeedID2, actor("oracleClient1")); + sortedOracles.addOracle(rateFeedID1, makeAddr("oracleClient1")); + sortedOracles.addOracle(rateFeedID2, makeAddr("oracleClient1")); - breakerBox = new BreakerBox(testRateFeedIDs, ISortedOracles(address(sortedOracles))); + breakerBox = IBreakerBox(deployCode("BreakerBox", abi.encode(testRateFeedIDs, sortedOracles))); breakerBox.addBreaker(address(mockBreaker1), 1); } @@ -74,24 +70,14 @@ contract BreakerBoxTest is BaseTest { } } - function setUpBreaker( - MockBreaker breaker, - uint8 tradingMode, - uint256 cooldown, - bool reset, - bool trigger - ) public { + function setUpBreaker(MockBreaker breaker, uint8 tradingMode, uint256 cooldown, bool reset, bool trigger) public { breaker.setCooldown(cooldown); breaker.setReset(reset); breaker.setTrigger(trigger); breakerBox.addBreaker(address(breaker), tradingMode); } - function toggleAndAssertBreaker( - address breaker, - address rateFeedID, - bool status - ) public { + function toggleAndAssertBreaker(address breaker, address rateFeedID, bool status) public { vm.expectEmit(true, true, true, true); emit BreakerStatusUpdated(breaker, rateFeedID, status); breakerBox.toggleBreaker(breaker, rateFeedID, status); @@ -102,20 +88,20 @@ contract BreakerBoxTest is BaseTest { contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { /* ---------- Constructor ---------- */ - function test_constructor_shouldSetOwner() public { - assertEq(breakerBox.owner(), deployer); + function test_constructor_shouldSetOwner() public view { + assertEq(breakerBox.owner(), address(this)); } - function test_constructor_shouldSetInitialBreaker() public { + function test_constructor_shouldSetInitialBreaker() public view { assertEq(uint256(breakerBox.breakerTradingMode(address(mockBreaker1))), 1); assertTrue(breakerBox.isBreaker(address(mockBreaker1))); } - function test_constructor_shouldSetSortedOracles() public { + function test_constructor_shouldSetSortedOracles() public view { assertEq(address(breakerBox.sortedOracles()), address(sortedOracles)); } - function test_constructor_shouldAddRateFeedIdsWithDefaultMode() public { + function test_constructor_shouldAddRateFeedIdsWithDefaultMode() public view { assertTrue(breakerBox.rateFeedStatus(rateFeedID1)); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); @@ -127,7 +113,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { function test_addBreaker_whenNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); - changePrank(notDeployer); + vm.prank(notDeployer); breakerBox.addBreaker(address(mockBreaker1), 2); } @@ -153,7 +139,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { function test_removeBreaker_whenNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); - changePrank(notDeployer); + vm.prank(notDeployer); breakerBox.removeBreaker(address(mockBreaker1)); } @@ -186,20 +172,19 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { vm.warp(1672527600); // 2023-01-01 00:00:00 setUpBreaker(mockBreaker2, 2, 10, false, true); breakerBox.toggleBreaker(address(mockBreaker2), rateFeedID1, true); - changePrank(address(sortedOracles)); + IBreakerBox.BreakerStatus memory status; + + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - changePrank(deployer); + assertTrue(breakerBox.isBreaker(address(mockBreaker2))); assertTrue(breakerBox.isBreakerEnabled(address(mockBreaker2), rateFeedID1)); assertEq(uint256(breakerBox.breakerTradingMode(address(mockBreaker2))), 2); - (uint256 tradingModeBefore, uint256 lastUpdatedTimeBefore, bool enabledBefore) = breakerBox.rateFeedBreakerStatus( - rateFeedID1, - address(mockBreaker2) - ); - assertEq(tradingModeBefore, 2); - assertEq(lastUpdatedTimeBefore, 1672527600); - assertTrue(enabledBefore); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker2)); + assertEq(status.tradingMode, 2); + assertEq(status.lastUpdatedTime, 1672527600); + assertTrue(status.enabled); vm.expectEmit(true, false, false, false); emit BreakerRemoved(address(mockBreaker2)); @@ -209,18 +194,15 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { assertFalse(breakerBox.isBreakerEnabled(address(mockBreaker2), rateFeedID1)); assertEq(uint256(breakerBox.breakerTradingMode(address(mockBreaker2))), 0); - (uint256 tradingModeAfter, uint256 lastUpdatedTimeAfter, bool enabledAfter) = breakerBox.rateFeedBreakerStatus( - rateFeedID1, - address(mockBreaker2) - ); - assertEq(tradingModeAfter, 0); - assertEq(lastUpdatedTimeAfter, 0); - assertFalse(enabledAfter); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker2)); + assertEq(status.tradingMode, 0); + assertEq(status.lastUpdatedTime, 0); + assertFalse(status.enabled); } function test_toggleBreaker_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(notDeployer); breakerBox.toggleBreaker(address(mockBreaker1), rateFeedID1, true); } @@ -247,22 +229,19 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); toggleAndAssertBreaker(address(mockBreaker4), rateFeedID1, true); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - changePrank(deployer); + assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), breaker3TradingMode | breaker4TradingMode); toggleAndAssertBreaker(address(mockBreaker4), rateFeedID1, false); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), breaker3TradingMode); assertFalse(breakerBox.isBreakerEnabled(address(mockBreaker4), rateFeedID1)); - (uint256 tradingModeAfter, uint256 lastUpdatedTimeAfter, bool enabledAfter) = breakerBox.rateFeedBreakerStatus( - rateFeedID1, - address(mockBreaker4) - ); - assertEq(tradingModeAfter, 0); - assertEq(lastUpdatedTimeAfter, 0); - assertFalse(enabledAfter); + IBreakerBox.BreakerStatus memory status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker4)); + assertEq(status.tradingMode, 0); + assertEq(status.lastUpdatedTime, 0); + assertFalse(status.enabled); } function test_toggleBreaker_whenBreakerIsAdded_shouldCheckAndSetBreakers() public { @@ -281,7 +260,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { /* ---------- Rate Feed IDs ---------- */ function test_addRateFeed_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breakerBox.addRateFeed(rateFeedID3); } @@ -297,7 +276,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { } function test_addRateFeed_whenRateFeedExistsInOracleList_shouldSetDefaultModeAndEmit() public { - sortedOracles.addOracle(rateFeedID3, actor("oracleAddress")); + sortedOracles.addOracle(rateFeedID3, makeAddr("oracleAddress")); assertFalse(isRateFeed(rateFeedID3)); assertFalse(breakerBox.rateFeedStatus(rateFeedID3)); @@ -317,7 +296,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { testRateFeedIDs[0] = rateFeedID2; testRateFeedIDs[1] = rateFeedID3; - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breakerBox.setRateFeedDependencies(rateFeedID1, testRateFeedIDs); } @@ -328,7 +307,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { testRateFeedIDs[1] = rateFeedID3; vm.expectRevert("Rate feed ID has not been added"); - breakerBox.setRateFeedDependencies(actor("notRateFeed"), testRateFeedIDs); + breakerBox.setRateFeedDependencies(makeAddr("notRateFeed"), testRateFeedIDs); } function test_setRateFeedDependencies_whenRateFeedExists_shouldUpdateDependenciesAndEmit() public { @@ -344,7 +323,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { } function test_removeRateFeed_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breakerBox.removeRateFeed(rateFeedID1); } @@ -381,13 +360,14 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { function test_removeRateFeed_whenRateFeedExists_shouldResetTradingModeInfoAndEmit() public { toggleAndAssertBreaker(address(mockBreaker1), rateFeedID1, true); breakerBox.setRateFeedTradingMode(rateFeedID1, 1); + IBreakerBox.BreakerStatus memory status; uint256 tradingModeBefore = breakerBox.getRateFeedTradingMode(rateFeedID1); assertEq(tradingModeBefore, 1); assertTrue(isRateFeed(rateFeedID1)); assertTrue(breakerBox.rateFeedStatus(rateFeedID1)); - (, , bool breakerStatusBefore) = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker1)); - assertTrue(breakerStatusBefore); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker1)); + assertTrue(status.enabled); vm.expectEmit(true, true, true, true); emit RateFeedRemoved(rateFeedID1); @@ -397,12 +377,12 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { assertEq(tradingModeAfter, 0); assertFalse(isRateFeed(rateFeedID1)); assertFalse(breakerBox.rateFeedStatus(rateFeedID1)); - (, , bool breakerStatusAfter) = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker1)); - assertFalse(breakerStatusAfter); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker1)); + assertFalse(status.enabled); } function test_setRateFeedTradingMode_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breakerBox.setRateFeedTradingMode(rateFeedID1, 9); } @@ -426,7 +406,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { function test_getRateFeedTradingMode_whenRateFeedHasNotBeenAdded_shouldRevert() public { vm.expectRevert("Rate feed ID has not been added"); - breakerBox.getRateFeedTradingMode(actor("notRateFeed")); + breakerBox.getRateFeedTradingMode(makeAddr("notRateFeed")); } function test_getRateFeedTradingMode_whenNoDependencies_shouldReturnRateFeedsTradingMode() public { @@ -436,7 +416,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { } function test_getRateFeedTradingMode_whenMultipleDependencies_shouldAggregateTradingModes() public { - sortedOracles.addOracle(rateFeedID3, actor("oracleClient")); + sortedOracles.addOracle(rateFeedID3, makeAddr("oracleClient")); breakerBox.addRateFeed(rateFeedID3); address[] memory testRateFeedIDs = new address[](2); testRateFeedIDs[0] = rateFeedID2; @@ -457,7 +437,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { /* ---------- Sorted Oracles ---------- */ function test_setSortedOracles_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breakerBox.setSortedOracles(ISortedOracles(address(0))); } @@ -468,7 +448,7 @@ contract BreakerBoxTest_constructorAndSetters is BreakerBoxTest { } function test_setSortedOracles_whenValidOracle_shouldUpdateAndEmit() public { - address newSortedOracles = actor("newSortedOracles"); + address newSortedOracles = makeAddr("newSortedOracles"); vm.expectEmit(true, true, true, true); emit SortedOraclesUpdated(newSortedOracles); @@ -487,7 +467,7 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { function test_checkAndSetBreakers_whenRateFeedIsNotInDefaultModeAndCooldownNotPassed_shouldEmitNotCool() public { setUpBreaker(mockBreaker3, 3, 3600, false, true); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); skip(3599); @@ -496,6 +476,7 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { vm.expectEmit(true, true, true, true); emit ResetAttemptNotCool(rateFeedID1, address(mockBreaker3)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 3); } @@ -503,7 +484,7 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { function test_checkAndSetBreakers_whenRateFeedIsNotInDefaultModeAndCantReset_shouldEmitCriteriaFail() public { setUpBreaker(mockBreaker3, 3, 3600, false, true); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); skip(3600); @@ -511,15 +492,16 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { vm.expectCall(address(mockBreaker3), abi.encodeWithSelector(mockBreaker3.shouldReset.selector, rateFeedID1)); vm.expectEmit(true, true, true, true); emit ResetAttemptCriteriaFail(rateFeedID1, address(mockBreaker3)); - breakerBox.checkAndSetBreakers(rateFeedID1); + vm.prank(address(sortedOracles)); + breakerBox.checkAndSetBreakers(rateFeedID1); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 3); } function test_checkAndSetBreakers_whenRateFeedIsNotInDefaultModeAndCanReset_shouldResetMode() public { setUpBreaker(mockBreaker3, 3, 3600, true, true); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); skip(3600); @@ -528,8 +510,9 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { vm.expectCall(address(mockBreaker3), abi.encodeWithSelector(mockBreaker3.shouldReset.selector, rateFeedID1)); vm.expectEmit(true, true, true, true); emit ResetSuccessful(rateFeedID1, address(mockBreaker3)); - breakerBox.checkAndSetBreakers(rateFeedID1); + vm.prank(address(sortedOracles)); + breakerBox.checkAndSetBreakers(rateFeedID1); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); } @@ -538,7 +521,8 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { { setUpBreaker(mockBreaker3, 3, 0, true, true); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); - changePrank(address(sortedOracles)); + + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); skip(3600); @@ -546,8 +530,9 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { vm.expectCall(address(mockBreaker3), abi.encodeWithSelector(mockBreaker3.getCooldown.selector)); vm.expectEmit(true, true, true, true); emit ResetAttemptNotCool(rateFeedID1, address(mockBreaker3)); - breakerBox.checkAndSetBreakers(rateFeedID1); + vm.prank(address(sortedOracles)); + breakerBox.checkAndSetBreakers(rateFeedID1); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 3); } @@ -557,7 +542,7 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); toggleAndAssertBreaker(address(mockBreaker4), rateFeedID1, true); - assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); + assertEq(breakerBox.getRateFeedTradingMode(rateFeedID1), 0); vm.expectCall( address(mockBreaker3), @@ -567,7 +552,7 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { address(mockBreaker4), abi.encodeWithSelector(mockBreaker4.shouldTrigger.selector, address(rateFeedID1)) ); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); @@ -577,10 +562,11 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { vm.warp(1672527600); // 2023-01-01 00:00:00 setUpBreaker(mockBreaker3, 3, 3600, false, false); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); + IBreakerBox.BreakerStatus memory status; - assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); - (uint256 breakerTradingModeBefore, , ) = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - assertEq(breakerTradingModeBefore, 0); + assertEq(breakerBox.getRateFeedTradingMode(rateFeedID1), 0); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + assertEq(status.tradingMode, 0); mockBreaker3.setTrigger(true); vm.expectCall( @@ -589,26 +575,26 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { ); vm.expectEmit(true, true, true, true); emit BreakerTripped(address(mockBreaker3), rateFeedID1); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 3); - (uint256 breakerTradingModeAfter, uint256 breakerLastUpdatedTime, bool breakerEnabled) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - assertEq(breakerTradingModeAfter, 3); - assertEq(breakerLastUpdatedTime, 1672527600); - assertTrue(breakerEnabled); + assertEq(breakerBox.getRateFeedTradingMode(rateFeedID1), 3); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + assertEq(status.tradingMode, 3); + assertEq(status.lastUpdatedTime, 1672527600); + assertTrue(status.enabled); } function test_checkAndSetBreakers_whenABreakerIsNotEnabled_shouldNotTrigger() public { setUpBreaker(mockBreaker3, 3, 3600, false, true); + IBreakerBox.BreakerStatus memory status; assertTrue(breakerBox.isBreaker(address(mockBreaker3))); - assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); - (, , bool breakerEnabled) = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - assertFalse(breakerEnabled); + assertEq(breakerBox.getRateFeedTradingMode(rateFeedID1), 0); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + assertFalse(status.enabled); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); assertEq(uint256(breakerBox.getRateFeedTradingMode(rateFeedID1)), 0); } @@ -617,90 +603,91 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { vm.warp(1672527600); // 2023-01-01 00:00:00 setUpBreaker(mockBreaker3, 2, 4, false, true); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - (uint256 breakerTradingMode1, uint256 breakerLastUpdatedTime1, bool breakerEnabled1) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - uint256 tradingMode1 = breakerBox.getRateFeedTradingMode(rateFeedID1); - assertEq(breakerTradingMode1, 2); - assertEq(breakerLastUpdatedTime1, 1672527600); - assertTrue(breakerEnabled1); - assertEq(tradingMode1, 2); + uint8 tradingMode; + IBreakerBox.BreakerStatus memory status; + + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedID1); + assertEq(status.tradingMode, 2); + assertEq(status.lastUpdatedTime, 1672527600); + assertTrue(status.enabled); + assertEq(tradingMode, 2); vm.warp(1672527605); // 2023-01-01 00:00:05 mockBreaker3.setTrigger(false); mockBreaker3.setReset(true); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - (uint256 breakerTradingMode2, uint256 breakerLastUpdatedTime2, bool breakerEnabled2) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - uint256 tradingMode2 = breakerBox.getRateFeedTradingMode(rateFeedID1); - assertEq(breakerTradingMode2, 0); - assertEq(breakerLastUpdatedTime2, 1672527605); - assertTrue(breakerEnabled2); - assertEq(tradingMode2, 0); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedID1); + assertEq(status.tradingMode, 0); + assertEq(status.lastUpdatedTime, 1672527605); + assertTrue(status.enabled); + assertEq(tradingMode, 0); vm.warp(1672527610); // 2023-01-01 00:00:10 mockBreaker3.setTrigger(true); mockBreaker3.setReset(false); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - (uint256 breakerTradingMode3, uint256 breakerLastUpdatedTime3, bool breakerEnabled3) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - uint256 tradingMode3 = breakerBox.getRateFeedTradingMode(rateFeedID1); - assertEq(breakerTradingMode3, 2); - assertEq(breakerLastUpdatedTime3, 1672527610); - assertTrue(breakerEnabled3); - assertEq(tradingMode3, 2); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedID1); + assertEq(status.tradingMode, 2); + assertEq(status.lastUpdatedTime, 1672527610); + assertTrue(status.enabled); + assertEq(tradingMode, 2); } function test_checkAndSetBreakers_whenCooldownTenSeconds_shouldSetStatusCorrectly() public { vm.warp(1672527600); // 2023-01-01 00:00:00 setUpBreaker(mockBreaker3, 2, 9, false, true); toggleAndAssertBreaker(address(mockBreaker3), rateFeedID1, true); + uint8 tradingMode; + IBreakerBox.BreakerStatus memory status; - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - (uint256 breakerTradingMode1, uint256 breakerLastUpdatedTime1, bool breakerEnabled1) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - uint256 tradingMode1 = breakerBox.getRateFeedTradingMode(rateFeedID1); - assertEq(breakerTradingMode1, 2); - assertEq(breakerLastUpdatedTime1, 1672527600); - assertTrue(breakerEnabled1); - assertEq(tradingMode1, 2); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedID1); + assertEq(status.tradingMode, 2); + assertEq(status.lastUpdatedTime, 1672527600); + assertTrue(status.enabled); + assertEq(tradingMode, 2); vm.warp(1672527610); // 2023-01-01 00:00:10 mockBreaker3.setTrigger(false); mockBreaker3.setReset(true); - changePrank(address(sortedOracles)); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - (uint256 breakerTradingMode2, uint256 breakerLastUpdatedTime2, bool breakerEnabled2) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - uint256 tradingMode2 = breakerBox.getRateFeedTradingMode(rateFeedID1); - assertEq(breakerTradingMode2, 0); - assertEq(breakerLastUpdatedTime2, 1672527610); - assertTrue(breakerEnabled2); - assertEq(tradingMode2, 0); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedID1); + assertEq(status.tradingMode, 0); + assertEq(status.lastUpdatedTime, 1672527610); + assertTrue(status.enabled); + assertEq(tradingMode, 0); vm.warp(1672527620); // 2023-01-01 00:00:20 mockBreaker3.setTrigger(true); mockBreaker3.setReset(false); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); - (uint256 breakerTradingMode3, uint256 breakerLastUpdatedTime3, bool breakerEnabled3) = breakerBox - .rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); - uint256 tradingMode3 = breakerBox.getRateFeedTradingMode(rateFeedID1); - assertEq(breakerTradingMode3, 2); - assertEq(breakerLastUpdatedTime3, 1672527620); - assertTrue(breakerEnabled3); - assertEq(tradingMode3, 2); + status = breakerBox.rateFeedBreakerStatus(rateFeedID1, address(mockBreaker3)); + tradingMode = breakerBox.getRateFeedTradingMode(rateFeedID1); + assertEq(status.tradingMode, 2); + assertEq(status.lastUpdatedTime, 1672527620); + assertTrue(status.enabled); + assertEq(tradingMode, 2); } function test_checkAndSetBreakers_whenMultipleBreakersAreEnabled_shouldCalculateTradingModeCorrectly() public { @@ -716,8 +703,10 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { mockBreaker3.setTrigger(true); mockBreaker4.setTrigger(true); - changePrank(address(sortedOracles)); + + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); + uint256 tradingModeAfter = breakerBox.getRateFeedTradingMode(rateFeedID1); assertEq(tradingModeAfter, tradingModeBreaker3 | tradingModeBreaker4); @@ -725,6 +714,7 @@ contract BreakerBoxTest_checkAndSetBreakers is BreakerBoxTest { mockBreaker3.setReset(true); skip(60); + vm.prank(address(sortedOracles)); breakerBox.checkAndSetBreakers(rateFeedID1); uint256 tradingModeAfter2 = breakerBox.getRateFeedTradingMode(rateFeedID1); assertEq(tradingModeAfter2, tradingModeBreaker4); diff --git a/test/oracles/ChainlinkRelayerFactory.t.sol b/test/unit/oracles/ChainlinkRelayerFactory.t.sol similarity index 96% rename from test/oracles/ChainlinkRelayerFactory.t.sol rename to test/unit/oracles/ChainlinkRelayerFactory.t.sol index e4491523..d18ef63e 100644 --- a/test/oracles/ChainlinkRelayerFactory.t.sol +++ b/test/unit/oracles/ChainlinkRelayerFactory.t.sol @@ -3,13 +3,14 @@ // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase, one-contract-per-file pragma solidity ^0.8.18; -import { Ownable } from "openzeppelin-contracts-next/contracts/access/Ownable.sol"; -import { BaseTest } from "../utils/BaseTest.next.sol"; +import { Ownable } from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import { Test } from "mento-std/Test.sol"; + import { IChainlinkRelayerFactory } from "contracts/interfaces/IChainlinkRelayerFactory.sol"; import { IChainlinkRelayer } from "contracts/interfaces/IChainlinkRelayer.sol"; import { ChainlinkRelayerFactory } from "contracts/oracles/ChainlinkRelayerFactory.sol"; -contract ChainlinkRelayerFactoryTest is BaseTest { +contract ChainlinkRelayerFactoryTest is Test { IChainlinkRelayerFactory relayerFactory; address owner = makeAddr("owner"); address relayerDeployer = makeAddr("relayerDeployer"); @@ -37,11 +38,9 @@ contract ChainlinkRelayerFactoryTest is BaseTest { event RelayerRemoved(address indexed relayerAddress, address indexed rateFeedId); event RelayerDeployerUpdated(address indexed newRelayerDeployer, address indexed oldRelayerDeployer); - function oneAggregator(uint256 aggregatorIndex) - internal - view - returns (IChainlinkRelayer.ChainlinkAggregator[] memory aggregators) - { + function oneAggregator( + uint256 aggregatorIndex + ) internal view returns (IChainlinkRelayer.ChainlinkAggregator[] memory aggregators) { aggregators = new IChainlinkRelayer.ChainlinkAggregator[](1); aggregators[0] = IChainlinkRelayer.ChainlinkAggregator(mockAggregators[aggregatorIndex], false); } @@ -80,7 +79,7 @@ contract ChainlinkRelayerFactoryTest is BaseTest { salt, keccak256( abi.encodePacked( - vm.getCode(factory.contractPath("ChainlinkRelayerV1")), + vm.getCode("ChainlinkRelayerV1"), abi.encode(rateFeedId, rateFeedDescription, sortedOracles, maxTimestampSpread, aggregators) ) ) @@ -91,11 +90,10 @@ contract ChainlinkRelayerFactoryTest is BaseTest { ); } - function contractAlreadyExistsError(address relayerAddress, address rateFeedId) - public - pure - returns (bytes memory ContractAlreadyExistsError) - { + function contractAlreadyExistsError( + address relayerAddress, + address rateFeedId + ) public pure returns (bytes memory ContractAlreadyExistsError) { return abi.encodeWithSignature("ContractAlreadyExists(address,address)", relayerAddress, rateFeedId); } @@ -114,7 +112,7 @@ contract ChainlinkRelayerFactoryTest_initialize is ChainlinkRelayerFactoryTest { assertEq(realSortedOracles, mockSortedOracles); } - function test_setsOwner() public { + function test_setsOwner() public view { address realOwner = Ownable(address(relayerFactory)).owner(); assertEq(realOwner, owner); } @@ -256,7 +254,6 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes } function test_revertsWhenDeployingToAddressWithCode() public { - vm.prank(relayerDeployer); address futureAddress = expectedRelayerAddress( aRateFeed, aRateFeedDescription, @@ -300,7 +297,7 @@ contract ChainlinkRelayerFactoryTest_deployRelayer is ChainlinkRelayerFactoryTes } contract ChainlinkRelayerFactoryTest_getRelayers is ChainlinkRelayerFactoryTest { - function test_emptyWhenNoRelayers() public { + function test_emptyWhenNoRelayers() public view { address[] memory relayers = relayerFactory.getRelayers(); assertEq(relayers.length, 0); } diff --git a/test/oracles/ChainlinkRelayerV1.t.sol b/test/unit/oracles/ChainlinkRelayerV1.t.sol similarity index 95% rename from test/oracles/ChainlinkRelayerV1.t.sol rename to test/unit/oracles/ChainlinkRelayerV1.t.sol index 9730f75f..3da73980 100644 --- a/test/oracles/ChainlinkRelayerV1.t.sol +++ b/test/unit/oracles/ChainlinkRelayerV1.t.sol @@ -3,29 +3,23 @@ // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase pragma solidity ^0.8.18; -import { console } from "forge-std-next/console.sol"; -import "../utils/BaseTest.next.sol"; -import "../mocks/MockAggregatorV3.sol"; +import { console } from "forge-std/console.sol"; +import { Test } from "mento-std/Test.sol"; + +import "test/utils/mocks/MockAggregatorV3.sol"; import "contracts/interfaces/IChainlinkRelayer.sol"; import "contracts/oracles/ChainlinkRelayerV1.sol"; import { UD60x18, ud, intoUint256 } from "prb/math/UD60x18.sol"; interface ISortedOracles { + function initialize(uint256) external; + function addOracle(address, address) external; - function removeOracle( - address, - address, - uint256 - ) external; + function removeOracle(address, address, uint256) external; - function report( - address, - uint256, - address, - address - ) external; + function report(address, uint256, address, address) external; function setTokenReportExpiry(address, uint256) external; @@ -33,16 +27,10 @@ interface ISortedOracles { function medianTimestamp(address token) external returns (uint256); - function getRates(address rateFeedId) - external - returns ( - address[] memory, - uint256[] memory, - uint256[] memory - ); + function getRates(address rateFeedId) external returns (address[] memory, uint256[] memory, uint256[] memory); } -contract ChainlinkRelayerV1Test is BaseTest { +contract ChainlinkRelayerV1Test is Test { bytes constant EXPIRED_TIMESTAMP_ERROR = abi.encodeWithSignature("ExpiredTimestamp()"); bytes constant INVALID_PRICE_ERROR = abi.encodeWithSignature("InvalidPrice()"); bytes constant INVALID_AGGREGATOR_ERROR = abi.encodeWithSignature("InvalidAggregator()"); @@ -110,9 +98,8 @@ contract ChainlinkRelayerV1Test is BaseTest { } function setUp() public virtual { - sortedOracles = ISortedOracles( - factory.createFromPath("contracts/common/SortedOracles.sol:SortedOracles", abi.encode(true), address(this)) - ); + sortedOracles = ISortedOracles(deployCode("SortedOracles", abi.encode(true))); + sortedOracles.initialize(expirySeconds); sortedOracles.setTokenReportExpiry(rateFeedId, expirySeconds); mockAggregator0 = new MockAggregatorV3(8); @@ -277,12 +264,12 @@ contract ChainlinkRelayerV1Test_fuzz_single is ChainlinkRelayerV1Test { } function testFuzz_convertsChainlinkToUD60x18Correctly(int256 _rate) public { - int256 rate = bound(_rate, 1, type(int256).max / 10**18); + int256 rate = bound(_rate, 1, type(int256).max / 10 ** 18); mockAggregator0.setRoundData(rate, uint256(block.timestamp)); relayer.relay(); (uint256 medianRate, ) = sortedOracles.medianRate(rateFeedId); - assertEq(medianRate, uint256(rate) * 10**(24 - mockAggregator0.decimals())); + assertEq(medianRate, uint256(rate) * 10 ** (24 - mockAggregator0.decimals())); } } @@ -380,13 +367,14 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { } function test_relaysTheRate_withLesserGreater_whenLesser_andCantExpireDirectly() public { + uint256 t0 = block.timestamp; address oldRelayer = makeAddr("oldRelayer"); sortedOracles.addOracle(rateFeedId, oldRelayer); vm.prank(oldRelayer); sortedOracles.report(rateFeedId, expectedReport + 2, address(0), address(0)); - vm.warp(block.timestamp + 200); // Not enough to be able to expire the first report + vm.warp(t0 + 200); // Not enough to be able to expire the first report setAggregatorPrices(); // Update timestamps relayAndLogGas("other report no expiry"); // relayer.relay(); @@ -396,7 +384,7 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { assertEq(oracles[1], address(relayer)); assertEq(rates[1], expectedReport); - vm.warp(block.timestamp + 400); // First report should be expired + vm.warp(t0 + 600); // First report should be expired setAggregatorPrices(); // Update timestamps relayAndLogGas("2 reports with expiry"); // relayer.relay(); @@ -410,13 +398,14 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { } function test_relaysTheRate_withLesserGreater_whenGreater_andCantExpireDirectly() public { + uint256 t0 = block.timestamp; address oldRelayer = makeAddr("oldRelayer"); sortedOracles.addOracle(rateFeedId, oldRelayer); vm.prank(oldRelayer); sortedOracles.report(rateFeedId, expectedReport - 2, address(0), address(0)); - vm.warp(block.timestamp + 200); // Not enough to be able to expire the first report + vm.warp(t0 + 200); // Not enough to be able to expire the first report setAggregatorPrices(); // Update timestamps relayAndLogGas("other report no expiry"); // relayer.relay(); @@ -426,7 +415,7 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { assertEq(oracles[0], address(relayer)); assertEq(rates[0], expectedReport); - vm.warp(block.timestamp + 400); // First report should be expired + vm.warp(t0 + 600); // First report should be expired setAggregatorPrices(); // Update timestamps relayAndLogGas("2 reports with expiry"); // relayer.relay(); @@ -440,13 +429,14 @@ contract ChainlinkRelayerV1Test_relay_single is ChainlinkRelayerV1Test { } function test_relaysTheRate_withLesserGreater_whenLesser_andCanExpire() public { + uint256 t0 = block.timestamp; address oldRelayer = makeAddr("oldRelayer"); sortedOracles.addOracle(rateFeedId, oldRelayer); vm.prank(oldRelayer); sortedOracles.report(rateFeedId, expectedReport + 2, address(0), address(0)); - vm.warp(block.timestamp + 600); // Not enough to be able to expire the first report + vm.warp(t0 + 600); // Not enough to be able to expire the first report setAggregatorPrices(); // Update timestamps relayAndLogGas("other report with expiry"); diff --git a/test/oracles/breakers/MedianDeltaBreaker.t.sol b/test/unit/oracles/breakers/MedianDeltaBreaker.t.sol similarity index 76% rename from test/oracles/breakers/MedianDeltaBreaker.t.sol rename to test/unit/oracles/breakers/MedianDeltaBreaker.t.sol index aef4cbf3..7a2f93ed 100644 --- a/test/oracles/breakers/MedianDeltaBreaker.t.sol +++ b/test/unit/oracles/breakers/MedianDeltaBreaker.t.sol @@ -1,30 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import { console2 as console } from "forge-std/console2.sol"; - -import { BaseTest } from "../../utils/BaseTest.t.sol"; -import { MockSortedOracles } from "../../mocks/MockSortedOracles.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { Test } from "mento-std/Test.sol"; +import { MockSortedOracles } from "test/utils/mocks/MockSortedOracles.sol"; +import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; -import { SortedLinkedListWithMedian } from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; -import { MedianDeltaBreaker } from "contracts/oracles/breakers/MedianDeltaBreaker.sol"; -contract MedianDeltaBreakerTest is BaseTest { - address notDeployer; +contract MedianDeltaBreakerTest is Test { + address notOwner; address rateFeedID1; address rateFeedID2; address rateFeedID3; address breakerBox; MockSortedOracles sortedOracles; - MedianDeltaBreaker breaker; + IMedianDeltaBreaker breaker; - uint256 defaultThreshold = 0.15 * 10**24; // 15% + uint256 defaultThreshold = 0.15 * 10 ** 24; // 15% uint256 defaultCooldownTime = 5 minutes; address[] rateFeedIDs = new address[](1); @@ -43,31 +39,34 @@ contract MedianDeltaBreakerTest is BaseTest { event MedianRateEMAReset(address rateFeedID); function setUp() public { - notDeployer = actor("notDeployer"); - rateFeedID1 = actor("rateFeedID1"); - rateFeedID2 = actor("rateFeedID2"); - rateFeedID3 = actor("rateFeedID3"); - breakerBox = actor("breakerBox"); + notOwner = makeAddr("notOwner"); + rateFeedID1 = makeAddr("rateFeedID1"); + rateFeedID2 = makeAddr("rateFeedID2"); + rateFeedID3 = makeAddr("rateFeedID3"); + breakerBox = makeAddr("breakerBox"); rateFeedIDs[0] = rateFeedID2; - rateChangeThresholds[0] = 0.9 * 10**24; + rateChangeThresholds[0] = 0.9 * 10 ** 24; cooldownTimes[0] = 10 minutes; - vm.startPrank(deployer); sortedOracles = new MockSortedOracles(); - sortedOracles.addOracle(rateFeedID1, actor("OracleClient")); - sortedOracles.addOracle(rateFeedID2, actor("oracleClient")); - sortedOracles.addOracle(rateFeedID3, actor("oracleClient1")); - - breaker = new MedianDeltaBreaker( - defaultCooldownTime, - defaultThreshold, - ISortedOracles(address(sortedOracles)), - breakerBox, - rateFeedIDs, - rateChangeThresholds, - cooldownTimes + sortedOracles.addOracle(rateFeedID1, makeAddr("OracleClient")); + sortedOracles.addOracle(rateFeedID2, makeAddr("oracleClient")); + sortedOracles.addOracle(rateFeedID3, makeAddr("oracleClient1")); + breaker = IMedianDeltaBreaker( + deployCode( + "MedianDeltaBreaker", + abi.encode( + defaultCooldownTime, + defaultThreshold, + ISortedOracles(address(sortedOracles)), + breakerBox, + rateFeedIDs, + rateChangeThresholds, + cooldownTimes + ) + ) ); } } @@ -75,31 +74,31 @@ contract MedianDeltaBreakerTest is BaseTest { contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest { /* ---------- Constructor ---------- */ - function test_constructor_shouldSetOwner() public { - assertEq(breaker.owner(), deployer); + function test_constructor_shouldSetOwner() public view { + assertEq(breaker.owner(), address(this)); } - function test_constructor_shouldSetDefaultCooldownTime() public { + function test_constructor_shouldSetDefaultCooldownTime() public view { assertEq(breaker.defaultCooldownTime(), defaultCooldownTime); } - function test_constructor_shouldSetDefaultRateChangeThreshold() public { + function test_constructor_shouldSetDefaultRateChangeThreshold() public view { assertEq(breaker.defaultRateChangeThreshold(), defaultThreshold); } - function test_constructor_shouldSetSortedOracles() public { + function test_constructor_shouldSetSortedOracles() public view { assertEq(address(breaker.sortedOracles()), address(sortedOracles)); } - function test_constructor_shouldSetBreakerBox() public { + function test_constructor_shouldSetBreakerBox() public view { assertEq(breaker.breakerBox(), breakerBox); } - function test_constructor_shouldSetRateChangeThresholds() public { + function test_constructor_shouldSetRateChangeThresholds() public view { assertEq(breaker.rateChangeThreshold(rateFeedIDs[0]), rateChangeThresholds[0]); } - function test_constructor_shouldSetCooldownTimes() public { + function test_constructor_shouldSetCooldownTimes() public view { assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); } @@ -107,7 +106,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest function test_setDefaultCooldownTime_whenCallerIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); - changePrank(notDeployer); + vm.prank(notOwner); breaker.setDefaultCooldownTime(2 minutes); } @@ -120,18 +119,18 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest function test_setRateChangeThreshold_whenCallerIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); - changePrank(notDeployer); + vm.prank(notOwner); breaker.setDefaultRateChangeThreshold(123456); } function test_setRateChangeThreshold_whenValueGreaterThanOne_shouldRevert() public { vm.expectRevert("value must be less than 1"); - breaker.setDefaultRateChangeThreshold(1 * 10**24); + breaker.setDefaultRateChangeThreshold(1 * 10 ** 24); } function test_setRateChangeThreshold_whenCallerIsOwner_shouldUpdateAndEmit() public { - uint256 testThreshold = 0.1 * 10**24; + uint256 testThreshold = 0.1 * 10 ** 24; vm.expectEmit(false, false, false, true); emit DefaultRateChangeThresholdUpdated(testThreshold); @@ -141,7 +140,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setSortedOracles_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); breaker.setSortedOracles(ISortedOracles(address(0))); } @@ -152,7 +151,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setSortedOracles_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newSortedOracles = actor("newSortedOracles"); + address newSortedOracles = makeAddr("newSortedOracles"); vm.expectEmit(true, true, true, true); emit SortedOraclesUpdated(newSortedOracles); @@ -162,7 +161,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setBreakerBox_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); breaker.setBreakerBox(address(0)); } @@ -173,7 +172,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setBreakerBox_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newBreakerBox = actor("newBreakerBox"); + address newBreakerBox = makeAddr("newBreakerBox"); vm.expectEmit(true, true, true, true); emit BreakerBoxUpdated(newBreakerBox); @@ -183,21 +182,21 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setRateChangeThreshold_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } function test_setRateChangeThreshold_whenValuesAreDifferentLengths_shouldRevert() public { address[] memory rateFeedIDs2 = new address[](2); - rateFeedIDs2[0] = actor("randomRateFeed"); - rateFeedIDs2[1] = actor("randomRateFeed2"); + rateFeedIDs2[0] = makeAddr("randomRateFeed"); + rateFeedIDs2[1] = makeAddr("randomRateFeed2"); vm.expectRevert("array length missmatch"); breaker.setRateChangeThresholds(rateFeedIDs2, rateChangeThresholds); } function test_setRateChangeThreshold_whenThresholdIsExactly1_shouldRevert() public { - rateChangeThresholds[0] = 1 * 10**24; + rateChangeThresholds[0] = 1 * 10 ** 24; vm.expectRevert("value must be less than 1"); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } @@ -210,7 +209,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_setSmoothingFactor_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); breaker.setSmoothingFactor(rateFeedIDs[0], 0.8 * 1e24); } @@ -231,7 +230,7 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } function test_resetMedianRateEMA_whenCallerIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notOwner); vm.expectRevert("Ownable: caller is not the owner"); breaker.resetMedianRateEMA(address(0)); } @@ -246,9 +245,8 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest vm.mockCall(address(sortedOracles), abi.encodeWithSelector(sortedOracles.medianRate.selector), abi.encode(1, 1)); // Update ema for rate feed - changePrank(breakerBox); + vm.prank(breakerBox); breaker.shouldTrigger(rateFeedIDs[0]); - changePrank(deployer); // Verify median is not zero before reset uint256 medianEMABefore = breaker.medianRatesEMA(rateFeedIDs[0]); @@ -263,15 +261,15 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } /* ---------- Getters ---------- */ - function test_getCooldown_withDefault_shouldReturnDefaultCooldown() public { + function test_getCooldown_withDefault_shouldReturnDefaultCooldown() public view { assertEq(breaker.getCooldown(rateFeedID1), defaultCooldownTime); } - function test_getCooldown_withoutdefault_shouldReturnSpecificCooldown() public { + function test_getCooldown_withoutdefault_shouldReturnSpecificCooldown() public view { assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); } - function test_getSmoothingFactor_whenNotSet_shouldReturnDefaultSmoothingFactor() public { + function test_getSmoothingFactor_whenNotSet_shouldReturnDefaultSmoothingFactor() public view { assertEq(breaker.getSmoothingFactor(rateFeedIDs[0]), 1e24); } @@ -283,6 +281,8 @@ contract MedianDeltaBreakerTest_constructorAndSetters is MedianDeltaBreakerTest } contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { + using stdStorage for StdStorage; + function setSortedOraclesMedian(uint256 median) public { vm.mockCall( address(sortedOracles), @@ -292,8 +292,8 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { } function updatePreviousEMAByPercent(uint256 medianChangeScaleFactor, address _rateFeedID) public { - uint256 previousEMA = 0.98 * 10**24; - uint256 currentMedianRate = (previousEMA * medianChangeScaleFactor) / 10**24; + uint256 previousEMA = 0.98 * 10 ** 24; + uint256 currentMedianRate = (previousEMA * medianChangeScaleFactor) / 10 ** 24; stdstore.target(address(breaker)).sig(breaker.medianRatesEMA.selector).with_key(_rateFeedID).checked_write( previousEMA ); @@ -309,40 +309,40 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { function test_shouldTrigger_withDefaultThreshold_shouldTrigger() public { assertEq(breaker.rateChangeThreshold(rateFeedID1), 0); - updatePreviousEMAByPercent(0.7 * 10**24, rateFeedID1); - changePrank(breakerBox); + updatePreviousEMAByPercent(0.7 * 10 ** 24, rateFeedID1); + vm.prank(breakerBox); assertTrue(breaker.shouldTrigger(rateFeedID1)); } function test_shouldTrigger_whenThresholdIsLargerThanMedian_shouldNotTrigger() public { - updatePreviousEMAByPercent(0.7 * 10**24, rateFeedID1); + updatePreviousEMAByPercent(0.7 * 10 ** 24, rateFeedID1); - rateChangeThresholds[0] = 0.8 * 10**24; + rateChangeThresholds[0] = 0.8 * 10 ** 24; rateFeedIDs[0] = rateFeedID1; breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); assertEq(breaker.rateChangeThreshold(rateFeedID1), rateChangeThresholds[0]); - changePrank(breakerBox); + vm.prank(breakerBox); assertFalse(breaker.shouldTrigger(rateFeedID1)); } function test_shouldTrigger_whithDefaultThreshold_ShouldNotTrigger() public { assertEq(breaker.rateChangeThreshold(rateFeedID3), 0); - updatePreviousEMAByPercent(1.1 * 10**24, rateFeedID3); + updatePreviousEMAByPercent(1.1 * 10 ** 24, rateFeedID3); - changePrank(breakerBox); + vm.prank(breakerBox); assertFalse(breaker.shouldTrigger(rateFeedID3)); } function test_shouldTrigger_whenThresholdIsSmallerThanMedian_ShouldTrigger() public { - updatePreviousEMAByPercent(1.1 * 10**24, rateFeedID3); - rateChangeThresholds[0] = 0.01 * 10**24; + updatePreviousEMAByPercent(1.1 * 10 ** 24, rateFeedID3); + rateChangeThresholds[0] = 0.01 * 10 ** 24; rateFeedIDs[0] = rateFeedID3; breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); assertEq(breaker.rateChangeThreshold(rateFeedID3), rateChangeThresholds[0]); - changePrank(breakerBox); + vm.prank(breakerBox); assertTrue(breaker.shouldTrigger(rateFeedID3)); } @@ -353,37 +353,38 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { (uint256 beforeRate, ) = sortedOracles.medianRate(rateFeed); assertEq(beforeRate, 0); - uint256 median = 0.9836 * 10**24; + uint256 median = 0.9836 * 10 ** 24; setSortedOraclesMedian(median); (uint256 afterRate, ) = sortedOracles.medianRate(rateFeed); assertEq(afterRate, median); - changePrank(breakerBox); + vm.prank(breakerBox); assertFalse(breaker.shouldTrigger(rateFeed)); assertEq(breaker.medianRatesEMA(rateFeed), median); } function test_shouldTrigger_whenMedianDrops_shouldCalculateEMACorrectlyAndTrigger() public { address rateFeed = rateFeedIDs[0]; - uint256 smoothingFactor = 0.1 * 10**24; - rateChangeThresholds[0] = 0.03 * 10**24; + uint256 smoothingFactor = 0.1 * 10 ** 24; + rateChangeThresholds[0] = 0.03 * 10 ** 24; rateFeedIDs[0] = rateFeed; breaker.setSmoothingFactor(rateFeed, smoothingFactor); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); - uint256 firstMedian = 1.05 * 10**24; + uint256 firstMedian = 1.05 * 10 ** 24; setSortedOraclesMedian(firstMedian); - changePrank(breakerBox); + vm.prank(breakerBox); assertFalse(breaker.shouldTrigger(rateFeed)); assertEq(breaker.medianRatesEMA(rateFeed), firstMedian); - uint256 secondMedian = 1.0164 * 10**24; + uint256 secondMedian = 1.0164 * 10 ** 24; setSortedOraclesMedian(secondMedian); + vm.prank(breakerBox); bool triggered = breaker.shouldTrigger((rateFeed)); // 0.1*1.0164 + (1.05 * 0.9) = 1.04664 - assertEq(breaker.medianRatesEMA(rateFeed), 1.04664 * 10**24); + assertEq(breaker.medianRatesEMA(rateFeed), 1.04664 * 10 ** 24); // (1.0164-1.05)/1.05 = -0.03200000000000007 assertTrue(triggered); @@ -391,24 +392,25 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { function test_shouldTrigger_whenMedianJumps_shouldCalculateEMACorrectlyAndTrigger() public { address rateFeed = rateFeedIDs[0]; - uint256 smoothingFactor = 0.1 * 10**24; - rateChangeThresholds[0] = 0.03 * 10**24; + uint256 smoothingFactor = 0.1 * 10 ** 24; + rateChangeThresholds[0] = 0.03 * 10 ** 24; rateFeedIDs[0] = rateFeed; breaker.setSmoothingFactor(rateFeed, smoothingFactor); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); - uint256 firstMedian = 1.05 * 10**24; + uint256 firstMedian = 1.05 * 10 ** 24; setSortedOraclesMedian(firstMedian); - changePrank(breakerBox); + vm.prank(breakerBox); assertFalse(breaker.shouldTrigger(rateFeed)); assertEq(breaker.medianRatesEMA(rateFeed), firstMedian); - uint256 secondMedian = 1.0836 * 10**24; + uint256 secondMedian = 1.0836 * 10 ** 24; setSortedOraclesMedian(secondMedian); + vm.prank(breakerBox); bool triggered = breaker.shouldTrigger((rateFeed)); // 0.1*1.0836 + (1.05 * 0.9) = 1.05336 - assertEq(breaker.medianRatesEMA(rateFeed), 1.05336 * 10**24); + assertEq(breaker.medianRatesEMA(rateFeed), 1.05336 * 10 ** 24); // (1.0836-1.05)/1.05 = 0.031999999999999855 assertTrue(triggered); @@ -418,15 +420,15 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { address rateFeed = rateFeedIDs[0]; uint256[5] memory medians; - medians[0] = 0.997 * 10**24; - medians[1] = 0.9968 * 10**24; - medians[2] = 0.9769 * 10**24; - medians[3] = 0.9759 * 10**24; - medians[4] = 0.9854 * 10**24; + medians[0] = 0.997 * 10 ** 24; + medians[1] = 0.9968 * 10 ** 24; + medians[2] = 0.9769 * 10 ** 24; + medians[3] = 0.9759 * 10 ** 24; + medians[4] = 0.9854 * 10 ** 24; for (uint256 i = 0; i < medians.length; i++) { setSortedOraclesMedian(medians[i]); - changePrank(breakerBox); + vm.prank(breakerBox); breaker.shouldTrigger(rateFeed); assertEq(breaker.medianRatesEMA(rateFeed), medians[i]); } @@ -434,26 +436,26 @@ contract MedianDeltaBreakerTest_shouldTrigger is MedianDeltaBreakerTest { function test_shouldTrigger_withLongSequencesOfUpdates_shouldCalculateEMACorrectly() public { address rateFeed = rateFeedIDs[0]; - uint256 smoothingFactor = 0.1 * 10**24; + uint256 smoothingFactor = 0.1 * 10 ** 24; breaker.setSmoothingFactor(rateFeed, smoothingFactor); uint256[5] memory medians; - medians[0] = 0.997 * 10**24; - medians[1] = 0.9968 * 10**24; - medians[2] = 0.9769 * 10**24; - medians[3] = 0.9759 * 10**24; - medians[4] = 0.9854 * 10**24; + medians[0] = 0.997 * 10 ** 24; + medians[1] = 0.9968 * 10 ** 24; + medians[2] = 0.9769 * 10 ** 24; + medians[3] = 0.9759 * 10 ** 24; + medians[4] = 0.9854 * 10 ** 24; uint256[5] memory expectedEMAs; - expectedEMAs[0] = 0.997 * 10**24; - expectedEMAs[1] = 0.99698 * 10**24; - expectedEMAs[2] = 0.994972 * 10**24; - expectedEMAs[3] = 0.9930648 * 10**24; - expectedEMAs[4] = 0.99229832 * 10**24; + expectedEMAs[0] = 0.997 * 10 ** 24; + expectedEMAs[1] = 0.99698 * 10 ** 24; + expectedEMAs[2] = 0.994972 * 10 ** 24; + expectedEMAs[3] = 0.9930648 * 10 ** 24; + expectedEMAs[4] = 0.99229832 * 10 ** 24; for (uint256 i = 0; i < medians.length; i++) { setSortedOraclesMedian(medians[i]); - changePrank(breakerBox); + vm.prank(breakerBox); breaker.shouldTrigger(rateFeed); assertEq(breaker.medianRatesEMA(rateFeed), expectedEMAs[i]); } diff --git a/test/oracles/breakers/ValueDeltaBreaker.t.sol b/test/unit/oracles/breakers/ValueDeltaBreaker.t.sol similarity index 75% rename from test/oracles/breakers/ValueDeltaBreaker.t.sol rename to test/unit/oracles/breakers/ValueDeltaBreaker.t.sol index 6f547909..514c673a 100644 --- a/test/oracles/breakers/ValueDeltaBreaker.t.sol +++ b/test/unit/oracles/breakers/ValueDeltaBreaker.t.sol @@ -1,21 +1,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; -import { console2 as console } from "forge-std/console2.sol"; +import { Test } from "mento-std/Test.sol"; -import { BaseTest } from "../../utils/BaseTest.t.sol"; -import { MockSortedOracles } from "../../mocks/MockSortedOracles.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { SortedLinkedListWithMedian } from "contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; +import { MockSortedOracles } from "test/utils/mocks/MockSortedOracles.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; -import { ValueDeltaBreaker } from "contracts/oracles/breakers/ValueDeltaBreaker.sol"; -contract ValueDeltaBreakerTest is BaseTest { +contract ValueDeltaBreakerTest is Test { using FixidityLib for FixidityLib.Fraction; address notDeployer; @@ -24,9 +20,9 @@ contract ValueDeltaBreakerTest is BaseTest { address rateFeedID2; address rateFeedID3; MockSortedOracles sortedOracles; - ValueDeltaBreaker breaker; + IValueDeltaBreaker breaker; - uint256 defaultThreshold = 0.15 * 10**24; // 15% + uint256 defaultThreshold = 0.15 * 10 ** 24; // 15% uint256 defaultCooldownTime = 5 minutes; address[] rateFeedIDs = new address[](1); @@ -41,30 +37,34 @@ contract ValueDeltaBreakerTest is BaseTest { event SortedOraclesUpdated(address newSortedOracles); event RateChangeThresholdUpdated(address rateFeedID1, uint256 rateChangeThreshold); - function setUp() public { - notDeployer = actor("notDeployer"); - rateFeedID1 = actor("rateFeedID1"); - rateFeedID2 = actor("rateFeedID2"); - rateFeedID3 = actor("rateFeedID3"); + function setUp() public virtual { + notDeployer = makeAddr("notDeployer"); + rateFeedID1 = makeAddr("rateFeedID1"); + rateFeedID2 = makeAddr("rateFeedID2"); + rateFeedID3 = makeAddr("rateFeedID3"); rateFeedIDs[0] = rateFeedID2; - rateChangeThresholds[0] = 0.9 * 10**24; + rateChangeThresholds[0] = 0.9 * 10 ** 24; cooldownTimes[0] = 10 minutes; - vm.startPrank(deployer); sortedOracles = new MockSortedOracles(); - sortedOracles.addOracle(rateFeedID1, actor("OracleClient")); - sortedOracles.addOracle(rateFeedID2, actor("oracleClient")); - sortedOracles.addOracle(rateFeedID3, actor("oracleClient1")); - - breaker = new ValueDeltaBreaker( - defaultCooldownTime, - defaultThreshold, - ISortedOracles(address(sortedOracles)), - rateFeedIDs, - rateChangeThresholds, - cooldownTimes + sortedOracles.addOracle(rateFeedID1, makeAddr("OracleClient")); + sortedOracles.addOracle(rateFeedID2, makeAddr("oracleClient")); + sortedOracles.addOracle(rateFeedID3, makeAddr("oracleClient1")); + + breaker = IValueDeltaBreaker( + deployCode( + "ValueDeltaBreaker", + abi.encode( + defaultCooldownTime, + defaultThreshold, + ISortedOracles(address(sortedOracles)), + rateFeedIDs, + rateChangeThresholds, + cooldownTimes + ) + ) ); } } @@ -72,27 +72,27 @@ contract ValueDeltaBreakerTest is BaseTest { contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { /* ---------- Constructor ---------- */ - function test_constructor_shouldSetOwner() public { - assertEq(breaker.owner(), deployer); + function test_constructor_shouldSetOwner() public view { + assertEq(breaker.owner(), address(this)); } - function test_constructor_shouldSetDefaultCooldownTime() public { + function test_constructor_shouldSetDefaultCooldownTime() public view { assertEq(breaker.defaultCooldownTime(), defaultCooldownTime); } - function test_constructor_shouldSetDefaultRateChangeThreshold() public { + function test_constructor_shouldSetDefaultRateChangeThreshold() public view { assertEq(breaker.defaultRateChangeThreshold(), defaultThreshold); } - function test_constructor_shouldSetSortedOracles() public { + function test_constructor_shouldSetSortedOracles() public view { assertEq(address(breaker.sortedOracles()), address(sortedOracles)); } - function test_constructor_shouldSetRateChangeThresholds() public { + function test_constructor_shouldSetRateChangeThresholds() public view { assertEq(breaker.rateChangeThreshold(rateFeedIDs[0]), rateChangeThresholds[0]); } - function test_constructor_shouldSetCooldownTimes() public { + function test_constructor_shouldSetCooldownTimes() public view { assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); } @@ -100,7 +100,7 @@ contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { function test_setDefaultCooldownTime_whenCallerIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); - changePrank(notDeployer); + vm.prank(notDeployer); breaker.setDefaultCooldownTime(2 minutes); } @@ -116,18 +116,18 @@ contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { function test_setRateChangeThreshold_whenCallerIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); - changePrank(notDeployer); + vm.prank(notDeployer); breaker.setDefaultRateChangeThreshold(123456); } function test_setDefaultRateChangeThreshold_whenValueGreaterThanOne_shouldRevert() public { vm.expectRevert("value must be less than 1"); - breaker.setDefaultRateChangeThreshold(1 * 10**24); + breaker.setDefaultRateChangeThreshold(1 * 10 ** 24); } function test_setDefaultRateChangeThreshold_whenCallerIsOwner_shouldUpdateAndEmit() public { - uint256 testThreshold = 0.1 * 10**24; + uint256 testThreshold = 0.1 * 10 ** 24; vm.expectEmit(false, false, false, true); emit DefaultRateChangeThresholdUpdated(testThreshold); @@ -137,7 +137,7 @@ contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { } function test_setSortedOracles_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breaker.setSortedOracles(ISortedOracles(address(0))); } @@ -148,7 +148,7 @@ contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { } function test_setSortedOracles_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newSortedOracles = actor("newSortedOracles"); + address newSortedOracles = makeAddr("newSortedOracles"); vm.expectEmit(true, true, true, true); emit SortedOraclesUpdated(newSortedOracles); @@ -158,21 +158,21 @@ contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { } function test_setRateChangeThresholds_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } function test_setRateChangeThresholds_whenValuesAreDifferentLengths_shouldRevert() public { address[] memory rateFeedIDs2 = new address[](2); - rateFeedIDs2[0] = actor("randomRateFeed"); - rateFeedIDs2[1] = actor("randomRateFeed2"); + rateFeedIDs2[0] = makeAddr("randomRateFeed"); + rateFeedIDs2[1] = makeAddr("randomRateFeed2"); vm.expectRevert("array length missmatch"); breaker.setRateChangeThresholds(rateFeedIDs2, rateChangeThresholds); } function test_setRateChangeThresholds_whenThresholdIsMoreThan0_shouldRevert() public { - rateChangeThresholds[0] = 1 * 10**24; + rateChangeThresholds[0] = 1 * 10 ** 24; vm.expectRevert("value must be less than 1"); breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); } @@ -185,17 +185,19 @@ contract ValueDeltaBreakerTest_constructorAndSetters is ValueDeltaBreakerTest { } /* ---------- Getters ---------- */ - function test_getCooldown_withDefault_shouldReturnDefaultCooldown() public { + function test_getCooldown_withDefault_shouldReturnDefaultCooldown() public view { assertEq(breaker.getCooldown(rateFeedID1), defaultCooldownTime); } - function test_getCooldown_withoutdefault_shouldReturnSpecificCooldown() public { + function test_getCooldown_withoutdefault_shouldReturnSpecificCooldown() public view { assertEq(breaker.getCooldown(rateFeedIDs[0]), cooldownTimes[0]); } } contract ValueDeltaBreakerTest_shouldTrigger is ValueDeltaBreakerTest { - function setUp() public { + using FixidityLib for FixidityLib.Fraction; + + function setUp() public override { super.setUp(); address[] memory _rateFeedIDs = new address[](3); @@ -211,8 +213,8 @@ contract ValueDeltaBreakerTest_shouldTrigger is ValueDeltaBreakerTest { } function updateMedianByPercent(uint256 medianChangeScaleFactor, address _rateFeedID) public { - uint256 previousMedianRate = 10**24; - uint256 currentMedianRate = (previousMedianRate * medianChangeScaleFactor) / 10**24; + uint256 previousMedianRate = 10 ** 24; + uint256 currentMedianRate = (previousMedianRate * medianChangeScaleFactor) / 10 ** 24; vm.mockCall( address(sortedOracles), abi.encodeWithSelector(sortedOracles.medianRate.selector), @@ -223,14 +225,14 @@ contract ValueDeltaBreakerTest_shouldTrigger is ValueDeltaBreakerTest { function test_shouldTrigger_withDefaultThreshold_shouldTrigger() public { assertEq(breaker.rateChangeThreshold(rateFeedID1), 0); - updateMedianByPercent(0.7 * 10**24, rateFeedID1); + updateMedianByPercent(0.7 * 10 ** 24, rateFeedID1); assertTrue(breaker.shouldTrigger(rateFeedID1)); } function test_shouldTrigger_whenThresholdIsLargerThanMedian_shouldNotTrigger() public { - updateMedianByPercent(0.7 * 10**24, rateFeedID1); + updateMedianByPercent(0.7 * 10 ** 24, rateFeedID1); - rateChangeThresholds[0] = 0.8 * 10**24; + rateChangeThresholds[0] = 0.8 * 10 ** 24; rateFeedIDs[0] = rateFeedID1; breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); assertEq(breaker.rateChangeThreshold(rateFeedID1), rateChangeThresholds[0]); @@ -241,14 +243,14 @@ contract ValueDeltaBreakerTest_shouldTrigger is ValueDeltaBreakerTest { function test_shouldTrigger_whithDefaultThreshold_ShouldNotTrigger() public { assertEq(breaker.rateChangeThreshold(rateFeedID3), 0); - updateMedianByPercent(1.1 * 10**24, rateFeedID3); + updateMedianByPercent(1.1 * 10 ** 24, rateFeedID3); assertFalse(breaker.shouldTrigger(rateFeedID3)); } function test_shouldTrigger_whenThresholdIsSmallerThanMedian_ShouldTrigger() public { - updateMedianByPercent(1.1 * 10**24, rateFeedID3); - rateChangeThresholds[0] = 0.01 * 10**24; + updateMedianByPercent(1.1 * 10 ** 24, rateFeedID3); + rateChangeThresholds[0] = 0.01 * 10 ** 24; rateFeedIDs[0] = rateFeedID3; breaker.setRateChangeThresholds(rateFeedIDs, rateChangeThresholds); assertEq(breaker.rateChangeThreshold(rateFeedID3), rateChangeThresholds[0]); diff --git a/test/oracles/breakers/WithCooldown.t.sol b/test/unit/oracles/breakers/WithCooldown.t.sol similarity index 61% rename from test/oracles/breakers/WithCooldown.t.sol rename to test/unit/oracles/breakers/WithCooldown.t.sol index 3e824e28..9a2d097a 100644 --- a/test/oracles/breakers/WithCooldown.t.sol +++ b/test/unit/oracles/breakers/WithCooldown.t.sol @@ -1,18 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; -import { WithCooldown } from "contracts/oracles/breakers/WithCooldown.sol"; +import { Test } from "mento-std/Test.sol"; +import { IWithCooldownHarness } from "test/utils/harnesses/IWithCooldownHarness.sol"; + +contract WithCooldownTest is Test { + event DefaultCooldownTimeUpdated(uint256 newCooldownTime); + event RateFeedCooldownTimeUpdated(address rateFeedID, uint256 newCooldownTime); + + IWithCooldownHarness harness; + + function setUp() public { + harness = IWithCooldownHarness(deployCode("WithCooldownHarness")); + } -contract WithCooldownTest is WithCooldown, Test { function test_setDefaultCooldownTime() public { uint256 testCooldown = 39 minutes; vm.expectEmit(true, true, true, true); emit DefaultCooldownTimeUpdated(testCooldown); - _setDefaultCooldownTime(testCooldown); - assertEq(defaultCooldownTime, testCooldown); + harness.setDefaultCooldownTime(testCooldown); + assertEq(harness.defaultCooldownTime(), testCooldown); } function test_setCooldownTimes_withZeroAddress_reverts() public { @@ -20,7 +29,7 @@ contract WithCooldownTest is WithCooldown, Test { uint256[] memory cooldownTimes = new uint256[](1); vm.expectRevert("rate feed invalid"); - _setCooldownTimes(rateFeedIDs, cooldownTimes); + harness.setCooldownTimes(rateFeedIDs, cooldownTimes); } function test_setCooldownTimes_withMismatchingArrays_reverts() public { @@ -28,7 +37,7 @@ contract WithCooldownTest is WithCooldown, Test { uint256[] memory cooldownTimes = new uint256[](1); vm.expectRevert("array length missmatch"); - _setCooldownTimes(rateFeedIDs, cooldownTimes); + harness.setCooldownTimes(rateFeedIDs, cooldownTimes); } function test_setCoolDownTimes_emitsEvents() public { @@ -45,18 +54,18 @@ contract WithCooldownTest is WithCooldown, Test { vm.expectEmit(true, true, true, true); emit RateFeedCooldownTimeUpdated(rateFeedIDs[1], cooldownTimes[1]); - _setCooldownTimes(rateFeedIDs, cooldownTimes); + harness.setCooldownTimes(rateFeedIDs, cooldownTimes); - assertEq(rateFeedCooldownTime[rateFeedIDs[0]], cooldownTimes[0]); - assertEq(rateFeedCooldownTime[rateFeedIDs[1]], cooldownTimes[1]); + assertEq(harness.rateFeedCooldownTime(rateFeedIDs[0]), cooldownTimes[0]); + assertEq(harness.rateFeedCooldownTime(rateFeedIDs[1]), cooldownTimes[1]); } function test_getCooldown_whenNoRateFeedSpecific_usesDefault() public { uint256 testCooldown = 39 minutes; address rateFeedID = address(1111); - _setDefaultCooldownTime(testCooldown); + harness.setDefaultCooldownTime(testCooldown); - assertEq(getCooldown(rateFeedID), testCooldown); + assertEq(harness.getCooldown(rateFeedID), testCooldown); } function test_getCooldown_whenRateFeedSpecific_usesRateSpecific() public { @@ -69,9 +78,9 @@ contract WithCooldownTest is WithCooldown, Test { uint256[] memory cooldownTimes = new uint256[](1); cooldownTimes[0] = testCooldown; - _setCooldownTimes(rateFeedIDs, cooldownTimes); - _setDefaultCooldownTime(defaultCooldown); + harness.setCooldownTimes(rateFeedIDs, cooldownTimes); + harness.setDefaultCooldownTime(defaultCooldown); - assertEq(getCooldown(rateFeedID), testCooldown); + assertEq(harness.getCooldown(rateFeedID), testCooldown); } } diff --git a/test/unit/oracles/breakers/WithThreshold.t.sol b/test/unit/oracles/breakers/WithThreshold.t.sol new file mode 100644 index 00000000..80b94fad --- /dev/null +++ b/test/unit/oracles/breakers/WithThreshold.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.8; + +import { Test } from "mento-std/Test.sol"; +import { IWithThresholdHarness } from "test/utils/harnesses/IWithThresholdHarness.sol"; + +contract WithThresholdTest is Test { + event DefaultRateChangeThresholdUpdated(uint256 defaultRateChangeThreshold); + event RateChangeThresholdUpdated(address rateFeedID, uint256 rateChangeThreshold); + + IWithThresholdHarness harness; + + function setUp() public virtual { + harness = IWithThresholdHarness(deployCode("WithThresholdHarness")); + } + + function test_setDefaultRateChangeThreshold() public { + uint256 testThreshold = 1e20; + vm.expectEmit(true, true, true, true); + emit DefaultRateChangeThresholdUpdated(testThreshold); + harness.setDefaultRateChangeThreshold(testThreshold); + assertEq(harness.defaultRateChangeThreshold(), testThreshold); + } + + function test_setRateChangeThresholds_withZeroAddress_reverts() public { + address[] memory rateFeedIDs = new address[](1); + uint256[] memory thresholds = new uint256[](1); + + vm.expectRevert("rate feed invalid"); + harness.setRateChangeThresholds(rateFeedIDs, thresholds); + } + + function test_setRateChangeThresholds_withMismatchingArrays_reverts() public { + address[] memory rateFeedIDs = new address[](2); + uint256[] memory thresholds = new uint256[](1); + + vm.expectRevert("array length missmatch"); + harness.setRateChangeThresholds(rateFeedIDs, thresholds); + } + + function test_setRateChangeThresholds_emitsEvents() public { + address[] memory rateFeedIDs = new address[](2); + uint256[] memory thresholds = new uint256[](2); + + rateFeedIDs[0] = address(1111); + rateFeedIDs[1] = address(2222); + thresholds[0] = 1e20; + thresholds[1] = 2e20; + + vm.expectEmit(true, true, true, true); + emit RateChangeThresholdUpdated(rateFeedIDs[0], thresholds[0]); + vm.expectEmit(true, true, true, true); + emit RateChangeThresholdUpdated(rateFeedIDs[1], thresholds[1]); + + harness.setRateChangeThresholds(rateFeedIDs, thresholds); + + assertEq(harness.rateChangeThreshold(rateFeedIDs[0]), thresholds[0]); + assertEq(harness.rateChangeThreshold(rateFeedIDs[1]), thresholds[1]); + } +} + +contract WithThresholdTest_exceedsThreshold is WithThresholdTest { + uint256 constant _1PC = 0.01 * 1e24; // 1% + uint256 constant _10PC = 0.1 * 1e24; // 10% + uint256 constant _20PC = 0.2 * 1e24; // 20% + + address rateFeedID0 = makeAddr("rateFeedID0-10%"); + address rateFeedID1 = makeAddr("rateFeedID2-1%"); + address rateFeedID2 = makeAddr("rateFeedID3-default-20%"); + + function setUp() public override { + super.setUp(); + uint256[] memory ts = new uint256[](2); + ts[0] = _10PC; + ts[1] = _1PC; + address[] memory rateFeedIDs = new address[](2); + rateFeedIDs[0] = rateFeedID0; + rateFeedIDs[1] = rateFeedID1; + + harness.setDefaultRateChangeThreshold(_20PC); + harness.setRateChangeThresholds(rateFeedIDs, ts); + } + + function test_exceedsThreshold_withDefault_whenWithin_isFalse() public view { + assertEq(harness.exceedsThreshold(1e24, 1.1 * 1e24, rateFeedID2), false); + assertEq(harness.exceedsThreshold(1e24, 0.9 * 1e24, rateFeedID2), false); + } + + function test_exceedsThreshold_withDefault_whenNotWithin_isTrue() public view { + assertEq(harness.exceedsThreshold(1e24, 1.3 * 1e24, rateFeedID2), true); + assertEq(harness.exceedsThreshold(1e24, 0.7 * 1e24, rateFeedID2), true); + } + + function test_exceedsThreshold_withOverride_whenWithin_isTrue() public view { + assertEq(harness.exceedsThreshold(1e24, 1.1 * 1e24, rateFeedID1), true); + assertEq(harness.exceedsThreshold(1e24, 0.9 * 1e24, rateFeedID1), true); + assertEq(harness.exceedsThreshold(1e24, 1.11 * 1e24, rateFeedID0), true); + assertEq(harness.exceedsThreshold(1e24, 0.89 * 1e24, rateFeedID0), true); + } + + function test_exceedsThreshold_withOverride_whenNotWithin_isFalse() public view { + assertEq(harness.exceedsThreshold(1e24, 1.01 * 1e24, rateFeedID1), false); + assertEq(harness.exceedsThreshold(1e24, 1.01 * 1e24, rateFeedID0), false); + } +} diff --git a/test/swap/BiPoolManager.t.sol b/test/unit/swap/BiPoolManager.t.sol similarity index 87% rename from test/swap/BiPoolManager.t.sol rename to test/unit/swap/BiPoolManager.t.sol index 7bbe33ee..45f24def 100644 --- a/test/swap/BiPoolManager.t.sol +++ b/test/unit/swap/BiPoolManager.t.sol @@ -1,27 +1,24 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; +import { Test } from "mento-std/Test.sol"; +import { bytes32s, addresses } from "mento-std/Array.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { MockReserve } from "../mocks/MockReserve.sol"; -import { MockBreakerBox } from "../mocks/MockBreakerBox.sol"; -import { MockERC20 } from "../mocks/MockERC20.sol"; -import { MockPricingModule } from "../mocks/MockPricingModule.sol"; -import { MockSortedOracles } from "../mocks/MockSortedOracles.sol"; +import { MockReserve } from "test/utils//mocks/MockReserve.sol"; +import { MockBreakerBox } from "test/utils/mocks/MockBreakerBox.sol"; +import { MockERC20 } from "test/utils/mocks/MockERC20.sol"; +import { MockPricingModule } from "test/utils/mocks/MockPricingModule.sol"; +import { MockSortedOracles } from "test/utils/mocks/MockSortedOracles.sol"; -import { Arrays } from "../utils/Arrays.sol"; - -import { FixidityLib } from "contracts/common/FixidityLib.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; -import { BiPoolManager } from "contracts/swap/BiPoolManager.sol"; // forge test --match-contract BiPoolManager -vvv contract BiPoolManagerTest is Test { @@ -61,26 +58,23 @@ contract BiPoolManagerTest is Test { IPricingModule constantProduct; IPricingModule constantSum; - MockSortedOracles sortedOracles; + MockSortedOracles sortedOracles; MockReserve reserve; - BiPoolManager biPoolManager; MockBreakerBox breaker; - function newMockERC20( - string memory name, - string memory symbol, - uint256 decimals - ) internal returns (MockERC20 token) { + IBiPoolManager biPoolManager; + + function newMockERC20(string memory name, string memory symbol, uint256 decimals) internal returns (MockERC20 token) { token = new MockERC20(name, symbol, decimals); vm.label(address(token), symbol); } - function setUp() public { - vm.warp(60 * 60 * 24 * 30); // if we start at now == 0 we get some underflows - deployer = actor("deployer"); - notDeployer = actor("notDeployer"); - broker = actor("broker"); + function setUp() public virtual { + vm.warp(60 * 60 * 24 * 30); // if we start at block.timestamp == 0 we get some underflows + deployer = makeAddr("deployer"); + notDeployer = makeAddr("notDeployer"); + broker = makeAddr("broker"); cUSD = newMockERC20("Celo Dollar", "cUSD", 18); cEUR = newMockERC20("Celo Euro", "cEUR", 18); @@ -92,7 +86,7 @@ contract BiPoolManagerTest is Test { sortedOracles = new MockSortedOracles(); reserve = new MockReserve(); - biPoolManager = new BiPoolManager(true); + biPoolManager = IBiPoolManager(deployCode("BiPoolManager", abi.encode(true))); breaker = new MockBreakerBox(); vm.mockCall( @@ -128,12 +122,12 @@ contract BiPoolManagerTest is Test { IBreakerBox(address(breaker)) ); - bytes32[] memory pricingModuleIdentifiers = Arrays.bytes32s( + bytes32[] memory pricingModuleIdentifiers = bytes32s( keccak256(abi.encodePacked(constantProduct.name())), keccak256(abi.encodePacked(constantSum.name())) ); - address[] memory pricingModules = Arrays.addresses(address(constantProduct), address(constantSum)); + address[] memory pricingModules = addresses(address(constantProduct), address(constantSum)); biPoolManager.setPricingModules(pricingModuleIdentifiers, pricingModules); } @@ -200,12 +194,12 @@ contract BiPoolManagerTest is Test { uint256 stablePoolResetSize, uint256 minimumReports ) internal returns (bytes32 exchangeId) { - BiPoolManager.PoolExchange memory exchange; + IBiPoolManager.PoolExchange memory exchange; exchange.asset0 = address(asset0); exchange.asset1 = address(asset1); exchange.pricingModule = pricingModule; - BiPoolManager.PoolConfig memory config; + IBiPoolManager.PoolConfig memory config; config.referenceRateFeedID = referenceRateFeedID; config.stablePoolResetSize = stablePoolResetSize; config.referenceRateResetFrequency = 60 * 5; // 5 minutes @@ -229,23 +223,23 @@ contract BiPoolManagerTest is Test { contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { /* ---------- Initilizer ---------- */ - function test_initilize_shouldSetOwner() public { + function test_initilize_shouldSetOwner() public view { assertEq(biPoolManager.owner(), deployer); } - function test_initilize_shouldSetBroker() public { + function test_initilize_shouldSetBroker() public view { assertEq(biPoolManager.broker(), broker); } - function test_initilize_shouldSetReserve() public { + function test_initilize_shouldSetReserve() public view { assertEq(address(biPoolManager.reserve()), address(reserve)); } - function test_initilize_shouldSetSortedOracles() public { + function test_initilize_shouldSetSortedOracles() public view { assertEq(address(biPoolManager.sortedOracles()), address(sortedOracles)); } - function test_initialize_shouldSetBreakerBox() public { + function test_initialize_shouldSetBreakerBox() public view { assertEq(address(biPoolManager.breakerBox()), address(breaker)); } @@ -263,7 +257,7 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { } function test_setBroker_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newBroker = actor("newBroker"); + address newBroker = makeAddr("newBroker"); vm.expectEmit(true, true, true, true); emit BrokerUpdated(newBroker); @@ -284,7 +278,7 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { } function test_setReserve_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newReserve = actor("newReserve"); + address newReserve = makeAddr("newReserve"); vm.expectEmit(true, true, true, true); emit ReserveUpdated(newReserve); @@ -305,7 +299,7 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { } function test_setSortedOracles_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newSortedOracles = actor("newSortedOracles"); + address newSortedOracles = makeAddr("newSortedOracles"); vm.expectEmit(true, true, true, true); emit SortedOraclesUpdated(newSortedOracles); @@ -326,7 +320,7 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { } function test_setBreakerBox_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newBreakerBox = actor("newBreakerBox"); + address newBreakerBox = makeAddr("newBreakerBox"); vm.expectEmit(true, true, true, true); emit BreakerBoxUpdated(newBreakerBox); @@ -338,23 +332,23 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { function test_setPricingModules_whenCallerIsNotOwner_shouldRevert() public { changePrank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); - biPoolManager.setPricingModules(Arrays.bytes32s(""), Arrays.addresses(address(0))); + biPoolManager.setPricingModules(bytes32s(""), addresses(address(0))); } function test_setPricingModules_whenArrayLengthMismatch_shouldRevert() public { vm.expectRevert("identifiers and modules must be the same length"); - biPoolManager.setPricingModules(Arrays.bytes32s(""), Arrays.addresses(address(0), address(0xf))); + biPoolManager.setPricingModules(bytes32s(""), addresses(address(0), address(0xf))); } function test_setPricingModules_whenCallerIsOwner_shouldUpdateAndEmit() public { - bytes32[] memory newIdentifiers = Arrays.bytes32s( + bytes32[] memory newIdentifiers = bytes32s( keccak256(abi.encodePacked("TestModuleIdentifier1")), keccak256(abi.encodePacked("TestModuleIdentifier2")) ); - address[] memory newPricingModules = Arrays.addresses( - actor("TestModuleIdentifier1"), - actor("TestModuleIdentifier2") + address[] memory newPricingModules = addresses( + makeAddr("TestModuleIdentifier1"), + makeAddr("TestModuleIdentifier2") ); vm.expectEmit(true, true, true, true); @@ -375,7 +369,7 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { function test_getPoolExchange_whenPoolExists_shouldReturnPool() public { mockOracleRate(address(cUSD), 1e24); bytes32 exchangeId = createExchange(cUSD, bridgedUSDC); - BiPoolManager.PoolExchange memory existingExchange = biPoolManager.getPoolExchange(exchangeId); + IBiPoolManager.PoolExchange memory existingExchange = biPoolManager.getPoolExchange(exchangeId); assertEq(existingExchange.asset0, address(cUSD)); assertEq(existingExchange.asset1, address(bridgedUSDC)); } @@ -383,7 +377,7 @@ contract BiPoolManagerTest_initilizerSettersGetters is BiPoolManagerTest { contract BiPoolManagerTest_createExchange is BiPoolManagerTest { function test_createExchange_whenNotCalledByOwner_shouldRevert() public { - BiPoolManager.PoolExchange memory newexchange; + IBiPoolManager.PoolExchange memory newexchange; changePrank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); @@ -453,12 +447,12 @@ contract BiPoolManagerTest_createExchange is BiPoolManagerTest { } function test_createExchange_whenPricingModuleIsOutdated_shouldRevert() public { - bytes32[] memory newIdentifiers = Arrays.bytes32s( + bytes32[] memory newIdentifiers = bytes32s( keccak256(abi.encodePacked(constantProduct.name())), keccak256(abi.encodePacked(constantSum.name())) ); - address[] memory newPricingModules = Arrays.addresses(actor("ConstantProduct 2.0"), address(constantSum)); + address[] memory newPricingModules = addresses(makeAddr("ConstantProduct 2.0"), address(constantSum)); biPoolManager.setPricingModules(newIdentifiers, newPricingModules); @@ -497,7 +491,7 @@ contract BiPoolManagerTest_createExchange is BiPoolManagerTest { 1e24 // stablePoolResetSize ); - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId); assertEq(exchange.bucket0, 1e24); assertEq(exchange.bucket1, 5e23); // exchange.bucket0 / 2 } @@ -557,7 +551,7 @@ contract BiPoolManagerTest_withExchange is BiPoolManagerTest { bytes32 exchangeId_cUSD_bridgedUSDC; bytes32 exchangeId_cEUR_bridgedUSDC; - function setUp() public { + function setUp() public virtual override { super.setUp(); mockOracleRate(address(cUSD), 2e24); @@ -715,7 +709,7 @@ contract BiPoolManagerTest_quote is BiPoolManagerTest_withExchange { } contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { - function setUp() public { + function setUp() public override { super.setUp(); changePrank(broker); } @@ -754,7 +748,7 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { } function test_swapIn_whenTokenInIsAsset0_itDelegatesToThePricingModule() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); uint256 amountIn = 1e24; uint256 mockAmountOut = 0.5 * 1e24; @@ -762,14 +756,14 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { mockGetAmountOut(constantProduct, mockAmountOut); uint256 amountOut = biPoolManager.swapIn(exchangeId_cUSD_CELO, address(cUSD), address(CELO), amountIn); - BiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); assertEq(amountOut, mockAmountOut); assertEq(exchangeAfter.bucket0, exchange.bucket0 + amountIn); assertEq(exchangeAfter.bucket1, exchange.bucket1 - amountOut); } function test_swapIn_whenTokenInIsAsset1_itDelegatesToThePricingModule() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); uint256 amountIn = 1e24; uint256 mockAmountOut = 0.5 * 1e24; @@ -777,25 +771,25 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { mockGetAmountOut(constantProduct, mockAmountOut); uint256 amountOut = biPoolManager.swapIn(exchangeId_cUSD_CELO, address(CELO), address(cUSD), amountIn); - BiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); assertEq(amountOut, mockAmountOut); assertEq(exchangeAfter.bucket0, exchange.bucket0 - amountOut); assertEq(exchangeAfter.bucket1, exchange.bucket1 + amountIn); } function test_swapIn_whenCSAndValidMedian_shouldNotUpdateBuckets() public { - BiPoolManager.PoolExchange memory exchangeBefore = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); + IBiPoolManager.PoolExchange memory exchangeBefore = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); biPoolManager.swapIn(exchangeId_cUSD_bridgedUSDC, address(bridgedUSDC), address(cUSD), 1e6); - BiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); + IBiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); assertEq(exchangeBefore.bucket0, exchangeAfter.bucket0); assertEq(exchangeBefore.bucket1, exchangeAfter.bucket1); } function test_swapIn_whenTokenHasNonstandardPrecision_shouldUpdateBucketsCorrectly() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cEUR_bridgedUSDC); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cEUR_bridgedUSDC); assertEq(exchange.bucket0, 1e26); // stablePoolResetSize = 1e26 assertEq(exchange.bucket1, 0.5 * 1e26); // mock orackle rate = 2e24 @@ -849,7 +843,7 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { } function test_swapOut_whenTokenInIsAsset0_itDelegatesToThePricingModule() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); uint256 amountOut = 1e24; uint256 mockAmountIn = 0.5 * 1e24; @@ -857,14 +851,14 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { mockGetAmountIn(constantProduct, mockAmountIn); uint256 amountIn = biPoolManager.swapOut(exchangeId_cUSD_CELO, address(cUSD), address(CELO), amountOut); - BiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); assertEq(amountIn, mockAmountIn); assertEq(exchangeAfter.bucket0, exchange.bucket0 + amountIn); assertEq(exchangeAfter.bucket1, exchange.bucket1 - amountOut); } function test_swapOut_whenTokenInIsAsset1_itDelegatesToThePricingModule() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); uint256 amountOut = 1e24; uint256 mockAmountIn = 0.6 * 1e24; @@ -872,25 +866,25 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { mockGetAmountIn(constantProduct, mockAmountIn); uint256 amountIn = biPoolManager.swapOut(exchangeId_cUSD_CELO, address(CELO), address(cUSD), amountOut); - BiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); assertEq(amountIn, mockAmountIn); assertEq(exchangeAfter.bucket0, exchange.bucket0 - amountOut); assertEq(exchangeAfter.bucket1, exchange.bucket1 + amountIn); } function test_swapOut_whenCSValidMedian_shouldNotUpdateBuckets() public { - BiPoolManager.PoolExchange memory exchangeBefore = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); + IBiPoolManager.PoolExchange memory exchangeBefore = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); biPoolManager.swapOut(exchangeId_cUSD_bridgedUSDC, address(bridgedUSDC), address(cUSD), 1e18); - BiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); + IBiPoolManager.PoolExchange memory exchangeAfter = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); assertEq(exchangeBefore.bucket0, exchangeAfter.bucket0); assertEq(exchangeBefore.bucket1, exchangeAfter.bucket1); } function test_swapOut_whenTokenHasNonstandardPrecision_shouldUpdateBucketsCorrectly() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cEUR_bridgedUSDC); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cEUR_bridgedUSDC); assertEq(exchange.bucket0, 1e26); // stablePoolResetSize = 1e26 assertEq(exchange.bucket1, 0.5 * 1e26); // mock orackle rate = 2e24 @@ -912,28 +906,24 @@ contract BiPoolManagerTest_swap is BiPoolManagerTest_withExchange { } contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { - function setUp() public { + function setUp() public override { super.setUp(); changePrank(broker); } - function swap( - bytes32 exchangeId_cUSD_CELO, - uint256 amountIn, - uint256 amountOut - ) internal { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + function swap(bytes32 exchangeId_cUSD_CELO, uint256 amountIn, uint256 amountOut) internal { + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); mockGetAmountOut(constantProduct, amountOut); biPoolManager.swapIn(exchangeId_cUSD_CELO, exchange.asset0, exchange.asset1, amountIn); } function test_swapIn_whenBucketsAreStale_updatesBuckets() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); swap(exchangeId_cUSD_CELO, exchange.bucket0 / 2, exchange.bucket1 / 2); // debalance exchange vm.warp(exchange.config.referenceRateResetFrequency + 1); sortedOracles.setNumRates(address(cUSD), 10); - sortedOracles.setMedianTimestamp(address(cUSD), now); + sortedOracles.setMedianTimestamp(address(cUSD), block.timestamp); vm.expectEmit(true, true, true, true); uint256 stablePoolResetSize = exchange.config.stablePoolResetSize; @@ -954,7 +944,7 @@ contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { } function test_swapIn_whenBucketsAreNotStale_doesNotUpdateBuckets() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); swap(exchangeId_cUSD_CELO, exchange.bucket0 / 2, exchange.bucket1 / 2); // debalance exchange exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); // Refresh exchange uint256 bucket0BeforeSwap = exchange.bucket0; @@ -977,7 +967,7 @@ contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { } function test_swapIn_whenBucketsAreStale_butMinReportsNotMet_doesNotUpdateBuckets() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); swap(exchangeId_cUSD_CELO, exchange.bucket0 / 2, exchange.bucket1 / 2); // debalance exchange exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); // Refresh exchange uint256 bucket0BeforeSwap = exchange.bucket0; @@ -997,7 +987,7 @@ contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { } function test_swapIn_whenBucketsAreStale_butReportIsExpired_doesNotUpdateBuckets() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); swap(exchangeId_cUSD_CELO, exchange.bucket0 / 2, exchange.bucket1 / 2); // debalance exchange exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); // Refresh exchange uint256 bucket0BeforeSwap = exchange.bucket0; @@ -1018,7 +1008,7 @@ contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { } function test_swapIn_whenBucketsAreStale_butMedianTimestampIsOld_doesNotUpdateBuckets() public { - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); swap(exchangeId_cUSD_CELO, exchange.bucket0 / 2, exchange.bucket1 / 2); // debalance exchange exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_CELO); // Refresh exchange uint256 bucket0BeforeSwap = exchange.bucket0; @@ -1026,7 +1016,7 @@ contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { vm.warp(exchange.config.referenceRateResetFrequency + 1); sortedOracles.setNumRates(address(cUSD), 10); - sortedOracles.setMedianTimestamp(address(cUSD), now - exchange.config.referenceRateResetFrequency); + sortedOracles.setMedianTimestamp(address(cUSD), block.timestamp - exchange.config.referenceRateResetFrequency); uint256 amountIn = 1e24; uint256 amountOut = biPoolManager.swapIn(exchangeId_cUSD_CELO, exchange.asset0, exchange.asset1, 1e24); @@ -1040,9 +1030,8 @@ contract BiPoolManagerTest_bucketUpdates is BiPoolManagerTest_withExchange { contract BiPoolManagerTest_ConstantSum is BiPoolManagerTest_withExchange { address EURUSDC_rateFeedID; - bytes32 exchangeId_cEUR_bridgedUSDC; - function setUp() public { + function setUp() public override { super.setUp(); EURUSDC_rateFeedID = address(uint160(uint256(keccak256(abi.encodePacked("EURUSDC"))))); mockOracleRate(EURUSDC_rateFeedID, 1e24); @@ -1060,8 +1049,8 @@ contract BiPoolManagerTest_ConstantSum is BiPoolManagerTest_withExchange { function test_quotesAndSwaps_whenMedianNotRecent_shouldRevert() public { address USDUSDC_rateFeedID = address(uint160(uint256(keccak256(abi.encodePacked("USDUSDC"))))); - BiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); - sortedOracles.setMedianTimestamp(USDUSDC_rateFeedID, now - exchange.config.referenceRateResetFrequency); + IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(exchangeId_cUSD_bridgedUSDC); + sortedOracles.setMedianTimestamp(USDUSDC_rateFeedID, block.timestamp - exchange.config.referenceRateResetFrequency); vm.expectRevert("no valid median"); biPoolManager.getAmountOut(exchangeId_cUSD_bridgedUSDC, address(cUSD), address(bridgedUSDC), 1e18); diff --git a/test/swap/Broker.t.sol b/test/unit/swap/Broker.t.sol similarity index 86% rename from test/swap/Broker.t.sol rename to test/unit/swap/Broker.t.sol index b19050f2..06ef2d80 100644 --- a/test/swap/Broker.t.sol +++ b/test/unit/swap/Broker.t.sol @@ -1,23 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { Test } from "celo-foundry/Test.sol"; +import { Test } from "mento-std/Test.sol"; -import { MockStableToken } from "../mocks/MockStableToken.sol"; -import { MockExchangeProvider } from "../mocks/MockExchangeProvider.sol"; -import { MockReserve } from "../mocks/MockReserve.sol"; -import { DummyERC20 } from "../utils/DummyErc20.sol"; +import { MockExchangeProvider } from "test/utils/mocks/MockExchangeProvider.sol"; +import { MockReserve } from "test/utils/mocks/MockReserve.sol"; +import { TestERC20 } from "test/utils/mocks/TestERC20.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; -import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; -import { Broker } from "contracts/swap/Broker.sol"; +import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; +import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; // forge test --match-contract Broker -vvv contract BrokerTest is Test { @@ -35,79 +31,75 @@ contract BrokerTest is Test { event ExchangeProviderAdded(address indexed exchangeProvider); event ExchangeProviderRemoved(address indexed exchangeProvider); event ReserveSet(address indexed newAddress, address indexed prevAddress); - event TradingLimitConfigured(bytes32 exchangeId, address token, TradingLimits.Config config); + event TradingLimitConfigured(bytes32 exchangeId, address token, ITradingLimits.Config config); - address deployer = actor("deployer"); - address notDeployer = actor("notDeployer"); - address trader = actor("trader"); - address randomExchangeProvider = actor("randomExchangeProvider"); - address randomAsset = actor("randomAsset"); + address deployer = makeAddr("deployer"); + address notDeployer = makeAddr("notDeployer"); + address trader = makeAddr("trader"); + address randomExchangeProvider = makeAddr("randomExchangeProvider"); + address randomAsset = makeAddr("randomAsset"); MockReserve reserve; - MockStableToken stableAsset; - DummyERC20 collateralAsset; + TestERC20 stableAsset; + TestERC20 collateralAsset; - Broker broker; + IBroker broker; MockExchangeProvider exchangeProvider; - address exchangeProvider1 = actor("exchangeProvider1"); - address exchangeProvider2 = actor("exchangeProvider2"); + address exchangeProvider1 = makeAddr("exchangeProvider1"); + address exchangeProvider2 = makeAddr("exchangeProvider2"); address[] public exchangeProviders; - function setUp() public { - /* Dependencies and actors */ + function setUp() public virtual { + /* Dependencies and makeAddrs */ reserve = new MockReserve(); - collateralAsset = new DummyERC20("Collateral", "CL", 18); - stableAsset = new MockStableToken(); - randomAsset = actor("randomAsset"); - broker = new Broker(true); + collateralAsset = new TestERC20("Collateral", "CL"); + stableAsset = new TestERC20("StableAsset", "SA0"); + randomAsset = makeAddr("randomAsset"); + broker = IBroker(deployCode("Broker", abi.encode(true))); exchangeProvider = new MockExchangeProvider(); reserve.addToken(address(stableAsset)); reserve.addCollateralAsset(address(collateralAsset)); - vm.startPrank(deployer); exchangeProviders.push(exchangeProvider1); exchangeProviders.push(exchangeProvider2); exchangeProviders.push((address(exchangeProvider))); broker.initialize(exchangeProviders, address(reserve)); - changePrank(trader); } } contract BrokerTest_initilizerAndSetters is BrokerTest { /* ---------- Initilizer ---------- */ - function test_initilize_shouldSetOwner() public { - assertEq(broker.owner(), deployer); + function test_initilize_shouldSetOwner() public view { + assertEq(broker.owner(), address(this)); } - function test_initilize_shouldSetExchangeProviderAddresseses() public { + function test_initilize_shouldSetExchangeProviderAddresseses() public view { assertEq(broker.getExchangeProviders(), exchangeProviders); } - function test_initilize_shouldSetReserve() public { + function test_initilize_shouldSetReserve() public view { assertEq(address(broker.reserve()), address(reserve)); } /* ---------- Setters ---------- */ function test_addExchangeProvider_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); + vm.prank(notDeployer); broker.addExchangeProvider(address(0)); } function test_addExchangeProvider_whenAddressIsZero_shouldRevert() public { - changePrank(deployer); vm.expectRevert("ExchangeProvider address can't be 0"); broker.addExchangeProvider(address(0)); } function test_addExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { - changePrank(deployer); - address newExchangeProvider = actor("newExchangeProvider"); + address newExchangeProvider = makeAddr("newExchangeProvider"); vm.expectEmit(true, false, false, false); emit ExchangeProviderAdded(newExchangeProvider); broker.addExchangeProvider(newExchangeProvider); @@ -117,13 +109,11 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { } function test_addExchangeProvider_whenAlreadyAdded_shouldRevert() public { - changePrank(deployer); vm.expectRevert("ExchangeProvider already exists in the list"); broker.addExchangeProvider(address(exchangeProvider)); } function test_removeExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { - changePrank(deployer); vm.expectEmit(true, true, true, true); emit ExchangeProviderRemoved(exchangeProvider1); broker.removeExchangeProvider(exchangeProvider1, 0); @@ -131,38 +121,34 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { } function test_removeExchangeProvider_whenAddressDoesNotExist_shouldRevert() public { - changePrank(deployer); vm.expectRevert("index doesn't match provider"); broker.removeExchangeProvider(notDeployer, 1); } function test_removeExchangeProvider_whenIndexOutOfRange_shouldRevert() public { - changePrank(deployer); vm.expectRevert("index doesn't match provider"); broker.removeExchangeProvider(exchangeProvider1, 1); } function test_removeExchangeProvider_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); broker.removeExchangeProvider(exchangeProvider1, 0); } function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); broker.setReserve(address(0)); } function test_setReserve_whenAddressIsZero_shouldRevert() public { - changePrank(deployer); vm.expectRevert("Reserve address must be set"); broker.setReserve(address(0)); } function test_setReserve_whenSenderIsOwner_shouldUpdateAndEmit() public { - changePrank(deployer); - address newReserve = actor("newReserve"); + address newReserve = makeAddr("newReserve"); vm.expectEmit(true, false, false, false); emit ReserveSet(newReserve, address(reserve)); @@ -172,9 +158,10 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { } contract BrokerTest_getAmounts is BrokerTest { + using FixidityLib for FixidityLib.Fraction; bytes32 exchangeId = keccak256(abi.encode("exhcangeId")); - function setUp() public { + function setUp() public override { super.setUp(); exchangeProvider.setRate( exchangeId, @@ -189,7 +176,7 @@ contract BrokerTest_getAmounts is BrokerTest { broker.getAmountIn(randomExchangeProvider, exchangeId, address(stableAsset), address(collateralAsset), 1e24); } - function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public { + function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public view { uint256 amountIn = broker.getAmountIn( address(exchangeProvider), exchangeId, @@ -233,15 +220,15 @@ contract BrokerTest_BurnStableTokens is BrokerTest { uint256 burnAmount = 1; function test_burnStableTokens_whenTokenIsNotReserveStable_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Token must be a reserve stable asset"); broker.burnStableTokens(randomAsset, 2); } function test_burnStableTokens_whenTokenIsAReserveStable_shouldBurnAndEmit() public { stableAsset.mint(notDeployer, 2); - - changePrank(notDeployer); + vm.prank(notDeployer); + stableAsset.approve(address(broker), 2); vm.expectCall( address(IStableTokenV2(address(stableAsset))), @@ -258,6 +245,7 @@ contract BrokerTest_BurnStableTokens is BrokerTest { abi.encodeWithSelector(IStableTokenV2(address(stableAsset)).burn.selector, burnAmount) ); + vm.prank(notDeployer); bool result = broker.burnStableTokens(address(stableAsset), 1); assertEq(result, true); @@ -267,6 +255,7 @@ contract BrokerTest_BurnStableTokens is BrokerTest { } contract BrokerTest_swap is BrokerTest { + using FixidityLib for FixidityLib.Fraction; struct AccountBalanceSnapshot { uint256 stable; uint256 collateral; @@ -280,7 +269,7 @@ contract BrokerTest_swap is BrokerTest { bytes32 exchangeId = keccak256(abi.encode("exhcangeId")); - function setUp() public { + function setUp() public override { super.setUp(); exchangeProvider.setRate( exchangeId, @@ -314,7 +303,6 @@ contract BrokerTest_swap is BrokerTest { } function test_swapIn_whenTokenInStableAsset_shouldUpdateAndEmit() public { - changePrank(trader); uint256 amountIn = 1e16; uint256 expectedAmountOut = exchangeProvider.getAmountOut( exchangeId, @@ -323,6 +311,9 @@ contract BrokerTest_swap is BrokerTest { amountIn ); + vm.prank(trader); + stableAsset.approve(address(broker), amountIn); + BalanceSnapshot memory balBefore = makeBalanceSnapshot(); vm.expectEmit(true, true, true, true); @@ -335,6 +326,7 @@ contract BrokerTest_swap is BrokerTest { amountIn, expectedAmountOut ); + vm.prank(trader); uint256 amountOut = broker.swapIn( address(exchangeProvider), exchangeId, @@ -355,7 +347,6 @@ contract BrokerTest_swap is BrokerTest { } function test_swapIn_whenTokenInCollateralAsset_shouldUpdateAndEmit() public { - changePrank(trader); uint256 amountIn = 1e16; uint256 expectedAmountOut = exchangeProvider.getAmountOut( exchangeId, @@ -364,6 +355,7 @@ contract BrokerTest_swap is BrokerTest { amountIn ); + vm.prank(trader); collateralAsset.approve(address(broker), amountIn); BalanceSnapshot memory balBefore = makeBalanceSnapshot(); @@ -377,6 +369,7 @@ contract BrokerTest_swap is BrokerTest { amountIn, expectedAmountOut ); + vm.prank(trader); uint256 amountOut = broker.swapIn( address(exchangeProvider), exchangeId, @@ -397,7 +390,6 @@ contract BrokerTest_swap is BrokerTest { } function test_swapOut_whenTokenInStableAsset_shoulUpdateAndEmit() public { - changePrank(trader); uint256 amountOut = 1e16; uint256 expectedAmountIn = exchangeProvider.getAmountIn( exchangeId, @@ -406,7 +398,10 @@ contract BrokerTest_swap is BrokerTest { amountOut ); + vm.prank(trader); + stableAsset.approve(address(broker), expectedAmountIn); BalanceSnapshot memory balBefore = makeBalanceSnapshot(); + vm.expectEmit(true, true, true, true); emit Swap( address(exchangeProvider), @@ -417,7 +412,7 @@ contract BrokerTest_swap is BrokerTest { expectedAmountIn, amountOut ); - + vm.prank(trader); uint256 amountIn = broker.swapOut( address(exchangeProvider), exchangeId, @@ -438,7 +433,6 @@ contract BrokerTest_swap is BrokerTest { } function test_swapOut_whenTokenInCollateralAsset_shouldUpdateAndEmit() public { - changePrank(trader); uint256 amountOut = 1e16; uint256 expectedAmountIn = exchangeProvider.getAmountIn( exchangeId, @@ -447,6 +441,7 @@ contract BrokerTest_swap is BrokerTest { amountOut ); + vm.prank(trader); collateralAsset.approve(address(broker), expectedAmountIn); BalanceSnapshot memory balBefore = makeBalanceSnapshot(); @@ -460,6 +455,7 @@ contract BrokerTest_swap is BrokerTest { expectedAmountIn, amountOut ); + vm.prank(trader); uint256 amountIn = broker.swapOut( address(exchangeProvider), exchangeId, @@ -485,32 +481,33 @@ contract BrokerTest_swap is BrokerTest { } function test_swapIn_whenTradingLimitWasNotMet_shouldSwap() public { - TradingLimits.Config memory config; + ITradingLimits.Config memory config; config.flags = 1; config.timestep0 = 10000; config.limit0 = 1000; - changePrank(deployer); + vm.expectEmit(true, true, true, true); emit TradingLimitConfigured(exchangeId, address(stableAsset), config); broker.configureTradingLimit(exchangeId, address(stableAsset), config); - changePrank(trader); + vm.prank(trader); collateralAsset.approve(address(broker), 1e21); + vm.prank(trader); broker.swapIn(address(exchangeProvider), exchangeId, address(collateralAsset), address(stableAsset), 1e20, 1e16); } function test_swapIn_whenTradingLimitWasMet_shouldNotSwap() public { - TradingLimits.Config memory config; + ITradingLimits.Config memory config; config.flags = 1; config.timestep0 = 10000; config.limit0 = 100; - changePrank(deployer); + vm.expectEmit(true, true, true, true); emit TradingLimitConfigured(exchangeId, address(stableAsset), config); broker.configureTradingLimit(exchangeId, address(stableAsset), config); - changePrank(trader); vm.expectRevert(bytes("L0 Exceeded")); + vm.prank(trader); broker.swapIn(address(exchangeProvider), exchangeId, address(stableAsset), address(collateralAsset), 5e20, 0); } } diff --git a/test/unit/swap/ConstantProductPricingModule.t.sol b/test/unit/swap/ConstantProductPricingModule.t.sol new file mode 100644 index 00000000..6780a10c --- /dev/null +++ b/test/unit/swap/ConstantProductPricingModule.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase +pragma solidity ^0.8; + +import { Test } from "mento-std/Test.sol"; + +import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; + +contract ConstantProductPricingModuleTest is Test { + IPricingModule constantProduct; + + uint256 pc1 = 1 * 1e22; + uint256 pc5 = 5 * 1e22; + uint256 pc10 = 1e23; + + function setUp() public { + vm.warp(24 * 60 * 60); + } + + // TODO: Add tests that don't rely on the legacy exchange. + + // function test_getAmountOut_compareWithLegacyExchange_t1() public { + // uint256 expectedAmountOut = legacyExchange.getAmountOut(1e24, 2e24, pc1, 1e23); + // uint256 newAmountOut = constantProduct.getAmountOut(1e24, 2e24, pc1, 1e23); + + // assertEq(newAmountOut, expectedAmountOut); + // } + + // function test_getAmountOut_compareWithLegacyExchange_t2() public { + // uint256 expectedAmountOut = legacyExchange.getAmountOut(11e24, 23e24, pc5, 3e23); + // uint256 newAmountOut = constantProduct.getAmountOut(11e24, 23e24, pc5, 3e23); + + // assertEq(newAmountOut, expectedAmountOut); + // } + + // function test_getAmountIn_compareWithLegacyExchange_t1() public { + // uint256 expectedAmountIn = legacyExchange.getAmountIn(1e24, 2e24, pc1, 1e23); + // uint256 newAmountIn = constantProduct.getAmountIn(1e24, 2e24, pc1, 1e23); + + // assertEq(newAmountIn, expectedAmountIn); + // } + + // function test_getAmountIn_compareWithLegacyExchange_t2() public { + // uint256 expectedAmountIn = legacyExchange.getAmountIn(11e24, 23e24, pc5, 3e23); + // uint256 newAmountIn = constantProduct.getAmountIn(11e24, 23e24, pc5, 3e23); + + // assertEq(newAmountIn, expectedAmountIn); + // } +} diff --git a/test/swap/ConstantSumPricingModule.t.sol b/test/unit/swap/ConstantSumPricingModule.t.sol similarity index 86% rename from test/swap/ConstantSumPricingModule.t.sol rename to test/unit/swap/ConstantSumPricingModule.t.sol index d3655fbc..0f8ce20a 100644 --- a/test/swap/ConstantSumPricingModule.t.sol +++ b/test/unit/swap/ConstantSumPricingModule.t.sol @@ -1,28 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; +pragma solidity ^0.8; -import { Test, console2 as console } from "celo-foundry/Test.sol"; +import { Test } from "mento-std/Test.sol"; import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { ConstantSumPricingModule } from "contracts/swap/ConstantSumPricingModule.sol"; contract ConstantSumPricingModuleTest is Test { IPricingModule constantSum; function setUp() public { - constantSum = new ConstantSumPricingModule(); + constantSum = IPricingModule(deployCode("ConstantSumPricingModule")); } /* ---------- getAmountOut ---------- */ - function test_getAmountOut_whenAmountInZero_shouldReturnZero() public { + function test_getAmountOut_whenAmountInZero_shouldReturnZero() public view { assertEq(constantSum.getAmountOut(10e24, 10e24, 1e23, 0), 0); } - function test_getAmountOut_whenSpreadZero_shouldReturnAmounInInTokenOutValue() public { + function test_getAmountOut_whenSpreadZero_shouldReturnAmounInInTokenOutValue() public view { uint256 tokenInBucketSize = 10e24; uint256 tokenOutBucketSize = 20e24; uint256 spread = 0; @@ -31,7 +29,7 @@ contract ConstantSumPricingModuleTest is Test { assertEq(amountOut, amountIn * 2); } - function test_getAmountOut_whenSmallAmounts_shouldReturnCorrectCalculation() public { + function test_getAmountOut_whenSmallAmounts_shouldReturnCorrectCalculation() public view { uint256 amountOut = constantSum.getAmountOut(20e24, 10e24, 0, 1); // ≈ 0.5 Wei rounded down assertEq(amountOut, 0); @@ -45,18 +43,18 @@ contract ConstantSumPricingModuleTest is Test { //Testing concrete Case //amountOut = (1 - spread) * amountIn * tokenOutBucketSize) / tokenInBucketSize // = (1-0.1) * 10e18 * 10e24 / 20e24 = 0.9 * 10e18 * 1/2 = 4500000000000000000 Wei - function test_getAmountOut_whenValidInput_shouldReturnCorrectCalculation() public { + function test_getAmountOut_whenValidInput_shouldReturnCorrectCalculation() public view { uint256 amountOut = constantSum.getAmountOut(20e24, 10e24, 1e23, 10e18); assertEq(amountOut, 4500000000000000000); } /* ---------- getAmountIn ---------- */ - function test_getAmountIn_whenAmountOutZero_shouldReturnZero() public { + function test_getAmountIn_whenAmountOutZero_shouldReturnZero() public view { assertEq(constantSum.getAmountIn(10e24, 10e24, 1e23, 0), 0); } - function test_getAmountIn_whenSpreadIsZero_shouldReturnAmountOutInTokenInValue() public { + function test_getAmountIn_whenSpreadIsZero_shouldReturnAmountOutInTokenInValue() public view { uint256 tokenInBucketSize = 10e24; uint256 tokenOutBucketSize = 20e24; uint256 spread = 0; @@ -65,7 +63,7 @@ contract ConstantSumPricingModuleTest is Test { assertEq(amountIn, amountOut / 2); } - function test_getAmountIn_whenSmallAmounts_shouldReturnCorrectCalculation() public { + function test_getAmountIn_whenSmallAmounts_shouldReturnCorrectCalculation() public view { uint256 amountIn = constantSum.getAmountIn(20e24, 10e24, 0, 1); assertEq(amountIn, 2); @@ -81,14 +79,14 @@ contract ConstantSumPricingModuleTest is Test { // = 10e18 * 20e24 / (10e24 * (1 - 0.1)) // = 10e18 * 20e24 / (10e24 * 0.9) ≈ 22222222222222222222.22222222222222222 // = 22222222222222222222 Wei - function test_getAmountIn_whenValidInput_shouldReturnCorrectCalculation() public { + function test_getAmountIn_whenValidInput_shouldReturnCorrectCalculation() public view { uint256 amountIn = constantSum.getAmountIn(20e24, 10e24, 1e23, 10e18); assertEq(amountIn, 22222222222222222222); } /* ---------- name ---------- */ - function test_name_shouldReturnCorrectName() public { + function test_name_shouldReturnCorrectName() public view { assertEq(constantSum.name(), "ConstantSum"); } } diff --git a/test/swap/Reserve.t.sol b/test/unit/swap/Reserve.t.sol similarity index 85% rename from test/swap/Reserve.t.sol rename to test/unit/swap/Reserve.t.sol index 30f9c944..45697663 100644 --- a/test/swap/Reserve.t.sol +++ b/test/unit/swap/Reserve.t.sol @@ -1,22 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { Test } from "mento-std/Test.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; -import { BaseTest } from "../utils/BaseTest.t.sol"; -import { TokenHelpers } from "../utils/TokenHelpers.t.sol"; -import { DummyERC20 } from "../utils/DummyErc20.sol"; -import { MockSortedOracles } from "../mocks/MockSortedOracles.sol"; -import { MockStableToken } from "../mocks/MockStableToken.sol"; +import { WithRegistry } from "test/utils/WithRegistry.sol"; +import { MockSortedOracles } from "test/utils/mocks/MockSortedOracles.sol"; +import { TestERC20 } from "test/utils/mocks/TestERC20.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; -import { Reserve } from "contracts/swap/Reserve.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; -contract ReserveTest is BaseTest, TokenHelpers { - using SafeMath for uint256; +contract ReserveTest is Test, WithRegistry { using FixidityLib for FixidityLib.Fraction; event TobinTaxStalenessThresholdSet(uint256 value); @@ -47,18 +44,17 @@ contract ReserveTest is BaseTest, TokenHelpers { address notDeployer; address broker; - Reserve reserve; + IReserve reserve; MockSortedOracles sortedOracles; - DummyERC20 dummyToken1 = new DummyERC20("DummyToken1", "DT1", 18); - DummyERC20 dummyToken2 = new DummyERC20("DummyToken2", "DT2", 18); - DummyERC20 dummyToken3 = new DummyERC20("DummyToken3", "DT3", 18); - - function setUp() public { - notDeployer = actor("notDeployer"); - vm.startPrank(deployer); - reserve = new Reserve(true); + TestERC20 dummyToken1 = new TestERC20("DummyToken1", "DT1"); + TestERC20 dummyToken2 = new TestERC20("DummyToken2", "DT2"); + TestERC20 dummyToken3 = new TestERC20("DummyToken3", "DT3"); + + function setUp() public virtual { + notDeployer = makeAddr("notDeployer"); + reserve = IReserve(deployCode("Reserve", abi.encode(true))); sortedOracles = new MockSortedOracles(); - broker = actor("broker"); + broker = makeAddr("broker"); registry.setAddressFor("SortedOracles", address(sortedOracles)); registry.setAddressFor("Exchange", exchangeAddress); @@ -74,7 +70,7 @@ contract ReserveTest is BaseTest, TokenHelpers { collateralAssetDailySpendingRatios[0] = 100000000000000000000000; // Donate 10k DT3 to the reserve - deal(address(dummyToken3), address(reserve), 10000 * 10**18); + deal(address(dummyToken3), address(reserve), 10000 * 10 ** 18); // Only 10% of reserve DT3 should be spendable per day collateralAssetDailySpendingRatios[1] = FixidityLib.newFixedFraction(1, 10).unwrap(); collateralAssets[1] = address(dummyToken3); @@ -96,8 +92,10 @@ contract ReserveTest is BaseTest, TokenHelpers { } contract ReserveTest_initAndSetters is ReserveTest { + using FixidityLib for FixidityLib.Fraction; + function test_init_setsParameters() public { - assertEq(reserve.owner(), deployer); + assertEq(reserve.owner(), address(this)); assertEq(address(reserve.registry()), address(registry)); assertEq(reserve.tobinTaxStalenessThreshold(), tobinTaxStalenessThreshold); @@ -125,9 +123,9 @@ contract ReserveTest_initAndSetters is ReserveTest { assertEq(reserve.tobinTax(), newValue); vm.expectRevert("tobin tax cannot be larger than 1"); - reserve.setTobinTax(FixidityLib.newFixed(1).unwrap().add(1)); + reserve.setTobinTax(FixidityLib.newFixed(1).unwrap() + 1); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setTobinTax(100); } @@ -139,7 +137,7 @@ contract ReserveTest_initAndSetters is ReserveTest { reserve.setTobinTaxReserveRatio(newValue); assertEq(reserve.tobinTaxReserveRatio(), newValue); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setTobinTaxReserveRatio(100); } @@ -152,9 +150,9 @@ contract ReserveTest_initAndSetters is ReserveTest { assertEq(reserve.getDailySpendingRatio(), newValue); vm.expectRevert("spending ratio cannot be larger than 1"); - reserve.setDailySpendingRatio(FixidityLib.newFixed(1).unwrap().add(1)); + reserve.setDailySpendingRatio(FixidityLib.newFixed(1).unwrap() + 1); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setDailySpendingRatio(100); } @@ -198,14 +196,14 @@ contract ReserveTest_initAndSetters is ReserveTest { address[] memory collateralAssets = new address[](1); uint256[] memory collateralAssetDailySpendingRatios = new uint256[](1); collateralAssets[0] = address(dummyToken1); - collateralAssetDailySpendingRatios[0] = FixidityLib.newFixed(1).unwrap().add(1); + collateralAssetDailySpendingRatios[0] = FixidityLib.newFixed(1).unwrap() + 1; vm.expectRevert("spending ratio cannot be larger than 1"); reserve.setDailySpendingRatioForCollateralAssets(collateralAssets, collateralAssetDailySpendingRatios); } function test_setDailySpendingRatioForCollateralAssets_whenSenderIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setDailySpendingRatioForCollateralAssets(new address[](0), new uint256[](0)); } @@ -230,7 +228,7 @@ contract ReserveTest_initAndSetters is ReserveTest { } function test_addCollateralAsset_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.addCollateralAsset(address(0x1234)); } @@ -253,7 +251,7 @@ contract ReserveTest_initAndSetters is ReserveTest { } function test_removeCollateralAsset_whenNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.removeCollateralAsset(address(dummyToken1), 0); } @@ -263,7 +261,7 @@ contract ReserveTest_initAndSetters is ReserveTest { reserve.setRegistry(newValue); assertEq(address(reserve.registry()), newValue); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setRegistry(address(0x1234)); } @@ -278,7 +276,7 @@ contract ReserveTest_initAndSetters is ReserveTest { vm.expectRevert("token addr already registered"); reserve.addToken(token); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.addToken(address(0x1234)); } @@ -296,7 +294,7 @@ contract ReserveTest_initAndSetters is ReserveTest { emit TokenRemoved(token, 0); reserve.removeToken(token, 0); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.removeToken(address(0x1234), 0); } @@ -308,7 +306,7 @@ contract ReserveTest_initAndSetters is ReserveTest { reserve.setTobinTaxStalenessThreshold(newThreshold); assertEq(reserve.tobinTaxStalenessThreshold(), newThreshold); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setTobinTaxStalenessThreshold(newThreshold); } @@ -325,10 +323,9 @@ contract ReserveTest_initAndSetters is ReserveTest { vm.expectRevert("reserve addr already added"); reserve.addOtherReserveAddress(otherReserveAddresses[0]); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.addOtherReserveAddress(otherReserveAddresses[1]); - changePrank(deployer); vm.expectEmit(true, true, true, true, address(reserve)); emit OtherReserveAddressAdded(otherReserveAddresses[1]); @@ -354,10 +351,9 @@ contract ReserveTest_initAndSetters is ReserveTest { reserve.addOtherReserveAddress(otherReserveAddresses[0]); reserve.addOtherReserveAddress(otherReserveAddresses[1]); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.removeOtherReserveAddress(otherReserveAddresses[0], 0); - changePrank(deployer); vm.expectEmit(true, true, true, true, address(reserve)); emit OtherReserveAddressRemoved(otherReserveAddresses[0], 0); @@ -375,7 +371,7 @@ contract ReserveTest_initAndSetters is ReserveTest { uint256[] memory assetAllocationWeights = new uint256[](3); assetAllocationWeights[0] = FixidityLib.newFixedFraction(1, 3).unwrap(); assetAllocationWeights[1] = FixidityLib.newFixedFraction(1, 3).unwrap(); - assetAllocationWeights[2] = FixidityLib.newFixedFraction(1, 3).unwrap().add(1); + assetAllocationWeights[2] = FixidityLib.newFixedFraction(1, 3).unwrap() + 1; vm.expectEmit(true, true, true, true, address(reserve)); emit AssetAllocationSet(assetAllocationSymbols, assetAllocationWeights); @@ -383,15 +379,14 @@ contract ReserveTest_initAndSetters is ReserveTest { assertEq(reserve.getAssetAllocationSymbols(), assetAllocationSymbols); assertEq(reserve.getAssetAllocationWeights(), assetAllocationWeights); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.setAssetAllocations(assetAllocationSymbols, assetAllocationWeights); - changePrank(deployer); - assetAllocationWeights[2] = FixidityLib.newFixedFraction(1, 3).unwrap().add(100); + assetAllocationWeights[2] = FixidityLib.newFixedFraction(1, 3).unwrap() + 100; vm.expectRevert("Sum of asset allocation must be 1"); reserve.setAssetAllocations(assetAllocationSymbols, assetAllocationWeights); - assetAllocationWeights[2] = FixidityLib.newFixedFraction(1, 3).unwrap().add(1); + assetAllocationWeights[2] = FixidityLib.newFixedFraction(1, 3).unwrap() + 1; assetAllocationSymbols[2] = bytes32("BTC"); vm.expectRevert("Cannot set weight twice"); @@ -405,16 +400,18 @@ contract ReserveTest_initAndSetters is ReserveTest { } contract ReserveTest_transfers is ReserveTest { + using FixidityLib for FixidityLib.Fraction; + uint256 constant reserveCeloBalance = 100000; uint256 constant reserveDummyToken1Balance = 10000000; uint256 constant reserveDummyToken2Balance = 20000000; - address payable constant otherReserveAddress = address(0x1234); - address payable constant trader = address(0x1245); - address payable spender; + address payable otherReserveAddress = payable(makeAddr("otherReserveAddress")); + address payable notOtherReserveAddress = payable(makeAddr("notOtherReserveAddress")); + address payable trader = payable(makeAddr("trader")); + address payable spender = payable(makeAddr("spender")); - function setUp() public { + function setUp() public override { super.setUp(); - spender = address(uint160(actor("spender"))); address[] memory collateralAssets = new address[](1); uint256[] memory collateralAssetDailySpendingRatios = new uint256[](1); @@ -434,83 +431,85 @@ contract ReserveTest_transfers is ReserveTest { /* ---------- Transfer Gold ---------- */ function test_transferGold() public { - changePrank(spender); - uint256 amount = reserveCeloBalance.div(10); + uint256 amount = reserveCeloBalance / 10; + vm.prank(spender); reserve.transferGold(otherReserveAddress, amount); assertEq(otherReserveAddress.balance, amount); assertEq(address(reserve).balance, reserveCeloBalance - amount); vm.expectRevert("Exceeding spending limit"); - reserve.transferGold(otherReserveAddress, amount.mul(2)); + vm.prank(spender); + reserve.transferGold(otherReserveAddress, amount * 2); vm.warp(block.timestamp + 24 * 3600); - reserve.transferGold(otherReserveAddress, amount.mul(2)); + vm.prank(spender); + reserve.transferGold(otherReserveAddress, amount * 2); assertEq(otherReserveAddress.balance, 3 * amount); vm.expectRevert("can only transfer to other reserve address"); - reserve.transferGold(address(0x234), amount); + vm.prank(spender); + reserve.transferGold(notOtherReserveAddress, amount); - changePrank(deployer); reserve.removeSpender(spender); - changePrank(spender); + vm.warp(block.timestamp + 24 * 3600); vm.expectRevert("sender not allowed to transfer Reserve funds"); + vm.prank(spender); reserve.transferGold(otherReserveAddress, amount); } /* ---------- Transfer Collateral Asset ---------- */ function test_transferCollateralAsset_whenParametersAreCorrect_shouldUpdate() public { - changePrank(spender); - uint256 amount = reserveDummyToken1Balance.div(10); + vm.prank(spender); + uint256 amount = reserveDummyToken1Balance / 10; reserve.transferCollateralAsset(address(dummyToken1), otherReserveAddress, amount); assertEq(dummyToken1.balanceOf(otherReserveAddress), amount); assertEq(dummyToken1.balanceOf(address(reserve)), reserveDummyToken1Balance - amount); } function test_transferCollateralAsset_whenItExceedsSpendingLimit_shouldRevert() public { - changePrank(spender); + vm.prank(spender); vm.expectRevert("Exceeding spending limit"); - reserve.transferCollateralAsset(address(dummyToken1), otherReserveAddress, reserveDummyToken1Balance.add(2)); + reserve.transferCollateralAsset(address(dummyToken1), otherReserveAddress, reserveDummyToken1Balance + 2); vm.warp(block.timestamp + 24 * 3600); } function test_transferCollateralAsset_whenItTransfersToARandomAddress_shouldRevert() public { - uint256 amount = reserveDummyToken1Balance.div(100); - changePrank(spender); + uint256 amount = reserveDummyToken1Balance / 100; + vm.prank(spender); vm.expectRevert("can only transfer to other reserve address"); reserve.transferCollateralAsset(address(dummyToken1), spender, amount); } function test_transferCollateralAsset_whenSpendingRatioWasNotSet_shouldRevert() public { - changePrank(spender); + vm.prank(spender); vm.expectRevert("this asset has no spending ratio, therefore can't be transferred"); reserve.transferCollateralAsset(address(dummyToken2), otherReserveAddress, reserveDummyToken2Balance); } function test_transferCollateralAsset_whenSpenderWasRemoved_shouldRevert() public { - changePrank(deployer); reserve.removeSpender(spender); - changePrank(spender); + vm.prank(spender); vm.warp(block.timestamp + 24 * 3600); vm.expectRevert("sender not allowed to transfer Reserve funds"); - reserve.transferCollateralAsset(address(dummyToken1), address(0x234), reserveDummyToken1Balance); + reserve.transferCollateralAsset(address(dummyToken1), otherReserveAddress, reserveDummyToken1Balance); } function test_transferCollateralAsset_whenMultipleTransfersDoNotHitDailySpend_shouldTransferCorrectAmounts() public { - changePrank(spender); - - uint256 transfer1Amount = 500 * 10**18; - uint256 transfer2Amount = 400 * 10**18; + uint256 transfer1Amount = 500 * 10 ** 18; + uint256 transfer2Amount = 400 * 10 ** 18; uint256 totalTransferAmount = transfer1Amount + transfer2Amount; uint256 reserveBalanceBefore = dummyToken3.balanceOf(address(reserve)); // Spend 500 DT3 (50% of daily limit) + vm.prank(spender); reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, transfer1Amount); // Spend 400 DT3 (LT remaining daily limit) + vm.prank(spender); reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, transfer2Amount); uint256 traderBalanceAfter = dummyToken3.balanceOf(otherReserveAddress); @@ -521,31 +520,31 @@ contract ReserveTest_transfers is ReserveTest { } function test_transferCollateralAsset_whenMultipleTransfersHitDailySpend_shouldRevert() public { - changePrank(spender); - // Spend 500 DT3 (50% of daily limit) - reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, 500 * 10**18); + vm.prank(spender); + reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, 500 * 10 ** 18); uint256 spendingLimitAfter = reserve.collateralAssetSpendingLimit(address(dummyToken3)); // (collateralAssetDailySpendingRatio * reserve DT3 balance before transfer) - transfer amount - assertEq(spendingLimitAfter, 500 * 10**18); + assertEq(spendingLimitAfter, 500 * 10 ** 18); vm.expectRevert("Exceeding spending limit"); + vm.prank(spender); // Spend amount GT remaining daily limit reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, spendingLimitAfter + 1); } function test_transferCollateralAsset_whenSpendingLimitIsHit_shoudResetNextDay() public { - changePrank(spender); - - uint256 transfer1Amount = 500 * 10**18; - uint256 transfer2Amount = 600 * 10**18; + uint256 transfer1Amount = 500 * 10 ** 18; + uint256 transfer2Amount = 600 * 10 ** 18; // Spend 500 DT3 (50% of daily limit) + vm.prank(spender); reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, transfer1Amount); vm.expectRevert("Exceeding spending limit"); // Spend 600 DT3 (GT remaining daily limit) + vm.prank(spender); reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, transfer2Amount); uint256 traderBalanceAfterFirstDay = dummyToken3.balanceOf(otherReserveAddress); @@ -554,6 +553,7 @@ contract ReserveTest_transfers is ReserveTest { vm.warp(block.timestamp + 24 * 3600); // Spend 600 DT3 (LT remaining daily limit on new day) + vm.prank(spender); reserve.transferCollateralAsset(address(dummyToken3), otherReserveAddress, transfer2Amount); uint256 traderBalanceAfterSecondDay = dummyToken3.balanceOf(otherReserveAddress); @@ -564,13 +564,13 @@ contract ReserveTest_transfers is ReserveTest { function test_transferExchangeCollateralAsset_whenSenderIsBroker_shouldTransfer() public { reserve.addExchangeSpender(broker); - changePrank(broker); + vm.prank(broker); reserve.transferExchangeCollateralAsset(address(dummyToken1), otherReserveAddress, 1000); assertEq(dummyToken1.balanceOf(otherReserveAddress), 1000); } function test_transferExchangeCollateralAsset_notExchangeSender_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Address not allowed to spend"); reserve.transferExchangeCollateralAsset(address(dummyToken1), otherReserveAddress, 1000); } @@ -579,11 +579,10 @@ contract ReserveTest_transfers is ReserveTest { address exchangeSpender0 = address(0x22222); address exchangeSpender1 = address(0x33333); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.addExchangeSpender(exchangeSpender0); - changePrank(deployer); vm.expectEmit(true, true, true, true, address(reserve)); emit ExchangeSpenderAdded(exchangeSpender0); reserve.addExchangeSpender(exchangeSpender0); @@ -602,11 +601,10 @@ contract ReserveTest_transfers is ReserveTest { address exchangeSpender1 = address(0x33333); reserve.addExchangeSpender(exchangeSpender0); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.removeExchangeSpender(exchangeSpender0, 0); - changePrank(deployer); vm.expectEmit(true, true, true, true, address(reserve)); emit ExchangeSpenderRemoved(exchangeSpender0); reserve.removeExchangeSpender(exchangeSpender0, 0); @@ -630,11 +628,10 @@ contract ReserveTest_transfers is ReserveTest { function test_addSpender() public { address _spender = address(0x4444); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.addSpender(_spender); - changePrank(deployer); vm.expectEmit(true, true, true, true, address(reserve)); emit SpenderAdded(_spender); reserve.addSpender(_spender); @@ -644,7 +641,7 @@ contract ReserveTest_transfers is ReserveTest { } function test_removeSpender_whenCallerIsOwner_shouldRemove() public { - address _spender = actor("_spender"); + address _spender = makeAddr("_spender"); reserve.addSpender(_spender); vm.expectEmit(true, true, true, true, address(reserve)); @@ -653,7 +650,7 @@ contract ReserveTest_transfers is ReserveTest { } function test_removeSpender_whenCallerIsNotOwner_shouldRevert() public { - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); reserve.removeSpender(spender); } @@ -672,25 +669,24 @@ contract ReserveTest_transfers is ReserveTest { reserve.addExchangeSpender(additionalExchange); transferExchangeGoldSpecs(exchangeAddress); - changePrank(deployer); reserve.removeExchangeSpender(additionalExchange, 0); - changePrank(additionalExchange); + vm.prank(additionalExchange); vm.expectRevert("Address not allowed to spend"); - reserve.transferExchangeGold(address(0x1111), 1000); + reserve.transferExchangeGold(otherReserveAddress, 1000); } function transferExchangeGoldSpecs(address caller) public { - changePrank(caller); - address payable dest = address(0x1111); + vm.prank(caller); + address payable dest = payable(makeAddr("dest")); reserve.transferExchangeGold(dest, 1000); assertEq(dest.balance, 1000); - changePrank(spender); + vm.prank(spender); vm.expectRevert("Address not allowed to spend"); reserve.transferExchangeGold(dest, 1000); - changePrank(notDeployer); + vm.prank(notDeployer); vm.expectRevert("Address not allowed to spend"); reserve.transferExchangeGold(dest, 1000); } @@ -699,29 +695,40 @@ contract ReserveTest_transfers is ReserveTest { reserve.setDailySpendingRatio(FixidityLib.fixed1().unwrap()); vm.expectRevert("Cannot freeze more than balance"); reserve.setFrozenGold(reserveCeloBalance + 1, 1); - uint256 dailyUnlock = reserveCeloBalance.div(3); + uint256 dailyUnlock = reserveCeloBalance / 3; reserve.setFrozenGold(reserveCeloBalance, 3); - changePrank(spender); + vm.startPrank(spender); vm.expectRevert("Exceeding spending limit"); reserve.transferGold(otherReserveAddress, 1); - vm.warp(block.timestamp + 3600 * 24); + + uint256 day1 = block.timestamp + 3600 * 24; + uint256 day2 = day1 + 3600 * 24; + uint256 day3 = day2 + 3600 * 24; + + vm.warp(day1); assertEq(reserve.getUnfrozenBalance(), dailyUnlock); reserve.transferGold(otherReserveAddress, dailyUnlock); - vm.warp(block.timestamp + 3600 * 24); + + vm.warp(day2); assertEq(reserve.getUnfrozenBalance(), dailyUnlock); reserve.transferGold(otherReserveAddress, dailyUnlock); - vm.warp(block.timestamp + 3600 * 24); + + vm.warp(day3); assertEq(reserve.getUnfrozenBalance(), dailyUnlock + 1); reserve.transferGold(otherReserveAddress, dailyUnlock); + vm.stopPrank(); } } contract ReserveTest_tobinTax is ReserveTest { - MockStableToken stableToken0; - MockStableToken stableToken1; + using FixidityLib for FixidityLib.Fraction; + using stdStorage for StdStorage; - function setUp() public { + TestERC20 stableToken0 = new TestERC20("Stable0", "ST0"); + TestERC20 stableToken1 = new TestERC20("Stable1", "ST1"); + + function setUp() public override { super.setUp(); bytes32[] memory assetAllocationSymbols = new bytes32[](2); @@ -733,24 +740,19 @@ contract ReserveTest_tobinTax is ReserveTest { reserve.setAssetAllocations(assetAllocationSymbols, assetAllocationWeights); - stableToken0 = new MockStableToken(); - sortedOracles.setMedianRate(address(stableToken0), sortedOraclesDenominator.mul(10)); - - stableToken1 = new MockStableToken(); - sortedOracles.setMedianRate(address(stableToken1), sortedOraclesDenominator.mul(10)); + sortedOracles.setMedianRate(address(stableToken0), sortedOraclesDenominator * 10); + sortedOracles.setMedianRate(address(stableToken1), sortedOraclesDenominator * 10); reserve.addToken(address(stableToken0)); reserve.addToken(address(stableToken1)); } - function setValues( - uint256 reserveBalance, - uint256 stableToken0Supply, - uint256 stableToken1Supply - ) internal { + function setValues(uint256 reserveBalance, uint256 stableToken0Supply, uint256 stableToken1Supply) internal { deal(address(reserve), reserveBalance); - stableToken0.setTotalSupply(stableToken0Supply); - stableToken1.setTotalSupply(stableToken1Supply); + stdstore.target(address(stableToken0)).sig(stableToken0.totalSupply.selector).checked_write(stableToken0Supply); + stdstore.target(address(stableToken1)).sig(stableToken1.totalSupply.selector).checked_write(stableToken1Supply); + // stableToken0.setTotalSupply(stableToken0Supply); + // stableToken1.setTotalSupply(stableToken1Supply); } function getOrComputeTobinTaxFraction() internal returns (uint256) { diff --git a/test/tokens/StableTokenV2.t.sol b/test/unit/tokens/StableTokenV2.t.sol similarity index 96% rename from test/tokens/StableTokenV2.t.sol rename to test/unit/tokens/StableTokenV2.t.sol index 655eb189..0909341e 100644 --- a/test/tokens/StableTokenV2.t.sol +++ b/test/unit/tokens/StableTokenV2.t.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility // solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase -pragma solidity ^0.8.0; +pragma solidity ^0.8; -import { Arrays } from "../utils/Arrays.sol"; -import { BaseTest } from "../utils/BaseTest.next.sol"; +import { addresses, uints } from "mento-std/Array.sol"; +import { Test } from "mento-std/Test.sol"; import { StableTokenV2 } from "contracts/tokens/StableTokenV2.sol"; -contract StableTokenV2Test is BaseTest { +contract StableTokenV2Test is Test { event TransferComment(string comment); event Transfer(address indexed from, address indexed to, uint256 value); @@ -48,18 +48,14 @@ contract StableTokenV2Test is BaseTest { address(0), // deprecated 0, // deprecated 0, // deprecated - Arrays.addresses(holder0, holder1, holder2, broker, exchange), - Arrays.uints(1000, 1000, 1000, 1000, 1000), + addresses(holder0, holder1, holder2, broker, exchange), + uints(1000, 1000, 1000, 1000, 1000), "" // deprecated ); token.initializeV2(broker, validators, exchange); } - function mintAndAssert( - address minter, - address to, - uint256 value - ) public { + function mintAndAssert(address minter, address to, uint256 value) public { uint256 balanceBefore = token.balanceOf(to); vm.prank(minter); token.mint(to, value); diff --git a/test/utils/Arrays.sol b/test/utils/Arrays.sol deleted file mode 100644 index d857bfd3..00000000 --- a/test/utils/Arrays.sol +++ /dev/null @@ -1,320 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.17; - -library Arrays { - function uints(uint256 e0) internal pure returns (uint256[] memory arr) { - arr = new uint256[](1); - arr[0] = e0; - return arr; - } - - function uints(uint256 e0, uint256 e1) internal pure returns (uint256[] memory arr) { - arr = new uint256[](2); - arr[0] = e0; - arr[1] = e1; - return arr; - } - - function uints( - uint256 e0, - uint256 e1, - uint256 e2 - ) internal pure returns (uint256[] memory arr) { - arr = new uint256[](3); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - return arr; - } - - function uints( - uint256 e0, - uint256 e1, - uint256 e2, - uint256 e3 - ) internal pure returns (uint256[] memory arr) { - arr = new uint256[](4); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - return arr; - } - - function uints( - uint256 e0, - uint256 e1, - uint256 e2, - uint256 e3, - uint256 e4 - ) internal pure returns (uint256[] memory arr) { - arr = new uint256[](5); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - return arr; - } - - function uints( - uint256 e0, - uint256 e1, - uint256 e2, - uint256 e3, - uint256 e4, - uint256 e5 - ) internal pure returns (uint256[] memory arr) { - arr = new uint256[](6); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - arr[5] = e5; - return arr; - } - - function uints( - uint256 e0, - uint256 e1, - uint256 e2, - uint256 e3, - uint256 e4, - uint256 e5, - uint256 e6 - ) internal pure returns (uint256[] memory arr) { - arr = new uint256[](7); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - arr[5] = e5; - arr[6] = e6; - return arr; - } - - function addresses(address e0) internal pure returns (address[] memory arr) { - arr = new address[](1); - arr[0] = e0; - return arr; - } - - function addresses(address e0, address e1) internal pure returns (address[] memory arr) { - arr = new address[](2); - arr[0] = e0; - arr[1] = e1; - return arr; - } - - function addresses( - address e0, - address e1, - address e2 - ) internal pure returns (address[] memory arr) { - arr = new address[](3); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - return arr; - } - - function addresses( - address e0, - address e1, - address e2, - address e3 - ) internal pure returns (address[] memory arr) { - arr = new address[](4); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - return arr; - } - - function addresses( - address e0, - address e1, - address e2, - address e3, - address e4 - ) internal pure returns (address[] memory arr) { - arr = new address[](5); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - return arr; - } - - function addresses( - address e0, - address e1, - address e2, - address e3, - address e4, - address e5, - address e6 - ) internal pure returns (address[] memory arr) { - arr = new address[](7); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - arr[5] = e5; - arr[6] = e6; - return arr; - } - - function bytes32s(bytes32 e0) internal pure returns (bytes32[] memory arr) { - arr = new bytes32[](1); - arr[0] = e0; - return arr; - } - - function bytes32s(bytes32 e0, bytes32 e1) internal pure returns (bytes32[] memory arr) { - arr = new bytes32[](2); - arr[0] = e0; - arr[1] = e1; - return arr; - } - - function bytes32s( - bytes32 e0, - bytes32 e1, - bytes32 e2 - ) internal pure returns (bytes32[] memory arr) { - arr = new bytes32[](3); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - return arr; - } - - function bytes32s( - bytes32 e0, - bytes32 e1, - bytes32 e2, - bytes32 e3 - ) internal pure returns (bytes32[] memory arr) { - arr = new bytes32[](4); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - return arr; - } - - function bytes32s( - bytes32 e0, - bytes32 e1, - bytes32 e2, - bytes32 e3, - bytes32 e4 - ) internal pure returns (bytes32[] memory arr) { - arr = new bytes32[](5); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - return arr; - } - - function bytess(bytes memory e0) internal pure returns (bytes[] memory arr) { - arr = new bytes[](1); - arr[0] = e0; - return arr; - } - - function bytess(bytes memory e0, bytes memory e1) internal pure returns (bytes[] memory arr) { - arr = new bytes[](2); - arr[0] = e0; - arr[1] = e1; - return arr; - } - - function bytess( - bytes memory e0, - bytes memory e1, - bytes memory e2 - ) internal pure returns (bytes[] memory arr) { - arr = new bytes[](3); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - return arr; - } - - function bytess( - bytes memory e0, - bytes memory e1, - bytes memory e2, - bytes memory e3 - ) internal pure returns (bytes[] memory arr) { - arr = new bytes[](4); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - return arr; - } - - function bytess( - bytes memory e0, - bytes memory e1, - bytes memory e2, - bytes memory e3, - bytes memory e4 - ) internal pure returns (bytes[] memory arr) { - arr = new bytes[](5); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - return arr; - } - - function bytess( - bytes memory e0, - bytes memory e1, - bytes memory e2, - bytes memory e3, - bytes memory e4, - bytes memory e5 - ) internal pure returns (bytes[] memory arr) { - arr = new bytes[](6); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - arr[5] = e5; - return arr; - } - - function bytess( - bytes memory e0, - bytes memory e1, - bytes memory e2, - bytes memory e3, - bytes memory e4, - bytes memory e5, - bytes memory e6 - ) internal pure returns (bytes[] memory arr) { - arr = new bytes[](7); - arr[0] = e0; - arr[1] = e1; - arr[2] = e2; - arr[3] = e3; - arr[4] = e4; - arr[5] = e5; - arr[6] = e6; - return arr; - } -} diff --git a/test/utils/BaseTest.next.sol b/test/utils/BaseTest.next.sol deleted file mode 100644 index c0cc0e11..00000000 --- a/test/utils/BaseTest.next.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; - -import { Test } from "forge-std-next/Test.sol"; -import { IRegistry } from "contracts/common/interfaces/IRegistry.sol"; -import { Factory } from "./Factory.sol"; - -interface IRegistryInit { - function initialize() external; -} - -contract BaseTest is Test { - address public constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; - IRegistry public registry = IRegistry(REGISTRY_ADDRESS); - - // solhint-disable-next-line const-name-snakecase - address public constant deployer = address(0x31337); - Factory public factory; - - constructor() { - address _factory = address(new Factory()); - vm.etch(deployer, _factory.code); - factory = Factory(deployer); - factory.createAt("Registry", REGISTRY_ADDRESS, abi.encode(true)); - vm.prank(deployer); - IRegistryInit(REGISTRY_ADDRESS).initialize(); - - // Deploy required libraries so that vm.getCode will automatically link - factory.createFromPath( - "contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian", - abi.encodePacked() - ); - } -} diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol deleted file mode 100644 index b4d22682..00000000 --- a/test/utils/BaseTest.t.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import { Test } from "celo-foundry/Test.sol"; -import { IRegistry } from "contracts/common/interfaces/IRegistry.sol"; -import { Registry } from "contracts/common/Registry.sol"; -import { Factory } from "./Factory.sol"; -import { GetCode } from "./GetCode.sol"; - -contract BaseTest is Test { - address public constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; - IRegistry public registry = IRegistry(REGISTRY_ADDRESS); - - // solhint-disable-next-line const-name-snakecase - address public constant deployer = address(0x31337); - Factory public factory; - - constructor() public { - address _factory = address(new Factory()); - vm.etch(deployer, GetCode.at(_factory)); - factory = Factory(deployer); - factory.createAt("Registry", REGISTRY_ADDRESS, abi.encode(true)); - vm.prank(deployer); - Registry(REGISTRY_ADDRESS).initialize(); - - // Deploy required libraries so that vm.getCode will automatically link - factory.createFromPath( - "contracts/common/linkedlists/AddressSortedLinkedListWithMedian.sol:AddressSortedLinkedListWithMedian", - abi.encodePacked() - ); - } -} diff --git a/test/utils/Chain.sol b/test/utils/Chain.sol deleted file mode 100644 index c0ec0b41..00000000 --- a/test/utils/Chain.sol +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; - -import { Vm } from "forge-std/Vm.sol"; - -library Chain { - address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); - // solhint-disable-next-line const-name-snakecase - Vm public constant vm = Vm(VM_ADDRESS); - - uint256 public constant NETWORK_ANVIL = 0; - - uint256 public constant NETWORK_CELO_CHAINID = 42220; - string public constant NETWORK_CELO_CHAINID_STRING = "42220"; - string public constant NETWORK_CELO_RPC = "celo_mainnet"; - string public constant NETWORK_CELO_PK_ENV_VAR = "CELO_MAINNET_DEPLOYER_PK"; - - uint256 public constant NETWORK_BAKLAVA_CHAINID = 62320; - string public constant NETWORK_BAKLAVA_CHAINID_STRING = "62320"; - string public constant NETWORK_BAKLAVA_RPC = "baklava"; - string public constant NETWORK_BAKLAVA_PK_ENV_VAR = "BAKLAVA_DEPLOYER_PK"; - - uint256 public constant NETWORK_ALFAJORES_CHAINID = 44787; - string public constant NETWORK_ALFAJORES_CHAINID_STRING = "44787"; - string public constant NETWORK_ALFAJORES_RPC = "alfajores"; - string public constant NETWORK_ALFAJORES_PK_ENV_VAR = "ALFAJORES_DEPLOYER_PK"; - - /** - * @notice Get the current chainId - * @return the chain id - */ - function id() internal pure returns (uint256 _chainId) { - // solhint-disable-next-line no-inline-assembly - assembly { - _chainId := chainid - } - } - - function idString() internal pure returns (string memory) { - uint256 _chainId = id(); - if (_chainId == NETWORK_CELO_CHAINID) return NETWORK_CELO_CHAINID_STRING; - if (_chainId == NETWORK_BAKLAVA_CHAINID) return NETWORK_BAKLAVA_CHAINID_STRING; - if (_chainId == NETWORK_ALFAJORES_CHAINID) return NETWORK_ALFAJORES_CHAINID_STRING; - revert("unexpected network"); - } - - function rpcToken() internal pure returns (string memory) { - uint256 _chainId = id(); - return rpcToken(_chainId); - } - - function rpcToken(uint256 chainId) internal pure returns (string memory) { - if (chainId == NETWORK_CELO_CHAINID) return NETWORK_CELO_RPC; - if (chainId == NETWORK_BAKLAVA_CHAINID) return NETWORK_BAKLAVA_RPC; - if (chainId == NETWORK_ALFAJORES_CHAINID) return NETWORK_ALFAJORES_RPC; - revert("unexpected network"); - } - - function deployerPrivateKey() internal view returns (uint256) { - uint256 _chainId = id(); - if (_chainId == NETWORK_CELO_CHAINID) return vm.envUint(NETWORK_CELO_PK_ENV_VAR); - if (_chainId == NETWORK_BAKLAVA_CHAINID) return vm.envUint(NETWORK_BAKLAVA_PK_ENV_VAR); - if (_chainId == NETWORK_ALFAJORES_CHAINID) return vm.envUint(NETWORK_ALFAJORES_PK_ENV_VAR); - revert("unexpected network"); - } - - /** - * @notice Setup a fork environment for the current chain - */ - function fork() internal { - uint256 forkId = vm.createFork(rpcToken()); - vm.selectFork(forkId); - } - - /** - * @notice Setup a fork environment for a given chain - */ - function fork(uint256 chainId) internal { - uint256 forkId = vm.createFork(rpcToken(chainId)); - vm.selectFork(forkId); - } - - function isCelo() internal pure returns (bool) { - return id() == NETWORK_CELO_CHAINID; - } - - function isBaklava() internal pure returns (bool) { - return id() == NETWORK_BAKLAVA_CHAINID; - } - - function isAlfajores() internal pure returns (bool) { - return id() == NETWORK_ALFAJORES_CHAINID; - } -} diff --git a/test/utils/DummyErc20.sol b/test/utils/DummyErc20.sol deleted file mode 100644 index 85ee3bb1..00000000 --- a/test/utils/DummyErc20.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; -import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; - -contract DummyERC20 is ERC20, ERC20Detailed { - constructor( - string memory name, - string memory symbol, - uint8 decimals - ) public ERC20Detailed(name, symbol, decimals) {} -} diff --git a/test/utils/Factory.sol b/test/utils/Factory.sol deleted file mode 100644 index 976cbb3d..00000000 --- a/test/utils/Factory.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >=0.5.17 <0.8.19; - -import { console } from "forge-std/console.sol"; -import { GetCode } from "./GetCode.sol"; - -interface MiniVM { - function etch(address _addr, bytes calldata _code) external; - - function getCode(string calldata _path) external view returns (bytes memory); - - function getDeployedCode(string calldata _path) external view returns (bytes memory); - - function prank(address pranker) external; -} - -/** - * @title Factory - * @dev Should be use to allow interoperability between solidity versions. - * Contracts with a newer solidity version should be initialized through this contract. - * See initilization of StableToken in Exchange.t.sol setup() for an example. - */ -contract Factory { - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - MiniVM internal constant VM = MiniVM(VM_ADDRESS); - - function createFromPath(string memory _path, bytes memory args) public returns (address) { - bytes memory bytecode = abi.encodePacked(VM.getCode(_path), args); - address addr; - - // solhint-disable-next-line no-inline-assembly - assembly { - addr := create(0, add(bytecode, 0x20), mload(bytecode)) - } - return addr; - } - - function createFromPath( - string memory _path, - bytes memory args, - address deployer - ) public returns (address) { - bytes memory bytecode = abi.encodePacked(VM.getCode(_path), args); - address addr; - - VM.prank(deployer); - // solhint-disable-next-line no-inline-assembly - assembly { - addr := create(0, add(bytecode, 0x20), mload(bytecode)) - } - return addr; - } - - function createContract(string memory _contract, bytes memory args) public returns (address addr) { - string memory path = contractPath(_contract); - addr = createFromPath(path, args); - console.log("Deployed %s to %s", _contract, addr); - return addr; - } - - function contractPath(string memory _contract) public pure returns (string memory) { - return string(abi.encodePacked("out/", _contract, ".sol", "/", _contract, ".json")); - } - - function createAt( - string memory _contract, - address dest, - bytes memory args - ) public { - address addr = createContract(_contract, args); - VM.etch(dest, GetCode.at(addr)); - console.log("Etched %s to %s", _contract, dest); - } -} diff --git a/test/utils/GetCode.sol b/test/utils/GetCode.sol deleted file mode 100644 index 940d3ab2..00000000 --- a/test/utils/GetCode.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable var-name-mixedcase -pragma solidity >=0.5.17 <0.8.19; - -library GetCode { - function at(address _addr) public view returns (bytes memory o_code) { - // solhint-disable-next-line no-inline-assembly - assembly { - // retrieve the size of the code - let size := extcodesize(_addr) - // allocate output byte array - // by using o_code = new bytes(size) - o_code := mload(0x40) - // new "memory end" including padding - mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) - // store length in memory - mstore(o_code, size) - // actually retrieve the code, this needs assembly - extcodecopy(_addr, add(o_code, 0x20), 0, size) - } - } -} diff --git a/test/utils/TestERC20.sol b/test/utils/TestERC20.sol deleted file mode 100644 index aa49b1bc..00000000 --- a/test/utils/TestERC20.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; - -import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; - -contract TestERC20 is ERC20Upgradeable { - function mint(address to, uint256 amount) external { - _mint(to, amount); - } - - function init() external { - __ERC20_init("TestERC20", "TE20"); - } -} diff --git a/test/utils/Token.sol b/test/utils/Token.sol deleted file mode 100644 index c3a6136b..00000000 --- a/test/utils/Token.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import { ERC20 } from "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; -import { ERC20Detailed } from "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; - -contract Token is ERC20, ERC20Detailed { - constructor( - string memory name, - string memory symbol, - uint8 decimals - ) public ERC20Detailed(name, symbol, decimals) {} -} diff --git a/test/utils/TokenHelpers.t.sol b/test/utils/TokenHelpers.t.sol deleted file mode 100644 index 270b0887..00000000 --- a/test/utils/TokenHelpers.t.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "celo-foundry/Test.sol"; - -import "contracts/legacy/StableToken.sol"; -import "contracts/common/GoldToken.sol"; -import "contracts/common/interfaces/IRegistry.sol"; -import "contracts/interfaces/IStableTokenV2.sol"; - -contract TokenHelpers is Test { - address public constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; - IRegistry public registry = IRegistry(REGISTRY_ADDRESS); - - function mint( - address token, - address to, - uint256 amount - ) public { - if (token == registry.getAddressForString("GoldToken")) { - mint(GoldToken(token), to, amount); - } else if (token == registry.getAddressForStringOrDie("StableToken")) { - mint(IStableTokenV2(token), to, amount); - } else if (token == registry.getAddressForStringOrDie("StableTokenEUR")) { - mint(IStableTokenV2(token), to, amount); - } else if (token == registry.getAddressForStringOrDie("StableTokenBRL")) { - mint(IStableTokenV2(token), to, amount); - } else if (token == registry.getAddressForStringOrDie("StableTokenXOF")) { - mint(IStableTokenV2(token), to, amount); - } else if (token == registry.getAddressForStringOrDie("StableTokenKES")) { - mint(IStableTokenV2(token), to, amount); - } else { - deal(token, to, amount); - } - } - - function mintCelo(address to, uint256 amount) public { - mint(registry.getAddressForString("GoldToken"), to, amount); - } - - function mint( - GoldToken celoToken, - address to, - uint256 amount - ) internal { - address pranker = currentPrank; - changePrank(address(0)); - celoToken.mint(to, amount); - changePrank(pranker); - } - - // TODO: delete after the migration to StableTokenV2 is done on mainnet - function mint( - StableToken stableToken, - address to, - uint256 amount - ) internal { - address pranker = currentPrank; - changePrank(stableToken.registry().getAddressForString("Broker")); - stableToken.mint(to, amount); - changePrank(pranker); - } - - function mint( - IStableTokenV2 stableToken, - address to, - uint256 amount - ) internal { - address pranker = currentPrank; - changePrank(stableToken.broker()); - stableToken.mint(to, amount); - changePrank(pranker); - } -} diff --git a/test/utils/VmExtension.sol b/test/utils/VmExtension.sol index 17fee265..b89e0652 100644 --- a/test/utils/VmExtension.sol +++ b/test/utils/VmExtension.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase, max-line-length, no-inline-assembly -import { Vm } from "forge-std-next/Vm.sol"; +import { Vm } from "forge-std/Vm.sol"; import { ECDSA } from "openzeppelin-contracts-next/contracts/utils/cryptography/ECDSA.sol"; import { Strings } from "openzeppelin-contracts-next/contracts/utils/Strings.sol"; @@ -54,12 +54,7 @@ library VmExtension { /// @param r The first 32 bytes of the signature, representing the R value in ECDSA. /// @param s The next 32 bytes of the signature, representing the S value in ECDSA. /// @return signature A 65-byte long digital signature composed of r, s, and v. - function constructSignature( - Vm, - uint8 v, - bytes32 r, - bytes32 s - ) internal pure returns (bytes memory signature) { + function constructSignature(Vm, uint8 v, bytes32 r, bytes32 s) internal pure returns (bytes memory signature) { signature = new bytes(65); assembly { diff --git a/test/utils/WithRegistry.sol b/test/utils/WithRegistry.sol new file mode 100644 index 00000000..63d6d8c3 --- /dev/null +++ b/test/utils/WithRegistry.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { Test } from "mento-std/Test.sol"; +import { CELO_REGISTRY_ADDRESS } from "mento-std/Constants.sol"; + +import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; + +interface IRegistryInit { + function initialize() external; +} + +contract WithRegistry is Test { + IRegistry public registry = IRegistry(CELO_REGISTRY_ADDRESS); + + constructor() { + deployCodeTo("Registry", abi.encode(true), CELO_REGISTRY_ADDRESS); + IRegistryInit(CELO_REGISTRY_ADDRESS).initialize(); + } +} diff --git a/test/utils/WithRegistry.t.sol b/test/utils/WithRegistry.t.sol deleted file mode 100644 index 249c5061..00000000 --- a/test/utils/WithRegistry.t.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import { Test } from "celo-foundry/Test.sol"; - -import { IRegistry } from "contracts/common/interfaces/IRegistry.sol"; -import { Registry } from "contracts/common/Registry.sol"; -import { Factory } from "./Factory.sol"; - -contract WithRegistry is Test { - address public constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; - IRegistry public registry = IRegistry(REGISTRY_ADDRESS); - - constructor() public { - Factory factory = new Factory(); - factory.createAt("Registry", REGISTRY_ADDRESS, abi.encode(true)); - Registry(REGISTRY_ADDRESS).initialize(); - } -} diff --git a/test/governance/GovernanceFactoryHarness.t.sol b/test/utils/harnesses/GovernanceFactoryHarness.t.sol similarity index 80% rename from test/governance/GovernanceFactoryHarness.t.sol rename to test/utils/harnesses/GovernanceFactoryHarness.t.sol index 9e18cae9..0f26a217 100644 --- a/test/governance/GovernanceFactoryHarness.t.sol +++ b/test/utils/harnesses/GovernanceFactoryHarness.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; // solhint-disable func-name-mixedcase -import { GovernanceFactory } from "../../contracts/governance/GovernanceFactory.sol"; +import { GovernanceFactory } from "contracts/governance/GovernanceFactory.sol"; contract GovernanceFactoryHarness is GovernanceFactory { constructor(address owner_) GovernanceFactory(owner_) {} diff --git a/test/utils/harnesses/ITradingLimitsHarness.sol b/test/utils/harnesses/ITradingLimitsHarness.sol new file mode 100644 index 00000000..bccb30b5 --- /dev/null +++ b/test/utils/harnesses/ITradingLimitsHarness.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity >0.5.13 <0.9; +pragma experimental ABIEncoderV2; + +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +interface ITradingLimitsHarness { + function validate(ITradingLimits.Config calldata config) external view; + + function verify(ITradingLimits.State calldata state, ITradingLimits.Config calldata config) external view; + + function reset( + ITradingLimits.State calldata state, + ITradingLimits.Config calldata config + ) external view returns (ITradingLimits.State memory); + + function update( + ITradingLimits.State calldata state, + ITradingLimits.Config calldata config, + int256 netflow, + uint8 decimals + ) external view returns (ITradingLimits.State memory); +} diff --git a/test/utils/harnesses/IWithCooldownHarness.sol b/test/utils/harnesses/IWithCooldownHarness.sol new file mode 100644 index 00000000..0535f0be --- /dev/null +++ b/test/utils/harnesses/IWithCooldownHarness.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +interface IWithCooldownHarness { + function setDefaultCooldownTime(uint256 cooldownTime) external; + + function setCooldownTimes(address[] calldata rateFeedIDs, uint256[] calldata cooldownTimes) external; + + function getCooldown(address rateFeedID) external view returns (uint256); + + function defaultCooldownTime() external view returns (uint256); + + function rateFeedCooldownTime(address rateFeedID) external view returns (uint256); +} diff --git a/test/utils/harnesses/IWithThresholdHarness.sol b/test/utils/harnesses/IWithThresholdHarness.sol new file mode 100644 index 00000000..7f6236cc --- /dev/null +++ b/test/utils/harnesses/IWithThresholdHarness.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +interface IWithThresholdHarness { + function setDefaultRateChangeThreshold(uint256 testThreshold) external; + + function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata thresholds) external; + + function rateChangeThreshold(address rateFeedID) external view returns (uint256); + + function defaultRateChangeThreshold() external view returns (uint256); + + function rateFeedRateChangeThreshold(address rateFeedID) external view returns (uint256); + + function exceedsThreshold( + uint256 referenceValue, + uint256 currentValue, + address rateFeedID + ) external view returns (bool); +} diff --git a/test/utils/TestLocking.sol b/test/utils/harnesses/LockingHarness.sol similarity index 75% rename from test/utils/TestLocking.sol rename to test/utils/harnesses/LockingHarness.sol index f2dc698e..440708f7 100644 --- a/test/utils/TestLocking.sol +++ b/test/utils/harnesses/LockingHarness.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; +pragma solidity ^0.8; -import "../../contracts/governance/locking/Locking.sol"; +import { Locking } from "contracts/governance/locking/Locking.sol"; -contract TestLocking is Locking { +contract LockingHarness is Locking { uint32 public blockNumberMocked; uint32 public epochShift; @@ -15,6 +15,14 @@ contract TestLocking is Locking { return blockNumberMocked; } + function getMaxSlopePeriod() public pure returns (uint32) { + return MAX_SLOPE_PERIOD; + } + + function getMaxCliffPeriod() public pure returns (uint32) { + return MAX_CLIFF_PERIOD; + } + function getLockTest( uint96 amount, uint32 slope, diff --git a/test/utils/harnesses/TradingLimitsHarness.sol b/test/utils/harnesses/TradingLimitsHarness.sol new file mode 100644 index 00000000..723ac7ae --- /dev/null +++ b/test/utils/harnesses/TradingLimitsHarness.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.13; +pragma experimental ABIEncoderV2; + +import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { ITradingLimitsHarness } from "./ITradingLimitsHarness.sol"; + +contract TradingLimitsHarness is ITradingLimitsHarness { + using TradingLimits for ITradingLimits.State; + using TradingLimits for ITradingLimits.Config; + + function validate(ITradingLimits.Config memory config) public view { + return config.validate(); + } + + function verify(ITradingLimits.State memory state, ITradingLimits.Config memory config) public view { + return state.verify(config); + } + + function reset( + ITradingLimits.State memory state, + ITradingLimits.Config memory config + ) public view returns (ITradingLimits.State memory) { + return state.reset(config); + } + + function update( + ITradingLimits.State memory state, + ITradingLimits.Config memory config, + int256 netflow, + uint8 decimals + ) public view returns (ITradingLimits.State memory) { + return state.update(config, netflow, decimals); + } +} diff --git a/test/utils/harnesses/WithCooldownHarness.sol b/test/utils/harnesses/WithCooldownHarness.sol new file mode 100644 index 00000000..58fc249e --- /dev/null +++ b/test/utils/harnesses/WithCooldownHarness.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.13; + +import { WithCooldown } from "contracts/oracles/breakers/WithCooldown.sol"; + +contract WithCooldownHarness is WithCooldown { + function setDefaultCooldownTime(uint256 cooldownTime) external { + _setDefaultCooldownTime(cooldownTime); + } + + function setCooldownTimes(address[] calldata rateFeedIDs, uint256[] calldata cooldownTimes) external { + _setCooldownTimes(rateFeedIDs, cooldownTimes); + } +} diff --git a/test/utils/harnesses/WithThresholdHarness.sol b/test/utils/harnesses/WithThresholdHarness.sol new file mode 100644 index 00000000..af225c8f --- /dev/null +++ b/test/utils/harnesses/WithThresholdHarness.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.13; + +import { WithThreshold } from "contracts/oracles/breakers/WithThreshold.sol"; + +contract WithThresholdHarness is WithThreshold { + function setDefaultRateChangeThreshold(uint256 testThreshold) external { + _setDefaultRateChangeThreshold(testThreshold); + } + + function setRateChangeThresholds(address[] calldata rateFeedIDs, uint256[] calldata thresholds) external { + _setRateChangeThresholds(rateFeedIDs, thresholds); + } +} diff --git a/test/mocks/MockAggregatorV3.sol b/test/utils/mocks/MockAggregatorV3.sol similarity index 67% rename from test/mocks/MockAggregatorV3.sol rename to test/utils/mocks/MockAggregatorV3.sol index 13c6b5db..5f37c12e 100644 --- a/test/mocks/MockAggregatorV3.sol +++ b/test/utils/mocks/MockAggregatorV3.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity >0.5.13 <0.8.19; +pragma solidity ^0.8; contract MockAggregatorV3 { int256 public _answer; uint256 public _updatedAt; uint8 public decimals; - constructor(uint8 _decimals) public { + constructor(uint8 _decimals) { decimals = _decimals; } @@ -18,13 +18,7 @@ contract MockAggregatorV3 { function latestRoundData() external view - returns ( - uint80 roundId, - int256 answer, - uint256 startedAt, - uint256 updatedAt, - uint80 answeredInRound - ) + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { return (uint80(0), _answer, uint256(0), _updatedAt, uint80(0)); } diff --git a/test/mocks/MockBreaker.sol b/test/utils/mocks/MockBreaker.sol similarity index 66% rename from test/mocks/MockBreaker.sol rename to test/utils/mocks/MockBreaker.sol index 7345a86b..f9e55778 100644 --- a/test/mocks/MockBreaker.sol +++ b/test/utils/mocks/MockBreaker.sol @@ -1,18 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity ^0.8; -import { IBreaker } from "../../contracts/interfaces/IBreaker.sol"; +import { IBreaker } from "contracts/interfaces/IBreaker.sol"; contract MockBreaker is IBreaker { uint256 public cooldown; bool public trigger; bool public reset; - constructor( - uint256 _cooldown, - bool _trigger, - bool _reset - ) public { + constructor(uint256 _cooldown, bool _trigger, bool _reset) { cooldown = _cooldown; trigger = _trigger; reset = _reset; @@ -26,7 +22,7 @@ contract MockBreaker is IBreaker { cooldown = _cooldown; } - function shouldTrigger(address) external returns (bool) { + function shouldTrigger(address) external view returns (bool) { return trigger; } @@ -34,7 +30,7 @@ contract MockBreaker is IBreaker { trigger = _trigger; } - function shouldReset(address) external returns (bool) { + function shouldReset(address) external view returns (bool) { return reset; } diff --git a/test/utils/mocks/MockBreakerBox.sol b/test/utils/mocks/MockBreakerBox.sol new file mode 100644 index 00000000..64259808 --- /dev/null +++ b/test/utils/mocks/MockBreakerBox.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +contract MockBreakerBox { + uint256 public tradingMode; + + function setTradingMode(uint256 _tradingMode) external { + tradingMode = _tradingMode; + } + + function getBreakers() external pure returns (address[] memory) { + return new address[](0); + } + + function isBreaker(address) external pure returns (bool) { + return true; + } + + function getRateFeedTradingMode(address) external pure returns (uint8) { + return 0; + } + + function checkAndSetBreakers(address) external {} +} diff --git a/test/mocks/MockERC20.sol b/test/utils/mocks/MockERC20.sol similarity index 78% rename from test/mocks/MockERC20.sol rename to test/utils/mocks/MockERC20.sol index 7bb12aec..c2569999 100644 --- a/test/mocks/MockERC20.sol +++ b/test/utils/mocks/MockERC20.sol @@ -1,16 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity ^0.8; contract MockERC20 { string private _name; string private _symbol; uint256 private _decimals; - constructor( - string memory name_, - string memory symbol_, - uint256 decimals_ - ) public { + constructor(string memory name_, string memory symbol_, uint256 decimals_) { _name = name_; _symbol = symbol_; _decimals = decimals_; diff --git a/test/mocks/MockExchangeProvider.sol b/test/utils/mocks/MockExchangeProvider.sol similarity index 87% rename from test/mocks/MockExchangeProvider.sol rename to test/utils/mocks/MockExchangeProvider.sol index 67acc6ee..6743b91b 100644 --- a/test/mocks/MockExchangeProvider.sol +++ b/test/utils/mocks/MockExchangeProvider.sol @@ -1,20 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; -pragma experimental ABIEncoderV2; +pragma solidity ^0.8; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; -import { FixidityLib } from "contracts/common/FixidityLib.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; contract MockExchangeProvider is IExchangeProvider { using FixidityLib for FixidityLib.Fraction; mapping(bytes32 => uint256) private exchangeRate; - function setRate( - bytes32 exchangeId, - address base, - address quote, - uint256 rate - ) external { + function setRate(bytes32 exchangeId, address base, address quote, uint256 rate) external { bytes32 rateId = keccak256(abi.encodePacked(exchangeId, base, quote)); exchangeRate[rateId] = rate; rateId = keccak256(abi.encodePacked(exchangeId, quote, base)); @@ -44,7 +38,7 @@ contract MockExchangeProvider is IExchangeProvider { address tokenIn, address tokenOut, uint256 amountIn - ) external returns (uint256 amountOut) { + ) external view returns (uint256 amountOut) { return _getAmountOut(exchangeId, tokenIn, tokenOut, amountIn); } @@ -53,7 +47,7 @@ contract MockExchangeProvider is IExchangeProvider { address tokenIn, address tokenOut, uint256 amountOut - ) external returns (uint256 amountIn) { + ) external view returns (uint256 amountIn) { return _getAmountIn(exchangeId, tokenIn, tokenOut, amountOut); } diff --git a/test/utils/mocks/MockLocking.sol b/test/utils/mocks/MockLocking.sol new file mode 100644 index 00000000..74ca6005 --- /dev/null +++ b/test/utils/mocks/MockLocking.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +import "contracts/governance/locking/Locking.sol"; + +contract MockLocking { + function initiateData(uint256 idLock, LibBrokenLine.Line memory line, address locker, address delegate) external {} +} diff --git a/test/mocks/MockMentoToken.sol b/test/utils/mocks/MockMentoToken.sol similarity index 100% rename from test/mocks/MockMentoToken.sol rename to test/utils/mocks/MockMentoToken.sol diff --git a/test/mocks/MockOwnable.sol b/test/utils/mocks/MockOwnable.sol similarity index 100% rename from test/mocks/MockOwnable.sol rename to test/utils/mocks/MockOwnable.sol diff --git a/test/mocks/MockPricingModule.sol b/test/utils/mocks/MockPricingModule.sol similarity index 68% rename from test/mocks/MockPricingModule.sol rename to test/utils/mocks/MockPricingModule.sol index d9177a09..c4cafa30 100644 --- a/test/mocks/MockPricingModule.sol +++ b/test/utils/mocks/MockPricingModule.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity ^0.8; import { IPricingModule } from "contracts/interfaces/IPricingModule.sol"; @@ -8,7 +8,7 @@ contract MockPricingModule is IPricingModule { uint256 private _nextGetAmountOut; uint256 private _nextGetAmountIn; - constructor(string memory name_) public { + constructor(string memory name_) { _name = name_; } @@ -24,21 +24,11 @@ contract MockPricingModule is IPricingModule { _nextGetAmountIn = amount; } - function getAmountOut( - uint256, - uint256, - uint256, - uint256 - ) external view returns (uint256) { + function getAmountOut(uint256, uint256, uint256, uint256) external view returns (uint256) { return _nextGetAmountOut; } - function getAmountIn( - uint256, - uint256, - uint256, - uint256 - ) external view returns (uint256) { + function getAmountIn(uint256, uint256, uint256, uint256) external view returns (uint256) { return _nextGetAmountIn; } } diff --git a/test/mocks/MockReserve.sol b/test/utils/mocks/MockReserve.sol similarity index 86% rename from test/mocks/MockReserve.sol rename to test/utils/mocks/MockReserve.sol index d5f14848..95176eba 100644 --- a/test/mocks/MockReserve.sol +++ b/test/utils/mocks/MockReserve.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity ^0.8; // solhint-disable no-unused-vars -import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; /** * @title A mock Reserve for testing. @@ -16,7 +16,10 @@ contract MockReserve { bool public reserveSpender; // solhint-disable-next-line no-empty-blocks - function() external payable {} + fallback() external payable {} + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} function setGoldToken(address goldTokenAddress) external { goldToken = IERC20(goldTokenAddress); @@ -32,11 +35,7 @@ contract MockReserve { return true; } - function transferCollateralAsset( - address tokenAddress, - address payable to, - uint256 amount - ) external returns (bool) { + function transferCollateralAsset(address tokenAddress, address payable to, uint256 amount) external returns (bool) { require(IERC20(tokenAddress).transfer(to, amount), "asset transfer failed"); return true; } diff --git a/test/mocks/MockSortedOracles.sol b/test/utils/mocks/MockSortedOracles.sol similarity index 79% rename from test/mocks/MockSortedOracles.sol rename to test/utils/mocks/MockSortedOracles.sol index 9d932157..def3d26d 100644 --- a/test/mocks/MockSortedOracles.sol +++ b/test/utils/mocks/MockSortedOracles.sol @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; - -import "../../contracts/common/linkedlists/SortedLinkedListWithMedian.sol"; +pragma solidity ^0.8; /** * @title A mock SortedOracles for testing. @@ -25,7 +23,7 @@ contract MockSortedOracles { function setMedianTimestampToNow(address token) external { // solhint-disable-next-line not-rely-on-time - medianTimestamp[token] = uint128(now); + medianTimestamp[token] = uint128(block.timestamp); } function setNumRates(address token, uint256 rate) external { @@ -47,16 +45,8 @@ contract MockSortedOracles { expired[token] = true; } - function getTimestamps(address) - external - pure - returns ( - address[] memory, - uint256[] memory, - SortedLinkedListWithMedian.MedianRelation[] memory - ) - { - return (new address[](1), new uint256[](1), new SortedLinkedListWithMedian.MedianRelation[](1)); + function getTimestamps(address) external pure returns (address[] memory, uint256[] memory, uint256[] memory) { + return (new address[](1), new uint256[](1), new uint256[](1)); } function previousMedianRate(address) public pure returns (uint256) { diff --git a/test/mocks/MockVeMento.sol b/test/utils/mocks/MockVeMento.sol similarity index 100% rename from test/mocks/MockVeMento.sol rename to test/utils/mocks/MockVeMento.sol diff --git a/test/utils/mocks/TestERC20.sol b/test/utils/mocks/TestERC20.sol new file mode 100644 index 00000000..477dd311 --- /dev/null +++ b/test/utils/mocks/TestERC20.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; + +contract TestERC20 is ERC20 { + constructor(string memory name, string memory symbol) ERC20(name, symbol) {} + + function mint(address to, uint256 amount) external returns (bool) { + _mint(to, amount); + return true; + } + + function burn(uint256 amount) public returns (bool) { + _burn(msg.sender, amount); + return true; + } +} diff --git a/test/utils/mocks/USDC.sol b/test/utils/mocks/USDC.sol new file mode 100644 index 00000000..f3c41eb1 --- /dev/null +++ b/test/utils/mocks/USDC.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import "./TestERC20.sol"; + +contract USDC is TestERC20 { + constructor(string memory name, string memory symbol) TestERC20(name, symbol) {} + + function decimals() public pure override returns (uint8) { + return 6; + } +} diff --git a/yarn.lock b/yarn.lock index f2e4a453..2a4de44f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,11 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@celo/contracts@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@celo/contracts/-/contracts-11.0.0.tgz#0a7e85b4582badde7791b18b6ee2dd4ee24323ef" + integrity sha512-SNdWU+KU+2CMpGf/GdP79tXXBDQvNwrp7G6CqZjvdu5bG6eUq4tKwO7osMNxzrxWiDaV0tEhEY04VJayrhH2Pw== + "@commitlint/cli@^17.0.3": version "17.0.3" resolved "https://registry.npmjs.org/@commitlint/cli/-/cli-17.0.3.tgz" @@ -201,12 +206,48 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@solidity-parser/parser@^0.14.1", "@solidity-parser/parser@^0.14.3": - version "0.14.3" - resolved "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.3.tgz" - integrity sha512-29g2SZ29HtsqA58pLCtopI1P/cPy5/UAzlcAXO6T/CNJimG6yA8kx4NaseMyJULiC+TEs02Y9/yeHzClqoA0hw== +"@pnpm/config.env-replace@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" + integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== + +"@pnpm/network.ca-file@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" + integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== dependencies: - antlr4ts "^0.5.0-alpha.4" + graceful-fs "4.2.10" + +"@pnpm/npm-conf@^2.1.0": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz#bb375a571a0bd63ab0a23bece33033c683e9b6b0" + integrity sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw== + dependencies: + "@pnpm/config.env-replace" "^1.1.0" + "@pnpm/network.ca-file" "^1.0.1" + config-chain "^1.1.11" + +"@prettier/sync@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@prettier/sync/-/sync-0.3.0.tgz#91f2cfc23490a21586d1cf89c6f72157c000ca1e" + integrity sha512-3dcmCyAxIcxy036h1I7MQU/uEEBq8oLwf1CE3xeze+MPlgkdlb/+w6rGR/1dhp6Hqi17fRS6nvwnOzkESxEkOw== + +"@sindresorhus/is@^5.2.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" + integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== + +"@solidity-parser/parser@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@solidity-parser/parser/-/parser-0.18.0.tgz#8e77a02a09ecce957255a2f48c9a7178ec191908" + integrity sha512-yfORGUIPgLck41qyN7nbwJRAx17/jAIXCTanHOJZhB6PJ1iAk/84b/xlsVKFSyNyLXIj0dhppoE0+CRws7wlzA== + +"@szmarczak/http-timer@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" + integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== + dependencies: + defer-to-connect "^2.0.1" "@tsconfig/node10@^1.0.7": version "1.0.9" @@ -228,6 +269,11 @@ resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/http-cache-semantics@^4.0.2": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + "@types/minimist@^1.2.0": version "1.2.2" resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz" @@ -256,21 +302,11 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" -acorn-jsx@^5.0.0: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - acorn-walk@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^6.0.7: - version "6.4.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - acorn@^8.4.1: version "8.8.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz" @@ -284,9 +320,9 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.2, ajv@^6.6.1, ajv@^6.9.1: +ajv@^6.12.6: version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== dependencies: fast-deep-equal "^3.1.1" @@ -294,6 +330,16 @@ ajv@^6.10.2, ajv@^6.6.1, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ajv@^8.11.0: version "8.11.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz" @@ -304,11 +350,6 @@ ajv@^8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" @@ -316,16 +357,6 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: dependencies: type-fest "^0.21.3" -ansi-regex@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz" - integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== - -ansi-regex@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -336,7 +367,7 @@ ansi-regex@^6.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -355,27 +386,20 @@ ansi-styles@^6.0.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz" integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== -antlr4@4.7.1: - version "4.7.1" - resolved "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz" - integrity sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ== - -antlr4ts@^0.5.0-alpha.4: - version "0.5.0-alpha.4" - resolved "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz" - integrity sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ== +antlr4@^4.13.1-patch-1: + version "4.13.2" + resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.13.2.tgz#0d084ad0e32620482a9c3a0e2470c02e72e4006d" + integrity sha512-QiVbZhyy4xAZ17UPEuG3YTOt8ZaoeOR1CvEAqrEsDBsOqINslaB147i9xqljZqoyf5S+EUlGStaj+t22LT9MOg== arg@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== array-ify@^1.0.0: version "1.0.0" @@ -387,16 +411,11 @@ arrify@^1.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== -ast-parents@0.0.1: +ast-parents@^0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/ast-parents/-/ast-parents-0.0.1.tgz" + resolved "https://registry.yarnpkg.com/ast-parents/-/ast-parents-0.0.1.tgz#508fd0f05d0c48775d9eccda2e174423261e8dd3" integrity sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA== -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" @@ -434,6 +453,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -449,30 +475,29 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +cacheable-lookup@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" + integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== + +cacheable-request@^10.2.8: + version "10.2.14" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" + integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== + dependencies: + "@types/http-cache-semantics" "^4.0.2" + get-stream "^6.0.1" + http-cache-semantics "^4.1.1" + keyv "^4.5.3" + mimic-response "^4.0.0" + normalize-url "^8.0.0" + responselike "^3.0.0" + cachedir@2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz" integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz" - integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz" - integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz" - integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== - callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -492,7 +517,7 @@ camelcase@^5.3.1: resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.4.1: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -501,7 +526,7 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0, chalk@^4.1.1: +chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -519,13 +544,6 @@ clean-stack@^2.0.0: resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz" - integrity sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw== - dependencies: - restore-cursor "^2.0.0" - cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" @@ -554,11 +572,6 @@ cli-truncate@^3.1.0: slice-ansi "^5.0.0" string-width "^5.0.0" -cli-width@^2.0.0: - version "2.2.1" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz" - integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== - cli-width@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" @@ -607,10 +620,10 @@ colorette@^2.0.16, colorette@^2.0.17: resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== -commander@2.18.0: - version "2.18.0" - resolved "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz" - integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== commander@^9.3.0: version "9.4.0" @@ -650,6 +663,14 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +config-chain@^1.1.11: + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + conventional-changelog-angular@^5.0.11: version "5.0.13" resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz" @@ -692,16 +713,6 @@ cosmiconfig-typescript-loader@^2.0.0: cosmiconfig "^7" ts-node "^10.8.1" -cosmiconfig@^5.0.7: - version "5.2.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - cosmiconfig@^7, cosmiconfig@^7.0.0: version "7.0.1" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz" @@ -713,22 +724,21 @@ cosmiconfig@^7, cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.0.0: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -757,7 +767,7 @@ dargs@^7.0.0: resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== -debug@^4.0.1, debug@^4.3.4: +debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -777,15 +787,22 @@ decamelize@^1.1.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== -deep-is@~0.1.3: - version "0.1.4" - resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== defaults@^1.0.3: version "1.0.3" @@ -794,6 +811,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + detect-file@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz" @@ -809,13 +831,6 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dot-prop@^5.1.0: version "5.3.0" resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" @@ -828,16 +843,6 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -emoji-regex@^10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.1.0.tgz" - integrity sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg== - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -865,116 +870,6 @@ escape-string-regexp@^1.0.5: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.3.1: - version "1.4.3" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint@^5.6.0: - version "5.16.0" - resolved "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz" - integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.9.1" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^4.0.3" - eslint-utils "^1.3.1" - eslint-visitor-keys "^1.0.0" - espree "^5.0.1" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob "^7.1.2" - globals "^11.7.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^6.2.2" - js-yaml "^3.13.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.11" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - path-is-inside "^1.0.2" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^5.5.1" - strip-ansi "^4.0.0" - strip-json-comments "^2.0.1" - table "^5.2.3" - text-table "^0.2.0" - -espree@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz" - integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A== - dependencies: - acorn "^6.0.7" - acorn-jsx "^5.0.0" - eslint-visitor-keys "^1.0.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.0.1: - version "1.4.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - execa@^5.0.0: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -1021,7 +916,7 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -1031,22 +926,20 @@ fast-diff@^1.1.2: resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== +fast-diff@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz" - integrity sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA== - dependencies: - escape-string-regexp "^1.0.5" +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== figures@^3.0.0: version "3.2.0" @@ -1055,13 +948,6 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -1108,19 +994,10 @@ findup-sync@^4.0.0: micromatch "^4.0.2" resolve-dir "^1.0.1" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +form-data-encoder@^2.1.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" + integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== fs-extra@9.1.0: version "9.1.0" @@ -1151,11 +1028,6 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" @@ -1177,7 +1049,7 @@ git-raw-commits@^2.0.0: split2 "^3.0.0" through2 "^4.0.0" -glob@7.2.3, glob@^7.1.2, glob@^7.1.3: +glob@7.2.3: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1189,6 +1061,17 @@ glob@7.2.3, glob@^7.1.2, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-dirs@^0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz" @@ -1216,12 +1099,24 @@ global-prefix@^1.0.1: is-windows "^1.0.1" which "^1.2.14" -globals@^11.7.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +got@^12.1.0: + version "12.6.1" + resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" + integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== + dependencies: + "@sindresorhus/is" "^5.2.0" + "@szmarczak/http-timer" "^5.0.1" + cacheable-lookup "^7.0.0" + cacheable-request "^10.2.8" + decompress-response "^6.0.0" + form-data-encoder "^2.1.2" + get-stream "^6.0.1" + http2-wrapper "^2.1.10" + lowercase-keys "^3.0.0" + p-cancelable "^3.0.0" + responselike "^3.0.0" -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@4.2.10, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -1267,6 +1162,19 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" +http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http2-wrapper@^2.1.10: + version "2.2.1" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" + integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.2.0" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" @@ -1294,20 +1202,12 @@ ieee754@^1.1.13: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz" - integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" +ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -1315,11 +1215,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" @@ -1338,7 +1233,7 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.4: +ini@^1.3.4, ini@~1.3.0: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== @@ -1364,25 +1259,6 @@ inquirer@8.2.4: through "^2.3.6" wrap-ansi "^7.0.0" -inquirer@^6.2.2: - version "6.5.2" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -1395,21 +1271,11 @@ is-core-module@^2.5.0, is-core-module@^2.9.0: dependencies: has "^1.0.3" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz" - integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" - integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" @@ -1489,18 +1355,17 @@ js-tokens@^4.0.0: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: - argparse "^1.0.7" - esprima "^4.0.0" + argparse "^2.0.1" -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== json-parse-even-better-errors@^2.3.0: version "2.3.1" @@ -1517,11 +1382,6 @@ json-schema-traverse@^1.0.0: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" @@ -1536,18 +1396,24 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== +latest-version@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" + integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" + package-json "^8.1.0" lilconfig@2.0.5: version "2.0.5" @@ -1611,7 +1477,12 @@ lodash.map@^4.5.1: resolved "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz" integrity sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q== -lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1639,6 +1510,11 @@ longest@^2.0.1: resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz" integrity sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q== +lowercase-keys@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" + integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" @@ -1696,11 +1572,6 @@ micromatch@^4.0.2, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -1711,18 +1582,35 @@ mimic-fn@^4.0.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz" integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +mimic-response@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" + integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@^3.0.4, minimatch@^3.1.1: +minimatch@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" @@ -1732,43 +1620,26 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@1.2.6, minimist@^1.2.6: +minimist@1.2.6: version "1.2.6" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== -mkdirp@^0.5.1: - version "0.5.6" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" +minimist@^1.2.0: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz" - integrity sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ== - mute-stream@0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" @@ -1794,6 +1665,11 @@ normalize-path@^3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@^8.0.0: + version "8.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" + integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" @@ -1820,13 +1696,6 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz" - integrity sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ== - dependencies: - mimic-fn "^1.0.0" - onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" @@ -1841,18 +1710,6 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -optionator@^0.8.2: - version "0.8.3" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - ora@^5.4.1: version "5.4.1" resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" @@ -1873,6 +1730,11 @@ os-tmpdir@~1.0.2: resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -1913,6 +1775,16 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" + integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== + dependencies: + got "^12.1.0" + registry-auth-token "^5.0.1" + registry-url "^6.0.0" + semver "^7.3.7" + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -1920,15 +1792,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" - integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -1953,16 +1817,6 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-is-inside@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz" - integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== - path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" @@ -1993,10 +1847,10 @@ pidtree@^0.6.0: resolved "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz" integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== prettier-linter-helpers@^1.0.0: version "1.0.0" @@ -2005,32 +1859,28 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier-plugin-solidity@^1.0.0-dev.22: - version "1.0.0-dev.23" - resolved "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.0.0-dev.23.tgz" - integrity sha512-440/jZzvtDJcqtoRCQiigo1DYTPAZ85pjNg7gvdd+Lds6QYgID8RyOdygmudzHdFmV2UfENt//A8tzx7iS58GA== +prettier-plugin-solidity@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/prettier-plugin-solidity/-/prettier-plugin-solidity-1.4.1.tgz#8060baf18853a9e34d2e09e47e87b4f19e15afe9" + integrity sha512-Mq8EtfacVZ/0+uDKTtHZGW3Aa7vEbX/BNx63hmVg6YTiTXSiuKP0amj0G6pGwjmLaOfymWh3QgXEZkjQbU8QRg== dependencies: - "@solidity-parser/parser" "^0.14.3" - emoji-regex "^10.1.0" - escape-string-regexp "^4.0.0" - semver "^7.3.7" - solidity-comments-extractor "^0.0.7" - string-width "^4.2.3" + "@solidity-parser/parser" "^0.18.0" + semver "^7.5.4" -prettier@^1.14.3: - version "1.19.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz" - integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +prettier@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== -prettier@^2.7.1: - version "2.7.1" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz" - integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== +prettier@^2.8.3: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" + integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== punycode@^2.1.0: version "2.1.1" @@ -2047,6 +1897,21 @@ quick-lru@^4.0.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +rc@1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" @@ -2083,10 +1948,19 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== +registry-auth-token@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" + integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== + dependencies: + "@pnpm/npm-conf" "^2.1.0" + +registry-url@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" + integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== + dependencies: + rc "1.2.8" require-directory@^2.1.1: version "2.1.1" @@ -2098,6 +1972,11 @@ require-from-string@^2.0.2: resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +resolve-alpn@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + resolve-dir@^1.0.0, resolve-dir@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz" @@ -2111,11 +1990,6 @@ resolve-from@5.0.0, resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz" - integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -2137,13 +2011,12 @@ resolve@^1.10.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz" - integrity sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q== +responselike@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" + integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" + lowercase-keys "^3.0.0" restore-cursor@^3.1.0: version "3.1.0" @@ -2158,25 +2031,11 @@ rfdc@^1.3.0: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -run-async@^2.2.0, run-async@^2.4.0: +run-async@^2.4.0: version "2.4.1" resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== -rxjs@^6.4.0: - version "6.6.7" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - rxjs@^7.5.5: version "7.5.6" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz" @@ -2194,7 +2053,7 @@ safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.5.1: +"semver@2 || 3 || 4 || 5": version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -2206,17 +2065,10 @@ semver@7.3.7, semver@^7.3.4, semver@^7.3.7: dependencies: lru-cache "^6.0.0" -semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" - integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== - dependencies: - shebang-regex "^1.0.0" +semver@^7.5.2, semver@^7.5.4: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== shebang-command@^2.0.0: version "2.0.0" @@ -2225,11 +2077,6 @@ shebang-command@^2.0.0: dependencies: shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" - integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== - shebang-regex@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" @@ -2240,15 +2087,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz" @@ -2275,39 +2113,39 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -solhint-plugin-prettier@^0.0.5: - version "0.0.5" - resolved "https://registry.npmjs.org/solhint-plugin-prettier/-/solhint-plugin-prettier-0.0.5.tgz" - integrity sha512-7jmWcnVshIrO2FFinIvDQmhQpfpS2rRRn3RejiYgnjIE68xO2bvrYvjqVNfrio4xH9ghOqn83tKuTzLjEbmGIA== +solhint-plugin-prettier@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/solhint-plugin-prettier/-/solhint-plugin-prettier-0.1.0.tgz#2f46999e26d6c6bc80281c22a7a21e381175bef7" + integrity sha512-SDOTSM6tZxZ6hamrzl3GUgzF77FM6jZplgL2plFBclj/OjKP8Z3eIPojKU73gRr0MvOS8ACZILn8a5g0VTz/Gw== dependencies: + "@prettier/sync" "^0.3.0" prettier-linter-helpers "^1.0.0" -solhint@^3.3.7: - version "3.3.7" - resolved "https://registry.npmjs.org/solhint/-/solhint-3.3.7.tgz" - integrity sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ== - dependencies: - "@solidity-parser/parser" "^0.14.1" - ajv "^6.6.1" - antlr4 "4.7.1" - ast-parents "0.0.1" - chalk "^2.4.2" - commander "2.18.0" - cosmiconfig "^5.0.7" - eslint "^5.6.0" - fast-diff "^1.1.2" - glob "^7.1.3" - ignore "^4.0.6" - js-yaml "^3.12.0" - lodash "^4.17.11" - semver "^6.3.0" +solhint@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/solhint/-/solhint-5.0.3.tgz#b57f6d2534fe09a60f9db1b92e834363edd1cbde" + integrity sha512-OLCH6qm/mZTCpplTXzXTJGId1zrtNuDYP5c2e6snIv/hdRVxPfBBz/bAlL91bY/Accavkayp2Zp2BaDSrLVXTQ== + dependencies: + "@solidity-parser/parser" "^0.18.0" + ajv "^6.12.6" + antlr4 "^4.13.1-patch-1" + ast-parents "^0.0.1" + chalk "^4.1.2" + commander "^10.0.0" + cosmiconfig "^8.0.0" + fast-diff "^1.2.0" + glob "^8.0.3" + ignore "^5.2.4" + js-yaml "^4.1.0" + latest-version "^7.0.0" + lodash "^4.17.21" + pluralize "^8.0.0" + semver "^7.5.2" + strip-ansi "^6.0.1" + table "^6.8.1" + text-table "^0.2.0" optionalDependencies: - prettier "^1.14.3" - -solidity-comments-extractor@^0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.7.tgz" - integrity sha512-wciNMLg/Irp8OKGrh3S2tfvZiZ0NEyILfcRCXCD4mp7SgK/i9gzLfhY2hY7VMCQJ3kH9UB9BzNdibIVMchzyYw== + prettier "^2.8.3" spdx-correct@^3.0.0: version "3.1.1" @@ -2342,33 +2180,11 @@ split2@^3.0.0: dependencies: readable-stream "^3.0.0" -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - string-argv@^0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -string-width@^2.1.0: - version "2.1.1" - resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -2394,20 +2210,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" - integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.1.0: - version "5.2.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -2449,9 +2251,9 @@ strip-json-comments@3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@^2.0.1: +strip-json-comments@~2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== supports-color@^5.3.0: @@ -2473,15 +2275,16 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -table@^5.2.3: - version "5.4.6" - resolved "https://registry.npmjs.org/table/-/table-5.4.6.tgz" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== +table@^6.8.1: + version "6.8.2" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" + integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" text-extensions@^1.0.0: version "1.9.0" @@ -2543,23 +2346,11 @@ ts-node@^10.8.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.1.0: version "2.4.0" resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - type-fest@^0.18.0: version "0.18.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" @@ -2622,7 +2413,7 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -which@^1.2.14, which@^1.2.9: +which@^1.2.14: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -2636,7 +2427,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@^1.0.3, word-wrap@~1.2.3: +word-wrap@^1.0.3: version "1.2.3" resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -2664,13 +2455,6 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write@1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/write/-/write-1.0.3.tgz" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - y18n@^5.0.5: version "5.0.8" resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" From d916e50921ad754d13412636842abf903717f4fa Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:56:38 +0200 Subject: [PATCH 12/38] Remove the need for `--via-ir` in our contracts (#502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description After getting more and more annoyed with the CI time and other small issues around this I decided to really understand how big is our need for `--via-ir` compilation. Turns out it's really small. I mean really small. There are just a couple of places where this is causing issues: 1. `Airgrab#claim` has too many variables. I fixed this with a `struct` for the fractal proof, and an internal function to do the lock. Changing it is ok in my opinion. If we want to reuse this contract we'll have to redeploy it anyway, we have a tag for the version when it was deployed, all is well. 2. `GovernanceFactory#createGovernance` has too many variables. I just broke it down in internal functions. Again, I don't see a problem in changing this, even if it's deployed and etc. 3. `StableTokenV2#initialize` has too many arguments. When we made StableTokenV2 we wanted to keep the same interface for the `initialize` method to keep the interfaces between stable tokens consistent, but it was actually a bad call. There's no good reason why we have it that way and it's a method that can anyway only be called once. I would say it's ok to also change this without making a StableTokenV3 for it. Basically we will have a few contracts which will have a different signature for the initialize method that anyway can't be called anymore. But this is the one change the feels least nice. 4. A couple of tests with many variables where I used structs for grouping or broke down into internal functions. What happens afterwards, well: ``` ╰─ forge test [⠊] Compiling... [⠒] Compiling 64 files with Solc 0.5.17 [⠢] Compiling 55 files with Solc 0.8.26 [⠒] Compiling 223 files with Solc 0.8.18 [⠒] Solc 0.5.17 finished in 307.90ms [⠘] Solc 0.8.26 finished in 1.75s [⠒] Solc 0.8.18 finished in 5.50s Compiler run successful with warnings: ``` The whole fucking shabang runs in 5.5s. Jesus. CI takes ~1minute instead of ~15minutes. P.S. Thank you Claude for the bash script. ### Other changes It changed my life. ### Tested Thoroughly. ### Related issues Sleep deprivation. ### Backwards compatibility Who are you calling backward? ### Documentation I read it, yes. --------- Co-authored-by: Bayological <6872903+bayological@users.noreply.github.com> Co-authored-by: chapati Co-authored-by: philbow61 <80156619+philbow61@users.noreply.github.com> Co-authored-by: baroooo --- .github/workflows/lint_test.yaml | 6 +- .prettierrc.yml | 11 +- bin/check-contracts.sh | 65 ++++++++ contracts/governance/Airgrab.sol | 62 +++---- contracts/governance/GovernanceFactory.sol | 155 +++++++++++++----- contracts/interfaces/IStableTokenV2.sol | 7 +- .../IStableTokenV2DeprecatedInit.sol | 27 +++ contracts/tokens/StableTokenV2.sol | 7 +- foundry.toml | 19 +-- package.json | 4 +- test/fork/ChainForkTest.sol | 22 ++- .../governance/GovernanceIntegration.t.sol | 99 ++++++----- test/integration/governance/Proposals.sol | 84 +++++----- test/integration/protocol/ProtocolTest.sol | 36 +--- test/unit/governance/Airgrab.t.sol | 10 +- test/unit/tokens/StableTokenV2.t.sol | 27 +-- 16 files changed, 386 insertions(+), 255 deletions(-) create mode 100755 bin/check-contracts.sh create mode 100644 contracts/interfaces/IStableTokenV2DeprecatedInit.sol diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index b71d29c2..3fd28969 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -47,10 +47,8 @@ jobs: - name: "Run the tests" run: "forge test" - - name: "Build the contracts" - run: | - forge --version - forge build --sizes --skip test/**/* + - name: "Check contract sizes" + run: "yarn run check-contract-sizes" - name: "Add test summary" run: | diff --git a/.prettierrc.yml b/.prettierrc.yml index c33ae4d0..028dcf34 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -6,12 +6,21 @@ singleQuote: false tabWidth: 2 trailingComma: all -plugins: ["prettier-plugin-solidity"] +plugins: [prettier-plugin-solidity] overrides: - files: ["*.sol"] options: compiler: 0.5.17 + - files: [contracts/interfaces/*.sol] + options: + compiler: 0.8.18 + - files: + - contracts/interfaces/IBrokerAdmin.sol + - contracts/interfaces/ICeloToken.sol + - contracts/interfaces/IExchange.sol + options: + compiler: 0.5.17 - files: [contracts/tokens/patched/*.sol] options: compiler: 0.8.18 diff --git a/bin/check-contracts.sh b/bin/check-contracts.sh new file mode 100755 index 00000000..6b89269b --- /dev/null +++ b/bin/check-contracts.sh @@ -0,0 +1,65 @@ +#!/bin/bash +############################################################################## +# Sometimes when hitting AST compiler errors, the output doesn't tell you +# what file is causing the issue. This script helps you identify which +# contract is failing by compiling each contract individually. +############################################################################## + +# Get all contract files +IFS=$'\n' read -r -d '' -a contract_files < <(find contracts test -name "*.sol" && printf '\0') + +# Initialize an array to store contracts that need --via-ir +via_ir_contracts=() + +# Function to check if a contract builds without --via-ir +check_contract() { + local target_contract=$1 + local skip_contracts=() + + for contract in "${contract_files[@]}"; do + if [ "$contract" != "$target_contract" ]; then + skip_contracts+=("$contract") + fi + done + + forge clean + if forge build --skip ${skip_contracts[*]}; then + return 0 + else + return 1 + fi +} + +# Iterate through each contract +for contract in "${contract_files[@]}"; do + echo "----------------------------------------" + echo "Checking $contract..." + if check_contract "$contract"; then + echo "$contract does not require --via-ir" + else + echo "$contract requires --via-ir" + via_ir_contracts+=("$contract") + fi + echo "----------------------------------------" + echo +done + +# Print the results +if [ ${#via_ir_contracts[@]} -eq 0 ]; then + echo "All contracts can be built without --via-ir." +else + echo "The following contracts require --via-ir:" + printf '%s\n' "${via_ir_contracts[@]}" + echo + echo "Use the following command to build:" + echo -n "forge build --via-ir --skip " + + contracts_to_skip=() + for contract in "${contract_files[@]}"; do + if [[ ! " ${via_ir_contracts[*]} " =~ " ${contract} " ]]; then + contracts_to_skip+=("$contract") + fi + done + + echo "${contracts_to_skip[*]} test/**/*" +fi diff --git a/contracts/governance/Airgrab.sol b/contracts/governance/Airgrab.sol index 424b0920..6629364b 100644 --- a/contracts/governance/Airgrab.sol +++ b/contracts/governance/Airgrab.sol @@ -25,6 +25,20 @@ contract Airgrab is ReentrancyGuard { uint32 public constant MAX_CLIFF_PERIOD = 103; uint32 public constant MAX_SLOPE_PERIOD = 104; + /** + * @notice FractalProof struct for KYC/KYB verification. + * @param sig The signature of the Fractal Credential. + * @param validUntil The timestamp when the Fractal Credential expires. + * @param approvedAt The timestamp when the Fractal Credential was approved. + * @param fractalId The Fractal Credential ID. + */ + struct FractalProof { + bytes sig; + uint256 validUntil; + uint256 approvedAt; + string fractalId; + } + /** * @notice Emitted when tokens are claimed * @param claimer The account claiming the tokens @@ -68,31 +82,22 @@ contract Airgrab is ReentrancyGuard { * https://github.com/trustfractal/credentials-api-verifiers * @notice This function checks the kyc signature with the data provided. * @param account The address of the account to check. - * @param proof The kyc proof for the account. - * @param validUntil The kyc proof valid until timestamp. - * @param approvedAt The kyc proof approved at timestamp. - * @param fractalId The kyc proof fractal id. + * @param proof FractaLProof kyc proof data for the account. */ - modifier hasValidKyc( - address account, - bytes memory proof, - uint256 validUntil, - uint256 approvedAt, - string memory fractalId - ) { - require(block.timestamp < validUntil, "Airgrab: KYC no longer valid"); - require(fractalMaxAge == 0 || block.timestamp < approvedAt + fractalMaxAge, "Airgrab: KYC not recent enough"); + modifier hasValidKyc(address account, FractalProof memory proof) { + require(block.timestamp < proof.validUntil, "Airgrab: KYC no longer valid"); + require(fractalMaxAge == 0 || block.timestamp < proof.approvedAt + fractalMaxAge, "Airgrab: KYC not recent enough"); string memory accountString = Strings.toHexString(uint256(uint160(account)), 20); bytes32 signedMessageHash = ECDSA.toEthSignedMessageHash( abi.encodePacked( accountString, ";", - fractalId, + proof.fractalId, ";", - Strings.toString(approvedAt), + Strings.toString(proof.approvedAt), ";", - Strings.toString(validUntil), + Strings.toString(proof.validUntil), ";", // ISO 3166-1 alpha-2 country codes // DRC, CUBA, GB, IRAN, DPKR, MALI, MYANMAR, SOUTH SUDAN, SYRIA, US, YEMEN @@ -100,7 +105,7 @@ contract Airgrab is ReentrancyGuard { ) ); - require(SignatureChecker.isValidSignatureNow(fractalSigner, signedMessageHash, proof), "Airgrab: Invalid KYC"); + require(SignatureChecker.isValidSignatureNow(fractalSigner, signedMessageHash, proof.sig), "Airgrab: Invalid KYC"); _; } @@ -178,26 +183,23 @@ contract Airgrab is ReentrancyGuard { * @param delegate The address of the account that gets voting power delegated * @param merkleProof The merkle proof for the account. * @param fractalProof The Fractal KYC proof for the account. - * @param fractalProofValidUntil The Fractal KYC proof valid until timestamp. - * @param fractalProofApprovedAt The Fractal KYC proof approved at timestamp. - * @param fractalId The Fractal KYC ID. */ function claim( uint96 amount, address delegate, bytes32[] calldata merkleProof, - bytes calldata fractalProof, - uint256 fractalProofValidUntil, - uint256 fractalProofApprovedAt, - string memory fractalId - ) - external - hasValidKyc(msg.sender, fractalProof, fractalProofValidUntil, fractalProofApprovedAt, fractalId) - canClaim(msg.sender, amount, merkleProof) - nonReentrant - { + FractalProof calldata fractalProof + ) external hasValidKyc(msg.sender, fractalProof) canClaim(msg.sender, amount, merkleProof) nonReentrant { require(token.balanceOf(address(this)) >= amount, "Airgrab: insufficient balance"); + _claim(amount, delegate); + } + /** + * @dev Internal function to claim tokens and lock them. + * @param amount The amount of tokens to be claimed. + * @param delegate The address of the account that gets voting power delegated + */ + function _claim(uint96 amount, address delegate) internal { claimed[msg.sender] = true; uint256 lockId = locking.lock(msg.sender, delegate, amount, slopePeriod, cliffPeriod); emit TokensClaimed(msg.sender, amount, lockId); diff --git a/contracts/governance/GovernanceFactory.sol b/contracts/governance/GovernanceFactory.sol index 736cb844..7ea85a5d 100644 --- a/contracts/governance/GovernanceFactory.sol +++ b/contracts/governance/GovernanceFactory.sol @@ -51,6 +51,16 @@ contract GovernanceFactory is Ownable { uint256[] additionalAllocationAmounts; } + /// @dev Precalculated addresses by nonce for the contracts to be deployed + struct PrecalculatedAddresses { + address mentoToken; + address emission; + address airgrab; + address locking; + address governanceTimelock; + address mentoGovernor; + } + ProxyAdmin public proxyAdmin; MentoToken public mentoToken; Emission public emission; @@ -109,22 +119,47 @@ contract GovernanceFactory is Ownable { // slither-disable-next-line missing-zero-check watchdogMultiSig = watchdogMultiSig_; - // Precalculated contract addresses: - address tokenPrecalculated = addressForNonce(2); - address emissionPrecalculated = addressForNonce(4); - address airgrabPrecalculated = addressForNonce(5); - address lockingPrecalculated = addressForNonce(7); - address governanceTimelockPrecalculated = addressForNonce(9); - address governorPrecalculated = addressForNonce(11); + PrecalculatedAddresses memory addr = getPrecalculatedAddresses(); - address[] memory owners = new address[](1); - owners[0] = governanceTimelockPrecalculated; + deployProxyAdmin(); + deployMentoToken(allocationParams, addr); + deployEmission(addr); + deployAirgrab(airgrabRoot, fractalSigner, addr); + deployLocking(addr); + deployTimelock(addr); + deployMentoGovernor(addr); + transferOwnership(); + + emit GovernanceCreated( + address(proxyAdmin), + address(emission), + address(mentoToken), + address(airgrab), + address(locking), + address(governanceTimelock), + address(mentoGovernor) + ); + } + /** + * @notice Deploys the ProxyAdmin contract. + */ + function deployProxyAdmin() internal { // ========================================= // ========== Deploy 1: ProxyAdmin ========= // ========================================= proxyAdmin = ProxyDeployerLib.deployAdmin(); // NONCE:1 + } + /** + * @notice Deploys the MentoToken contract. + * @param allocationParams Parameters for the initial token allocation + * @param addr Precalculated addresses for the contracts to be deployed. + */ + function deployMentoToken( + MentoTokenAllocationParams memory allocationParams, + PrecalculatedAddresses memory addr + ) internal { // =========================================== // ========== Deploy 2: MentoToken =========== // =========================================== @@ -132,9 +167,9 @@ contract GovernanceFactory is Ownable { address[] memory allocationRecipients = new address[](numberOfRecipients); uint256[] memory allocationAmounts = new uint256[](numberOfRecipients); - allocationRecipients[0] = airgrabPrecalculated; + allocationRecipients[0] = addr.airgrab; allocationAmounts[0] = allocationParams.airgrabAllocation; - allocationRecipients[1] = governanceTimelockPrecalculated; + allocationRecipients[1] = addr.governanceTimelock; allocationAmounts[1] = allocationParams.mentoTreasuryAllocation; for (uint256 i = 0; i < allocationParams.additionalAllocationRecipients.length; i++) { @@ -142,15 +177,16 @@ contract GovernanceFactory is Ownable { allocationAmounts[i + 2] = allocationParams.additionalAllocationAmounts[i]; } - mentoToken = MentoTokenDeployerLib.deploy( // NONCE:2 - allocationRecipients, - allocationAmounts, - emissionPrecalculated, - lockingPrecalculated - ); + mentoToken = MentoTokenDeployerLib.deploy(allocationRecipients, allocationAmounts, addr.emission, addr.locking); // NONCE:2 - assert(address(mentoToken) == tokenPrecalculated); + assert(address(mentoToken) == addr.mentoToken); + } + /** + * @notice Deploys the Emission contract. + * @param addr Precalculated addresses for the contracts to be deployed. + */ + function deployEmission(PrecalculatedAddresses memory addr) internal { // ========================================= // ========== Deploy 3: Emission =========== // ========================================= @@ -161,15 +197,23 @@ contract GovernanceFactory is Ownable { address(proxyAdmin), abi.encodeWithSelector( emissionImpl.initialize.selector, - tokenPrecalculated, /// @param mentoToken_ The address of the MentoToken contract. - governanceTimelockPrecalculated, /// @param governanceTimelock_ The address of the mento treasury contract. + addr.mentoToken, /// @param mentoToken_ The address of the MentoToken contract. + addr.governanceTimelock, /// @param governanceTimelock_ The address of the mento treasury contract. mentoToken.emissionSupply() /// @param emissionSupply_ The total amount of tokens that can be emitted. ) ); emission = Emission(address(emissionProxy)); - assert(address(emission) == emissionPrecalculated); + assert(address(emission) == addr.emission); + } + /** + * @notice Deploys the Airgrab contract. + * @param airgrabRoot Root hash for the airgrab Merkle tree. + * @param fractalSigner Signer of fractal kyc. + * @param addr Precalculated addresses for the contracts to be deployed. + */ + function deployAirgrab(bytes32 airgrabRoot, address fractalSigner, PrecalculatedAddresses memory addr) internal { // ======================================== // ========== Deploy 4: Airgrab =========== // ======================================== @@ -182,12 +226,18 @@ contract GovernanceFactory is Ownable { airgrabEnds, AIRGRAB_LOCK_CLIFF, AIRGRAB_LOCK_SLOPE, - tokenPrecalculated, - lockingPrecalculated, - payable(governanceTimelockPrecalculated) + addr.mentoToken, + addr.locking, + payable(addr.governanceTimelock) ); - assert(address(airgrab) == airgrabPrecalculated); + assert(address(airgrab) == addr.airgrab); + } + /** + * @notice Deploys the Locking contract. + * @param addr Precalculated addresses for the contracts to be deployed. + */ + function deployLocking(PrecalculatedAddresses memory addr) internal { // ========================================== // ========== Deploy 5-6: Locking =========== // ========================================== @@ -206,8 +256,14 @@ contract GovernanceFactory is Ownable { ) ); locking = Locking(address(lockingProxy)); - assert(address(locking) == lockingPrecalculated); + assert(address(locking) == addr.locking); + } + /** + * @notice Deploys the Timelock Controller and Governance Timelock contracts. + * @param addr Precalculated addresses for the contracts to be deployed. + */ + function deployTimelock(PrecalculatedAddresses memory addr) internal { // =================================================================== // ========== Deploy 7: Timelock Controller Implementation =========== // =================================================================== @@ -219,7 +275,7 @@ contract GovernanceFactory is Ownable { // ==================================================== address[] memory governanceProposers = new address[](1); address[] memory governanceExecutors = new address[](1); - governanceProposers[0] = governorPrecalculated; // Only MentoGovernor can propose + governanceProposers[0] = addr.mentoGovernor; // Only MentoGovernor can propose governanceExecutors[0] = address(0); // Anyone can execute passed proposals // slither-disable-next-line reentrancy-benign @@ -236,8 +292,14 @@ contract GovernanceFactory is Ownable { ) ); governanceTimelock = TimelockController(payable(governanceTimelockProxy)); - assert(address(governanceTimelock) == governanceTimelockPrecalculated); + assert(address(governanceTimelock) == addr.governanceTimelock); + } + /** + * @notice Deploys the MentoGovernor contract. + * @param addr Precalculated addresses for the contracts to be deployed. + */ + function deployMentoGovernor(PrecalculatedAddresses memory addr) internal { // ================================================== // ========== Deploy 9-10: Mento Governor =========== // ================================================== @@ -248,17 +310,24 @@ contract GovernanceFactory is Ownable { address(proxyAdmin), abi.encodeWithSelector( mentoGovernorImpl.__MentoGovernor_init.selector, - address(lockingProxy), /// @param veToken The escrowed Mento Token used for voting. - governanceTimelockProxy, /// @param timelockController The timelock controller used by the governor. + address(locking), /// @param veToken The escrowed Mento Token used for voting. + address(governanceTimelock), /// @param timelockController The timelock controller used by the governor. GOVERNOR_VOTING_DELAY, /// @param votingDelay_ The delay time in blocks between the proposal creation and the start of voting. GOVERNOR_VOTING_PERIOD, /// @param votingPeriod_ The voting duration in blocks between the vote start and vote end. GOVERNOR_PROPOSAL_THRESHOLD, /// @param threshold_ The number of votes required in order for a voter to become a proposer. GOVERNOR_QUORUM /// @param quorum_ The minimum number of votes in percent of total supply required in order for a proposal to succeed. ) ); + + // slither-disable-next-line reentrancy-benign mentoGovernor = MentoGovernor(payable(mentoGovernorProxy)); - assert(address(mentoGovernor) == governorPrecalculated); + assert(address(mentoGovernor) == addr.mentoGovernor); + } + /** + * @notice Transfers the ownership of the contracts to the governance timelock. + */ + function transferOwnership() internal { // ============================================= // =========== Configure Ownership ============= // ============================================= @@ -266,16 +335,22 @@ contract GovernanceFactory is Ownable { locking.transferOwnership(address(governanceTimelock)); proxyAdmin.transferOwnership(address(governanceTimelock)); mentoToken.transferOwnership(address(governanceTimelock)); + } - emit GovernanceCreated( - address(proxyAdmin), - address(emission), - address(mentoToken), - address(airgrab), - address(locking), - address(governanceTimelock), - address(mentoGovernor) - ); + /** + * @notice Returns the precalculated addresses for the contracts to be deployed. + * @return The precalculated addresses. + */ + function getPrecalculatedAddresses() internal view returns (PrecalculatedAddresses memory) { + return + PrecalculatedAddresses({ + mentoToken: addressForNonce(2), + emission: addressForNonce(4), + airgrab: addressForNonce(5), + locking: addressForNonce(7), + governanceTimelock: addressForNonce(9), + mentoGovernor: addressForNonce(11) + }); } /** diff --git a/contracts/interfaces/IStableTokenV2.sol b/contracts/interfaces/IStableTokenV2.sol index c6e0e613..cbf49418 100644 --- a/contracts/interfaces/IStableTokenV2.sol +++ b/contracts/interfaces/IStableTokenV2.sol @@ -54,13 +54,8 @@ interface IStableTokenV2 { function initialize( string calldata _name, string calldata _symbol, - uint8, // deprecated: decimals - address, // deprecated: registryAddress, - uint256, // deprecated: inflationRate, - uint256, // deprecated: inflationFactorUpdatePeriod, address[] calldata initialBalanceAddresses, - uint256[] calldata initialBalanceValues, - string calldata // deprecated: exchangeIdentifier + uint256[] calldata initialBalanceValues ) external; /** diff --git a/contracts/interfaces/IStableTokenV2DeprecatedInit.sol b/contracts/interfaces/IStableTokenV2DeprecatedInit.sol new file mode 100644 index 00000000..c852b791 --- /dev/null +++ b/contracts/interfaces/IStableTokenV2DeprecatedInit.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { IStableTokenV2 } from "./IStableTokenV2.sol"; + +/** + * @title IStableTokenV2DeprecatedInit + * @notice Interface for the deprecated initialize function in StableTokenV2 + * @dev In order to improve our DX and get rid of `via-ir` interfaces we + * are deprecating the old initialize function in favor of the new one. + * Keeping this interface for backwards compatibility, in fork tests, + * because in practice we will never be able to call this function again, anyway. + * More details: https://github.com/mento-protocol/mento-core/pull/502 + */ +interface IStableTokenV2DeprecatedInit is IStableTokenV2 { + function initialize( + string calldata _name, + string calldata _symbol, + uint8, // deprecated: decimals + address, // deprecated: registryAddress, + uint256, // deprecated: inflationRate, + uint256, // deprecated: inflationFactorUpdatePeriod, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues, + string calldata // deprecated: exchangeIdentifier + ) external; +} diff --git a/contracts/tokens/StableTokenV2.sol b/contracts/tokens/StableTokenV2.sol index 54f9ab5f..edcb9f34 100644 --- a/contracts/tokens/StableTokenV2.sol +++ b/contracts/tokens/StableTokenV2.sol @@ -75,13 +75,8 @@ contract StableTokenV2 is ERC20PermitUpgradeable, IStableTokenV2, CalledByVm { string calldata _name, string calldata _symbol, // slither-disable-end shadowing-local - uint8, // deprecated: decimals - address, // deprecated: registryAddress, - uint256, // deprecated: inflationRate, - uint256, // deprecated: inflationFactorUpdatePeriod, address[] calldata initialBalanceAddresses, - uint256[] calldata initialBalanceValues, - string calldata // deprecated: exchangeIdentifier + uint256[] calldata initialBalanceValues ) external initializer { __ERC20_init_unchained(_name, _symbol); __ERC20Permit_init(_symbol); diff --git a/foundry.toml b/foundry.toml index b631be6f..9c05e199 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,6 @@ bytecode_hash = "none" fuzz_runs = 256 gas_reports = ["*"] optimizer = false -optimizer_runs = 200 legacy = true no_match_contract = "(ForkTest)|(GovernanceGasTest)" @@ -18,25 +17,17 @@ allow_paths = [ ] fs_permissions = [ - { access = "read", path = "out" }, - { access = "read-write", path = "test/fixtures" } -] - -additional_compiler_profiles = [ - { name = "via-ir-opt", via_ir = true, optimizer = true } -] - -compilation_restrictions = [ - { paths = "contracts/governance/Airgrab.sol", via_ir = true, optimizer = true }, - { paths = "contracts/tokens/StableTokenV2.sol", via_ir = true, optimizer = true }, + { access = "read", path = "out" } ] [profile.ci] -via_ir=true -optimizer=true fuzz_runs = 1_000 verbosity = 3 +[profile.optimized] +optimizer = true +optimizer_runs = 200 + [profile.fork-tests] no_match_contract = "_random" # in order to reset the no_match_contract match_contract = "ForkTest" diff --git a/package.json b/package.json index 7170c96e..a5513317 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "fork-test": "env FOUNDRY_PROFILE=fork-tests forge test", "fork-test:baklava": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Baklava", "fork-test:alfajores": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores", - "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract CeloMainnet" + "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract CeloMainnet", + "check-no-ir": "./bin/check-contracts.sh", + "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip test/**/*" }, "dependencies": { "@celo/contracts": "^11.0.0" diff --git a/test/fork/ChainForkTest.sol b/test/fork/ChainForkTest.sol index 0b9b802a..1617f299 100644 --- a/test/fork/ChainForkTest.sol +++ b/test/fork/ChainForkTest.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8; import "./BaseForkTest.sol"; import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; -import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; +import { IStableTokenV2DeprecatedInit } from "contracts/interfaces/IStableTokenV2DeprecatedInit.sol"; contract ChainForkTest is BaseForkTest { using FixidityLib for FixidityLib.Fraction; @@ -58,11 +58,21 @@ contract ChainForkTest is BaseForkTest { } function test_stableTokensCanNotBeReinitialized() public { - IStableTokenV2 stableToken = IStableTokenV2(registry.getAddressForStringOrDie("StableToken")); - IStableTokenV2 stableTokenEUR = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenEUR")); - IStableTokenV2 stableTokenBRL = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenBRL")); - IStableTokenV2 stableTokenXOF = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenXOF")); - IStableTokenV2 stableTokenKES = IStableTokenV2(registry.getAddressForStringOrDie("StableTokenKES")); + IStableTokenV2DeprecatedInit stableToken = IStableTokenV2DeprecatedInit( + registry.getAddressForStringOrDie("StableToken") + ); + IStableTokenV2DeprecatedInit stableTokenEUR = IStableTokenV2DeprecatedInit( + registry.getAddressForStringOrDie("StableTokenEUR") + ); + IStableTokenV2DeprecatedInit stableTokenBRL = IStableTokenV2DeprecatedInit( + registry.getAddressForStringOrDie("StableTokenBRL") + ); + IStableTokenV2DeprecatedInit stableTokenXOF = IStableTokenV2DeprecatedInit( + registry.getAddressForStringOrDie("StableTokenXOF") + ); + IStableTokenV2DeprecatedInit stableTokenKES = IStableTokenV2DeprecatedInit( + registry.getAddressForStringOrDie("StableTokenKES") + ); vm.expectRevert("Initializable: contract is already initialized"); stableToken.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); diff --git a/test/integration/governance/GovernanceIntegration.t.sol b/test/integration/governance/GovernanceIntegration.t.sol index adaede52..7d519768 100644 --- a/test/integration/governance/GovernanceIntegration.t.sol +++ b/test/integration/governance/GovernanceIntegration.t.sol @@ -294,16 +294,16 @@ contract GovernanceIntegrationTest is GovernanceTest { bytes[] memory calldatas, string memory description ) = Proposals._proposeChangeSettings( - mentoGovernor, - governanceTimelock, - locking, - newVotingDelay, - newVotingPeriod, - newThreshold, - newQuorum, - newMinDelay, - newMinCliff, - newMinSlope + Proposals.changeSettingsContracts(mentoGovernor, governanceTimelock, locking), + Proposals.changeSettingsVars( + newVotingDelay, + newVotingPeriod, + newThreshold, + newQuorum, + newMinDelay, + newMinCliff, + newMinSlope + ) ); // ~10 mins @@ -341,16 +341,16 @@ contract GovernanceIntegrationTest is GovernanceTest { vm.prank(alice); vm.expectRevert("Governor: proposer votes below proposal threshold"); Proposals._proposeChangeSettings( - mentoGovernor, - governanceTimelock, - locking, - newVotingDelay, - newVotingPeriod, - newThreshold, - newQuorum, - newMinDelay, - newMinCliff, - newMinSlope + Proposals.changeSettingsContracts(mentoGovernor, governanceTimelock, locking), + Proposals.changeSettingsVars( + newVotingDelay, + newVotingPeriod, + newThreshold, + newQuorum, + newMinDelay, + newMinCliff, + newMinSlope + ) ); // Lock reverts because new min period is higher vm.prank(alice); @@ -382,14 +382,24 @@ contract GovernanceIntegrationTest is GovernanceTest { // slope = 104 // cliff = 0 vm.prank(claimer0); - airgrab.claim(claimer0Amount, claimer0, claimer0Proof, fractalProof0, validUntil, approvedAt, "fractalId"); + airgrab.claim( + claimer0Amount, + claimer0, + claimer0Proof, + Airgrab.FractalProof(fractalProof0, validUntil, approvedAt, "fractalId") + ); // claimer1Amount = 20_000e18 // slope = 104 // cliff = 0 // claim with a delegate vm.prank(claimer1); - airgrab.claim(claimer1Amount, alice, claimer1Proof, fractalProof1, validUntil, approvedAt, "fractalId"); + airgrab.claim( + claimer1Amount, + alice, + claimer1Proof, + Airgrab.FractalProof(fractalProof1, validUntil, approvedAt, "fractalId") + ); // claimed amounts are locked automatically // 100e18 * (103 / 103) = 100e18 @@ -541,16 +551,19 @@ contract GovernanceIntegrationTest is GovernanceTest { function test_governor_propose_whenExecutedForImplementationUpgrade_shouldUpgradeTheContracts() public s_governance { // create new implementations - LockingHarness newLockingContract = new LockingHarness(); - TimelockController newGovernanceTimelockContract = new TimelockController(); - MentoGovernor newGovernorContract = new MentoGovernor(); - Emission newEmissionContract = new Emission(false); + address[] memory newImplementations = addresses( + address(new LockingHarness()), + address(new TimelockController()), + address(new MentoGovernor()), + address(new Emission(false)) + ); - // proxies of current implementations - ITransparentUpgradeableProxy lockingProxy = ITransparentUpgradeableProxy(address(locking)); - ITransparentUpgradeableProxy governanceTimelockProxy = ITransparentUpgradeableProxy(governanceTimelockAddress); - ITransparentUpgradeableProxy mentoGovernorProxy = ITransparentUpgradeableProxy(address(mentoGovernor)); - ITransparentUpgradeableProxy emissionProxy = ITransparentUpgradeableProxy(address(emission)); + address[] memory proxies = addresses( + address(locking), + governanceTimelockAddress, + address(mentoGovernor), + address(emission) + ); vm.prank(alice); ( @@ -559,18 +572,7 @@ contract GovernanceIntegrationTest is GovernanceTest { uint256[] memory values, bytes[] memory calldatas, string memory description - ) = Proposals._proposeUpgradeContracts( - mentoGovernor, - proxyAdmin, - lockingProxy, - governanceTimelockProxy, - mentoGovernorProxy, - emissionProxy, - address(newLockingContract), - address(newGovernanceTimelockContract), - address(newGovernorContract), - address(newEmissionContract) - ); + ) = Proposals._proposeUpgradeContracts(mentoGovernor, proxyAdmin, proxies, newImplementations); // ~10 mins vm.timeTravel(120); @@ -595,13 +597,10 @@ contract GovernanceIntegrationTest is GovernanceTest { mentoGovernor.execute(targets, values, calldatas, keccak256(bytes(description))); - assertEq(address(proxyAdmin.getProxyImplementation(lockingProxy)), address(newLockingContract)); - assertEq( - address(proxyAdmin.getProxyImplementation(governanceTimelockProxy)), - address(newGovernanceTimelockContract) - ); - assertEq(address(proxyAdmin.getProxyImplementation(mentoGovernorProxy)), address(newGovernorContract)); - assertEq(address(proxyAdmin.getProxyImplementation(emissionProxy)), address(newEmissionContract)); + for (uint256 i = 0; i < proxies.length; i++) { + ITransparentUpgradeableProxy proxy = ITransparentUpgradeableProxy(proxies[i]); + assertEq(address(proxyAdmin.getProxyImplementation(proxy)), newImplementations[i]); + } // new implementation has the method and governance upgraded the contract LockingHarness(address(locking)).setEpochShift(1); diff --git a/test/integration/governance/Proposals.sol b/test/integration/governance/Proposals.sol index d0b8b859..da109a39 100644 --- a/test/integration/governance/Proposals.sol +++ b/test/integration/governance/Proposals.sol @@ -5,7 +5,6 @@ pragma solidity 0.8.18; import { uints, addresses, bytesList } from "mento-std/Array.sol"; import { ProxyAdmin } from "openzeppelin-contracts-next/contracts/proxy/transparent/ProxyAdmin.sol"; -import { ITransparentUpgradeableProxy } from "openzeppelin-contracts-next/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { MentoGovernor } from "contracts/governance/MentoGovernor.sol"; import { TimelockController } from "contracts/governance/TimelockController.sol"; @@ -35,17 +34,25 @@ library Proposals { proposalId = mentoGovernor.propose(targets, values, calldatas, description); } + struct changeSettingsVars { + uint256 votingDelay; + uint256 votingPeriod; + uint256 threshold; + uint256 quorum; + uint256 minDelay; + uint32 minCliff; + uint32 minSlope; + } + + struct changeSettingsContracts { + MentoGovernor mentoGovernor; + TimelockController timelockController; + Locking locking; + } + function _proposeChangeSettings( - MentoGovernor mentoGovernor, - TimelockController timelockController, - Locking locking, - uint256 votingDelay, - uint256 votingPeriod, - uint256 threshold, - uint256 quorum, - uint256 minDelay, - uint32 minCliff, - uint32 minSlope + changeSettingsContracts memory _targets, + changeSettingsVars memory vars ) internal returns ( @@ -57,40 +64,34 @@ library Proposals { ) { targets = addresses( - address(mentoGovernor), - address(mentoGovernor), - address(mentoGovernor), - address(mentoGovernor), - address(timelockController), - address(locking), - address(locking) + address(_targets.mentoGovernor), + address(_targets.mentoGovernor), + address(_targets.mentoGovernor), + address(_targets.mentoGovernor), + address(_targets.timelockController), + address(_targets.locking), + address(_targets.locking) ); values = uints(0, 0, 0, 0, 0, 0, 0); calldatas = bytesList( - abi.encodeWithSelector(mentoGovernor.setVotingDelay.selector, votingDelay), - abi.encodeWithSelector(mentoGovernor.setVotingPeriod.selector, votingPeriod), - abi.encodeWithSelector(mentoGovernor.setProposalThreshold.selector, threshold), - abi.encodeWithSelector(mentoGovernor.updateQuorumNumerator.selector, quorum), - abi.encodeWithSelector(timelockController.updateDelay.selector, minDelay), - abi.encodeWithSelector(locking.setMinCliffPeriod.selector, minCliff), - abi.encodeWithSelector(locking.setMinSlopePeriod.selector, minSlope) + abi.encodeWithSelector(_targets.mentoGovernor.setVotingDelay.selector, vars.votingDelay), + abi.encodeWithSelector(_targets.mentoGovernor.setVotingPeriod.selector, vars.votingPeriod), + abi.encodeWithSelector(_targets.mentoGovernor.setProposalThreshold.selector, vars.threshold), + abi.encodeWithSelector(_targets.mentoGovernor.updateQuorumNumerator.selector, vars.quorum), + abi.encodeWithSelector(_targets.timelockController.updateDelay.selector, vars.minDelay), + abi.encodeWithSelector(_targets.locking.setMinCliffPeriod.selector, vars.minCliff), + abi.encodeWithSelector(_targets.locking.setMinSlopePeriod.selector, vars.minSlope) ); description = "Change governance config"; - proposalId = mentoGovernor.propose(targets, values, calldatas, description); + proposalId = _targets.mentoGovernor.propose(targets, values, calldatas, description); } function _proposeUpgradeContracts( MentoGovernor mentoGovernor, ProxyAdmin proxyAdmin, - ITransparentUpgradeableProxy proxy0, - ITransparentUpgradeableProxy proxy1, - ITransparentUpgradeableProxy proxy2, - ITransparentUpgradeableProxy proxy3, - address newImpl0, - address newImpl1, - address newImpl2, - address newImpl3 + address[] memory proxies, + address[] memory newImplementations ) internal returns ( @@ -101,14 +102,13 @@ library Proposals { string memory description ) { - targets = addresses(address(proxyAdmin), address(proxyAdmin), address(proxyAdmin), address(proxyAdmin)); - values = uints(0, 0, 0, 0); - calldatas = bytesList( - abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy0, newImpl0), - abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy1, newImpl1), - abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy2, newImpl2), - abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxy3, newImpl3) - ); + targets = new address[](proxies.length); + calldatas = new bytes[](proxies.length); + values = new uint256[](proxies.length); + for (uint256 i = 0; i < proxies.length; i++) { + targets[i] = address(proxyAdmin); + calldatas[i] = abi.encodeWithSelector(proxyAdmin.upgrade.selector, proxies[i], newImplementations[i]); + } description = "Upgrade upgradeable contracts"; proposalId = mentoGovernor.propose(targets, values, calldatas, description); diff --git a/test/integration/protocol/ProtocolTest.sol b/test/integration/protocol/ProtocolTest.sol index 0e18faee..04058559 100644 --- a/test/integration/protocol/ProtocolTest.sol +++ b/test/integration/protocol/ProtocolTest.sol @@ -97,45 +97,15 @@ contract ProtocolTest is Test, WithRegistry { uint256[] memory initialBalances = new uint256[](0); cUSDToken = IStableTokenV2(deployCode("StableTokenV2", abi.encode(false))); - cUSDToken.initialize( - "cUSD", - "cUSD", - 18, - CELO_REGISTRY_ADDRESS, - FixidityLib.unwrap(FixidityLib.fixed1()), - 60 * 60 * 24 * 7, - initialAddresses, - initialBalances, - "Exchange" - ); + cUSDToken.initialize("cUSD", "cUSD", initialAddresses, initialBalances); cUSDToken.initializeV2(address(broker), address(0x0), address(0x0)); cEURToken = IStableTokenV2(deployCode("StableTokenV2", abi.encode(false))); - cEURToken.initialize( - "cEUR", - "cEUR", - 18, - CELO_REGISTRY_ADDRESS, - FixidityLib.unwrap(FixidityLib.fixed1()), - 60 * 60 * 24 * 7, - initialAddresses, - initialBalances, - "Exchange" - ); + cEURToken.initialize("cEUR", "cEUR", initialAddresses, initialBalances); cEURToken.initializeV2(address(broker), address(0x0), address(0x0)); eXOFToken = IStableTokenV2(deployCode("StableTokenV2", abi.encode(false))); - eXOFToken.initialize( - "eXOF", - "eXOF", - 18, - CELO_REGISTRY_ADDRESS, - FixidityLib.unwrap(FixidityLib.fixed1()), - 60 * 60 * 24 * 7, - initialAddresses, - initialBalances, - "Exchange" - ); + eXOFToken.initialize("eXOF", "eXOF", initialAddresses, initialBalances); eXOFToken.initializeV2(address(broker), address(0x0), address(0x0)); vm.label(address(cUSDToken), "cUSD"); diff --git a/test/unit/governance/Airgrab.t.sol b/test/unit/governance/Airgrab.t.sol index 4f693627..1236d404 100644 --- a/test/unit/governance/Airgrab.t.sol +++ b/test/unit/governance/Airgrab.t.sol @@ -257,10 +257,12 @@ contract AirgrabTest is Test { cl_params.amount, cl_params.delegate, cl_params.merkleProof, - cl_params.fractalProof, - cl_params.fractalProofValidUntil, - cl_params.fractalProofApprovedAt, - cl_params.fractalId + Airgrab.FractalProof( + cl_params.fractalProof, + cl_params.fractalProofValidUntil, + cl_params.fractalProofApprovedAt, + cl_params.fractalId + ) ); } diff --git a/test/unit/tokens/StableTokenV2.t.sol b/test/unit/tokens/StableTokenV2.t.sol index 0909341e..1876c845 100644 --- a/test/unit/tokens/StableTokenV2.t.sol +++ b/test/unit/tokens/StableTokenV2.t.sol @@ -44,13 +44,8 @@ contract StableTokenV2Test is Test { token.initialize( "cUSD", "cUSD", - 0, // deprecated - address(0), // deprecated - 0, // deprecated - 0, // deprecated addresses(holder0, holder1, holder2, broker, exchange), - uints(1000, 1000, 1000, 1000, 1000), - "" // deprecated + uints(1000, 1000, 1000, 1000, 1000) ); token.initializeV2(broker, validators, exchange); } @@ -69,17 +64,7 @@ contract StableTokenV2Test is Test { uint256[] memory initialBalances = new uint256[](0); vm.expectRevert(bytes("Initializable: contract is already initialized")); - disabledToken.initialize( - "cUSD", - "cUSD", - 0, // deprecated - address(0), // deprecated - 0, // deprecated - 0, // deprecated - initialAddresses, - initialBalances, - "" // deprecated - ); + disabledToken.initialize("cUSD", "cUSD", initialAddresses, initialBalances); vm.expectRevert(bytes("Initializable: contract is already initialized")); token.initializeV2(broker, validators, exchange); @@ -263,7 +248,7 @@ contract StableTokenV2Test is Test { assertEq(token.totalSupply(), tokenSupplyBefore + newlyMinted - baseTxFee); } - function test_creditGasFees_whenCalledByVm_withMultiple0xRecipients_shouldBurnTheirRespectiveFees() public { + function test_creditGasFees_whenCalledByVm_withMultiple0xRecipients_shouldBurnTheirRespectiveFees0() public { uint256 refund = 20; uint256 tipTxFee = 30; uint256 gatewayFee = 10; @@ -281,7 +266,13 @@ contract StableTokenV2Test is Test { assertEq(token.balanceOf(gatewayFeeRecipient), 0); assertEq(token.balanceOf(communityFund), 0); assertEq(token.totalSupply(), tokenSupplyBefore0 + newlyMinted0 - gatewayFee - baseTxFee); + } + function test_creditGasFees_whenCalledByVm_withMultiple0xRecipients_shouldBurnTheirRespectiveFees1() public { + uint256 refund = 20; + uint256 tipTxFee = 30; + uint256 gatewayFee = 10; + uint256 baseTxFee = 40; // case with both feeRecipient and communityFund both 0x uint256 holder1InitialBalance = token.balanceOf(holder1); uint256 feeRecipientBalance = token.balanceOf(feeRecipient); From 24a2e680e2780bf800a84dfc266d96e729f0b73e Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:40:48 +0200 Subject: [PATCH 13/38] =?UTF-8?q?Improve=20=F0=9F=8D=B4=20Fork=20Tests=20(?= =?UTF-8?q?#503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Another branch PR on THE BIG ONE. This is a long-overdue cleanup and improvement of our fork tests structure. These tests are important and we will want to maintain and grow them, but the structure they were in made this very hard. They were also terribly brittle and fleaky. The [new structure](https://github.com/mento-protocol/mento-core/blob/81bd18666ea088d6df1cfa461d5f902e36a2c49a/test/fork/README.md) I'm proposing aims to tackle two main issues: - Fork tests were executing assertions for all exchanges in a single test making it hard to debug and follow. - Helpers and utility functions weren't structured and were hard to debug and maintain. Note for reviewers: 99% of the code inside of the functions is just copied from the old structure so I wouldn't do a full review of everything, I would focus on the structure itself. I did make some small tweaks to improve flakiness, for example skipping some of the tests when the limits prevent them, which I think is a fair thing to do, as long as the tests run periodically. ### Other changes I added a CI job for running fork-tests daily on `develop`. ### Tested Yeah, they are, after all, tests. ### Related issues N/A ### Backwards compatibility N/A ### Documentation N/A --------- Co-authored-by: Bayological <6872903+bayological@users.noreply.github.com> Co-authored-by: chapati Co-authored-by: philbow61 <80156619+philbow61@users.noreply.github.com> Co-authored-by: baroooo --- .github/workflows/fork-tests.yaml | 43 ++ contracts/interfaces/IBroker.sol | 4 +- package.json | 2 +- test/fork/BaseForkTest.sol | 78 +-- test/fork/ChainForkTest.sol | 72 ++- test/fork/EnvForkTest.t.sol | 97 --- test/fork/ExchangeForkTest.sol | 214 ++++--- test/fork/ForkTests.t.sol | 102 ++++ test/fork/README.md | 133 ++++ test/fork/TestAsserts.sol | 570 ------------------ test/fork/TokenUpgrade.t.sol | 40 -- test/fork/Utils.sol | 520 ---------------- test/fork/actions/OracleActions.sol | 105 ++++ test/fork/actions/SwapActions.sol | 284 +++++++++ test/fork/actions/all.sol | 7 + .../assertions/CircuitBreakerAssertions.sol | 205 +++++++ test/fork/assertions/SwapAssertions.sol | 220 +++++++ test/fork/helpers/LogHelpers.sol | 79 +++ test/fork/helpers/OracleHelpers.sol | 120 ++++ test/fork/helpers/SwapHelpers.sol | 61 ++ test/fork/helpers/TokenHelpers.sol | 52 ++ test/fork/helpers/TradingLimitHelpers.sol | 180 ++++++ test/fork/helpers/misc.sol | 26 + 23 files changed, 1828 insertions(+), 1386 deletions(-) create mode 100644 .github/workflows/fork-tests.yaml delete mode 100644 test/fork/EnvForkTest.t.sol create mode 100644 test/fork/ForkTests.t.sol create mode 100644 test/fork/README.md delete mode 100644 test/fork/TestAsserts.sol delete mode 100644 test/fork/TokenUpgrade.t.sol delete mode 100644 test/fork/Utils.sol create mode 100644 test/fork/actions/OracleActions.sol create mode 100644 test/fork/actions/SwapActions.sol create mode 100644 test/fork/actions/all.sol create mode 100644 test/fork/assertions/CircuitBreakerAssertions.sol create mode 100644 test/fork/assertions/SwapAssertions.sol create mode 100644 test/fork/helpers/LogHelpers.sol create mode 100644 test/fork/helpers/OracleHelpers.sol create mode 100644 test/fork/helpers/SwapHelpers.sol create mode 100644 test/fork/helpers/TokenHelpers.sol create mode 100644 test/fork/helpers/TradingLimitHelpers.sol create mode 100644 test/fork/helpers/misc.sol diff --git a/.github/workflows/fork-tests.yaml b/.github/workflows/fork-tests.yaml new file mode 100644 index 00000000..29fd2050 --- /dev/null +++ b/.github/workflows/fork-tests.yaml @@ -0,0 +1,43 @@ +name: "ForkTests" + +env: + FOUNDRY_PROFILE: "fork-tests" + ALFAJORES_RPC_URL: ${{secrets.ALFAJORES_RPC_URL}} + CELO_MAINNET_RPC_URL: ${{secrets.CELO_MAINNET_RPC_URL}} + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" # everyday at midnight + +jobs: + test: + name: Run fork tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: "recursive" + ref: develop + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: "Install Node.js" + uses: "actions/setup-node@v3" + with: + cache: "yarn" + node-version: "20" + + - name: "Install the Node.js dependencies" + run: "yarn install --immutable" + + - name: "Show the Foundry config" + run: "forge config" + + - name: "Run the tests" + run: "forge test" + + - name: "Add test summary" + run: | + echo "## Tests" >> $GITHUB_STEP_SUMMARY diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index ae08b675..675713bd 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -114,7 +114,7 @@ interface IBroker { * @dev This can be used by UI or clients to discover all pairs. * @return exchangeProviders the addresses of all exchange providers. */ - function getExchangeProviders() external view returns (address[] memory exchangeProviders); + function getExchangeProviders() external view returns (address[] memory); function burnStableTokens(address token, uint256 amount) external returns (bool); @@ -149,4 +149,6 @@ interface IBroker { function tradingLimitsConfig(bytes32 id) external view returns (ITradingLimits.Config memory); function tradingLimitsState(bytes32 id) external view returns (ITradingLimits.State memory); + + function exchangeProviders(uint256 i) external view returns (address); } diff --git a/package.json b/package.json index a5513317..c6d5b41a 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "fork-test:alfajores": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores", "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract CeloMainnet", "check-no-ir": "./bin/check-contracts.sh", - "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip test/**/*" + "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip \"test/**/*\"" }, "dependencies": { "@celo/contracts": "^11.0.0" diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol index 804440c6..fe97e8a0 100644 --- a/test/fork/BaseForkTest.sol +++ b/test/fork/BaseForkTest.sol @@ -4,21 +4,16 @@ pragma solidity ^0.8; import { Test } from "mento-std/Test.sol"; import { CELO_REGISTRY_ADDRESS } from "mento-std/Constants.sol"; -import { console } from "forge-std/console.sol"; import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; -import { Utils } from "./Utils.sol"; -import { TestAsserts } from "./TestAsserts.sol"; - import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; import { IBroker } from "contracts/interfaces/IBroker.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; -import { IERC20 } from "contracts/interfaces/IERC20.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; +import { toRateFeed } from "./helpers/misc.sol"; interface IMint { function mint(address, uint256) external; @@ -32,19 +27,11 @@ interface IMint { * therfore it doesn't make assumptions about the systems, nor tries to configure * the system to test specific scenarios. * However, it should be exausitve in testing invariants across all tradable pairs - * in the system, therfore each test should. + * in the system, therefore each test should. */ -contract BaseForkTest is Test, TestAsserts { +abstract contract BaseForkTest is Test { using FixidityLib for FixidityLib.Fraction; - using Utils for Utils.Context; - using Utils for uint256; - - struct ExchangeWithProvider { - address exchangeProvider; - IExchangeProvider.Exchange exchange; - } - IRegistry public registry = IRegistry(CELO_REGISTRY_ADDRESS); address governance; @@ -56,12 +43,9 @@ contract BaseForkTest is Test, TestAsserts { address public trader; - ExchangeWithProvider[] public exchanges; - mapping(address => mapping(bytes32 => ExchangeWithProvider)) public exchangeMap; - - uint8 public constant L0 = 1; // 0b001 Limit0 - uint8 public constant L1 = 2; // 0b010 Limit1 - uint8 public constant LG = 4; // 0b100 LimitGlobal + // @dev The number of collateral assets 5 is hardcoded here: + // [CELO, AxelarUSDC, EUROC, NativeUSDC, NativeUSDT] + uint8 public constant COLLATERAL_ASSETS_COUNT = 5; uint256 targetChainId; @@ -77,8 +61,13 @@ contract BaseForkTest is Test, TestAsserts { return addr; } + mapping(address rateFeed => uint8 count) rateFeedDependenciesCount; + function setUp() public virtual { fork(targetChainId); + // @dev Updaing the target fork block every 200 blocks, about ~8 min. + // This means that when running locally RPC calls will be cached. + fork(targetChainId, (block.number / 100) * 100); // The precompile handler needs to be reinitialized after forking. __CeloPrecompiles_init(); @@ -91,43 +80,22 @@ contract BaseForkTest is Test, TestAsserts { trader = makeAddr("trader"); reserve = IReserve(broker.reserve()); - vm.startPrank(trader); - - address[] memory exchangeProviders = broker.getExchangeProviders(); - for (uint256 i = 0; i < exchangeProviders.length; i++) { - vm.label(exchangeProviders[i], "ExchangeProvider"); - IExchangeProvider.Exchange[] memory _exchanges = IExchangeProvider(exchangeProviders[i]).getExchanges(); - for (uint256 j = 0; j < _exchanges.length; j++) { - exchanges.push(ExchangeWithProvider(exchangeProviders[i], _exchanges[j])); - exchangeMap[exchangeProviders[i]][_exchanges[j].exchangeId] = ExchangeWithProvider( - exchangeProviders[i], - _exchanges[j] - ); - } - } - require(exchanges.length > 0, "No exchanges found"); - - // The number of collateral assets 5 is hardcoded here [CELO, AxelarUSDC, EUROC, NativeUSDC, NativeUSDT] - for (uint256 i = 0; i < 5; i++) { - address collateralAsset = reserve.collateralAssets(i); - vm.label(collateralAsset, IERC20(collateralAsset).symbol()); - _deal(collateralAsset, address(reserve), Utils.toSubunits(25_000_000, collateralAsset), true); - console.log("Minting 25mil %s to reserve", IERC20(collateralAsset).symbol()); - } - - console.log("Exchanges(%d): ", exchanges.length); - for (uint256 i = 0; i < exchanges.length; i++) { - Utils.Context memory ctx = Utils.newContext(address(this), i); - console.log("%d | %s | %s", i, ctx.ticker(), ctx.exchangeProvider); - console.logBytes32(ctx.exchange.exchangeId); - } + /// @dev Hardcoded number of dependencies for each ratefeed. + /// Should be updated when they change, there is a test that will + /// validate that. + rateFeedDependenciesCount[lookup("StableTokenXOF")] = 2; + rateFeedDependenciesCount[toRateFeed("EUROCXOF")] = 2; + rateFeedDependenciesCount[toRateFeed("USDCEUR")] = 1; + rateFeedDependenciesCount[toRateFeed("USDCBRL")] = 1; } - function _deal(address asset, address to, uint256 amount, bool updateSupply) public { + function mint(address asset, address to, uint256 amount, bool updateSupply) public { if (asset == lookup("GoldToken")) { - vm.startPrank(address(0)); + if (!updateSupply) { + revert("BaseForkTest: can't mint GoldToken without updating supply"); + } + vm.prank(address(0)); IMint(asset).mint(to, amount); - vm.startPrank(trader); return; } diff --git a/test/fork/ChainForkTest.sol b/test/fork/ChainForkTest.sol index 1617f299..c6f4c321 100644 --- a/test/fork/ChainForkTest.sol +++ b/test/fork/ChainForkTest.sol @@ -2,21 +2,26 @@ // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; +import { console } from "forge-std/console.sol"; import "./BaseForkTest.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; import { IStableTokenV2DeprecatedInit } from "contracts/interfaces/IStableTokenV2DeprecatedInit.sol"; -contract ChainForkTest is BaseForkTest { +abstract contract ChainForkTest is BaseForkTest { using FixidityLib for FixidityLib.Fraction; - using Utils for Utils.Context; - using Utils for uint256; + uint256 expectedExchangeProvidersCount; + uint256[] expectedExchangesCount; - uint256 expectedExchangesCount; - - constructor(uint256 _chainId, uint256 _expectedExchangesCount) BaseForkTest(_chainId) { + constructor( + uint256 _chainId, + uint256 _expectedExchangesProvidersCount, + uint256[] memory _expectedExchangesCount + ) BaseForkTest(_chainId) { expectedExchangesCount = _expectedExchangesCount; + expectedExchangeProvidersCount = _expectedExchangesProvidersCount; } function test_biPoolManagerCanNotBeReinitialized() public { @@ -53,8 +58,35 @@ contract ChainForkTest is BaseForkTest { ); } - function test_testsAreConfigured() public view { - assertEq(expectedExchangesCount, exchanges.length); + /** + * @dev If this fails it means we have added new exchanges + * and haven't updated the fork test configuration which + * can be found in ForkTests.t.sol. + */ + function test_exchangeProvidersAndExchangesCount() public view { + address[] memory exchangeProviders = broker.getExchangeProviders(); + assertEq(expectedExchangeProvidersCount, exchangeProviders.length); + for (uint256 i = 0; i < exchangeProviders.length; i++) { + address exchangeProvider = exchangeProviders[i]; + IBiPoolManager biPoolManager = IBiPoolManager(exchangeProvider); + IExchangeProvider.Exchange[] memory exchanges = biPoolManager.getExchanges(); + assertEq(expectedExchangesCount[i], exchanges.length); + } + } + + /** + * @dev If this fails it means we have added a new collateral + * so we need to update the COLLATERAL_ASSETS constant. + * This is because we don't have an easy way to determine + * the number of collateral assets in the system. + */ + function test_numberCollateralAssetsCount() public { + address collateral; + for (uint256 i = 0; i < COLLATERAL_ASSETS_COUNT; i++) { + collateral = reserve.collateralAssets(i); + } + vm.expectRevert(); + reserve.collateralAssets(COLLATERAL_ASSETS_COUNT); } function test_stableTokensCanNotBeReinitialized() public { @@ -89,4 +121,28 @@ contract ChainForkTest is BaseForkTest { vm.expectRevert("Initializable: contract is already initialized"); stableTokenKES.initialize("", "", 8, address(10), 0, 0, new address[](0), new uint256[](0), ""); } + + function test_rateFeedDependenciesCountIsCorrect() public { + address[] memory rateFeedIds = breakerBox.getRateFeeds(); + for (uint256 i = 0; i < rateFeedIds.length; i++) { + address rateFeedId = rateFeedIds[i]; + uint8 count = rateFeedDependenciesCount[rateFeedId]; + + vm.expectRevert(); + breakerBox.rateFeedDependencies(rateFeedId, count); // end of array + + for (uint256 j = 0; j < count; j++) { + (bool ok, ) = address(breakerBox).staticcall( + abi.encodeWithSelector(breakerBox.rateFeedDependencies.selector, rateFeedId, j) + ); + if (!ok) { + console.log("Dependency missing for rateFeedId=%s, expectedCount=%d, missingIndex=%d", rateFeedId, count, j); + console.log( + "If the configuration has changed, update the rateFeedDependenciesCount mapping in BaseForfTest.sol" + ); + } + require(ok, "rateFeedDependenciesCount out of sync"); + } + } + } } diff --git a/test/fork/EnvForkTest.t.sol b/test/fork/EnvForkTest.t.sol deleted file mode 100644 index e90d0e07..00000000 --- a/test/fork/EnvForkTest.t.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.8; - -import { ChainForkTest } from "./ChainForkTest.sol"; -import { ExchangeForkTest } from "./ExchangeForkTest.sol"; -import { CELO_ID, BAKLAVA_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; - -contract BaklavaChainForkTest is ChainForkTest(BAKLAVA_ID, 14) {} - -contract BaklavaExchangeForkTest0 is ExchangeForkTest(BAKLAVA_ID, 0) {} - -contract BaklavaExchangeForkTest1 is ExchangeForkTest(BAKLAVA_ID, 1) {} - -contract BaklavaExchangeForkTest2 is ExchangeForkTest(BAKLAVA_ID, 2) {} - -contract BaklavaExchangeForkTest3 is ExchangeForkTest(BAKLAVA_ID, 3) {} - -contract BaklavaExchangeForkTest4 is ExchangeForkTest(BAKLAVA_ID, 4) {} - -contract BaklavaExchangeForkTest5 is ExchangeForkTest(BAKLAVA_ID, 5) {} - -contract BaklavaExchangeForkTest6 is ExchangeForkTest(BAKLAVA_ID, 6) {} - -contract BaklavaExchangeForkTest7 is ExchangeForkTest(BAKLAVA_ID, 7) {} - -contract BaklavaExchangeForkTest8 is ExchangeForkTest(BAKLAVA_ID, 8) {} - -contract BaklavaExchangeForkTest9 is ExchangeForkTest(BAKLAVA_ID, 9) {} - -contract BaklavaExchangeForkTest10 is ExchangeForkTest(BAKLAVA_ID, 10) {} - -contract BaklavaExchangeForkTest11 is ExchangeForkTest(BAKLAVA_ID, 11) {} - -contract BaklavaExchangeForkTest12 is ExchangeForkTest(BAKLAVA_ID, 12) {} - -contract BaklavaExchangeForkTest13 is ExchangeForkTest(BAKLAVA_ID, 13) {} - -contract AlfajoresChainForkTest is ChainForkTest(ALFAJORES_ID, 14) {} - -contract AlfajoresExchangeForkTest0 is ExchangeForkTest(ALFAJORES_ID, 0) {} - -contract AlfajoresExchangeForkTest1 is ExchangeForkTest(ALFAJORES_ID, 1) {} - -contract AlfajoresExchangeForkTest2 is ExchangeForkTest(ALFAJORES_ID, 2) {} - -contract AlfajoresExchangeForkTest3 is ExchangeForkTest(ALFAJORES_ID, 3) {} - -contract AlfajoresExchangeForkTest4 is ExchangeForkTest(ALFAJORES_ID, 4) {} - -contract AlfajoresExchangeForkTest5 is ExchangeForkTest(ALFAJORES_ID, 5) {} - -contract AlfajoresExchangeForkTest6 is ExchangeForkTest(ALFAJORES_ID, 6) {} - -contract AlfajoresExchangeForkTest7 is ExchangeForkTest(ALFAJORES_ID, 7) {} - -contract AlfajoresExchangeForkTest8 is ExchangeForkTest(ALFAJORES_ID, 8) {} - -contract AlfajoresExchangeForkTest9 is ExchangeForkTest(ALFAJORES_ID, 9) {} - -contract AlfajoresExchangeForkTest10 is ExchangeForkTest(ALFAJORES_ID, 10) {} - -contract AlfajoresExchangeForkTest11 is ExchangeForkTest(ALFAJORES_ID, 11) {} - -contract AlfajoresExchangeForkTest12 is ExchangeForkTest(ALFAJORES_ID, 12) {} - -contract AlfajoresExchangeForkTest13 is ExchangeForkTest(ALFAJORES_ID, 13) {} - -contract CeloChainForkTest is ChainForkTest(CELO_ID, 14) {} - -contract CeloExchangeForkTest0 is ExchangeForkTest(CELO_ID, 0) {} - -contract CeloExchangeForkTest1 is ExchangeForkTest(CELO_ID, 1) {} - -contract CeloExchangeForkTest2 is ExchangeForkTest(CELO_ID, 2) {} - -contract CeloExchangeForkTest3 is ExchangeForkTest(CELO_ID, 3) {} - -contract CeloExchangeForkTest4 is ExchangeForkTest(CELO_ID, 4) {} - -contract CeloExchangeForkTest5 is ExchangeForkTest(CELO_ID, 5) {} - -contract CeloExchangeForkTest6 is ExchangeForkTest(CELO_ID, 6) {} - -contract CeloExchangeForkTest7 is ExchangeForkTest(CELO_ID, 7) {} - -contract CeloExchangeForkTest8 is ExchangeForkTest(CELO_ID, 8) {} - -contract CeloExchangeForkTest9 is ExchangeForkTest(CELO_ID, 9) {} - -contract CeloExchangeForkTest10 is ExchangeForkTest(CELO_ID, 10) {} - -contract CeloExchangeForkTest11 is ExchangeForkTest(CELO_ID, 11) {} - -contract CeloExchangeForkTest12 is ExchangeForkTest(CELO_ID, 12) {} - -contract CeloExchangeForkTest13 is ExchangeForkTest(CELO_ID, 13) {} diff --git a/test/fork/ExchangeForkTest.sol b/test/fork/ExchangeForkTest.sol index 0f61edd9..61ec0d77 100644 --- a/test/fork/ExchangeForkTest.sol +++ b/test/fork/ExchangeForkTest.sol @@ -2,39 +2,99 @@ // solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; -import "./BaseForkTest.sol"; - -contract ExchangeForkTest is BaseForkTest { +import { console } from "forge-std/console.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; + +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; + +import { BaseForkTest } from "./BaseForkTest.sol"; +import { SwapAssertions } from "./assertions/SwapAssertions.sol"; +import { CircuitBreakerAssertions } from "./assertions/CircuitBreakerAssertions.sol"; +import { TradingLimitHelpers } from "./helpers/TradingLimitHelpers.sol"; +import { TokenHelpers } from "./helpers/TokenHelpers.sol"; +import { OracleHelpers } from "./helpers/OracleHelpers.sol"; +import { SwapHelpers } from "./helpers/SwapHelpers.sol"; +import { LogHelpers } from "./helpers/LogHelpers.sol"; +import { L0, L1, LG } from "./helpers/misc.sol"; + +abstract contract ExchangeForkTest is SwapAssertions, CircuitBreakerAssertions, BaseForkTest { using FixidityLib for FixidityLib.Fraction; + using TradingLimitHelpers for *; + using OracleHelpers for *; + using SwapHelpers for *; + using TokenHelpers for *; + using LogHelpers for *; + + uint256 public exchangeIndex; + uint256 public exchangeProviderIndex; + bytes32 public exchangeId; + + address public exchangeProviderAddr; + IBiPoolManager public exchangeProvider; - using Utils for Utils.Context; - using Utils for uint256; + IExchangeProvider.Exchange public exchange; + IBiPoolManager.PoolExchange public poolExchange; + address public rateFeedId; - uint256 exchangeIndex; + ExchangeForkTest private ctx = this; - constructor(uint256 _chainId, uint256 _exchangeIndex) BaseForkTest(_chainId) { + constructor(uint256 _chainId, uint256 _exchangeProviderIndex, uint256 _exchangeIndex) BaseForkTest(_chainId) { + exchangeProviderIndex = _exchangeProviderIndex; exchangeIndex = _exchangeIndex; } - Utils.Context ctx; - function setUp() public override { super.setUp(); - ctx = Utils.newContext(address(this), exchangeIndex); + loadExchange(); + + console.log("%s | %s | %s", this.ticker(), exchangeProviderAddr, vm.toString(exchangeId)); + for (uint256 i = 0; i < COLLATERAL_ASSETS_COUNT; i++) { + address collateralAsset = reserve.collateralAssets(i); + vm.label(collateralAsset, IERC20(collateralAsset).symbol()); + mint(collateralAsset, address(reserve), uint256(25_000_000).toSubunits(collateralAsset), true); + } + } + + function loadExchange() internal { + exchangeProviderAddr = broker.exchangeProviders(exchangeProviderIndex); + exchangeProvider = IBiPoolManager(exchangeProviderAddr); + exchange = exchangeProvider.getExchanges()[exchangeIndex]; + vm.label(exchange.assets[0], exchange.assets[0].symbol()); + vm.label(exchange.assets[1], exchange.assets[1].symbol()); + exchangeId = exchange.exchangeId; + poolExchange = exchangeProvider.getPoolExchange(exchange.exchangeId); + rateFeedId = poolExchange.config.referenceRateFeedID; } - function test_swapsHappenInBothDirections() public { - IExchangeProvider.Exchange memory exchange = ctx.exchange; + function asset(uint256 index) public view returns (address) { + return exchange.assets[index]; + } + function getExchange() public view returns (IExchangeProvider.Exchange memory) { + return exchange; + } + + function getPool() public view returns (IBiPoolManager.PoolExchange memory) { + return poolExchange; + } + + function test_swapIn_worksInBothDirections() public { // asset0 -> asset1 - assert_swapIn(ctx, exchange.assets[0], exchange.assets[1], Utils.toSubunits(1000, exchange.assets[0])); + assert_swapIn(exchange.assets[0], exchange.assets[1]); // asset1 -> asset0 - assert_swapIn(ctx, exchange.assets[1], exchange.assets[0], Utils.toSubunits(1000, exchange.assets[1])); + assert_swapIn(exchange.assets[1], exchange.assets[0]); } - function test_tradingLimitsAreConfigured() public view { - IExchangeProvider.Exchange memory exchange = ctx.exchange; + function test_swapOut_worksInBothDirections() public { + // asset0 -> asset1 + assert_swapOut(exchange.assets[0], exchange.assets[1]); + // asset1 -> asset0 + assert_swapOut(exchange.assets[1], exchange.assets[0]); + } + function test_tradingLimitsAreConfigured() public view { bytes32 asset0Bytes32 = bytes32(uint256(uint160(exchange.assets[0]))); bytes32 limitIdForAsset0 = exchange.exchangeId ^ asset0Bytes32; bytes32 asset1Bytes32 = bytes32(uint256(uint160(exchange.assets[1]))); @@ -48,67 +108,50 @@ contract ExchangeForkTest is BaseForkTest { function test_tradingLimitsAreEnforced_0to1_L0() public { ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], L0); + assert_swapOverLimitFails(exchange.assets[0], exchange.assets[1], L0); } function test_tradingLimitsAreEnforced_0to1_L1() public { ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], L1); + assert_swapOverLimitFails(exchange.assets[0], exchange.assets[1], L1); } function test_tradingLimitsAreEnforced_0to1_LG() public { ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[0], exchange.assets[1], LG); + assert_swapOverLimitFails(exchange.assets[0], exchange.assets[1], LG); } function test_tradingLimitsAreEnforced_1to0_L0() public { ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], L0); + assert_swapOverLimitFails(exchange.assets[1], exchange.assets[0], L0); } function test_tradingLimitsAreEnforced_1to0_L1() public { ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], L1); + assert_swapOverLimitFails(exchange.assets[1], exchange.assets[0], L1); } function test_tradingLimitsAreEnforced_1to0_LG() public { ctx.logHeader(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - - assert_swapOverLimitFails(ctx, exchange.assets[1], exchange.assets[0], LG); + assert_swapOverLimitFails(exchange.assets[1], exchange.assets[0], LG); } - function test_circuitBreaker_rateFeedsAreProtected() public view { + function test_circuitBreaker_rateFeedIsProtected() public view { address[] memory breakers = breakerBox.getBreakers(); ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); bool found = false; for (uint256 j = 0; j < breakers.length && !found; j++) { - found = breakerBox.isBreakerEnabled(breakers[j], rateFeedID); + found = breakerBox.isBreakerEnabled(breakers[j], rateFeedId); } - require(found, "No breaker found for rateFeedID"); + require(found, "No breaker found for rateFeedId"); } function test_circuitBreaker_breaks() public { address[] memory breakers = breakerBox.getBreakers(); ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { - assert_breakerBreaks(ctx, breakers[j], j); - // we recover this breaker so that it doesn't affect other exchanges in this test, - // since the rateFeed for this exchange could be a dependency for other rateFeeds - assert_breakerRecovers(ctx, breakers[j], j); + if (breakerBox.isBreakerEnabled(breakers[j], rateFeedId)) { + assert_breakerBreaks(rateFeedId, breakers[j], j); } } } @@ -116,10 +159,9 @@ contract ExchangeForkTest is BaseForkTest { function test_circuitBreaker_recovers() public { address[] memory breakers = breakerBox.getBreakers(); ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { - assert_breakerRecovers(ctx, breakers[j], j); + if (breakerBox.isBreakerEnabled(breakers[j], rateFeedId)) { + assert_breakerRecovers(rateFeedId, breakers[j], j); } } } @@ -127,100 +169,84 @@ contract ExchangeForkTest is BaseForkTest { function test_circuitBreaker_haltsTrading() public { address[] memory breakers = breakerBox.getBreakers(); ctx.logHeader(); - address rateFeedID = ctx.getReferenceRateFeedID(); - IExchangeProvider.Exchange memory exchange = ctx.exchange; - for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], rateFeedID)) { - assert_breakerBreaks(ctx, breakers[j], j); + if (breakerBox.isBreakerEnabled(breakers[j], rateFeedId)) { + assert_breakerBreaks(rateFeedId, breakers[j], j); assert_swapInFails( - ctx, exchange.assets[0], exchange.assets[1], - Utils.toSubunits(1000, exchange.assets[0]), + uint256(10000).toSubunits(exchange.assets[0]), "Trading is suspended for this reference rate" ); assert_swapInFails( - ctx, exchange.assets[1], exchange.assets[0], - Utils.toSubunits(1000, exchange.assets[1]), + uint256(10000).toSubunits(exchange.assets[1]), "Trading is suspended for this reference rate" ); assert_swapOutFails( - ctx, exchange.assets[0], exchange.assets[1], - Utils.toSubunits(1000, exchange.assets[1]), + uint256(1000).toSubunits(exchange.assets[1]), "Trading is suspended for this reference rate" ); assert_swapOutFails( - ctx, exchange.assets[1], exchange.assets[0], - Utils.toSubunits(1000, exchange.assets[0]), + uint256(1000).toSubunits(exchange.assets[0]), "Trading is suspended for this reference rate" ); - - // we recover this breaker so that it doesn't affect other exchanges in this test, - // since the rateFeed for this exchange could be a dependency for other rateFeeds - assert_breakerRecovers(ctx, breakers[j], j); } } } - mapping(address => uint256) depsCount; - function test_rateFeedDependencies_haltsDependantTrading() public { - // Hardcoded number of dependencies for each ratefeed - depsCount[registry.getAddressForStringOrDie("StableToken")] = 0; - depsCount[registry.getAddressForStringOrDie("StableTokenEUR")] = 0; - depsCount[registry.getAddressForStringOrDie("StableTokenBRL")] = 0; - depsCount[registry.getAddressForStringOrDie("StableTokenXOF")] = 2; - depsCount[0xA1A8003936862E7a15092A91898D69fa8bCE290c] = 0; // USDC/USD - depsCount[0x206B25Ea01E188Ee243131aFdE526bA6E131a016] = 1; // USDC/EUR - depsCount[0x25F21A1f97607Edf6852339fad709728cffb9a9d] = 1; // USDC/BRL - depsCount[0x26076B9702885d475ac8c3dB3Bd9F250Dc5A318B] = 0; // EUROC/EUR + uint256 depsCount = rateFeedDependenciesCount[rateFeedId]; - address[] memory breakers = breakerBox.getBreakers(); + /// @dev If this doesn't revert thare are more dependencies than expected. + /// In which case the mapping in BaseForkTest should be updated. + vm.expectRevert(); + breakerBox.rateFeedDependencies(rateFeedId, depsCount); - address[] memory dependencies = new address[](depsCount[ctx.getReferenceRateFeedID()]); - for (uint256 d = 0; d < dependencies.length; d++) { - dependencies[d] = ctx.breakerBox.rateFeedDependencies(ctx.getReferenceRateFeedID(), d); - } - if (dependencies.length == 0) { + if (depsCount == 0) { return; } - Utils.logPool(ctx); - address rateFeedID = ctx.getReferenceRateFeedID(); + address[] memory breakers = breakerBox.getBreakers(); + address[] memory dependencies = new address[](depsCount); + for (uint256 i = 0; i < depsCount; i++) { + dependencies[i] = breakerBox.rateFeedDependencies(rateFeedId, i); + } + + ctx.logPool(); console.log( "\t exchangeIndex: %d | rateFeedId: %s | %s dependencies", exchangeIndex, - rateFeedID, + rateFeedId, dependencies.length ); - for (uint256 k = 0; k < dependencies.length; k++) { - Utils.Context memory dependencyContext = Utils.getContextForRateFeedID(address(this), dependencies[k]); - + for (uint256 i = 0; i < dependencies.length; i++) { + console.log("==========================================================="); + console.log("Dependency: %s", dependencies[i]); + console.log("==========================================================="); for (uint256 j = 0; j < breakers.length; j++) { - if (breakerBox.isBreakerEnabled(breakers[j], dependencies[k])) { - assert_breakerBreaks(dependencyContext, breakers[j], j); + if (breakerBox.isBreakerEnabled(breakers[j], dependencies[i])) { + assert_breakerBreaks(dependencies[i], breakers[j], j); assert_swapInFails( - ctx, - ctx.exchange.assets[0], - ctx.exchange.assets[1], - Utils.toSubunits(1000, ctx.exchange.assets[0]), + exchange.assets[0], + exchange.assets[1], + uint256(1000).toSubunits(exchange.assets[0]), "Trading is suspended for this reference rate" ); - assert_breakerRecovers(dependencyContext, breakers[j], j); + assert_breakerRecovers(dependencies[i], breakers[j], j); } } + console.log("\n"); } } } diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol new file mode 100644 index 00000000..26631f21 --- /dev/null +++ b/test/fork/ForkTests.t.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable func-name-mixedcase, max-line-length +pragma solidity ^0.8; +/** +@dev Fork tests for Mento! +This test suite tests invariants on a fork of a live Mento environemnts. + +Thare are two types of tests contracts: +- ChainForkTests: Tests that are specific to the chain, such as the number of exchanges, the number of collateral assets, contract initialization state, etc. +- ExchangeForkTests: Tests that are specific to the exchange, such as trading limits, swaps, circuit breakers, etc. + +To make it easier to debug and develop, we have one ChainForkTest for each chain (Alfajores, Celo) and +one ExchangeForkTest for each exchange provider and exchange pair. + +The ChainFork tests are instantiated with: +- Chain ID. +- Expected number of exchange providers. +- Expected number of exchanges per exchange provider. +If any of this assertions fail then the ChainTest will fail and that's the queue to update this file +and add additional ExchangeForkTests. + +The ExchangeForkTests are instantiated with: +- Chain ID. +- Exchange Provider Index. +- Exchange Index. + +And the naming convetion for them is: +${ChainName}_P${ExchangeProviderIndex}E${ExchangeIndex}_ExchangeForkTest +e.g. Alfajores_P0E00_ExchangeForkTest (Alfajores, Exchange Provider 0, Exchange 0) +The Exchange Index is 0 padded to make them align nicely in the file, exchange provider counts shouldn't +exceed 10, if they do, then we need to update the naming convention. + +This makes it easy to drill into which exchange is failing and debug it like: +$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract CELO_P0E12 +or run all tests for a chain: +$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores +*/ + +import { uints } from "mento-std/Array.sol"; +import { ChainForkTest } from "./ChainForkTest.sol"; +import { ExchangeForkTest } from "./ExchangeForkTest.sol"; +import { CELO_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; + +contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(14)) {} + +contract Alfajores_P0E00_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 0) {} + +contract Alfajores_P0E01_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 1) {} + +contract Alfajores_P0E02_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 2) {} + +contract Alfajores_P0E03_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 3) {} + +contract Alfajores_P0E04_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 4) {} + +contract Alfajores_P0E05_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 5) {} + +contract Alfajores_P0E06_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 6) {} + +contract Alfajores_P0E07_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 7) {} + +contract Alfajores_P0E08_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 8) {} + +contract Alfajores_P0E09_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 9) {} + +contract Alfajores_P0E10_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 10) {} + +contract Alfajores_P0E11_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 11) {} + +contract Alfajores_P0E12_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 12) {} + +contract Alfajores_P0E13_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 13) {} + +contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(14)) {} + +contract Celo_P0E00_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 0) {} + +contract Celo_P0E01_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 1) {} + +contract Celo_P0E02_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 2) {} + +contract Celo_P0E03_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 3) {} + +contract Celo_P0E04_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 4) {} + +contract Celo_P0E05_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 5) {} + +contract Celo_P0E06_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 6) {} + +contract Celo_P0E07_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 7) {} + +contract Celo_P0E08_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 8) {} + +contract Celo_P0E09_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 9) {} + +contract Celo_P0E10_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 10) {} + +contract Celo_P0E11_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 11) {} + +contract Celo_P0E12_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 12) {} + +contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} diff --git a/test/fork/README.md b/test/fork/README.md new file mode 100644 index 00000000..47d05297 --- /dev/null +++ b/test/fork/README.md @@ -0,0 +1,133 @@ +## Mento Protocol Fork Tests Suite + +### Structure + +```text + +-------------+ + | Test | + +-------------+ + ^ + | + +--------------+ + | BaseForkTest | + +--------------+ + ^ ^ + / \ + +-----------------+ +--------------------+ + | ChainForkTest | | ExchangeForkTest | + +-----------------+ +--------------------+ + ^ ^ + | | + +-------------------------+ +-------------------------+ + | Alfajores_ChainForkTest | | Alfajores_P0E00_... | + +-------------------------+ +-------------------------+ + +--------------------+ +-------------------------+ + | Celo_ChainForkTest | | Alfajores_P0E01_... | + +--------------------+ +-------------------------+ + +-------------------------+ + | Celo_P0E00_... | + +-------------------------+ +``` + +> TIL Claude knows how to draw ascii art. + +Base Contracts: + +- `BaseForkTest` implements fork-related shared setup logic. +- `ChainForkTest` tests for a given chain. +- `ExchangeForkTest` tests for a given exchange of an exchange provider on a given chain. + +These contracts are abstract and need to be extended by instance specific contracts which specify the target chain and exchange. +This happens in `ForkTests.t.sol`. For example: + +```solidity +contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(14)) {} +``` + +This represents a ChainForkTest for Alfajores, with the expectation that there's a single exchange provider, +and it has 14 exchanges. If the expectations change this will fail and need to be updated. + +```solidity +contract Alfajores_P0E00_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 0) {} +``` + +This represents an ExchangeForkTest for the 0th exchange of the 0th exchange provider on Alfajores. +These tests contracts need to be added manually when we add more pairs or exchange providers, but the +assertions at chain level gives us the heads up when this changes. + +### assertions, actions, helpers + +Fork tests can get quite complex because we need to understand the current chain state and manipualte it when needed to be able to test our assetions. +That resulted in the past in a mess of unstructure utility functions. This new structrue tries to improve that: + +``` + +------------------+ +------------------+ + | SwapActions | | OracleActions | + +------------------+ +------------------+ + ^ ^ + \ / + \ / + +------------------+ + | Actions | + +------------------+ + ^ ^ + / \ + +------------------+ +------------------+ + | SwapAssertions | | CircuitBreaker | + | | | Assertions | + +------------------+ +------------------+ + ^ ^ + \ / + \ / + +----------------------+ + | Celo_P0E00_... | + +----------------------+ +``` + +#### `Assertions` + +Assertions are contracts that implement high-level assertions about the protocol. +For example `SwapAssertions` contains `assert_swapIn` which asserts a swap is possible, or `assert_swapInFails` which asserts a swap fails with a given revert reason. + +These reasuable building blocks are used inside of the actual tests defined in the `ExchangeForkTest` contract. +All assertions extend and make us of `Actions`. + +If you're writing a test and you want to express an assertion about the protocol that either gets too complex, or should be reused, it can become an assertion. +Otherwise you can also simply use `Actions` in the tests and assert their outcome. + +#### `Actions` + +Actions are contract that implement utilities which modify the chain state, e.g. executing a swap, changing an oracle rate feed, swapping repeteadly until a limit, etc. + +If it's more than just calling a function on a contract, it can become an action. + +#### `Helpers` + +Helpers are libraries that just read chain state and expose it in an useful manner. +They're imported on all levels of the struture by doing: + +```solidity +contract OracleActions { + using OracleHelpers for *; + using SwapHelpers for *; + using TokenHelpers for *; + using TradingLimitHelpers for *; + using LogHelpers for *; +} +``` + +Most of them attach to `ExchangeForkTest` and are accessed using the `ctx` variable. +For example `ctx.tradingLimitsState(asset)` will load the trading limits state for the asset. +This works because the ctx contains all information about the current `Exchange`. + +### `ctx` + +To make it easy to get access to the current test context everywhere in the utility contracts, they all implement a private `ctx` var as: + +```solidity +ExchangeForkTest private ctx = ExchangeForkTest(address(this)); +``` + +This is because in the end this whole inheritance structure collapses to a single ExchangeForkTest contract and we already know this. +So we can introduce this magic `ctx` variable which gets you access to all assertions and actions (defined as public), +and all of the public variables of `ExchangeForkTest`, meaning all loaded contracts, the current exchange, etc. diff --git a/test/fork/TestAsserts.sol b/test/fork/TestAsserts.sol deleted file mode 100644 index 4d786b94..00000000 --- a/test/fork/TestAsserts.sol +++ /dev/null @@ -1,570 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.8; - -import { Test } from "mento-std/Test.sol"; -import { console } from "forge-std/console.sol"; -import { Utils } from "./Utils.sol"; - -import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; - -import { IERC20 } from "contracts/interfaces/IERC20.sol"; -import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; -import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; -import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; -import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; - -contract TestAsserts is Test { - using Utils for Utils.Context; - using Utils for ITradingLimits.Config; - using Utils for ITradingLimits.State; - using Utils for uint8; - using Utils for uint256; - using FixidityLib for FixidityLib.Fraction; - - uint8 private constant L0 = 1; // 0b001 Limit0 - uint8 private constant L1 = 2; // 0b010 Limit1 - uint8 private constant LG = 4; // 0b100 LimitGlobal - - uint256 fixed1 = FixidityLib.fixed1().unwrap(); - FixidityLib.Fraction pc10 = FixidityLib.newFixedFraction(10, 100); - - // ========================= Swap Asserts ========================= // - - function assert_swapIn(Utils.Context memory ctx, address from, address to, uint256 sellAmount) internal { - FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(from); - FixidityLib.Fraction memory amountIn = sellAmount.toUnitsFixed(from); - FixidityLib.Fraction memory amountOut = ctx.swapIn(from, to, sellAmount).toUnitsFixed(to); - FixidityLib.Fraction memory expectedAmountOut = amountIn.divide(rate); - - assertApproxEqAbs(amountOut.unwrap(), expectedAmountOut.unwrap(), pc10.multiply(expectedAmountOut).unwrap()); - } - - function assert_swapOut(Utils.Context memory ctx, address from, address to, uint256 buyAmount) internal { - FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(from); - FixidityLib.Fraction memory amountOut = buyAmount.toUnitsFixed(to); - FixidityLib.Fraction memory amountIn = ctx.swapOut(from, to, buyAmount).toUnitsFixed(from); - FixidityLib.Fraction memory expectedAmountIn = amountOut.multiply(rate); - - assertApproxEqAbs(amountIn.unwrap(), expectedAmountIn.unwrap(), pc10.multiply(expectedAmountIn).unwrap()); - } - - function assert_swapInFails( - Utils.Context memory ctx, - address from, - address to, - uint256 sellAmount, - string memory revertReason - ) internal { - ctx.addReportsIfNeeded(); - ctx.t._deal(from, ctx.trader, sellAmount, true); - IERC20(from).approve(address(ctx.broker), sellAmount); - uint256 minAmountOut = ctx.broker.getAmountOut(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount); - vm.expectRevert(bytes(revertReason)); - ctx.broker.swapIn(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount, minAmountOut); - } - - function assert_swapOutFails( - Utils.Context memory ctx, - address from, - address to, - uint256 buyAmount, - string memory revertReason - ) internal { - ctx.addReportsIfNeeded(); - uint256 maxAmountIn = ctx.broker.getAmountIn(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount); - ctx.t._deal(from, ctx.trader, maxAmountIn, true); - IERC20(from).approve(address(ctx.broker), maxAmountIn); - vm.expectRevert(bytes(revertReason)); - ctx.broker.swapOut(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount, maxAmountIn); - } - - // ========================= Trading Limit Asserts ========================= // - - function assert_swapOverLimitFails(Utils.Context memory ctx, address from, address to, uint8 limit) internal { - ITradingLimits.Config memory fromLimitConfig = ctx.tradingLimitsConfig(from); - ITradingLimits.Config memory toLimitConfig = ctx.tradingLimitsConfig(to); - console.log( - string(abi.encodePacked("Swapping ", IERC20(from).symbol(), " -> ", IERC20(to).symbol())), - "with limit", - limit.limitString() - ); - console.log("========================================"); - - if (fromLimitConfig.isLimitEnabled(limit) && toLimitConfig.isLimitEnabled(limit)) { - // TODO: Figure out best way to implement fork tests - // when two limits are configured. - console.log("Both Limits enabled skipping for now"); - } else if (fromLimitConfig.isLimitEnabled(limit)) { - assert_swapOverLimitFails_onInflow(ctx, from, to, limit); - } else if (toLimitConfig.isLimitEnabled(limit)) { - assert_swapOverLimitFails_onOutflow(ctx, from, to, limit); - } - } - - function assert_swapOverLimitFails_onInflow( - Utils.Context memory ctx, - address from, - address to, - uint8 limit - ) internal { - /* - * L*[from] -> to - * Assert that inflow on `from` is limited by the limit - * which can be any of L0, L1, LG. - * This is done by swapping from `from` to `to` until - * just before the limit is reached, within the constraints of - * the other limits, and then doing a final swap that fails. - */ - - if (limit == L0) { - swapUntilL0_onInflow(ctx, from, to); - } else if (limit == L1) { - swapUntilL1_onInflow(ctx, from, to); - } else if (limit == LG) { - swapUntilLG_onInflow(ctx, from, to); - } else { - revert("Invalid limit"); - } - - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - ITradingLimits.State memory limitState = ctx.tradingLimitsState(from); - - uint256 inflowRequiredUnits = uint256(int256(limitConfig.getLimit(limit)) - limitState.getNetflow(limit)) + 1; - console.log("Inflow required to pass limit: ", inflowRequiredUnits); - assert_swapInFails(ctx, from, to, inflowRequiredUnits.toSubunits(from), limit.revertReason()); - } - - function assert_swapOverLimitFails_onOutflow( - Utils.Context memory ctx, - address from, - address to, - uint8 limit - ) internal { - /* - * from -> L*[to] - * Assert that outflow on `to` is limited by the limit - * which can be any of L0, L1, LG. - * This is done by swapping from `from` to `to` until - * just before the limit is reached, within the constraints of - * the other limits, and then doing a final swap that fails. - */ - - // This should do valid swaps until just before the limit is reached - if (limit == L0) { - swapUntilL0_onOutflow(ctx, from, to); - } else if (limit == L1) { - swapUntilL1_onOutflow(ctx, from, to); - } else if (limit == LG) { - swapUntilLG_onOutflow(ctx, from, to); - } else { - revert("Invalid limit"); - } - - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - ITradingLimits.State memory limitState = ctx.tradingLimitsState(to); - - uint256 outflowRequiredUnits = uint256(int256(limitConfig.getLimit(limit)) + limitState.getNetflow(limit)) + 1; - console.log("Outflow required: ", outflowRequiredUnits); - assert_swapOutFails(ctx, from, to, outflowRequiredUnits.toSubunits(to), limit.revertReason()); - } - - function swapUntilL0_onInflow(Utils.Context memory ctx, address from, address to) internal { - /* - * L0[from] -> to - * This function will do valid swaps until just before L0 is hit - * during inflow on `from`, therfore we check the positive end - * of the limit because `from` flows into the reserve. - */ - - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - console.log(unicode"🏷️ [%d] Swap until L0=%d on inflow", block.timestamp, uint256(int256(limitConfig.limit0))); - uint256 maxPossible; - uint256 maxPossibleUntilLimit; - do { - int48 maxPossibleUntilLimitUnits = ctx.maxPossibleInflow(from); - require(maxPossibleUntilLimitUnits >= 0, "max possible trade amount is negative"); - maxPossibleUntilLimit = uint256(int256(maxPossibleUntilLimitUnits)).toSubunits(from); - maxPossible = ctx.maxSwapIn(maxPossibleUntilLimit, from, to); - - if (maxPossible > 0) { - ctx.swapIn(from, to, maxPossible); - } - } while (maxPossible > 0 && maxPossibleUntilLimit > maxPossible); - ctx.logNetflows(from); - } - - function swapUntilL1_onInflow(Utils.Context memory ctx, address from, address to) internal { - /* - * L1[from] -> to - * This function will do valid swaps until just before L1 is hit - * during inflow on `from`, therfore we check the positive end - * of the limit because `from` flows into the reserve. - */ - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); - console.log(unicode"🏷️ [%d] Swap until L1=%d on inflow", block.timestamp, uint256(int256(limitConfig.limit1))); - int48 maxPerSwap = limitConfig.limit0; - while (limitState.netflow1 + maxPerSwap <= limitConfig.limit1) { - skip(limitConfig.timestep0 + 1); - ensureRateActive(ctx); // needed because otherwise constantSum might revert if the median is stale due to the skip - - swapUntilL0_onInflow(ctx, from, to); - limitConfig = ctx.tradingLimitsConfig(from); - limitState = ctx.tradingLimitsState(from); - } - skip(limitConfig.timestep0 + 1); - ensureRateActive(ctx); - } - - function swapUntilLG_onInflow(Utils.Context memory ctx, address from, address to) internal { - /* - * L1[from] -> to - * This function will do valid swaps until just before LG is hit - * during inflow on `from`, therfore we check the positive end - * of the limit because `from` flows into the reserve. - */ - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); - ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); - console.log(unicode"🏷️ [%d] Swap until LG=%d on inflow", block.timestamp, uint256(int256(limitConfig.limitGlobal))); - - if (limitConfig.isLimitEnabled(L1)) { - int48 maxPerSwap = limitConfig.limit0; - while (limitState.netflowGlobal + maxPerSwap <= limitConfig.limitGlobal) { - skip(limitConfig.timestep1 + 1); - swapUntilL1_onInflow(ctx, from, to); - limitConfig = ctx.tradingLimitsConfig(from); - limitState = ctx.tradingLimitsState(from); - } - skip(limitConfig.timestep1 + 1); - } else if (limitConfig.isLimitEnabled(L0)) { - int48 maxPerSwap = limitConfig.limit0; - while (limitState.netflowGlobal + maxPerSwap <= limitConfig.limitGlobal) { - skip(limitConfig.timestep0 + 1); - swapUntilL0_onInflow(ctx, from, to); - limitConfig = ctx.tradingLimitsConfig(from); - limitState = ctx.tradingLimitsState(from); - } - skip(limitConfig.timestep0 + 1); - } - } - - function swapUntilL0_onOutflow(Utils.Context memory ctx, address from, address to) public { - /* - * from -> L0[to] - * This function will do valid swaps until just before L0 is hit - * during outflow on `to`, therfore we check the negative end - * of the limit because `to` flows out of the reserve. - */ - - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - console.log(unicode"🏷️ [%d] Swap until L0=%d on outflow", block.timestamp, uint256(int256(limitConfig.limit0))); - uint256 maxPossible; - uint256 maxPossibleUntilLimit; - do { - int48 maxPossibleUntilLimitUnits = ctx.maxPossibleOutflow(to); - require(maxPossibleUntilLimitUnits >= 0, "max possible trade amount is negative"); - maxPossibleUntilLimit = uint256(int256(maxPossibleUntilLimitUnits)).toSubunits(to); - maxPossible = ctx.maxSwapOut(maxPossibleUntilLimit, to); - - if (maxPossible > 0) { - ctx.swapOut(from, to, maxPossible); - } - } while (maxPossible > 0 && maxPossibleUntilLimit > maxPossible); - ctx.logNetflows(to); - } - - function swapUntilL1_onOutflow(Utils.Context memory ctx, address from, address to) public { - /* - * from -> L1[to] - * This function will do valid swaps until just before L1 is hit - * during outflow on `to`, therfore we check the negative end - * of the limit because `to` flows out of the reserve. - */ - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); - - console.log(unicode"🏷️ [%d] Swap until L1=%d on outflow", block.timestamp, uint48(limitConfig.limit1)); - int48 maxPerSwap = limitConfig.limit0; - - while (limitState.netflow1 - maxPerSwap >= -1 * limitConfig.limit1) { - skip(limitConfig.timestep0 + 1); - // Check that there's still outflow to trade as sometimes we hit LG while - // still having a bit of L1 left, which causes an infinite loop. - if (ctx.maxPossibleOutflow(to) == 0) { - break; - } - swapUntilL0_onOutflow(ctx, from, to); - limitConfig = ctx.tradingLimitsConfig(to); - limitState = ctx.tradingLimitsState(to); - } - skip(limitConfig.timestep0 + 1); - } - - function swapUntilLG_onOutflow(Utils.Context memory ctx, address from, address to) public { - /* - * from -> LG[to] - * This function will do valid swaps until just before LG is hit - * during outflow on `to`, therfore we check the negative end - * of the limit because `to` flows out of the reserve. - */ - ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); - ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); - console.log(unicode"🏷️ [%d] Swap until LG=%d on outflow", block.timestamp, uint48(limitConfig.limitGlobal)); - - if (limitConfig.isLimitEnabled(L1)) { - int48 maxPerSwap = limitConfig.limit0; - while (limitState.netflowGlobal - maxPerSwap >= -1 * limitConfig.limitGlobal) { - skip(limitConfig.timestep1 + 1); - swapUntilL1_onOutflow(ctx, from, to); - limitConfig = ctx.tradingLimitsConfig(to); - // Triger an update to reset netflows - limitState = ctx.tradingLimitsState(to); - } - skip(limitConfig.timestep1 + 1); - } else if (limitConfig.isLimitEnabled(L0)) { - int48 maxPerSwap = limitConfig.limit0; - while (limitState.netflowGlobal - maxPerSwap >= -1 * limitConfig.limitGlobal) { - skip(limitConfig.timestep0 + 1); - swapUntilL0_onOutflow(ctx, from, to); - limitConfig = ctx.tradingLimitsConfig(to); - // Triger an update to reset netflows - limitState = ctx.tradingLimitsState(to); - } - skip(limitConfig.timestep0 + 1); - } - } - - // ========================= Circuit Breaker Asserts ========================= // - - function assert_breakerBreaks(Utils.Context memory ctx, address breaker, uint256 breakerIndex) public { - // XXX: There is currently no straightforward way to determine what type of a breaker - // we are dealing with, so we will use the deployment setup that we currently chose, - // where the medianDeltaBreaker gets deployed first and the valueDeltaBreaker second. - bool isMedianDeltaBreaker = breakerIndex == 0; - bool isValueDeltaBreaker = breakerIndex == 1; - if (isMedianDeltaBreaker) { - assert_medianDeltaBreakerBreaks_onIncrease(ctx, breaker); - assert_medianDeltaBreakerBreaks_onDecrease(ctx, breaker); - } else if (isValueDeltaBreaker) { - assert_valueDeltaBreakerBreaks_onIncrease(ctx, breaker); - assert_valueDeltaBreakerBreaks_onDecrease(ctx, breaker); - } else { - revert("Unknown trading mode, can't infer breaker type"); - } - } - - function assert_medianDeltaBreakerBreaks_onIncrease(Utils.Context memory ctx, address _breaker) public { - uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 - - // trigger breaker by setting new median to ema - (threshold + 0.001% buffer) - uint256 currentEMA = IMedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); - uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); - uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); // small buffer because of rounding errors - uint256 maxPercent = fixed1 + rateChangeThreshold + thresholdBuffer; - uint256 newMedian = (currentEMA * maxPercent) / fixed1; - - console.log("Current Median: ", currentMedian); - console.log("Current EMA: ", currentEMA); - console.log("New Median: ", newMedian); - assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); - } - - function assert_medianDeltaBreakerBreaks_onDecrease(Utils.Context memory ctx, address _breaker) public { - uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 - - // trigger breaker by setting new median to ema + (threshold + 0.001% buffer) - uint256 currentEMA = IMedianDeltaBreaker(_breaker).medianRatesEMA(ctx.getReferenceRateFeedID()); - uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); - uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); // small buffer because of rounding errors - uint256 maxPercent = fixed1 - (rateChangeThreshold + thresholdBuffer); - uint256 newMedian = (currentEMA * maxPercent) / fixed1; - - console.log("Current Median: ", currentMedian); - console.log("Current EMA: ", currentEMA); - console.log("New Median: ", newMedian); - assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); - } - - function assert_valueDeltaBreakerBreaks_onIncrease(Utils.Context memory ctx, address _breaker) public { - uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 - - // trigger breaker by setting new median to reference value + threshold + 1 - uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); - uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(_breaker); - uint256 maxPercent = fixed1 + rateChangeThreshold; - uint256 newMedian = (referenceValue * maxPercent) / fixed1; - newMedian = newMedian + 1; - - console.log("Current Median: ", currentMedian); - console.log("Reference Value: ", referenceValue); - console.log("New Median: ", newMedian); - assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); - } - - function assert_valueDeltaBreakerBreaks_onDecrease(Utils.Context memory ctx, address _breaker) public { - uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 - - // trigger breaker by setting new median to reference value - threshold - 1 - uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); - uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(_breaker); - uint256 maxPercent = fixed1 - rateChangeThreshold; - uint256 newMedian = (referenceValue * maxPercent) / fixed1; - newMedian = newMedian - 1; - - console.log("Current Median: ", currentMedian); - console.log("Reference Value: ", referenceValue); - console.log("New Median: ", newMedian); - assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); - } - - function assert_breakerRecovers(Utils.Context memory ctx, address breaker, uint256 breakerIndex) public { - // XXX: There is currently no straightforward way to determine what type of a breaker - // we are dealing with, so we will use the deployment setup that we currently chose, - // where the medianDeltaBreaker gets deployed first and the valueDeltaBreaker second. - bool isMedianDeltaBreaker = breakerIndex == 0; - bool isValueDeltaBreaker = breakerIndex == 1; - if (isMedianDeltaBreaker) { - assert_medianDeltaBreakerRecovers(ctx, breaker); - } else if (isValueDeltaBreaker) { - assert_valueDeltaBreakerRecovers(ctx, breaker); - } else { - revert("Unknown trading mode, can't infer breaker type"); - } - } - - function assert_medianDeltaBreakerRecovers(Utils.Context memory ctx, address _breaker) internal { - uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 - IMedianDeltaBreaker breaker = IMedianDeltaBreaker(_breaker); - - // trigger breaker by setting new median to ema + threshold + 0.001% - uint256 currentEMA = breaker.medianRatesEMA(ctx.getReferenceRateFeedID()); - uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); - uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); - uint256 maxPercent = fixed1 + rateChangeThreshold + thresholdBuffer; - uint256 newMedian = (currentEMA * maxPercent) / fixed1; - - console.log("Current Median: ", currentMedian); - console.log("New Median: ", newMedian); - assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); - - // wait for cool down and reset by setting new median to ema - uint256 cooldown = breaker.getCooldown(ctx.getReferenceRateFeedID()); - if (cooldown == 0) { - changePrank(ctx.breakerBox.owner()); - ctx.breakerBox.setRateFeedTradingMode(ctx.getReferenceRateFeedID(), 0); - } else { - skip(cooldown); - currentEMA = breaker.medianRatesEMA(ctx.getReferenceRateFeedID()); - assert_breakerRecovers_withNewMedian(ctx, currentEMA); - } - } - - function assert_valueDeltaBreakerRecovers(Utils.Context memory ctx, address _breaker) internal { - uint256 currentMedian = ensureRateActive(ctx); // ensure trading mode is 0 - IValueDeltaBreaker breaker = IValueDeltaBreaker(_breaker); - - // trigger breaker by setting new median to reference value + threshold + 1 - uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(_breaker); - uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(_breaker); - uint256 maxPercent = fixed1 + rateChangeThreshold; - uint256 newMedian = (referenceValue * maxPercent) / fixed1; - newMedian = newMedian + 1; - - console.log("Current Median: ", currentMedian); - console.log("Reference Value: ", referenceValue); - console.log("New Median: ", newMedian); - assert_breakerBreaks_withNewMedian(ctx, newMedian, 3); - - // wait for cool down and reset by setting new median to refernece value - uint256 cooldown = breaker.getCooldown(ctx.getReferenceRateFeedID()); - if (cooldown == 0) { - changePrank(ctx.breakerBox.owner()); - ctx.breakerBox.setRateFeedTradingMode(ctx.getReferenceRateFeedID(), 0); - } else { - skip(cooldown); - assert_breakerRecovers_withNewMedian(ctx, referenceValue); - } - } - - function assert_breakerBreaks_withNewMedian( - Utils.Context memory ctx, - uint256 newMedian, - uint256 expectedTradingMode - ) public { - address rateFeedID = ctx.getReferenceRateFeedID(); - uint256 tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); - require(tradingMode == 0, "breaker should be recovered"); - - ctx.updateOracleMedianRate(newMedian); - tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); - require(tradingMode == expectedTradingMode, "trading more is different from expected"); - } - - function assert_breakerRecovers_withNewMedian(Utils.Context memory ctx, uint256 newMedian) public { - address rateFeedID = ctx.getReferenceRateFeedID(); - uint256 tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); - require(tradingMode != 0, "breaker should be triggered"); - - ctx.updateOracleMedianRate(newMedian); - tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); - require(tradingMode == 0, "breaker should be recovered"); - } - - function ensureRateActive(Utils.Context memory ctx) internal returns (uint256 newMedian) { - address rateFeedID = ctx.getReferenceRateFeedID(); - // Always do a small update in order to make sure - // the breakers are warm. - (uint256 currentRate, ) = ctx.sortedOracles.medianRate(rateFeedID); - newMedian = currentRate + (currentRate / 100_000_000); // a small increase - ctx.updateOracleMedianRate(newMedian); - uint8 tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); - uint256 attempts = 0; - while (tradingMode != 0 && attempts < 10) { - console.log("attempt #%d", attempts); - attempts++; - // while the breaker is active, we wait for the cooldown and try to update the median - console.log(block.timestamp, "Waiting for cooldown to pass"); - console.log("RateFeedID:", rateFeedID); - address[] memory _breakers = ctx.breakerBox.getBreakers(); - uint256 cooldown = 0; - uint256 breakerIndex; - for (uint256 i = 0; i < _breakers.length; i++) { - if (ctx.breakerBox.isBreakerEnabled(_breakers[i], rateFeedID)) { - IBreakerBox.BreakerStatus memory status = ctx.breakerBox.rateFeedBreakerStatus(rateFeedID, _breakers[i]); - if (status.tradingMode != 0) { - breakerIndex = i; - cooldown = IValueDeltaBreaker(_breakers[i]).getCooldown(rateFeedID); - break; - } - } - } - skip(cooldown); - newMedian = newMedianToResetBreaker(ctx, breakerIndex); - ctx.updateOracleMedianRate(newMedian); - if (cooldown == 0) { - console.log("Manual recovery required for breaker %s", _breakers[breakerIndex]); - changePrank(ctx.breakerBox.owner()); - ctx.breakerBox.setRateFeedTradingMode(rateFeedID, 0); - } - tradingMode = ctx.breakerBox.getRateFeedTradingMode(rateFeedID); - } - } - - function newMedianToResetBreaker( - Utils.Context memory ctx, - uint256 breakerIndex - ) internal view returns (uint256 newMedian) { - address[] memory _breakers = ctx.breakerBox.getBreakers(); - bool isMedianDeltaBreaker = breakerIndex == 0; - bool isValueDeltaBreaker = breakerIndex == 1; - if (isMedianDeltaBreaker) { - uint256 currentEMA = IMedianDeltaBreaker(_breakers[breakerIndex]).medianRatesEMA(ctx.getReferenceRateFeedID()); - return currentEMA; - } else if (isValueDeltaBreaker) { - return ctx.getValueDeltaBreakerReferenceValue(_breakers[breakerIndex]); - } else { - revert("can't infer corresponding breaker"); - } - } -} diff --git a/test/fork/TokenUpgrade.t.sol b/test/fork/TokenUpgrade.t.sol deleted file mode 100644 index 36e2bdba..00000000 --- a/test/fork/TokenUpgrade.t.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8; - -import { console } from "forge-std/console.sol"; -import { Test } from "mento-std/Test.sol"; -import { CELO_ID } from "mento-std/Constants.sol"; - -import { WithRegistry } from "../utils/WithRegistry.sol"; - -import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; -import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; - -contract TokenUpgradeForkTest is Test, WithRegistry { - // solhint-disable-next-line func-name-mixedcase - function test_upgrade() public { - fork(CELO_ID, 22856317); - - address stableToken = registry.getAddressForString("StableToken"); - ICeloProxy stableTokenProxy = ICeloProxy(stableToken); - console.log(ICeloProxy(stableToken)._getImplementation()); - console.log(ICeloProxy(stableToken)._getOwner()); - vm.startPrank(ICeloProxy(stableToken)._getOwner()); - address mentoERC20Impl = deployCode("StableTokenV2", abi.encode(false)); - stableTokenProxy._setImplementation(mentoERC20Impl); - - IStableTokenV2 cusd = IStableTokenV2(stableToken); - cusd.initializeV2( - registry.getAddressForString("Broker"), - registry.getAddressForString("Validators"), - registry.getAddressForString("Exchange") - ); - - address governance = registry.getAddressForString("Governance"); - cusd.balanceOf(governance); - - changePrank(governance); - cusd.transfer(address(this), 1 ether); - cusd.balanceOf(address(this)); - } -} diff --git a/test/fork/Utils.sol b/test/fork/Utils.sol deleted file mode 100644 index 737ff544..00000000 --- a/test/fork/Utils.sol +++ /dev/null @@ -1,520 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count -pragma solidity ^0.8; -pragma experimental ABIEncoderV2; - -import { console } from "forge-std/console.sol"; -import { Vm } from "forge-std/Vm.sol"; -import { VM_ADDRESS } from "mento-std/Constants.sol"; - -import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; - -import { BaseForkTest } from "./BaseForkTest.sol"; - -import { IERC20 } from "contracts/interfaces/IERC20.sol"; -import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; -import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; -import { IBroker } from "contracts/interfaces/IBroker.sol"; -import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; -import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; -import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; -import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; -import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; -import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; - -library Utils { - using FixidityLib for FixidityLib.Fraction; - - uint8 private constant L0 = 1; // 0b001 Limit0 - uint8 private constant L1 = 2; // 0b010 Limit1 - uint8 private constant LG = 4; // 0b100 LimitGlobal - - Vm public constant vm = Vm(VM_ADDRESS); - - struct Context { - BaseForkTest t; - IBroker broker; - ISortedOracles sortedOracles; - IBreakerBox breakerBox; - address exchangeProvider; - bytes32 exchangeId; - address rateFeedID; - IExchangeProvider.Exchange exchange; - address trader; - ITradingLimitsHarness tradingLimits; - } - - function newContext(address _t, uint256 index) public view returns (Context memory ctx) { - BaseForkTest t = BaseForkTest(_t); - (address exchangeProvider, IExchangeProvider.Exchange memory exchange) = t.exchanges(index); - - ctx = Context( - t, - t.broker(), - t.sortedOracles(), - t.breakerBox(), - exchangeProvider, - exchange.exchangeId, - address(0), - exchange, - t.trader(), - t.tradingLimits() - ); - } - - function newRateFeedContext(address _t, address rateFeed) public view returns (Context memory ctx) { - BaseForkTest t = BaseForkTest(_t); - - ctx = Context( - t, - t.broker(), - t.sortedOracles(), - t.breakerBox(), - address(0), - bytes32(0), - rateFeed, - IExchangeProvider.Exchange(0, new address[](0)), - t.trader(), - t.tradingLimits() - ); - } - - function getContextForRateFeedID(address _t, address rateFeedID) public view returns (Context memory) { - BaseForkTest t = BaseForkTest(_t); - (address biPoolManagerAddr, ) = t.exchanges(0); - uint256 nOfExchanges = IBiPoolManager(biPoolManagerAddr).getExchanges().length; - for (uint256 i = 0; i < nOfExchanges; i++) { - Context memory ctx = newContext(_t, i); - if (getReferenceRateFeedID(ctx) == rateFeedID) { - return ctx; - } - } - return newRateFeedContext(_t, rateFeedID); - } - - // ========================= Swaps ========================= - - function swapIn(Context memory ctx, address from, address to, uint256 sellAmount) public returns (uint256) { - ctx.t._deal(from, ctx.trader, sellAmount, true); - changePrank(ctx.trader); - IERC20(from).approve(address(ctx.broker), sellAmount); - - addReportsIfNeeded(ctx); - uint256 minAmountOut = ctx.broker.getAmountOut(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount); - console.log( - string( - abi.encodePacked(unicode"🤝 swapIn(", toSymbol(from), "->", toSymbol(to), ", amountIn: %d, minAmountOut:%d)") - ), - toUnits(sellAmount, from), - toUnits(minAmountOut, to) - ); - return ctx.broker.swapIn(ctx.exchangeProvider, ctx.exchangeId, from, to, sellAmount, minAmountOut); - } - - function swapOut(Context memory ctx, address from, address to, uint256 buyAmount) public returns (uint256) { - addReportsIfNeeded(ctx); - uint256 maxAmountIn = ctx.broker.getAmountIn(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount); - - ctx.t._deal(from, ctx.trader, maxAmountIn, true); - changePrank(ctx.trader); - IERC20(from).approve(address(ctx.broker), maxAmountIn); - - console.log( - string( - abi.encodePacked(unicode"🤝 swapOut(", toSymbol(from), "->", toSymbol(to), ",amountOut: %d, maxAmountIn: %d)") - ), - toUnits(buyAmount, to), - toUnits(maxAmountIn, from) - ); - return ctx.broker.swapOut(ctx.exchangeProvider, ctx.exchangeId, from, to, buyAmount, maxAmountIn); - } - - function shouldUpdateBuckets(Context memory ctx) internal view returns (bool, bool, bool, bool, bool) { - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); - - (bool isReportExpired, ) = ctx.sortedOracles.isOldestReportExpired(exchange.config.referenceRateFeedID); - // solhint-disable-next-line not-rely-on-time - bool timePassed = block.timestamp >= exchange.lastBucketUpdate + exchange.config.referenceRateResetFrequency; - bool enoughReports = (ctx.sortedOracles.numRates(exchange.config.referenceRateFeedID) >= - exchange.config.minimumReports); - // solhint-disable-next-line not-rely-on-time - bool medianReportRecent = ctx.sortedOracles.medianTimestamp(exchange.config.referenceRateFeedID) > - block.timestamp - exchange.config.referenceRateResetFrequency; - - return ( - timePassed, - enoughReports, - medianReportRecent, - isReportExpired, - timePassed && enoughReports && medianReportRecent && !isReportExpired - ); - } - - function getUpdatedBuckets(Context memory ctx) internal view returns (uint256 bucket0, uint256 bucket1) { - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); - - bucket0 = exchange.config.stablePoolResetSize; - uint256 exchangeRateNumerator; - uint256 exchangeRateDenominator; - (exchangeRateNumerator, exchangeRateDenominator) = getReferenceRate(ctx); - - bucket1 = (exchangeRateDenominator * bucket0) / exchangeRateNumerator; - } - - function addReportsIfNeeded(Context memory ctx) internal { - // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); - (bool timePassed, bool enoughReports, bool medianReportRecent, bool isReportExpired, ) = shouldUpdateBuckets(ctx); - // logPool(ctx); - if (timePassed && (!medianReportRecent || isReportExpired || !enoughReports)) { - (uint256 newMedian, ) = ctx.sortedOracles.medianRate(pool.config.referenceRateFeedID); - (timePassed, enoughReports, medianReportRecent, isReportExpired, ) = shouldUpdateBuckets(ctx); - updateOracleMedianRate(ctx, (newMedian * 1_000_001) / 1_000_000); - - // logPool(ctx); - return; - } - } - - function maxSwapIn( - Context memory ctx, - uint256 desired, - address from, - address to - ) internal view returns (uint256 maxPossible) { - // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); - uint256 toBucket = (pool.asset0 == to ? pool.bucket0 : pool.bucket1) - 1; - (, , , , bool shouldUpdate) = shouldUpdateBuckets(ctx); - if (shouldUpdate) { - (uint256 bucket0, uint256 bucket1) = getUpdatedBuckets(ctx); - toBucket = (pool.asset0 == to ? bucket0 : bucket1) - 1; - } - toBucket = toBucket / biPoolManager.tokenPrecisionMultipliers(to); - maxPossible = ctx.broker.getAmountIn(ctx.exchangeProvider, ctx.exchangeId, from, to, toBucket); - if (maxPossible > desired) { - maxPossible = desired; - } - } - - function maxSwapOut(Context memory ctx, uint256 desired, address to) internal view returns (uint256 maxPossible) { - // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); - uint256 maxPossible_ = (pool.asset0 == to ? pool.bucket0 : pool.bucket1) - 1; - (, , , , bool shouldUpdate) = shouldUpdateBuckets(ctx); - if (shouldUpdate) { - (uint256 bucket0, uint256 bucket1) = getUpdatedBuckets(ctx); - maxPossible_ = (pool.asset0 == to ? bucket0 : bucket1) - 1; - } - maxPossible = maxPossible / biPoolManager.tokenPrecisionMultipliers(to); - if (maxPossible > desired) { - maxPossible = desired; - } - } - - // ========================= Sorted Oracles ========================= - - function getReferenceRateFraction( - Context memory ctx, - address baseAsset - ) internal view returns (FixidityLib.Fraction memory) { - (uint256 numerator, uint256 denominator) = getReferenceRate(ctx); - address asset0 = ctx.exchange.assets[0]; - if (baseAsset == asset0) { - return FixidityLib.newFixedFraction(numerator, denominator); - } - return FixidityLib.newFixedFraction(denominator, numerator); - } - - function getReferenceRate(Context memory ctx) internal view returns (uint256, uint256) { - uint256 rateNumerator; - uint256 rateDenominator; - (rateNumerator, rateDenominator) = ctx.sortedOracles.medianRate(getReferenceRateFeedID(ctx)); - require(rateDenominator > 0, "exchange rate denominator must be greater than 0"); - return (rateNumerator, rateDenominator); - } - - function getReferenceRateFeedID(Context memory ctx) internal view returns (address) { - if (ctx.rateFeedID != address(0)) { - return ctx.rateFeedID; - } - // TODO: extend this when we have multiple exchange providers, for now assume it's a BiPoolManager - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory pool = biPoolManager.getPoolExchange(ctx.exchangeId); - return pool.config.referenceRateFeedID; - } - - function getValueDeltaBreakerReferenceValue(Context memory ctx, address _breaker) internal view returns (uint256) { - IValueDeltaBreaker breaker = IValueDeltaBreaker(_breaker); - address rateFeedID = getReferenceRateFeedID(ctx); - return breaker.referenceValues(rateFeedID); - } - - function getBreakerRateChangeThreshold(Context memory ctx, address _breaker) internal view returns (uint256) { - IMedianDeltaBreaker breaker = IMedianDeltaBreaker(_breaker); - address rateFeedID = getReferenceRateFeedID(ctx); - - uint256 rateChangeThreshold = breaker.defaultRateChangeThreshold(); - uint256 specificRateChangeThreshold = breaker.rateChangeThreshold(rateFeedID); - if (specificRateChangeThreshold != 0) { - rateChangeThreshold = specificRateChangeThreshold; - } - return rateChangeThreshold; - } - - function updateOracleMedianRate(Context memory ctx, uint256 newMedian) internal { - address rateFeedID = getReferenceRateFeedID(ctx); - address[] memory oracles = ctx.sortedOracles.getOracles(rateFeedID); - require(oracles.length > 0, "No oracles for rateFeedID"); - console.log(unicode"🔮 Updating oracles to new median: ", newMedian); - for (uint256 i = 0; i < oracles.length; i++) { - skip(5); - address oracle = oracles[i]; - address lesserKey; - address greaterKey; - (address[] memory keys, uint256[] memory values, ) = ctx.sortedOracles.getRates(rateFeedID); - for (uint256 j = 0; j < keys.length; j++) { - if (keys[j] == oracle) continue; - if (values[j] < newMedian) lesserKey = keys[j]; - if (values[j] >= newMedian) greaterKey = keys[j]; - } - - changePrank(oracle); - ctx.sortedOracles.report(rateFeedID, newMedian, lesserKey, greaterKey); - } - console.log("done with updateOracleMedianRate"); - changePrank(ctx.trader); - } - - // ========================= Trading Limits ========================= - - function isLimitConfigured(Context memory ctx, bytes32 limitId) public view returns (bool) { - ITradingLimits.Config memory limitConfig = ctx.broker.tradingLimitsConfig(limitId); - return limitConfig.flags > uint8(0); - } - - function tradingLimitsConfig(Context memory ctx, bytes32 limitId) public view returns (ITradingLimits.Config memory) { - return ctx.broker.tradingLimitsConfig(limitId); - } - - function tradingLimitsState(Context memory ctx, bytes32 limitId) public view returns (ITradingLimits.State memory) { - return ctx.broker.tradingLimitsState(limitId); - } - - function tradingLimitsConfig(Context memory ctx, address asset) public view returns (ITradingLimits.Config memory) { - bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.broker.tradingLimitsConfig(ctx.exchangeId ^ assetBytes32); - } - - function tradingLimitsState(Context memory ctx, address asset) public view returns (ITradingLimits.State memory) { - bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.broker.tradingLimitsState(ctx.exchangeId ^ assetBytes32); - } - - function refreshedTradingLimitsState( - Context memory ctx, - address asset - ) public view returns (ITradingLimits.State memory) { - ITradingLimits.Config memory config = tradingLimitsConfig(ctx, asset); - // Netflow might be outdated because of a skip(...) call and doing - // an update(0) would reset the netflow if enough time has passed. - return ctx.tradingLimits.update(tradingLimitsState(ctx, asset), config, 0, 0); - } - - function isLimitEnabled(ITradingLimits.Config memory config, uint8 limit) internal pure returns (bool) { - return (config.flags & limit) > 0; - } - - function getLimit(ITradingLimits.Config memory config, uint8 limit) internal pure returns (uint256) { - if (limit == L0) { - return uint256(int256(config.limit0)); - } else if (limit == L1) { - return uint256(int256(config.limit1)); - } else if (limit == LG) { - return uint256(int256(config.limitGlobal)); - } else { - revert("invalid limit"); - } - } - - function getNetflow(ITradingLimits.State memory state, uint8 limit) internal pure returns (int256) { - if (limit == L0) { - return state.netflow0; - } else if (limit == L1) { - return state.netflow1; - } else if (limit == LG) { - return state.netflowGlobal; - } else { - revert("invalid limit"); - } - } - - function revertReason(uint8 limit) internal pure returns (string memory) { - if (limit == L0) { - return "L0 Exceeded"; - } else if (limit == L1) { - return "L1 Exceeded"; - } else if (limit == LG) { - return "LG Exceeded"; - } else { - revert("invalid limit"); - } - } - - function limitString(uint8 limit) internal pure returns (string memory) { - if (limit == L0) { - return "L0"; - } else if (limit == L1) { - return "L1"; - } else if (limit == LG) { - return "LG"; - } else { - revert("invalid limit"); - } - } - - function maxPossibleInflow(Context memory ctx, address from) internal view returns (int48) { - ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, from); - ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, from); - int48 maxInflowL0 = limitConfig.limit0 - limitState.netflow0; - int48 maxInflowL1 = limitConfig.limit1 - limitState.netflow1; - int48 maxInflowLG = limitConfig.limitGlobal - limitState.netflowGlobal; - - if (limitConfig.flags == L0 | L1 | LG) { - return min(maxInflowL0, maxInflowL1, maxInflowLG); - } else if (limitConfig.flags == L0 | LG) { - return min(maxInflowL0, maxInflowLG); - } else if (limitConfig.flags == L0 | L1) { - return min(maxInflowL0, maxInflowL1); - } else if (limitConfig.flags == L0) { - return maxInflowL0; - } else { - revert("Unexpected limit config"); - } - } - - function maxPossibleOutflow(Context memory ctx, address to) internal view returns (int48) { - ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, to); - ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, to); - int48 maxOutflowL0 = limitConfig.limit0 + limitState.netflow0 - 1; - int48 maxOutflowL1 = limitConfig.limit1 + limitState.netflow1 - 1; - int48 maxOutflowLG = limitConfig.limitGlobal + limitState.netflowGlobal - 1; - - if (limitConfig.flags == L0 | L1 | LG) { - return min(maxOutflowL0, maxOutflowL1, maxOutflowLG); - } else if (limitConfig.flags == L0 | LG) { - return min(maxOutflowL0, maxOutflowLG); - } else if (limitConfig.flags == L0 | L1) { - return min(maxOutflowL0, maxOutflowL1); - } else if (limitConfig.flags == L0) { - return maxOutflowL0; - } else { - revert("Unexpected limit config"); - } - } - - // ========================= Misc ========================= - - function toSubunits(uint256 units, address token) internal view returns (uint256) { - uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); - return units * tokenBase; - } - - function toUnits(uint256 subunits, address token) internal view returns (uint256) { - uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); - return subunits / tokenBase; - } - - function toUnitsFixed(uint256 subunits, address token) internal view returns (FixidityLib.Fraction memory) { - uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); - return FixidityLib.newFixedFraction(subunits, tokenBase); - } - - function toSymbol(address token) internal view returns (string memory) { - return IERC20(token).symbol(); - } - - function ticker(Context memory ctx) internal view returns (string memory) { - return - string(abi.encodePacked(IERC20(ctx.exchange.assets[0]).symbol(), "/", IERC20(ctx.exchange.assets[1]).symbol())); - } - - function logHeader(Context memory ctx) internal view { - console.log("========================================"); - console.log(unicode"🔦 Testing pair:", ticker(ctx)); - console.log("========================================"); - } - - function min(int48 a, int48 b) internal pure returns (int48) { - return a > b ? b : a; - } - - function min(int48 a, int48 b, int48 c) internal pure returns (int48) { - return min(a, min(b, c)); - } - - function logPool(Context memory ctx) internal view { - if (ctx.exchangeId == 0) { - console.log(unicode"🎱 RateFeed: %s", ctx.rateFeedID); - return; - } - IBiPoolManager biPoolManager = IBiPoolManager(ctx.exchangeProvider); - IBiPoolManager.PoolExchange memory exchange = biPoolManager.getPoolExchange(ctx.exchangeId); - - (bool timePassed, bool enoughReports, bool medianReportRecent, bool isReportExpired, ) = shouldUpdateBuckets(ctx); - console.log(unicode"🎱 Pool: %s", ticker(ctx)); - console.log( - "\t timePassed: %s | enoughReports: %s", - timePassed ? "true" : "false", - enoughReports ? "true" : "false" - ); - console.log( - "\t medianReportRecent: %s | !isReportExpired: %s", - medianReportRecent ? "true" : "false", - !isReportExpired ? "true" : "false" - ); - console.log( - "\t exchange.bucket0: %d | exchange.bucket1: %d", - toUnits(exchange.bucket0, exchange.asset0), - toUnits(exchange.bucket1, exchange.asset1) - ); - console.log("\t exchange.lastBucketUpdate: %d", exchange.lastBucketUpdate); - } - - function logNetflows(Context memory ctx, address target) internal view { - ITradingLimits.State memory limitState = tradingLimitsState(ctx, target); - console.log( - "\t netflow0: %s%d", - limitState.netflow0 < 0 ? "-" : "", - uint256(int256(limitState.netflow0 < 0 ? limitState.netflow0 * -1 : limitState.netflow0)) - ); - console.log( - "\t netflow1: %s%d", - limitState.netflow1 < 0 ? "-" : "", - uint256(int256(limitState.netflow1 < 0 ? limitState.netflow1 * -1 : limitState.netflow1)) - ); - console.log( - "\t netflowGlobal: %s%d", - limitState.netflowGlobal < 0 ? "-" : "", - uint256(int256(limitState.netflowGlobal < 0 ? limitState.netflowGlobal * -1 : limitState.netflowGlobal)) - ); - } - - // ==================== Forge Cheats ====================== - // Pulling in some test helpers to not have to expose them in - // the test contract - - function skip(uint256 time) internal { - vm.warp(block.timestamp + time); - } - - function changePrank(address who) internal { - vm.stopPrank(); - vm.startPrank(who); - } -} diff --git a/test/fork/actions/OracleActions.sol b/test/fork/actions/OracleActions.sol new file mode 100644 index 00000000..49f31f02 --- /dev/null +++ b/test/fork/actions/OracleActions.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { VM_ADDRESS } from "mento-std/Constants.sol"; + +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { OracleHelpers } from "../helpers/OracleHelpers.sol"; +import { SwapHelpers } from "../helpers/SwapHelpers.sol"; + +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; + +contract OracleActions is StdCheats { + using OracleHelpers for *; + using SwapHelpers for *; + using TokenHelpers for *; + + Vm private vm = Vm(VM_ADDRESS); + ExchangeForkTest private ctx = ExchangeForkTest(address(this)); + + function updateOracleMedianRate(uint256 newMedian) public { + updateOracleMedianRate(ctx.rateFeedId(), newMedian); + } + + function updateOracleMedianRate(address rateFeedId, uint256 newMedian) public { + address[] memory oracles = ctx.sortedOracles().getOracles(rateFeedId); + require(oracles.length > 0, "No oracles for rateFeedId"); + console.log(unicode"🔮 Updating oracles to new median: ", newMedian); + for (uint256 i = 0; i < oracles.length; i++) { + skip(5); + address oracle = oracles[i]; + address lesserKey; + address greaterKey; + (address[] memory keys, uint256[] memory values, ) = ctx.sortedOracles().getRates(rateFeedId); + for (uint256 j = 0; j < keys.length; j++) { + if (keys[j] == oracle) continue; + if (values[j] < newMedian) lesserKey = keys[j]; + if (values[j] >= newMedian) greaterKey = keys[j]; + } + + vm.startPrank(oracle); + ctx.sortedOracles().report(rateFeedId, newMedian, lesserKey, greaterKey); + vm.stopPrank(); + } + } + + function addReportsIfNeeded() public { + IBiPoolManager.PoolExchange memory pool = ctx.exchangeProvider().getPoolExchange(ctx.exchangeId()); + (bool timePassed, bool enoughReports, bool medianReportRecent, bool isReportExpired, ) = ctx.shouldUpdateBuckets(); + if (timePassed && (!medianReportRecent || isReportExpired || !enoughReports)) { + (uint256 newMedian, ) = ctx.sortedOracles().medianRate(pool.config.referenceRateFeedID); + (timePassed, enoughReports, medianReportRecent, isReportExpired, ) = ctx.shouldUpdateBuckets(); + updateOracleMedianRate((newMedian * 1_000_001) / 1_000_000); + return; + } + } + + function ensureRateActive() public returns (uint256 newMedian) { + return ensureRateActive(ctx.rateFeedId()); + } + + function ensureRateActive(address rateFeedId) public returns (uint256 newMedian) { + // Always do a small update in order to make sure the breakers are warm. + (uint256 currentRate, ) = ctx.sortedOracles().medianRate(rateFeedId); + newMedian = currentRate + (currentRate / 100_000_000); // a small increase + updateOracleMedianRate(rateFeedId, newMedian); + uint8 tradingMode = ctx.breakerBox().getRateFeedTradingMode(rateFeedId); + uint256 attempts = 0; + while (tradingMode != 0 && attempts < 10) { + console.log("attempt #%d", attempts); + attempts++; + // while the breaker is active, we wait for the cooldown and try to update the median + console.log(block.timestamp, "Waiting for cooldown to pass"); + console.log("RateFeedID:", rateFeedId); + address[] memory _breakers = ctx.breakerBox().getBreakers(); + uint256 cooldown = 0; + uint256 breakerIndex; + for (uint256 i = 0; i < _breakers.length; i++) { + if (ctx.breakerBox().isBreakerEnabled(_breakers[i], rateFeedId)) { + IBreakerBox.BreakerStatus memory status = ctx.breakerBox().rateFeedBreakerStatus(rateFeedId, _breakers[i]); + if (status.tradingMode != 0) { + breakerIndex = i; + cooldown = IValueDeltaBreaker(_breakers[i]).getCooldown(rateFeedId); + break; + } + } + } + skip(cooldown); + newMedian = ctx.newMedianToResetBreaker(rateFeedId, breakerIndex); + ctx.updateOracleMedianRate(rateFeedId, newMedian); + if (cooldown == 0) { + console.log("Manual recovery required for breaker %s", _breakers[breakerIndex]); + vm.startPrank(ctx.breakerBox().owner()); + ctx.breakerBox().setRateFeedTradingMode(rateFeedId, 0); + vm.stopPrank(); + } + tradingMode = ctx.breakerBox().getRateFeedTradingMode(rateFeedId); + } + } +} diff --git a/test/fork/actions/SwapActions.sol b/test/fork/actions/SwapActions.sol new file mode 100644 index 00000000..ce4160cb --- /dev/null +++ b/test/fork/actions/SwapActions.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { StdCheats } from "forge-std/StdCheats.sol"; +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { VM_ADDRESS } from "mento-std/Constants.sol"; +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; + +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { OracleHelpers } from "../helpers/OracleHelpers.sol"; +import { SwapHelpers } from "../helpers/SwapHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; +import { LogHelpers } from "../helpers/LogHelpers.sol"; + +contract SwapActions is StdCheats { + Vm private vm = Vm(VM_ADDRESS); + ExchangeForkTest private ctx = ExchangeForkTest(address(this)); + + using FixidityLib for FixidityLib.Fraction; + using OracleHelpers for *; + using SwapHelpers for *; + using TokenHelpers for *; + using TradingLimitHelpers for *; + using LogHelpers for *; + + uint8 private constant L0 = 1; // 0b001 Limit0 + uint8 private constant L1 = 2; // 0b010 Limit1 + uint8 private constant LG = 4; // 0b100 LimitGlobal + + function swapIn(address from, address to, uint256 sellAmount) public returns (uint256 amountOut) { + ctx.mint(from, ctx.trader(), sellAmount, true); + vm.startPrank(ctx.trader()); + IERC20(from).approve(address(ctx.broker()), sellAmount); + vm.stopPrank(); + + ctx.addReportsIfNeeded(); + uint256 minAmountOut = ctx.broker().getAmountOut( + address(ctx.exchangeProvider()), + ctx.exchangeId(), + from, + to, + sellAmount + ); + + amountOut = brokerSwapIn(from, to, sellAmount, minAmountOut); + } + + function brokerSwapIn( + address from, + address to, + uint256 sellAmount, + uint256 minAmountOut + ) public returns (uint256 amountOut) { + console.log( + unicode"🤝 swapIn(%s, amountIn: %d, minAmountOut: %d)", + string(abi.encodePacked(from.symbol(), "->", to.symbol())), + sellAmount.toUnits(from), + minAmountOut.toUnits(to) + ); + vm.startPrank(ctx.trader()); + amountOut = ctx.broker().swapIn(ctx.exchangeProviderAddr(), ctx.exchangeId(), from, to, sellAmount, minAmountOut); + vm.stopPrank(); + } + + function swapOut(address from, address to, uint256 buyAmount) public returns (uint256) { + ctx.addReportsIfNeeded(); + uint256 maxAmountIn = ctx.getAmountIn(from, to, buyAmount); + + ctx.mint(from, ctx.trader(), maxAmountIn, true); + vm.startPrank(ctx.trader()); + IERC20(from).approve(address(ctx.broker()), maxAmountIn); + return brokerSwapOut(from, to, buyAmount, maxAmountIn); + } + + function brokerSwapOut( + address from, + address to, + uint256 buyAmount, + uint256 maxAmountIn + ) public returns (uint256 amountIn) { + console.log( + string( + abi.encodePacked(unicode"🤝 swapOut(", from.symbol(), "->", to.symbol(), ",amountOut: %d, maxAmountIn: %d)") + ), + buyAmount.toUnits(to), + maxAmountIn.toUnits(from) + ); + vm.startPrank(ctx.trader()); + amountIn = ctx.broker().swapOut(ctx.exchangeProviderAddr(), ctx.exchangeId(), from, to, buyAmount, maxAmountIn); + vm.stopPrank(); + } + + function swapUntilL0_onInflow(address from, address to) internal { + /* + * L0[from] -> to + * This function will do valid swaps until just before L0 is hit + * during inflow on `from`, therfore we check the positive end + * of the limit because `from` flows into the reserve. + */ + + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + console.log(unicode"🏷️ [%d] Swap until L0=%d on inflow", block.timestamp, uint256(int256(limitConfig.limit0))); + uint256 maxPossible; + uint256 maxPossibleUntilLimit; + do { + ctx.logLimits(from); + int48 maxPossibleUntilLimitUnits = ctx.maxInflow(from); + console.log("\tmaxPossibleUntilLimitUnits: %d", maxPossibleUntilLimitUnits); + require(maxPossibleUntilLimitUnits >= 0, "max possible trade amount is negative"); + maxPossibleUntilLimit = uint256(int256(maxPossibleUntilLimitUnits)).toSubunits(from); + console.log("\tmaxPossibleUntilLimit: %d", maxPossibleUntilLimit); + maxPossible = ctx.maxSwapIn(maxPossibleUntilLimit, from, to); + console.log("\tmaxPossible: %d", maxPossible); + + if (maxPossible > 0) { + ctx.swapIn(from, to, maxPossible); + } + } while (maxPossible > 0 && maxPossibleUntilLimit > maxPossible); + ctx.logLimits(from); + } + + function swapUntilL1_onInflow(address from, address to) internal { + /* + * L1[from] -> to + * This function will do valid swaps until just before L1 is hit + * during inflow on `from`, therfore we check the positive end + * of the limit because `from` flows into the reserve. + */ + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); + console.log(unicode"🏷️ [%d] Swap until L1=%d on inflow", block.timestamp, uint256(int256(limitConfig.limit1))); + int48 maxPerSwap = limitConfig.limit0; + while (limitState.netflow1 + maxPerSwap <= limitConfig.limit1) { + skip(limitConfig.timestep0 + 1); + ctx.ensureRateActive(); // needed because otherwise constantSum might revert if the median is stale due to the skip + + swapUntilL0_onInflow(from, to); + limitConfig = ctx.tradingLimitsConfig(from); + limitState = ctx.refreshedTradingLimitsState(from); + if (limitState.netflowGlobal == limitConfig.limitGlobal) { + console.log(unicode"🚨 LG reached during L1 inflow"); + break; + } + } + skip(limitConfig.timestep0 + 1); + ctx.ensureRateActive(); + } + + function swapUntilLG_onInflow(address from, address to) internal { + /* + * L1[from] -> to + * This function will do valid swaps until just before LG is hit + * during inflow on `from`, therfore we check the positive end + * of the limit because `from` flows into the reserve. + */ + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); + console.log(unicode"🏷️ [%d] Swap until LG=%d on inflow", block.timestamp, uint256(int256(limitConfig.limitGlobal))); + + if (limitConfig.isLimitEnabled(L1)) { + int48 maxPerSwap = limitConfig.limit0; + uint256 it; + while (limitState.netflowGlobal + maxPerSwap <= limitConfig.limitGlobal) { + skip(limitConfig.timestep1 + 1); + swapUntilL1_onInflow(from, to); + limitConfig = ctx.tradingLimitsConfig(from); + limitState = ctx.tradingLimitsState(from); + it++; + require(it < 50, "infinite loop"); + } + skip(limitConfig.timestep1 + 1); + } else if (limitConfig.isLimitEnabled(L0)) { + int48 maxPerSwap = limitConfig.limit0; + uint256 it; + while (limitState.netflowGlobal + maxPerSwap <= limitConfig.limitGlobal) { + skip(limitConfig.timestep0 + 1); + swapUntilL0_onInflow(from, to); + limitConfig = ctx.tradingLimitsConfig(from); + limitState = ctx.tradingLimitsState(from); + it++; + require(it < 50, "infinite loop"); + } + skip(limitConfig.timestep0 + 1); + } + } + + function swapUntilL0_onOutflow(address from, address to) public { + /* + * from -> L0[to] + * This function will do valid swaps until just before L0 is hit + * during outflow on `to`, therfore we check the negative end + * of the limit because `to` flows out of the reserve. + */ + + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + console.log(unicode"🏷️ [%d] Swap until L0=%d on outflow", block.timestamp, uint256(int256(limitConfig.limit0))); + uint256 maxPossible; + uint256 maxPossibleUntilLimit; + do { + ctx.logLimits(to); + int48 maxPossibleUntilLimitUnits = ctx.maxOutflow(to); + console.log("\tmaxPossibleUnits: %d", maxPossibleUntilLimitUnits); + require(maxPossibleUntilLimitUnits >= 0, "max possible trade amount is negative"); + maxPossibleUntilLimit = uint256(maxPossibleUntilLimitUnits.toSubunits(to)); + console.log("\tmaxPossibleUnits: %d", maxPossibleUntilLimit); + maxPossible = ctx.maxSwapOut(maxPossibleUntilLimit, to); + console.log("\tmaxPossibleActual: %d", maxPossible); + + if (maxPossible > 0) { + ctx.swapOut(from, to, maxPossible); + } + } while (maxPossible > 0 && maxPossibleUntilLimit > maxPossible); + ctx.logLimits(to); + } + + function swapUntilL1_onOutflow(address from, address to) public { + /* + * from -> L1[to] + * This function will do valid swaps until just before L1 is hit + * during outflow on `to`, therfore we check the negative end + * of the limit because `to` flows out of the reserve. + */ + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); + + console.log(unicode"🏷️ [%d] Swap until L1=%d on outflow", block.timestamp, uint48(limitConfig.limit1)); + int48 maxPerSwap = limitConfig.limit0; + uint256 it; + while (limitState.netflow1 - maxPerSwap >= -1 * limitConfig.limit1) { + skip(limitConfig.timestep0 + 1); + // Check that there's still outflow to trade as sometimes we hit LG while + // still having a bit of L1 left, which causes an infinite loop. + if (ctx.maxOutflow(to) == 0) { + break; + } + swapUntilL0_onOutflow(from, to); + limitConfig = ctx.tradingLimitsConfig(to); + limitState = ctx.tradingLimitsState(to); + it++; + require(it < 10, "infinite loop"); + } + skip(limitConfig.timestep0 + 1); + } + + function swapUntilLG_onOutflow(address from, address to) public { + /* + * from -> LG[to] + * This function will do valid swaps until just before LG is hit + * during outflow on `to`, therfore we check the negative end + * of the limit because `to` flows out of the reserve. + */ + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); + console.log(unicode"🏷️ [%d] Swap until LG=%d on outflow", block.timestamp, uint48(limitConfig.limitGlobal)); + + if (limitConfig.isLimitEnabled(L1)) { + int48 maxPerSwap = limitConfig.limit0; + while (limitState.netflowGlobal - maxPerSwap >= -1 * limitConfig.limitGlobal) { + skip(limitConfig.timestep1 + 1); + swapUntilL1_onOutflow(from, to); + limitConfig = ctx.tradingLimitsConfig(to); + // Triger an update to reset netflows + limitState = ctx.tradingLimitsState(to); + } + skip(limitConfig.timestep1 + 1); + } else if (limitConfig.isLimitEnabled(L0)) { + int48 maxPerSwap = limitConfig.limit0; + while (limitState.netflowGlobal - maxPerSwap >= -1 * limitConfig.limitGlobal) { + skip(limitConfig.timestep0 + 1); + swapUntilL0_onOutflow(from, to); + limitConfig = ctx.tradingLimitsConfig(to); + // Triger an update to reset netflows + limitState = ctx.tradingLimitsState(to); + } + skip(limitConfig.timestep0 + 1); + } + } +} diff --git a/test/fork/actions/all.sol b/test/fork/actions/all.sol new file mode 100644 index 00000000..093e0617 --- /dev/null +++ b/test/fork/actions/all.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { OracleActions } from "./OracleActions.sol"; +import { SwapActions } from "./SwapActions.sol"; + +contract Actions is OracleActions, SwapActions {} diff --git a/test/fork/assertions/CircuitBreakerAssertions.sol b/test/fork/assertions/CircuitBreakerAssertions.sol new file mode 100644 index 00000000..279426f2 --- /dev/null +++ b/test/fork/assertions/CircuitBreakerAssertions.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { StdAssertions } from "forge-std/StdAssertions.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { VM_ADDRESS } from "mento-std/Constants.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; + +import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; + +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; +import { Actions } from "../actions/all.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { OracleHelpers } from "../helpers/OracleHelpers.sol"; +import { SwapHelpers } from "../helpers/SwapHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; +import { LogHelpers } from "../helpers/LogHelpers.sol"; + +contract CircuitBreakerAssertions is StdAssertions, Actions { + using FixidityLib for FixidityLib.Fraction; + using OracleHelpers for *; + using SwapHelpers for *; + using TokenHelpers for *; + using TradingLimitHelpers for *; + using LogHelpers for *; + + Vm private vm = Vm(VM_ADDRESS); + ExchangeForkTest private ctx = ExchangeForkTest(address(this)); + + uint256 private constant fixed1 = 1e24; + + function assert_breakerBreaks(address rateFeedId, address breaker, uint256 breakerIndex) public { + // XXX: There is currently no straightforward way to determine what type of a breaker + // we are dealing with, so we will use the deployment setup that we currently chose, + // where the medianDeltaBreaker gets deployed first and the valueDeltaBreaker second. + bool isMedianDeltaBreaker = breakerIndex == 0; + bool isValueDeltaBreaker = breakerIndex == 1; + if (isMedianDeltaBreaker) { + assert_medianDeltaBreakerBreaks_onIncrease(rateFeedId, breaker); + assert_medianDeltaBreakerBreaks_onDecrease(rateFeedId, breaker); + } else if (isValueDeltaBreaker) { + assert_valueDeltaBreakerBreaks_onIncrease(rateFeedId, breaker); + assert_valueDeltaBreakerBreaks_onDecrease(rateFeedId, breaker); + } else { + revert("Unknown trading mode, can't infer breaker type"); + } + } + + function assert_medianDeltaBreakerBreaks_onIncrease(address rateFeedId, address _breaker) public { + uint256 currentMedian = ensureRateActive(rateFeedId); // ensure trading mode is 0 + + // trigger breaker by setting new median to ema - (threshold + 0.001% buffer) + uint256 currentEMA = IMedianDeltaBreaker(_breaker).medianRatesEMA(rateFeedId); + uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(rateFeedId, _breaker); + uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); // small buffer because of rounding errors + uint256 maxPercent = fixed1 + rateChangeThreshold + thresholdBuffer; + uint256 newMedian = (currentEMA * maxPercent) / fixed1; + + console.log("Current Median: ", currentMedian); + console.log("Current EMA: ", currentEMA); + console.log("New Median: ", newMedian); + assert_breakerBreaks_withNewMedian(rateFeedId, newMedian, 3); + } + + function assert_medianDeltaBreakerBreaks_onDecrease(address rateFeedId, address _breaker) public { + uint256 currentMedian = ensureRateActive(rateFeedId); // ensure trading mode is 0 + + // trigger breaker by setting new median to ema + (threshold + 0.001% buffer) + uint256 currentEMA = IMedianDeltaBreaker(_breaker).medianRatesEMA(rateFeedId); + uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(rateFeedId, _breaker); + uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); // small buffer because of rounding errors + uint256 maxPercent = fixed1 - (rateChangeThreshold + thresholdBuffer); + uint256 newMedian = (currentEMA * maxPercent) / fixed1; + + console.log("Current Median: ", currentMedian); + console.log("Current EMA: ", currentEMA); + console.log("New Median: ", newMedian); + assert_breakerBreaks_withNewMedian(rateFeedId, newMedian, 3); + } + + function assert_valueDeltaBreakerBreaks_onIncrease(address rateFeedId, address _breaker) public { + uint256 currentMedian = ensureRateActive(rateFeedId); // ensure trading mode is 0 + + // trigger breaker by setting new median to reference value + threshold + 1 + uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(rateFeedId, _breaker); + uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(rateFeedId, _breaker); + uint256 maxPercent = fixed1 + rateChangeThreshold; + uint256 newMedian = (referenceValue * maxPercent) / fixed1; + newMedian = newMedian + 1; + + console.log("Current Median: ", currentMedian); + console.log("Reference Value: ", referenceValue); + console.log("New Median: ", newMedian); + assert_breakerBreaks_withNewMedian(rateFeedId, newMedian, 3); + } + + function assert_valueDeltaBreakerBreaks_onDecrease(address rateFeedId, address _breaker) public { + uint256 currentMedian = ensureRateActive(rateFeedId); // ensure trading mode is 0 + + // trigger breaker by setting new median to reference value - threshold - 1 + uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(rateFeedId, _breaker); + uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(rateFeedId, _breaker); + uint256 maxPercent = fixed1 - rateChangeThreshold; + uint256 newMedian = (referenceValue * maxPercent) / fixed1; + newMedian = newMedian - 1; + + console.log("Current Median: ", currentMedian); + console.log("Reference Value: ", referenceValue); + console.log("New Median: ", newMedian); + assert_breakerBreaks_withNewMedian(rateFeedId, newMedian, 3); + } + + function assert_breakerRecovers(address rateFeedId, address breaker, uint256 breakerIndex) public { + // XXX: There is currently no straightforward way to determine what type of a breaker + // we are dealing with, so we will use the deployment setup that we currently chose, + // where the medianDeltaBreaker gets deployed first and the valueDeltaBreaker second. + bool isMedianDeltaBreaker = breakerIndex == 0; + bool isValueDeltaBreaker = breakerIndex == 1; + if (isMedianDeltaBreaker) { + assert_medianDeltaBreakerRecovers(rateFeedId, breaker); + } else if (isValueDeltaBreaker) { + assert_valueDeltaBreakerRecovers(rateFeedId, breaker); + } else { + revert("Unknown trading mode, can't infer breaker type"); + } + } + + function assert_medianDeltaBreakerRecovers(address rateFeedId, address _breaker) internal { + uint256 currentMedian = ensureRateActive(rateFeedId); // ensure trading mode is 0 + IMedianDeltaBreaker breaker = IMedianDeltaBreaker(_breaker); + + // trigger breaker by setting new median to ema + threshold + 0.001% + uint256 currentEMA = breaker.medianRatesEMA(rateFeedId); + uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(rateFeedId, _breaker); + uint256 thresholdBuffer = FixidityLib.newFixedFraction(1, 1000).unwrap(); + uint256 maxPercent = fixed1 + rateChangeThreshold + thresholdBuffer; + uint256 newMedian = (currentEMA * maxPercent) / fixed1; + + console.log("Current Median: ", currentMedian); + console.log("New Median: ", newMedian); + assert_breakerBreaks_withNewMedian(rateFeedId, newMedian, 3); + + // wait for cool down and reset by setting new median to ema + uint256 cooldown = breaker.getCooldown(rateFeedId); + if (cooldown == 0) { + changePrank(ctx.breakerBox().owner()); + ctx.breakerBox().setRateFeedTradingMode(rateFeedId, 0); + } else { + skip(cooldown); + currentEMA = breaker.medianRatesEMA(rateFeedId); + assert_breakerRecovers_withNewMedian(rateFeedId, currentEMA); + } + } + + function assert_valueDeltaBreakerRecovers(address rateFeedId, address _breaker) internal { + uint256 currentMedian = ensureRateActive(rateFeedId); // ensure trading mode is 0 + IValueDeltaBreaker breaker = IValueDeltaBreaker(_breaker); + + // trigger breaker by setting new median to reference value + threshold + 1 + uint256 rateChangeThreshold = ctx.getBreakerRateChangeThreshold(rateFeedId, _breaker); + uint256 referenceValue = ctx.getValueDeltaBreakerReferenceValue(rateFeedId, _breaker); + uint256 maxPercent = fixed1 + rateChangeThreshold; + uint256 newMedian = (referenceValue * maxPercent) / fixed1; + newMedian = newMedian + 1; + + console.log("Current Median: ", currentMedian); + console.log("Reference Value: ", referenceValue); + console.log("New Median: ", newMedian); + assert_breakerBreaks_withNewMedian(rateFeedId, newMedian, 3); + + // wait for cool down and reset by setting new median to refernece value + uint256 cooldown = breaker.getCooldown(rateFeedId); + if (cooldown == 0) { + changePrank(ctx.breakerBox().owner()); + ctx.breakerBox().setRateFeedTradingMode(rateFeedId, 0); + } else { + skip(cooldown); + assert_breakerRecovers_withNewMedian(rateFeedId, referenceValue); + } + } + + function assert_breakerBreaks_withNewMedian( + address rateFeedId, + uint256 newMedian, + uint256 expectedTradingMode + ) public { + uint256 tradingMode = ctx.breakerBox().getRateFeedTradingMode(rateFeedId); + require(tradingMode == 0, "breaker should be recovered"); + + ctx.updateOracleMedianRate(rateFeedId, newMedian); + tradingMode = ctx.breakerBox().getRateFeedTradingMode(ctx.rateFeedId()); + require(tradingMode == expectedTradingMode, "trading more is different from expected"); + } + + function assert_breakerRecovers_withNewMedian(address rateFeedId, uint256 newMedian) public { + uint256 tradingMode = ctx.breakerBox().getRateFeedTradingMode(rateFeedId); + require(tradingMode != 0, "breaker should be triggered"); + + ctx.updateOracleMedianRate(rateFeedId, newMedian); + tradingMode = ctx.breakerBox().getRateFeedTradingMode(rateFeedId); + require(tradingMode == 0, "breaker should be recovered"); + } +} diff --git a/test/fork/assertions/SwapAssertions.sol b/test/fork/assertions/SwapAssertions.sol new file mode 100644 index 00000000..2108fbba --- /dev/null +++ b/test/fork/assertions/SwapAssertions.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { StdAssertions } from "forge-std/StdAssertions.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { VM_ADDRESS } from "mento-std/Constants.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; + +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; + +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; +import { Actions } from "../actions/all.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { OracleHelpers } from "../helpers/OracleHelpers.sol"; +import { SwapHelpers } from "../helpers/SwapHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; +import { LogHelpers } from "../helpers/LogHelpers.sol"; +import { L0, L1, LG } from "../helpers/misc.sol"; + +contract SwapAssertions is StdAssertions, Actions { + using FixidityLib for FixidityLib.Fraction; + using OracleHelpers for *; + using SwapHelpers for *; + using TokenHelpers for *; + using TradingLimitHelpers for *; + using LogHelpers for *; + + Vm private vm = Vm(VM_ADDRESS); + ExchangeForkTest private ctx = ExchangeForkTest(address(this)); + + uint256 fixed1 = FixidityLib.fixed1().unwrap(); + FixidityLib.Fraction pc10 = FixidityLib.newFixedFraction(10, 100); + + function assert_swapIn(address from, address to) internal { + console.log("==========assert_swapIn(%s->%s)=============", from.symbol(), to.symbol()); + ctx.logLimits(from); + ctx.logLimits(to); + if (ctx.atInflowLimit(from, LG)) { + console.log(unicode"🚨 Cannot test swap beacause the global inflow limit is reached on %s", from); + return; + } else if (ctx.atOutflowLimit(to, LG)) { + console.log(unicode"🚨 Cannot test swap beacause the global outflow limit is reached on %s", to); + return; + } + + FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(from); + emit log_named_decimal_uint("Rate", rate.unwrap(), 24); + + // XXX: To avoid an edge case when dealing with subunit trades in the + // trading limits implementation, we want to make sure that we're always + // dealing with at least 1 units of tokens. Thus we have to + // figure out based on the rate how many units of [FROM] we need to sell + // in order to get at least 1 unit of [TO]. + + uint256 sellAmount; + if (rate.lt(FixidityLib.wrap(1e24))) { + sellAmount = uint256(1).toSubunits(from); + } else { + sellAmount = (rate.toSubunits(from) * 110) / 100; // 10% buffer + } + console.log("Sell amount: %d", sellAmount); + + FixidityLib.Fraction memory amountIn = sellAmount.toUnitsFixed(from); + FixidityLib.Fraction memory amountOut = swapIn(from, to, sellAmount).toUnitsFixed(to); + FixidityLib.Fraction memory expectedAmountOut = amountIn.divide(rate); + + assertApproxEqAbs(amountOut.unwrap(), expectedAmountOut.unwrap(), pc10.multiply(expectedAmountOut).unwrap()); + } + + function assert_swapOut(address from, address to) internal { + console.log("==========assert_swapOut(%s->%s)=============", from.symbol(), to.symbol()); + ctx.logLimits(from); + ctx.logLimits(to); + if (ctx.atInflowLimit(from, LG)) { + console.log(unicode"🚨 Cannot test swap beacause the global inflow limit is reached on %s", from); + return; + } else if (ctx.atOutflowLimit(to, LG)) { + console.log(unicode"🚨 Cannot test swap beacause the global outflow limit is reached on %s", to); + return; + } + + FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(to); + emit log_named_decimal_uint("Rate", rate.unwrap(), 24); + + // XXX: To avoid an edge case when dealing with subunit trades in the + // trading limits implementation, we want to make sure that we're always + // dealing with at least 1 units of tokens. Thus we have to + // figure out based on the rate how many units of [TO] we need to buy + // in order to get at least 1 unit of [FROM]. + + uint256 buyAmount; + if (rate.lt(FixidityLib.wrap(1e24))) { + buyAmount = uint256(1).toSubunits(to); + } else { + buyAmount = (rate.toSubunits(to) * 110) / 100; // 10% buffer + } + + FixidityLib.Fraction memory amountIn = swapOut(from, to, buyAmount).toUnitsFixed(from); + FixidityLib.Fraction memory amountOut = buyAmount.toUnitsFixed(to); + FixidityLib.Fraction memory expectedAmountIn = amountOut.divide(rate); + + assertApproxEqAbs(amountIn.unwrap(), expectedAmountIn.unwrap(), pc10.multiply(expectedAmountIn).unwrap()); + } + + function assert_swapInFails(address from, address to, uint256 sellAmount, string memory revertReason) internal { + ctx.addReportsIfNeeded(); + ctx.mint(from, ctx.trader(), sellAmount, true); + vm.startPrank(ctx.trader()); + IERC20(from).approve(address(ctx.broker()), sellAmount); + uint256 minAmountOut = ctx.broker().getAmountOut( + ctx.exchangeProviderAddr(), + ctx.exchangeId(), + from, + to, + sellAmount + ); + vm.expectRevert(bytes(revertReason)); + ctx.brokerSwapIn(from, to, sellAmount, minAmountOut); + vm.stopPrank(); + } + + function assert_swapOutFails(address from, address to, uint256 buyAmount, string memory revertReason) internal { + ctx.addReportsIfNeeded(); + uint256 maxAmountIn = ctx.broker().getAmountIn(ctx.exchangeProviderAddr(), ctx.exchangeId(), from, to, buyAmount); + ctx.mint(from, ctx.trader(), maxAmountIn, true); + vm.startPrank(ctx.trader()); + IERC20(from).approve(address(ctx.broker()), maxAmountIn); + vm.expectRevert(bytes(revertReason)); + ctx.brokerSwapOut(from, to, buyAmount, maxAmountIn); + vm.stopPrank(); + } + + function assert_swapOverLimitFails(address from, address to, uint8 limit) internal { + ITradingLimits.Config memory fromLimitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.Config memory toLimitConfig = ctx.tradingLimitsConfig(to); + console.log( + string(abi.encodePacked("Swapping ", from.symbol(), " -> ", to.symbol())), + "with limit", + limit.toString() + ); + console.log("========================================"); + + if (fromLimitConfig.isLimitEnabled(limit) && toLimitConfig.isLimitEnabled(limit)) { + // TODO: Figure out best way to implement fork tests + // when two limits are configured. + console.log("Both Limits enabled skipping for now"); + } else if (fromLimitConfig.isLimitEnabled(limit)) { + assert_swapOverLimitFails_onInflow(from, to, limit); + } else if (toLimitConfig.isLimitEnabled(limit)) { + assert_swapOverLimitFails_onOutflow(from, to, limit); + } + } + + function assert_swapOverLimitFails_onInflow(address from, address to, uint8 limit) internal { + /* + * L*[from] -> to + * Assert that inflow on `from` is limited by the limit + * which can be any of L0, L1, LG. + * This is done by swapping from `from` to `to` until + * just before the limit is reached, within the constraints of + * the other limits, and then doing a final swap that fails. + */ + + if (limit == L0) { + swapUntilL0_onInflow(from, to); + } else if (limit == L1) { + swapUntilL1_onInflow(from, to); + } else if (limit == LG) { + swapUntilLG_onInflow(from, to); + } else { + revert("Invalid limit"); + } + + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(from); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(from); + + uint256 inflowRequiredUnits = uint256(int256(limitConfig.getLimit(limit)) - limitState.getNetflow(limit)) + 1; + console.log("Inflow required to pass limit: %d", inflowRequiredUnits); + if (limit != LG && ctx.atInflowLimit(from, LG)) { + console.log(unicode"🚨 Cannot validate limit %s as LG is already reached.", limit.toString()); + } else { + assert_swapInFails(from, to, inflowRequiredUnits.toSubunits(from), limit.revertReason()); + } + } + + function assert_swapOverLimitFails_onOutflow(address from, address to, uint8 limit) internal { + /* + * from -> L*[to] + * Assert that outflow on `to` is limited by the limit + * which can be any of L0, L1, LG. + * This is done by swapping from `from` to `to` until + * just before the limit is reached, within the constraints of + * the other limits, and then doing a final swap that fails. + */ + + // This should do valid swaps until just before the limit is reached + if (limit == L0) { + swapUntilL0_onOutflow(from, to); + } else if (limit == L1) { + swapUntilL1_onOutflow(from, to); + } else if (limit == LG) { + swapUntilLG_onOutflow(from, to); + } else { + revert("Invalid limit"); + } + + ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); + ITradingLimits.State memory limitState = ctx.refreshedTradingLimitsState(to); + + uint256 outflowRequiredUnits = uint256(int256(limitConfig.getLimit(limit)) + limitState.getNetflow(limit)) + 1; + console.log("Outflow required: ", outflowRequiredUnits); + if (limit != LG && ctx.atOutflowLimit(from, LG)) { + console.log(unicode"🚨 Cannot validate limit %s as LG is already reached.", limit.toString()); + } else { + assert_swapOutFails(from, to, outflowRequiredUnits.toSubunits(to), limit.revertReason()); + } + } +} diff --git a/test/fork/helpers/LogHelpers.sol b/test/fork/helpers/LogHelpers.sol new file mode 100644 index 00000000..704a865c --- /dev/null +++ b/test/fork/helpers/LogHelpers.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; +import { SwapHelpers } from "./SwapHelpers.sol"; +import { TokenHelpers } from "./TokenHelpers.sol"; +import { OracleHelpers } from "./OracleHelpers.sol"; +import { TradingLimitHelpers } from "./TradingLimitHelpers.sol"; + +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { L0, L1, LG } from "./misc.sol"; + +library LogHelpers { + using SwapHelpers for *; + using OracleHelpers for *; + using TokenHelpers for *; + using TradingLimitHelpers for *; + + function logHeader(ExchangeForkTest ctx) internal view { + console.log("========================================"); + console.log(unicode"🔦 Testing pair:", ctx.ticker()); + console.log("========================================"); + } + + function logPool(ExchangeForkTest ctx) internal view { + IBiPoolManager.PoolExchange memory exchange = ctx.getPool(); + + (bool timePassed, bool enoughReports, bool medianReportRecent, bool isReportExpired, ) = ctx.shouldUpdateBuckets(); + console.log(unicode"🎱 Pool: %s", ctx.ticker()); + console.log( + "\t timePassed: %s | enoughReports: %s", + timePassed ? "true" : "false", + enoughReports ? "true" : "false" + ); + console.log( + "\t medianReportRecent: %s | !isReportExpired: %s", + medianReportRecent ? "true" : "false", + !isReportExpired ? "true" : "false" + ); + console.log( + "\t exchange.bucket0: %d | exchange.bucket1: %d", + exchange.bucket0.toUnits(ctx.asset(0)), + exchange.bucket1.toUnits(ctx.asset(1)) + ); + console.log("\t exchange.lastBucketUpdate: %d", exchange.lastBucketUpdate); + } + + function logLimits(ExchangeForkTest ctx, address target) internal view { + ITradingLimits.State memory state = ctx.refreshedTradingLimitsState(target); + ITradingLimits.Config memory config = ctx.tradingLimitsConfig(target); + console.log("TradingLimits[%s]:", target.symbol()); + if (config.flags & L0 > 0) { + console.log( + "\tL0: %s%d/%d", + state.netflow0 < 0 ? "-" : "", + uint256(int256(state.netflow0 < 0 ? state.netflow0 * -1 : state.netflow0)), + uint256(int256(config.limit0)) + ); + } + if (config.flags & L1 > 0) { + console.log( + "\tL1: %s%d/%d", + state.netflow1 < 0 ? "-" : "", + uint256(int256(state.netflow1 < 0 ? state.netflow1 * -1 : state.netflow1)), + uint256(int256(config.limit1)) + ); + } + if (config.flags & LG > 0) { + console.log( + "\tLG: %s%d/%d", + state.netflowGlobal < 0 ? "-" : "", + uint256(int256(state.netflowGlobal < 0 ? state.netflowGlobal * -1 : state.netflowGlobal)), + uint256(int256(config.limitGlobal)) + ); + } + } +} diff --git a/test/fork/helpers/OracleHelpers.sol b/test/fork/helpers/OracleHelpers.sol new file mode 100644 index 00000000..bb0e2cc3 --- /dev/null +++ b/test/fork/helpers/OracleHelpers.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; + +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IMedianDeltaBreaker } from "contracts/interfaces/IMedianDeltaBreaker.sol"; +import { IValueDeltaBreaker } from "contracts/interfaces/IValueDeltaBreaker.sol"; + +library OracleHelpers { + function getReferenceRate(ExchangeForkTest ctx) internal view returns (uint256, uint256) { + uint256 rateNumerator; + uint256 rateDenominator; + (rateNumerator, rateDenominator) = ctx.sortedOracles().medianRate(ctx.rateFeedId()); + require(rateDenominator > 0, "exchange rate denominator must be greater than 0"); + return (rateNumerator, rateDenominator); + } + + function getReferenceRateFraction( + ExchangeForkTest ctx, + address baseAsset + ) internal view returns (FixidityLib.Fraction memory) { + (uint256 numerator, uint256 denominator) = getReferenceRate(ctx); + address asset0 = ctx.asset(0); + if (baseAsset == asset0) { + return FixidityLib.newFixedFraction(numerator, denominator); + } + return FixidityLib.newFixedFraction(denominator, numerator); + } + + function shouldUpdateBuckets(ExchangeForkTest ctx) internal view returns (bool, bool, bool, bool, bool) { + IBiPoolManager.PoolExchange memory exchange = ctx.getPool(); + + (bool isReportExpired, ) = ctx.sortedOracles().isOldestReportExpired(exchange.config.referenceRateFeedID); + // solhint-disable-next-line not-rely-on-time + bool timePassed = block.timestamp >= exchange.lastBucketUpdate + exchange.config.referenceRateResetFrequency; + bool enoughReports = (ctx.sortedOracles().numRates(exchange.config.referenceRateFeedID) >= + exchange.config.minimumReports); + // solhint-disable-next-line not-rely-on-time + bool medianReportRecent = ctx.sortedOracles().medianTimestamp(exchange.config.referenceRateFeedID) > + block.timestamp - exchange.config.referenceRateResetFrequency; + + return ( + timePassed, + enoughReports, + medianReportRecent, + isReportExpired, + timePassed && enoughReports && medianReportRecent && !isReportExpired + ); + } + + function newMedianToResetBreaker( + ExchangeForkTest ctx, + uint256 breakerIndex + ) internal view returns (uint256 newMedian) { + return newMedianToResetBreaker(ctx, ctx.rateFeedId(), breakerIndex); + } + + function newMedianToResetBreaker( + ExchangeForkTest ctx, + address rateFeedId, + uint256 breakerIndex + ) internal view returns (uint256 newMedian) { + address[] memory _breakers = ctx.breakerBox().getBreakers(); + bool isMedianDeltaBreaker = breakerIndex == 0; + bool isValueDeltaBreaker = breakerIndex == 1; + if (isMedianDeltaBreaker) { + uint256 currentEMA = IMedianDeltaBreaker(_breakers[breakerIndex]).medianRatesEMA(rateFeedId); + return currentEMA; + } else if (isValueDeltaBreaker) { + return IValueDeltaBreaker(_breakers[breakerIndex]).referenceValues(rateFeedId); + } else { + revert("can't infer corresponding breaker"); + } + } + + function getValueDeltaBreakerReferenceValue(ExchangeForkTest ctx, address _breaker) internal view returns (uint256) { + return getValueDeltaBreakerReferenceValue(ctx, ctx.rateFeedId(), _breaker); + } + + function getValueDeltaBreakerReferenceValue( + ExchangeForkTest, // ctx + address rateFeedId, + address _breaker + ) internal view returns (uint256) { + IValueDeltaBreaker breaker = IValueDeltaBreaker(_breaker); + return breaker.referenceValues(rateFeedId); + } + + function getBreakerRateChangeThreshold(ExchangeForkTest ctx, address _breaker) internal view returns (uint256) { + return getBreakerRateChangeThreshold(ctx, ctx.rateFeedId(), _breaker); + } + + function getBreakerRateChangeThreshold( + ExchangeForkTest, //, + address rateFeedId, + address _breaker + ) internal view returns (uint256) { + IMedianDeltaBreaker breaker = IMedianDeltaBreaker(_breaker); + + uint256 rateChangeThreshold = breaker.defaultRateChangeThreshold(); + uint256 specificRateChangeThreshold = breaker.rateChangeThreshold(rateFeedId); + if (specificRateChangeThreshold != 0) { + rateChangeThreshold = specificRateChangeThreshold; + } + return rateChangeThreshold; + } + + function getUpdatedBuckets(ExchangeForkTest ctx) internal view returns (uint256 bucket0, uint256 bucket1) { + IBiPoolManager.PoolExchange memory exchange = ctx.getPool(); + + bucket0 = exchange.config.stablePoolResetSize; + uint256 exchangeRateNumerator; + uint256 exchangeRateDenominator; + (exchangeRateNumerator, exchangeRateDenominator) = getReferenceRate(ctx); + + bucket1 = (exchangeRateDenominator * bucket0) / exchangeRateNumerator; + } +} diff --git a/test/fork/helpers/SwapHelpers.sol b/test/fork/helpers/SwapHelpers.sol new file mode 100644 index 00000000..f3ad1b3e --- /dev/null +++ b/test/fork/helpers/SwapHelpers.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; + +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; + +import { OracleHelpers } from "./OracleHelpers.sol"; + +library SwapHelpers { + using OracleHelpers for ExchangeForkTest; + + function ticker(ExchangeForkTest ctx) internal view returns (string memory) { + return string(abi.encodePacked(IERC20(ctx.asset(0)).symbol(), "/", IERC20(ctx.asset(1)).symbol())); + } + + function maxSwapIn(ExchangeForkTest ctx, uint256 desired, address from, address to) internal view returns (uint256) { + IBiPoolManager.PoolExchange memory pool = ctx.getPool(); + uint256 leftInBucket = (pool.asset0 == to ? pool.bucket0 : pool.bucket1) - 1; + (, , , , bool shouldUpdate) = ctx.shouldUpdateBuckets(); + if (shouldUpdate) { + (uint256 bucket0, uint256 bucket1) = ctx.getUpdatedBuckets(); + leftInBucket = (pool.asset0 == to ? bucket0 : bucket1) - 1; + } + leftInBucket = leftInBucket / ctx.exchangeProvider().tokenPrecisionMultipliers(to); + uint256 maxPossible = getAmountIn(ctx, from, to, leftInBucket); + return maxPossible > desired ? desired : maxPossible; + } + + function maxSwapOut(ExchangeForkTest ctx, uint256 desired, address to) internal view returns (uint256 max) { + IBiPoolManager.PoolExchange memory pool = ctx.getPool(); + uint256 leftInBucket = (pool.asset0 == to ? pool.bucket0 : pool.bucket1); + (, , , , bool shouldUpdate) = ctx.shouldUpdateBuckets(); + if (shouldUpdate) { + (uint256 bucket0, uint256 bucket1) = ctx.getUpdatedBuckets(); + leftInBucket = (pool.asset0 == to ? bucket0 : bucket1) - 1; + } + + leftInBucket = leftInBucket / ctx.exchangeProvider().tokenPrecisionMultipliers(to); + return leftInBucket > desired ? desired : leftInBucket; + } + + function getAmountOut( + ExchangeForkTest ctx, + address from, + address to, + uint256 sellAmount + ) internal view returns (uint256) { + return ctx.broker().getAmountOut(ctx.exchangeProviderAddr(), ctx.exchangeId(), from, to, sellAmount); + } + + function getAmountIn( + ExchangeForkTest ctx, + address from, + address to, + uint256 buyAmount + ) internal view returns (uint256) { + return ctx.broker().getAmountIn(ctx.exchangeProviderAddr(), ctx.exchangeId(), from, to, buyAmount); + } +} diff --git a/test/fork/helpers/TokenHelpers.sol b/test/fork/helpers/TokenHelpers.sol new file mode 100644 index 00000000..d7c1441e --- /dev/null +++ b/test/fork/helpers/TokenHelpers.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { console } from "forge-std/console.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { Vm } from "forge-std/Vm.sol"; +import { VM_ADDRESS } from "mento-std/Constants.sol"; + +library TokenHelpers { + Vm internal constant vm = Vm(VM_ADDRESS); + bool internal constant DEBUG = true; + + using FixidityLib for FixidityLib.Fraction; + + function toSubunits(int48 units, address token) internal view returns (int256) { + if (DEBUG) { + console.log( + "\tTokenHelpers.toSubunits: units=%s, token=%s, decimals=%s", + vm.toString(units), + IERC20(token).symbol(), + vm.toString(IERC20(token).decimals()) + ); + } + int256 tokenBase = int256(10 ** uint256(IERC20(token).decimals())); + return int256(units) * tokenBase; + } + + function toSubunits(FixidityLib.Fraction memory subunits, address token) internal view returns (uint256) { + uint256 tokenScaler = 10 ** uint256(24 - IERC20(token).decimals()); + return subunits.unwrap() / tokenScaler; + } + + function toSubunits(uint256 units, address token) internal view returns (uint256) { + uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); + return units * tokenBase; + } + + function toUnits(uint256 subunits, address token) internal view returns (uint256) { + uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); + return subunits / tokenBase; + } + + function toUnitsFixed(uint256 subunits, address token) internal view returns (FixidityLib.Fraction memory) { + uint256 tokenBase = 10 ** uint256(IERC20(token).decimals()); + return FixidityLib.newFixedFraction(subunits, tokenBase); + } + + function symbol(address token) internal view returns (string memory) { + return IERC20(token).symbol(); + } +} diff --git a/test/fork/helpers/TradingLimitHelpers.sol b/test/fork/helpers/TradingLimitHelpers.sol new file mode 100644 index 00000000..5f47adb4 --- /dev/null +++ b/test/fork/helpers/TradingLimitHelpers.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; + +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +import { ExchangeForkTest } from "../ExchangeForkTest.sol"; +import { OracleHelpers } from "./OracleHelpers.sol"; +import { L0, L1, LG, min } from "./misc.sol"; + +library TradingLimitHelpers { + using FixidityLib for FixidityLib.Fraction; + using OracleHelpers for *; + + function isLimitConfigured(ExchangeForkTest ctx, bytes32 limitId) public view returns (bool) { + ITradingLimits.Config memory limitConfig = ctx.broker().tradingLimitsConfig(limitId); + return limitConfig.flags > uint8(0); + } + + function tradingLimitsConfig( + ExchangeForkTest ctx, + bytes32 limitId + ) public view returns (ITradingLimits.Config memory) { + return ctx.broker().tradingLimitsConfig(limitId); + } + + function tradingLimitsState(ExchangeForkTest ctx, bytes32 limitId) public view returns (ITradingLimits.State memory) { + return ctx.broker().tradingLimitsState(limitId); + } + + function tradingLimitsConfig(ExchangeForkTest ctx, address asset) public view returns (ITradingLimits.Config memory) { + bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); + return ctx.broker().tradingLimitsConfig(ctx.exchangeId() ^ assetBytes32); + } + + function tradingLimitsState(ExchangeForkTest ctx, address asset) public view returns (ITradingLimits.State memory) { + bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); + return ctx.broker().tradingLimitsState(ctx.exchangeId() ^ assetBytes32); + } + + function refreshedTradingLimitsState( + ExchangeForkTest ctx, + address asset + ) public view returns (ITradingLimits.State memory state) { + ITradingLimits.Config memory config = tradingLimitsConfig(ctx, asset); + // Netflow might be outdated because of a skip(...) call and doing + // an update(0) would reset the netflow if enough time has passed. + state = ctx.tradingLimits().update(tradingLimitsState(ctx, asset), config, 0, 0); + // XXX: There's a bug in our current TradingLimits library implementation where + // an update with 0 netflow will round to 1. So we do another update with -1 netflow + // to get it back to the actual value. + state = ctx.tradingLimits().update(state, config, -1, 0); + } + + function isLimitEnabled(ITradingLimits.Config memory config, uint8 limit) internal pure returns (bool) { + return (config.flags & limit) > 0; + } + + function getLimit(ITradingLimits.Config memory config, uint8 limit) internal pure returns (uint256) { + if (limit == L0) { + return uint256(int256(config.limit0)); + } else if (limit == L1) { + return uint256(int256(config.limit1)); + } else if (limit == LG) { + return uint256(int256(config.limitGlobal)); + } else { + revert("invalid limit"); + } + } + + function getNetflow(ITradingLimits.State memory state, uint8 limit) internal pure returns (int256) { + if (limit == L0) { + return state.netflow0; + } else if (limit == L1) { + return state.netflow1; + } else if (limit == LG) { + return state.netflowGlobal; + } else { + revert("invalid limit"); + } + } + + function revertReason(uint8 limit) internal pure returns (string memory) { + if (limit == L0) { + return "L0 Exceeded"; + } else if (limit == L1) { + return "L1 Exceeded"; + } else if (limit == LG) { + return "LG Exceeded"; + } else { + revert("invalid limit"); + } + } + + function toString(uint8 limit) internal pure returns (string memory) { + if (limit == L0) { + return "L0"; + } else if (limit == L1) { + return "L1"; + } else if (limit == LG) { + return "LG"; + } else { + revert("invalid limit"); + } + } + + function atInflowLimit(ExchangeForkTest ctx, address asset, uint8 limit) internal view returns (bool) { + ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, asset); + ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, asset); + int256 netflow = getNetflow(limitState, limit); + int256 limitValue = int256(getLimit(limitConfig, limit)); + // if (netflow > 0) return false; + return netflow >= limitValue; + } + + function atOutflowLimit(ExchangeForkTest ctx, address asset, uint8 limit) internal view returns (bool) { + ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, asset); + ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, asset); + if (limitConfig.flags & limit == 0) return false; + int256 netflow = getNetflow(limitState, limit); + int256 limitValue = int256(getLimit(limitConfig, limit)); + // if (netflow < 0) return false; + return netflow <= -1 * limitValue; + } + + function maxInflow(ExchangeForkTest ctx, address from, address to) internal view returns (int48) { + FixidityLib.Fraction memory rate = ctx.getReferenceRateFraction(from); + int48 inflow = maxInflow(ctx, from); + int48 outflow = maxOutflow(ctx, to); + int48 outflowAsInflow = int48( + uint48(FixidityLib.multiply(rate, FixidityLib.wrap(uint256(int256(outflow)) * 1e24)).unwrap() / 1e24) + ); + return min(inflow, outflowAsInflow); + } + + function maxOutflow(ExchangeForkTest ctx, address from, address to) internal view returns (int48) { + return maxInflow(ctx, to, from); + } + + function maxInflow(ExchangeForkTest ctx, address from) internal view returns (int48) { + ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, from); + ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, from); + int48 maxInflowL0 = limitConfig.limit0 - limitState.netflow0; + int48 maxInflowL1 = limitConfig.limit1 - limitState.netflow1; + int48 maxInflowLG = limitConfig.limitGlobal - limitState.netflowGlobal; + + if (limitConfig.flags == L0 | L1 | LG) { + return min(maxInflowL0, maxInflowL1, maxInflowLG); + } else if (limitConfig.flags == L0 | LG) { + return min(maxInflowL0, maxInflowLG); + } else if (limitConfig.flags == L0 | L1) { + return min(maxInflowL0, maxInflowL1); + } else if (limitConfig.flags == L0) { + return maxInflowL0; + } else { + revert("Unexpected limit config"); + } + } + + function maxOutflow(ExchangeForkTest ctx, address to) internal view returns (int48) { + ITradingLimits.Config memory limitConfig = tradingLimitsConfig(ctx, to); + ITradingLimits.State memory limitState = refreshedTradingLimitsState(ctx, to); + int48 maxOutflowL0 = limitConfig.limit0 + limitState.netflow0; + int48 maxOutflowL1 = limitConfig.limit1 + limitState.netflow1; + int48 maxOutflowLG = limitConfig.limitGlobal + limitState.netflowGlobal; + + if (limitConfig.flags == L0 | L1 | LG) { + return min(maxOutflowL0, maxOutflowL1, maxOutflowLG); + } else if (limitConfig.flags == L0 | LG) { + return min(maxOutflowL0, maxOutflowLG); + } else if (limitConfig.flags == L0 | L1) { + return min(maxOutflowL0, maxOutflowL1); + } else if (limitConfig.flags == L0) { + return maxOutflowL0; + } else { + revert("Unexpected limit config"); + } + } +} diff --git a/test/fork/helpers/misc.sol b/test/fork/helpers/misc.sol new file mode 100644 index 00000000..16b30280 --- /dev/null +++ b/test/fork/helpers/misc.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +function min(uint256 a, uint256 b) pure returns (uint256) { + return a > b ? b : a; +} + +function min(uint256 a, uint256 b, uint256 c) pure returns (uint256) { + return min(a, min(b, c)); +} + +function min(int48 a, int48 b) pure returns (int48) { + return a > b ? b : a; +} + +function min(int48 a, int48 b, int48 c) pure returns (int48) { + return min(a, min(b, c)); +} + +uint8 constant L0 = 1; // 0b001 Limit0 +uint8 constant L1 = 2; // 0b010 Limit1 +uint8 constant LG = 4; // 0b100 LimitGlobal + +function toRateFeed(string memory rateFeed) pure returns (address) { + return address(uint160(uint256(keccak256(abi.encodePacked(rateFeed))))); +} From e37f1e56febb52f5e5860374ce819d8302055d96 Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:40:58 +0200 Subject: [PATCH 14/38] chore: remove contracts that we no longer manage from storage-layout checks (#506) ### Description CI is currently red on `develop` because I forgot to remove these old contracts from the storage-layout check in CI, and that only happens on push to develop and PR to main (for artifact rate limiting reasons). This PR removes them from the job. ### Other changes I've also reenabled `continue-on-error` on slither because it makes it nicer to work with via the sarif file. In the past we've disabled it because we were hitting the rate-limit on artifact upload and that ended up being a false-nagative failure mode, but that was before we moved the storage-layout check to develop and main only and that was the main hog. We didn't reenable it after doing that but I think now would be a good time to see if it works. If you need to bring it back revert this commit: 754c8be ### Tested Yes ### Related issues N/A ### Backwards compatibility YES ### Documentation N/A --- .github/workflows/slither.yaml | 11 +---------- .github/workflows/storage-layout.yaml | 5 ----- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/slither.yaml b/.github/workflows/slither.yaml index f49d0679..c522ef70 100644 --- a/.github/workflows/slither.yaml +++ b/.github/workflows/slither.yaml @@ -34,16 +34,7 @@ jobs: with: sarif: results.sarif fail-on: "low" - # continue-on-error: true - # ----------------------- - # Ideally, we'd like to continue on error to allow uploading the SARIF file here. - # But we're often running into GitHub's API Rate Limit when uploading the SARIF file - # which would lead to lots of failed pipelines even if slither works fine: - # https://github.com/mento-protocol/mento-core/actions/runs/7167865576/job/19514794782 - # - # So for now it's better to just let the slither task fail directly so we at least - # know it failed. - # ----------------------- + continue-on-error: true - name: Upload SARIF file uses: github/codeql-action/upload-sarif@v3 with: diff --git a/.github/workflows/storage-layout.yaml b/.github/workflows/storage-layout.yaml index 17dc1806..b7583c89 100644 --- a/.github/workflows/storage-layout.yaml +++ b/.github/workflows/storage-layout.yaml @@ -18,15 +18,10 @@ jobs: fail-fast: false matrix: contract: - - contracts/legacy/ReserveSpenderMultiSig.sol:ReserveSpenderMultiSig - - contracts/legacy/StableToken.sol:StableToken - - contracts/legacy/Exchange.sol:Exchange - - contracts/legacy/GrandaMento.sol:GrandaMento - contracts/swap/Broker.sol:Broker - contracts/swap/BiPoolManager.sol:BiPoolManager - contracts/swap/Reserve.sol:Reserve - contracts/oracles/BreakerBox.sol:BreakerBox - - contracts/common/SortedOracles.sol:SortedOracles - contracts/tokens/StableTokenV2.sol:StableTokenV2 steps: - uses: actions/checkout@v3 From 2d340247166f1de44230ddd2c0c67c38009125da Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 28 Aug 2024 11:20:46 +0200 Subject: [PATCH 15/38] fix: gas tests (#507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description Context: We create 50k locks to check how much gas is used by governance and locking operations for users with high number of locks. It was working fine until a couple of weeks ago. Then we disabled the gas tests to make CI happy. The problem was related to a [recent change on foundry](https://github.com/foundry-rs/foundry/pull/8274/files). They reduced the default gas limit for tests from `9223372036854775807` to `1073741824` which is not enough to create as many locks as we want. This PR fixes the issue by setting the gas limit to the previous default value of `9223372036854775807` or `i64::MAX / 2` and re-enables the gas tests. I don't think this will cause any problems as the only downside of this is if we have an infinite loop in our tests, it will take some time to consume all the available gas before the test fails. ### Other changes - Used a common var `viewCallGas` instead of creating one for each test - Increased the expected view call gas usages from 20k to 30k as they consume slightly higher gas now, probably thats because we removed the gas optimization across repo recently ### Tested ✅ ### Related issues - Fixes [#495](https://github.com/mento-protocol/mento-general/issues/495) --- foundry.toml | 4 ++- .../GovernanceIntegration.gas.t.sol | 33 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/foundry.toml b/foundry.toml index 9c05e199..d1bc4ab2 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,7 +10,9 @@ fuzz_runs = 256 gas_reports = ["*"] optimizer = false legacy = true -no_match_contract = "(ForkTest)|(GovernanceGasTest)" +no_match_contract = "ForkTest" +gas_limit = 9223372036854775807 + allow_paths = [ "node_modules/@celo" diff --git a/test/integration/governance/GovernanceIntegration.gas.t.sol b/test/integration/governance/GovernanceIntegration.gas.t.sol index 86035df7..66d3c698 100644 --- a/test/integration/governance/GovernanceIntegration.gas.t.sol +++ b/test/integration/governance/GovernanceIntegration.gas.t.sol @@ -40,6 +40,7 @@ contract GovernanceGasTest is GovernanceTest { bytes32 public merkleRoot = 0x945d83ced94efc822fed712b4c4694b4e1129607ec5bbd2ab971bb08dca4d809; + uint256 public viewCallGas = 30_000; uint256 public proposalCreationGas = 500_000; uint256 public proposalQueueGas = 200_000; uint256 public proposalExecutionGas = 200_000; @@ -104,11 +105,9 @@ contract GovernanceGasTest is GovernanceTest { // PoC from: https://github.com/sherlock-audit/2024-02-mento-judging/issues/4 function test_totalSupply_whenUsedWith50_000Locks_shouldCostReasonableGas() public s_attack { - uint256 totalSupplyGas = 20_000; - uint256 gasLeftBefore = gasleft(); locking.totalSupply(); - assertLt(gasLeftBefore - gasleft(), totalSupplyGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.startPrank(alice); mentoToken.approve(address(locking), type(uint256).max); @@ -119,7 +118,7 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.totalSupply(); - assertLt(gasLeftBefore - gasleft(), totalSupplyGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.startPrank(alice); mentoToken.approve(address(locking), type(uint256).max); @@ -130,16 +129,14 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.totalSupply(); - assertLt(gasLeftBefore - gasleft(), totalSupplyGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); } // PoC from https://github.com/sherlock-audit/2024-02-mento-judging/issues/2 function test_getVotes_whenUsedWith50_000Locks_shouldCostReasonableGas() public s_attack { - uint256 getVotesGas = 20_000; - uint256 gasLeftBefore = gasleft(); locking.getVotes(bob); - assertLt(gasLeftBefore - gasleft(), getVotesGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.startPrank(alice); mentoToken.approve(address(locking), type(uint256).max); @@ -150,7 +147,7 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.getVotes(bob); - assertLt(gasLeftBefore - gasleft(), getVotesGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.startPrank(alice); mentoToken.approve(address(locking), type(uint256).max); @@ -161,15 +158,13 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.getVotes(bob); - assertLt(gasLeftBefore - gasleft(), getVotesGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); } function test_getPastVotes_whenUsedWith50_000Locks_shouldCostReasonableGas() public s_attack { - uint256 getPastVotesGas = 20_000; - uint256 gasLeftBefore = gasleft(); locking.getPastVotes(bob, block.number - BLOCKS_DAY); - assertLt(gasLeftBefore - gasleft(), getPastVotesGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.startPrank(alice); mentoToken.approve(address(locking), type(uint256).max); @@ -182,7 +177,7 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.getPastVotes(bob, block.number - 3 * BLOCKS_DAY); - assertLt(gasLeftBefore - gasleft(), getPastVotesGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.timeTravel(BLOCKS_DAY); @@ -197,15 +192,13 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.getPastVotes(bob, block.number - 2 * BLOCKS_WEEK); - assertLt(gasLeftBefore - gasleft(), getPastVotesGas); + assertLt(gasLeftBefore - gasleft(), viewCallGas); } function test_getPastTotalSupply_whenUsedWith50_000Locks_shouldCostReasonableGas() public s_attack { - uint256 getPastTotalSupply = 20_000; - uint256 gasLeftBefore = gasleft(); locking.getPastTotalSupply(block.number - BLOCKS_DAY); - assertLt(gasLeftBefore - gasleft(), getPastTotalSupply); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.startPrank(alice); mentoToken.approve(address(locking), type(uint256).max); @@ -218,7 +211,7 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.getPastTotalSupply(block.number - 3 * BLOCKS_DAY); - assertLt(gasLeftBefore - gasleft(), getPastTotalSupply); + assertLt(gasLeftBefore - gasleft(), viewCallGas); vm.timeTravel(BLOCKS_DAY); @@ -233,7 +226,7 @@ contract GovernanceGasTest is GovernanceTest { gasLeftBefore = gasleft(); locking.getPastTotalSupply(block.number - 2 * BLOCKS_WEEK); - assertLt(gasLeftBefore - gasleft(), getPastTotalSupply); + assertLt(gasLeftBefore - gasleft(), viewCallGas); } function test_queueAndExecute_whenUsedWith50_000LocksAndNewLocksInSameBlock_shouldCostReasonableGas() From ec87e4e9eb308bf5064f5cad8573d234ea75a340 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:37:52 +0200 Subject: [PATCH 16/38] test: add fork test for new PUSO exchange (#509) --- test/fork/ForkTests.t.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol index 26631f21..dd16b1d3 100644 --- a/test/fork/ForkTests.t.sol +++ b/test/fork/ForkTests.t.sol @@ -41,7 +41,7 @@ import { ChainForkTest } from "./ChainForkTest.sol"; import { ExchangeForkTest } from "./ExchangeForkTest.sol"; import { CELO_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; -contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(14)) {} +contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(15)) {} contract Alfajores_P0E00_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 0) {} @@ -71,6 +71,8 @@ contract Alfajores_P0E12_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 1 contract Alfajores_P0E13_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 13) {} +contract Alfajores_P0E14_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 14) {} + contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(14)) {} contract Celo_P0E00_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 0) {} From 6f8b5b7ea491e27ae5110f224cef1d50ffce69c7 Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:55:49 +0200 Subject: [PATCH 17/38] chore: add mainnet php/usd fork tests (#516) --- package.json | 2 +- test/fork/ForkTests.t.sol | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c6d5b41a..01572f79 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "fork-test": "env FOUNDRY_PROFILE=fork-tests forge test", "fork-test:baklava": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Baklava", "fork-test:alfajores": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores", - "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract CeloMainnet", + "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Celo", "check-no-ir": "./bin/check-contracts.sh", "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip \"test/**/*\"" }, diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol index dd16b1d3..0aaf6373 100644 --- a/test/fork/ForkTests.t.sol +++ b/test/fork/ForkTests.t.sol @@ -73,7 +73,7 @@ contract Alfajores_P0E13_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 1 contract Alfajores_P0E14_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 14) {} -contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(14)) {} +contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(15)) {} contract Celo_P0E00_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 0) {} @@ -102,3 +102,5 @@ contract Celo_P0E11_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 11) {} contract Celo_P0E12_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 12) {} contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} + +contract Celo_P0E14_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 14) {} From 9349b38a43f770b0fa14eb448571bd1c8bc67eaf Mon Sep 17 00:00:00 2001 From: baroooo Date: Thu, 24 Oct 2024 10:49:47 +0200 Subject: [PATCH 18/38] Feat add stable tokenc ghs proxy (#536) ### Description Adds the proxy for GHS which will be used for both cGHS(pilot) and the actual cGHS in future ### Other changes no ### Related issues - https://github.com/mento-protocol/mento-general/issues/576 --- contracts/tokens/StableTokenGHSProxy.sol | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 contracts/tokens/StableTokenGHSProxy.sol diff --git a/contracts/tokens/StableTokenGHSProxy.sol b/contracts/tokens/StableTokenGHSProxy.sol new file mode 100644 index 00000000..233f51ec --- /dev/null +++ b/contracts/tokens/StableTokenGHSProxy.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.5.13; + +import "celo/contracts/common/Proxy.sol"; + +/* solhint-disable-next-line no-empty-blocks */ +contract StableTokenGHSProxy is Proxy {} From 8722c6da3bb8a161996ca0b8fc2a4d0847b0916c Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:57:14 +0200 Subject: [PATCH 19/38] feat: GoodDollar (#522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7704a9b chore: reserve storage gap in new contracts 040b7fe GoodDollar Fork Tests (#530) 2ee14b4 Feat(454): Exchange provider tests (#529) 578747c test: add BancorExchangeProvider pricing tests (#532) fcd3d02 feat: improve tests around the expansion controller (#533) d3a31fa Streamline comments and errors (#528) ec64f0a fix: ☦ pray to the lords of echidna 13d3a98 fix: add back forge install to the CI step c236009 fix: echidna tests c50a198 feat: add Broker liquidity check (#523) 914ba7d Address Slither Issues in GoodDollar/Bancor Contracts (#526) 8bcf3fd Feat/change gdollar modifiers (#524) 379a511 feat: make validate() internal bcbd9aa chore: added "yarn todo" task to log out open todos in the code 0631c60 refactor: renamed SafeERC20 to SafeERC20MintableBurnable 2800297 feat: simplified SafeERC20 e858391 feat: extend open zeppelin's IERC20 instead of duplicating it 196f6be chore: fixed linter and compiler warnings 096efe2 test: fix fork integration test e5308d9 feat: add GoodDollar contracts + tests 3135626 feat: update Broker + TradingLimits to 0.8 and make G$ changes --- .env.example | 3 +- .github/workflows/echidna.yaml | 2 +- .github/workflows/lint_test.yaml | 50 +- .husky/pre-push | 1 + .prettierrc.yml | 35 +- .solhint.json | 30 +- contracts/common/IERC20MintableBurnable.sol | 12 + .../common/SafeERC20MintableBurnable.sol | 47 + .../goodDollar/BancorExchangeProvider.sol | 373 ++++ contracts/goodDollar/BancorFormula.sol | 712 +++++++ .../goodDollar/GoodDollarExchangeProvider.sol | 234 +++ .../GoodDollarExpansionController.sol | 245 +++ .../goodDollar/interfaces/IGoodProtocol.sol | 24 + contracts/import.sol | 1 - .../interfaces/IBancorExchangeProvider.sol | 121 ++ contracts/interfaces/IBiPoolManager.sol | 3 +- contracts/interfaces/IBroker.sol | 138 +- contracts/interfaces/IBrokerAdmin.sol | 11 +- contracts/interfaces/IExchangeProvider.sol | 2 +- .../IGoodDollarExchangeProvider.sol | 87 + .../IGoodDollarExpansionController.sol | 157 ++ contracts/interfaces/IStableTokenV2.sol | 4 +- contracts/libraries/TradingLimits.sol | 10 +- contracts/swap/Broker.sol | 191 +- foundry.toml | 13 +- lib/forge-std | 2 +- package.json | 7 +- .solhint.test.json => test/.solhint.json | 41 +- test/fork/BancorExchangeProviderForkTest.sol | 171 ++ test/fork/BaseForkTest.sol | 62 +- test/fork/ChainForkTest.sol | 11 +- test/fork/ExchangeForkTest.sol | 12 +- test/fork/ForkTests.t.sol | 40 +- test/fork/GoodDollar/ExpansionForkTest.sol | 68 + .../GoodDollar/GoodDollarBaseForkTest.sol | 303 +++ test/fork/GoodDollar/SwapForkTest.sol | 162 ++ .../fork/GoodDollar/TradingLimitsForkTest.sol | 230 +++ test/fork/actions/SwapActions.sol | 6 +- test/fork/assertions/SwapAssertions.sol | 8 +- test/fork/helpers/TradingLimitHelpers.sol | 69 +- test/integration/protocol/ProtocolTest.sol | 5 +- .../goodDollar/BancorExchangeProvider.t.sol | 1693 +++++++++++++++++ .../GoodDollarExchangeProvider.t.sol | 896 +++++++++ .../GoodDollarExpansionController.t.sol | 627 ++++++ test/unit/libraries/TradingLimits.t.sol | 13 +- test/unit/swap/Broker.t.sol | 248 ++- .../GoodDollarExpansionControllerHarness.sol | 13 + test/utils/harnesses/TradingLimitsHarness.sol | 8 +- 48 files changed, 6799 insertions(+), 402 deletions(-) create mode 100644 contracts/common/IERC20MintableBurnable.sol create mode 100644 contracts/common/SafeERC20MintableBurnable.sol create mode 100644 contracts/goodDollar/BancorExchangeProvider.sol create mode 100644 contracts/goodDollar/BancorFormula.sol create mode 100644 contracts/goodDollar/GoodDollarExchangeProvider.sol create mode 100644 contracts/goodDollar/GoodDollarExpansionController.sol create mode 100644 contracts/goodDollar/interfaces/IGoodProtocol.sol create mode 100644 contracts/interfaces/IBancorExchangeProvider.sol create mode 100644 contracts/interfaces/IGoodDollarExchangeProvider.sol create mode 100644 contracts/interfaces/IGoodDollarExpansionController.sol rename .solhint.test.json => test/.solhint.json (66%) create mode 100644 test/fork/BancorExchangeProviderForkTest.sol create mode 100644 test/fork/GoodDollar/ExpansionForkTest.sol create mode 100644 test/fork/GoodDollar/GoodDollarBaseForkTest.sol create mode 100644 test/fork/GoodDollar/SwapForkTest.sol create mode 100644 test/fork/GoodDollar/TradingLimitsForkTest.sol create mode 100644 test/unit/goodDollar/BancorExchangeProvider.t.sol create mode 100644 test/unit/goodDollar/GoodDollarExchangeProvider.t.sol create mode 100644 test/unit/goodDollar/GoodDollarExpansionController.t.sol create mode 100644 test/utils/harnesses/GoodDollarExpansionControllerHarness.sol diff --git a/.env.example b/.env.example index 45facb28..506a0579 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,3 @@ CELOSCAN_API_KEY= CELO_MAINNET_RPC_URL=https://forno.celo.org -BAKLAVA_RPC_URL=https://baklava-forno.celo-testnet.org -ALFAJORES_RPC_URL=https://alfajores-forno.celo-testnet.org +ALFAJORES_RPC_URL=https://alfajores-forno.celo-testnet.org \ No newline at end of file diff --git a/.github/workflows/echidna.yaml b/.github/workflows/echidna.yaml index a79c75bf..f73d4b66 100644 --- a/.github/workflows/echidna.yaml +++ b/.github/workflows/echidna.yaml @@ -56,7 +56,7 @@ jobs: "test/integration/**/*" \ "test/unit/**/*" \ "test/utils/**/*" \ - "script/**/" + "contracts/**/*" - name: "Run Echidna" uses: crytic/echidna-action@v2 diff --git a/.github/workflows/lint_test.yaml b/.github/workflows/lint_test.yaml index 3fd28969..7a9a9705 100644 --- a/.github/workflows/lint_test.yaml +++ b/.github/workflows/lint_test.yaml @@ -1,55 +1,59 @@ -name: "CI" +name: CI env: - FOUNDRY_PROFILE: "ci" + FOUNDRY_PROFILE: ci + ALFAJORES_RPC_URL: ${{secrets.ALFAJORES_RPC_URL}} + CELO_MAINNET_RPC_URL: ${{secrets.CELO_MAINNET_RPC_URL}} on: workflow_dispatch: pull_request: push: branches: - - "main" - - "develop" + - main + - develop + +permissions: read-all jobs: lint_and_test: name: Lint & Test - runs-on: "ubuntu-latest" + runs-on: ubuntu-latest steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" + - name: Check out the repo + uses: actions/checkout@v3 with: - submodules: "recursive" + submodules: recursive - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 - - name: "Install Node.js" - uses: "actions/setup-node@v3" + - name: Install Node.js + uses: actions/setup-node@v3 with: - cache: "yarn" + cache: yarn node-version: "20" - - name: "Install the Node.js dependencies" - run: "yarn install --immutable" + - name: Install the Node.js dependencies + run: yarn install --immutable - - name: "Lint the contracts" - run: "yarn lint" + - name: Lint the contracts + run: yarn lint - - name: "Add lint summary" + - name: Add lint summary run: | echo "## Lint" >> $GITHUB_STEP_SUMMARY echo "✅ Passed" >> $GITHUB_STEP_SUMMARY - - name: "Show the Foundry config" - run: "forge config" + - name: Show the Foundry config + run: forge config - - name: "Run the tests" - run: "forge test" + - name: Run the tests + run: forge test - - name: "Check contract sizes" - run: "yarn run check-contract-sizes" + - name: Check contract sizes + run: yarn run check-contract-sizes - - name: "Add test summary" + - name: Add test summary run: | echo "## Tests" >> $GITHUB_STEP_SUMMARY diff --git a/.husky/pre-push b/.husky/pre-push index 6cdaab7b..0b8daea1 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" yarn lint +yarn todo diff --git a/.prettierrc.yml b/.prettierrc.yml index 028dcf34..699e7964 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -9,10 +9,28 @@ trailingComma: all plugins: [prettier-plugin-solidity] overrides: + # General Config - files: ["*.sol"] options: compiler: 0.5.17 - - files: [contracts/interfaces/*.sol] + - files: [test/**/*.sol] + options: + compiler: "" + + # File-specific Config + - files: + [ + contracts/common/IERC20MintableBurnable.sol, + contracts/common/SafeERC20MintableBurnable.sol, + contracts/goodDollar/**/*.sol, + contracts/governance/**/*.sol, + contracts/interfaces/*.sol, + contracts/libraries/TradingLimits.sol, + contracts/oracles/Chainlink*.sol, + contracts/swap/Broker.sol, + contracts/tokens/patched/*.sol, + contracts/tokens/StableTokenV2.sol, + ] options: compiler: 0.8.18 - files: @@ -21,18 +39,3 @@ overrides: - contracts/interfaces/IExchange.sol options: compiler: 0.5.17 - - files: [contracts/tokens/patched/*.sol] - options: - compiler: 0.8.18 - - files: [contracts/tokens/StableTokenV2.sol] - options: - compiler: 0.8.18 - - files: [contracts/governance/**/*.sol] - options: - compiler: 0.8.18 - - files: [test/**/*.sol] - options: - compiler: "" - - files: [contracts/oracles/Chainlink*.sol] - options: - compiler: 0.8.18 diff --git a/.solhint.json b/.solhint.json index 4d94dc31..6b11569e 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,31 +2,17 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { - "no-global-import": "off", - "no-console": "off", "code-complexity": ["error", 8], "compiler-version": ["error", ">=0.5.13"], - "func-visibility": [ - "error", - { - "ignoreConstructors": true - } - ], - "max-line-length": ["error", 121], - "not-rely-on-time": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], "function-max-lines": ["error", 120], + "gas-custom-errors": "off", + "max-line-length": ["error", 121], + "no-console": "off", "no-empty-blocks": "off", - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ], - "reason-string": [ - "warn", - { - "maxLength": 64 - } - ] + "no-global-import": "off", + "not-rely-on-time": "off", + "prettier/prettier": ["error", { "endOfLine": "auto" }], + "reason-string": ["warn", { "maxLength": 64 }] } } diff --git a/contracts/common/IERC20MintableBurnable.sol b/contracts/common/IERC20MintableBurnable.sol new file mode 100644 index 00000000..4eecc696 --- /dev/null +++ b/contracts/common/IERC20MintableBurnable.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.8.0; + +import "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. Does not include + * the optional functions; to access them see {ERC20Detailed}. + */ +interface IERC20MintableBurnable is IERC20 { + function mint(address account, uint256 amount) external; + function burn(uint256 amount) external; +} diff --git a/contracts/common/SafeERC20MintableBurnable.sol b/contracts/common/SafeERC20MintableBurnable.sol new file mode 100644 index 00000000..eb8c311a --- /dev/null +++ b/contracts/common/SafeERC20MintableBurnable.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; + +import { IERC20MintableBurnable as IERC20 } from "contracts/common/IERC20MintableBurnable.sol"; +import { Address } from "openzeppelin-contracts-next/contracts/utils/Address.sol"; + +/** + * @title SafeERC20MintableBurnable + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20MintableBurnable for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20MintableBurnable { + using Address for address; + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + function safeMint(IERC20 token, address account, uint256 amount) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.mint.selector, account, amount)); + } + + function safeBurn(IERC20 token, uint256 amount) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.burn.selector, amount)); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); + if (returndata.length > 0) { + // Return data is optional + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol new file mode 100644 index 00000000..95391152 --- /dev/null +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; + +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; + +import { BancorFormula } from "contracts/goodDollar/BancorFormula.sol"; +import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; + +/** + * @title BancorExchangeProvider + * @notice Provides exchange functionality for Bancor pools. + */ +contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, BancorFormula, OwnableUpgradeable { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Address of the broker contract. + address public broker; + + // Address of the reserve contract. + IReserve public reserve; + + // Maps an exchange id to the corresponding PoolExchange struct. + // exchangeId is in the format "asset0Symbol:asset1Symbol" + mapping(bytes32 => PoolExchange) public exchanges; + bytes32[] public exchangeIds; + + // Token precision multiplier used to normalize values to the same precision when calculating amounts. + mapping(address => uint256) public tokenPrecisionMultipliers; + + /* ===================================================== */ + /* ==================== Constructor ==================== */ + /* ===================================================== */ + + /** + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IBancorExchangeProvider + function initialize(address _broker, address _reserve) public initializer { + _initialize(_broker, _reserve); + } + + function _initialize(address _broker, address _reserve) internal onlyInitializing { + __Ownable_init(); + + BancorFormula.init(); + setBroker(_broker); + setReserve(_reserve); + } + + /* =================================================== */ + /* ==================== Modifiers ==================== */ + /* =================================================== */ + + modifier onlyBroker() { + require(msg.sender == broker, "Caller is not the Broker"); + _; + } + + modifier verifyExchangeTokens(address tokenIn, address tokenOut, PoolExchange memory exchange) { + require( + (tokenIn == exchange.reserveAsset && tokenOut == exchange.tokenAddress) || + (tokenIn == exchange.tokenAddress && tokenOut == exchange.reserveAsset), + "tokenIn and tokenOut must match exchange" + ); + _; + } + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /// @inheritdoc IBancorExchangeProvider + function getPoolExchange(bytes32 exchangeId) public view returns (PoolExchange memory exchange) { + exchange = exchanges[exchangeId]; + require(exchange.tokenAddress != address(0), "Exchange does not exist"); + return exchange; + } + + /// @inheritdoc IBancorExchangeProvider + function getExchangeIds() external view returns (bytes32[] memory) { + return exchangeIds; + } + + /** + * @inheritdoc IExchangeProvider + * @dev We don't expect the number of exchanges to grow to + * astronomical values so this is safe gas-wise as is. + */ + function getExchanges() public view returns (Exchange[] memory _exchanges) { + uint256 numExchanges = exchangeIds.length; + _exchanges = new Exchange[](numExchanges); + for (uint256 i = 0; i < numExchanges; i++) { + _exchanges[i].exchangeId = exchangeIds[i]; + _exchanges[i].assets = new address[](2); + _exchanges[i].assets[0] = exchanges[exchangeIds[i]].reserveAsset; + _exchanges[i].assets[1] = exchanges[exchangeIds[i]].tokenAddress; + } + } + + /// @inheritdoc IExchangeProvider + function getAmountOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) external view virtual returns (uint256 amountOut) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; + return amountOut; + } + + /// @inheritdoc IExchangeProvider + function getAmountIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) external view virtual returns (uint256 amountIn) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + return amountIn; + } + + /// @inheritdoc IBancorExchangeProvider + function currentPrice(bytes32 exchangeId) public view returns (uint256 price) { + // calculates: reserveBalance / (tokenSupply * reserveRatio) + require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; + UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); + price = unwrap(wrap(exchange.reserveBalance).div(denominator)); + return price; + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IBancorExchangeProvider + function setBroker(address _broker) public onlyOwner { + require(_broker != address(0), "Broker address must be set"); + broker = _broker; + emit BrokerUpdated(_broker); + } + + /// @inheritdoc IBancorExchangeProvider + function setReserve(address _reserve) public onlyOwner { + require(address(_reserve) != address(0), "Reserve address must be set"); + reserve = IReserve(_reserve); + emit ReserveUpdated(address(_reserve)); + } + + /// @inheritdoc IBancorExchangeProvider + function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external virtual onlyOwner { + return _setExitContribution(exchangeId, exitContribution); + } + + /// @inheritdoc IBancorExchangeProvider + function createExchange(PoolExchange calldata _exchange) external virtual onlyOwner returns (bytes32 exchangeId) { + return _createExchange(_exchange); + } + + /// @inheritdoc IBancorExchangeProvider + function destroyExchange( + bytes32 exchangeId, + uint256 exchangeIdIndex + ) external virtual onlyOwner returns (bool destroyed) { + return _destroyExchange(exchangeId, exchangeIdIndex); + } + + /// @inheritdoc IExchangeProvider + function swapIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) public virtual onlyBroker returns (uint256 amountOut) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + + amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; + return amountOut; + } + + /// @inheritdoc IExchangeProvider + function swapOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) public virtual onlyBroker returns (uint256 amountIn) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; + uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + + amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + return amountIn; + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _createExchange(PoolExchange calldata _exchange) internal returns (bytes32 exchangeId) { + PoolExchange memory exchange = _exchange; + validateExchange(exchange); + + // slither-disable-next-line encode-packed-collision + exchangeId = keccak256( + abi.encodePacked(IERC20(exchange.reserveAsset).symbol(), IERC20(exchange.tokenAddress).symbol()) + ); + require(exchanges[exchangeId].reserveAsset == address(0), "Exchange already exists"); + + uint256 reserveAssetDecimals = IERC20(exchange.reserveAsset).decimals(); + uint256 tokenDecimals = IERC20(exchange.tokenAddress).decimals(); + require(reserveAssetDecimals <= 18, "Reserve asset decimals must be <= 18"); + require(tokenDecimals <= 18, "Token decimals must be <= 18"); + + tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); + tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); + + exchanges[exchangeId] = exchange; + exchangeIds.push(exchangeId); + emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); + } + + function _destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) internal returns (bool destroyed) { + require(exchangeIdIndex < exchangeIds.length, "exchangeIdIndex not in range"); + require(exchangeIds[exchangeIdIndex] == exchangeId, "exchangeId at index doesn't match"); + PoolExchange memory exchange = exchanges[exchangeId]; + + delete exchanges[exchangeId]; + exchangeIds[exchangeIdIndex] = exchangeIds[exchangeIds.length - 1]; + exchangeIds.pop(); + destroyed = true; + + emit ExchangeDestroyed(exchangeId, exchange.reserveAsset, exchange.tokenAddress); + } + + function _setExitContribution(bytes32 exchangeId, uint32 exitContribution) internal { + require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); + require(exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); + + PoolExchange storage exchange = exchanges[exchangeId]; + exchange.exitContribution = exitContribution; + emit ExitContributionSet(exchangeId, exitContribution); + } + + /** + * @notice Execute a swap against the in-memory exchange and write the new exchange state to storage. + * @param exchangeId The ID of the pool + * @param tokenIn The token to be sold + * @param scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals + * @param scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals + */ + function executeSwap(bytes32 exchangeId, address tokenIn, uint256 scaledAmountIn, uint256 scaledAmountOut) internal { + PoolExchange memory exchange = getPoolExchange(exchangeId); + if (tokenIn == exchange.reserveAsset) { + exchange.reserveBalance += scaledAmountIn; + exchange.tokenSupply += scaledAmountOut; + } else { + require(exchange.reserveBalance >= scaledAmountOut, "Insufficient reserve balance for swap"); + exchange.reserveBalance -= scaledAmountOut; + exchange.tokenSupply -= scaledAmountIn; + } + exchanges[exchangeId].reserveBalance = exchange.reserveBalance; + exchanges[exchangeId].tokenSupply = exchange.tokenSupply; + } + + /** + * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut + * @param exchange The pool exchange to operate on + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals + * @return scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals + */ + function _getScaledAmountIn( + PoolExchange memory exchange, + address tokenIn, + address tokenOut, + uint256 scaledAmountOut + ) internal view verifyExchangeTokens(tokenIn, tokenOut, exchange) returns (uint256 scaledAmountIn) { + if (tokenIn == exchange.reserveAsset) { + scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); + } else { + // apply exit contribution + scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); + } + } + + /** + * @notice Calculate the scaledAmountOut of tokenOut received for a given scaledAmountIn of tokenIn + * @param exchange The pool exchange to operate on + * @param tokenIn The token to be sold + * @param tokenOut The token to be bought + * @param scaledAmountIn The amount of tokenIn to be sold, scaled to 18 decimals + * @return scaledAmountOut The amount of tokenOut to be bought, scaled to 18 decimals + */ + function _getScaledAmountOut( + PoolExchange memory exchange, + address tokenIn, + address tokenOut, + uint256 scaledAmountIn + ) internal view verifyExchangeTokens(tokenIn, tokenOut, exchange) returns (uint256 scaledAmountOut) { + if (tokenIn == exchange.reserveAsset) { + scaledAmountOut = purchaseTargetAmount( + exchange.tokenSupply, + exchange.reserveBalance, + exchange.reserveRatio, + scaledAmountIn + ); + } else { + scaledAmountOut = saleTargetAmount( + exchange.tokenSupply, + exchange.reserveBalance, + exchange.reserveRatio, + scaledAmountIn + ); + // apply exit contribution + scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; + } + } + + /** + * @notice Validates a PoolExchange's parameters and configuration + * @dev Reverts if not valid + * @param exchange The PoolExchange to validate + */ + function validateExchange(PoolExchange memory exchange) internal view { + require(exchange.reserveAsset != address(0), "Invalid reserve asset"); + require( + reserve.isCollateralAsset(exchange.reserveAsset), + "Reserve asset must be a collateral registered with the reserve" + ); + require(exchange.tokenAddress != address(0), "Invalid token address"); + require(reserve.isStableAsset(exchange.tokenAddress), "Token must be a stable registered with the reserve"); + require(exchange.reserveRatio > 1, "Reserve ratio is too low"); + require(exchange.reserveRatio <= MAX_WEIGHT, "Reserve ratio is too high"); + require(exchange.exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/goodDollar/BancorFormula.sol b/contracts/goodDollar/BancorFormula.sol new file mode 100644 index 00000000..8ac8e06f --- /dev/null +++ b/contracts/goodDollar/BancorFormula.sol @@ -0,0 +1,712 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +// solhint-disable function-max-lines, max-line-length, code-complexity, reason-string +pragma solidity 0.8.18; + +/** + * @title BancorFormula contract by Bancor + * @dev https://github.com/bancorprotocol/contracts-solidity/blob/v0.6.39/solidity/contracts/converter/BancorFormula.sol + * + * Modified from the original by MentoLabs Team + * - bumped solidity version to 0.8.18 and removed SafeMath + * - removed unused functions and variables + * - scaled max weight from 1e6 to 1e8 reran all const python scripts for increased precision + * - added the saleCost() function that returns the amounIn of tokens required to receive a given amountOut of reserve tokens + * + */ + +contract BancorFormula { + uint256 private constant ONE = 1; + + uint32 public constant MAX_WEIGHT = 100000000; + uint8 private constant MIN_PRECISION = 32; + uint8 private constant MAX_PRECISION = 127; + + // Auto-generated via 'PrintIntScalingFactors.py' + uint256 private constant FIXED_1 = 0x080000000000000000000000000000000; + uint256 private constant FIXED_2 = 0x100000000000000000000000000000000; + uint256 private constant MAX_NUM = 0x200000000000000000000000000000000; + + // Auto-generated via 'PrintLn2ScalingFactors.py' + uint256 private constant LN2_NUMERATOR = 0x3f80fe03f80fe03f80fe03f80fe03f8; + uint256 private constant LN2_DENOMINATOR = 0x5b9de1d10bf4103d647b0955897ba80; + + // Auto-generated via 'PrintFunctionOptimalLog.py' and 'PrintFunctionOptimalExp.py' + uint256 private constant OPT_LOG_MAX_VAL = 0x15bf0a8b1457695355fb8ac404e7a79e3; + uint256 private constant OPT_EXP_MAX_VAL = 0x800000000000000000000000000000000; + + // Auto-generated via 'PrintMaxExpArray.py' + uint256[128] private maxExpArray; + + function initMaxExpArray() private { + // maxExpArray[ 0] = 0x6bffffffffffffffffffffffffffffffff; + // maxExpArray[ 1] = 0x67ffffffffffffffffffffffffffffffff; + // maxExpArray[ 2] = 0x637fffffffffffffffffffffffffffffff; + // maxExpArray[ 3] = 0x5f6fffffffffffffffffffffffffffffff; + // maxExpArray[ 4] = 0x5b77ffffffffffffffffffffffffffffff; + // maxExpArray[ 5] = 0x57b3ffffffffffffffffffffffffffffff; + // maxExpArray[ 6] = 0x5419ffffffffffffffffffffffffffffff; + // maxExpArray[ 7] = 0x50a2ffffffffffffffffffffffffffffff; + // maxExpArray[ 8] = 0x4d517fffffffffffffffffffffffffffff; + // maxExpArray[ 9] = 0x4a233fffffffffffffffffffffffffffff; + // maxExpArray[ 10] = 0x47165fffffffffffffffffffffffffffff; + // maxExpArray[ 11] = 0x4429afffffffffffffffffffffffffffff; + // maxExpArray[ 12] = 0x415bc7ffffffffffffffffffffffffffff; + // maxExpArray[ 13] = 0x3eab73ffffffffffffffffffffffffffff; + // maxExpArray[ 14] = 0x3c1771ffffffffffffffffffffffffffff; + // maxExpArray[ 15] = 0x399e96ffffffffffffffffffffffffffff; + // maxExpArray[ 16] = 0x373fc47fffffffffffffffffffffffffff; + // maxExpArray[ 17] = 0x34f9e8ffffffffffffffffffffffffffff; + // maxExpArray[ 18] = 0x32cbfd5fffffffffffffffffffffffffff; + // maxExpArray[ 19] = 0x30b5057fffffffffffffffffffffffffff; + // maxExpArray[ 20] = 0x2eb40f9fffffffffffffffffffffffffff; + // maxExpArray[ 21] = 0x2cc8340fffffffffffffffffffffffffff; + // maxExpArray[ 22] = 0x2af09481ffffffffffffffffffffffffff; + // maxExpArray[ 23] = 0x292c5bddffffffffffffffffffffffffff; + // maxExpArray[ 24] = 0x277abdcdffffffffffffffffffffffffff; + // maxExpArray[ 25] = 0x25daf6657fffffffffffffffffffffffff; + // maxExpArray[ 26] = 0x244c49c65fffffffffffffffffffffffff; + // maxExpArray[ 27] = 0x22ce03cd5fffffffffffffffffffffffff; + // maxExpArray[ 28] = 0x215f77c047ffffffffffffffffffffffff; + // maxExpArray[ 29] = 0x1fffffffffffffffffffffffffffffffff; + // maxExpArray[ 30] = 0x1eaefdbdabffffffffffffffffffffffff; + // maxExpArray[ 31] = 0x1d6bd8b2ebffffffffffffffffffffffff; + maxExpArray[32] = 0x1c35fedd14ffffffffffffffffffffffff; + maxExpArray[33] = 0x1b0ce43b323fffffffffffffffffffffff; + maxExpArray[34] = 0x19f0028ec1ffffffffffffffffffffffff; + maxExpArray[35] = 0x18ded91f0e7fffffffffffffffffffffff; + maxExpArray[36] = 0x17d8ec7f0417ffffffffffffffffffffff; + maxExpArray[37] = 0x16ddc6556cdbffffffffffffffffffffff; + maxExpArray[38] = 0x15ecf52776a1ffffffffffffffffffffff; + maxExpArray[39] = 0x15060c256cb2ffffffffffffffffffffff; + maxExpArray[40] = 0x1428a2f98d72ffffffffffffffffffffff; + maxExpArray[41] = 0x13545598e5c23fffffffffffffffffffff; + maxExpArray[42] = 0x1288c4161ce1dfffffffffffffffffffff; + maxExpArray[43] = 0x11c592761c666fffffffffffffffffffff; + maxExpArray[44] = 0x110a688680a757ffffffffffffffffffff; + maxExpArray[45] = 0x1056f1b5bedf77ffffffffffffffffffff; + maxExpArray[46] = 0x0faadceceeff8bffffffffffffffffffff; + maxExpArray[47] = 0x0f05dc6b27edadffffffffffffffffffff; + maxExpArray[48] = 0x0e67a5a25da4107fffffffffffffffffff; + maxExpArray[49] = 0x0dcff115b14eedffffffffffffffffffff; + maxExpArray[50] = 0x0d3e7a392431239fffffffffffffffffff; + maxExpArray[51] = 0x0cb2ff529eb71e4fffffffffffffffffff; + maxExpArray[52] = 0x0c2d415c3db974afffffffffffffffffff; + maxExpArray[53] = 0x0bad03e7d883f69bffffffffffffffffff; + maxExpArray[54] = 0x0b320d03b2c343d5ffffffffffffffffff; + maxExpArray[55] = 0x0abc25204e02828dffffffffffffffffff; + maxExpArray[56] = 0x0a4b16f74ee4bb207fffffffffffffffff; + maxExpArray[57] = 0x09deaf736ac1f569ffffffffffffffffff; + maxExpArray[58] = 0x0976bd9952c7aa957fffffffffffffffff; + maxExpArray[59] = 0x09131271922eaa606fffffffffffffffff; + maxExpArray[60] = 0x08b380f3558668c46fffffffffffffffff; + maxExpArray[61] = 0x0857ddf0117efa215bffffffffffffffff; + maxExpArray[62] = 0x07ffffffffffffffffffffffffffffffff; + maxExpArray[63] = 0x07abbf6f6abb9d087fffffffffffffffff; + maxExpArray[64] = 0x075af62cbac95f7dfa7fffffffffffffff; + maxExpArray[65] = 0x070d7fb7452e187ac13fffffffffffffff; + maxExpArray[66] = 0x06c3390ecc8af379295fffffffffffffff; + maxExpArray[67] = 0x067c00a3b07ffc01fd6fffffffffffffff; + maxExpArray[68] = 0x0637b647c39cbb9d3d27ffffffffffffff; + maxExpArray[69] = 0x05f63b1fc104dbd39587ffffffffffffff; + maxExpArray[70] = 0x05b771955b36e12f7235ffffffffffffff; + maxExpArray[71] = 0x057b3d49dda84556d6f6ffffffffffffff; + maxExpArray[72] = 0x054183095b2c8ececf30ffffffffffffff; + maxExpArray[73] = 0x050a28be635ca2b888f77fffffffffffff; + maxExpArray[74] = 0x04d5156639708c9db33c3fffffffffffff; + maxExpArray[75] = 0x04a23105873875bd52dfdfffffffffffff; + maxExpArray[76] = 0x0471649d87199aa990756fffffffffffff; + maxExpArray[77] = 0x04429a21a029d4c1457cfbffffffffffff; + maxExpArray[78] = 0x0415bc6d6fb7dd71af2cb3ffffffffffff; + maxExpArray[79] = 0x03eab73b3bbfe282243ce1ffffffffffff; + maxExpArray[80] = 0x03c1771ac9fb6b4c18e229ffffffffffff; + maxExpArray[81] = 0x0399e96897690418f785257fffffffffff; + maxExpArray[82] = 0x0373fc456c53bb779bf0ea9fffffffffff; + maxExpArray[83] = 0x034f9e8e490c48e67e6ab8bfffffffffff; + maxExpArray[84] = 0x032cbfd4a7adc790560b3337ffffffffff; + maxExpArray[85] = 0x030b50570f6e5d2acca94613ffffffffff; + maxExpArray[86] = 0x02eb40f9f620fda6b56c2861ffffffffff; + maxExpArray[87] = 0x02cc8340ecb0d0f520a6af58ffffffffff; + maxExpArray[88] = 0x02af09481380a0a35cf1ba02ffffffffff; + maxExpArray[89] = 0x0292c5bdd3b92ec810287b1b3fffffffff; + maxExpArray[90] = 0x0277abdcdab07d5a77ac6d6b9fffffffff; + maxExpArray[91] = 0x025daf6654b1eaa55fd64df5efffffffff; + maxExpArray[92] = 0x0244c49c648baa98192dce88b7ffffffff; + maxExpArray[93] = 0x022ce03cd5619a311b2471268bffffffff; + maxExpArray[94] = 0x0215f77c045fbe885654a44a0fffffffff; + maxExpArray[95] = 0x01ffffffffffffffffffffffffffffffff; + maxExpArray[96] = 0x01eaefdbdaaee7421fc4d3ede5ffffffff; + maxExpArray[97] = 0x01d6bd8b2eb257df7e8ca57b09bfffffff; + maxExpArray[98] = 0x01c35fedd14b861eb0443f7f133fffffff; + maxExpArray[99] = 0x01b0ce43b322bcde4a56e8ada5afffffff; + maxExpArray[100] = 0x019f0028ec1fff007f5a195a39dfffffff; + maxExpArray[101] = 0x018ded91f0e72ee74f49b15ba527ffffff; + maxExpArray[102] = 0x017d8ec7f04136f4e5615fd41a63ffffff; + maxExpArray[103] = 0x016ddc6556cdb84bdc8d12d22e6fffffff; + maxExpArray[104] = 0x015ecf52776a1155b5bd8395814f7fffff; + maxExpArray[105] = 0x015060c256cb23b3b3cc3754cf40ffffff; + maxExpArray[106] = 0x01428a2f98d728ae223ddab715be3fffff; + maxExpArray[107] = 0x013545598e5c23276ccf0ede68034fffff; + maxExpArray[108] = 0x01288c4161ce1d6f54b7f61081194fffff; + maxExpArray[109] = 0x011c592761c666aa641d5a01a40f17ffff; + maxExpArray[110] = 0x0110a688680a7530515f3e6e6cfdcdffff; + maxExpArray[111] = 0x01056f1b5bedf75c6bcb2ce8aed428ffff; + maxExpArray[112] = 0x00faadceceeff8a0890f3875f008277fff; + maxExpArray[113] = 0x00f05dc6b27edad306388a600f6ba0bfff; + maxExpArray[114] = 0x00e67a5a25da41063de1495d5b18cdbfff; + maxExpArray[115] = 0x00dcff115b14eedde6fc3aa5353f2e4fff; + maxExpArray[116] = 0x00d3e7a3924312399f9aae2e0f868f8fff; + maxExpArray[117] = 0x00cb2ff529eb71e41582cccd5a1ee26fff; + maxExpArray[118] = 0x00c2d415c3db974ab32a51840c0b67edff; + maxExpArray[119] = 0x00bad03e7d883f69ad5b0a186184e06bff; + maxExpArray[120] = 0x00b320d03b2c343d4829abd6075f0cc5ff; + maxExpArray[121] = 0x00abc25204e02828d73c6e80bcdb1a95bf; + maxExpArray[122] = 0x00a4b16f74ee4bb2040a1ec6c15fbbf2df; + maxExpArray[123] = 0x009deaf736ac1f569deb1b5ae3f36c130f; + maxExpArray[124] = 0x00976bd9952c7aa957f5937d790ef65037; + maxExpArray[125] = 0x009131271922eaa6064b73a22d0bd4f2bf; + maxExpArray[126] = 0x008b380f3558668c46c91c49a2f8e967b9; + maxExpArray[127] = 0x00857ddf0117efa215952912839f6473e6; + } + + /** + * @dev should be executed after construction (too large for the constructor) + */ + function init() public { + initMaxExpArray(); + } + + /** + * @dev given a token supply, reserve balance, weight and a deposit amount (in the reserve token), + * calculates the target amount for a given conversion (in the main token) + * + * Formula: + * return = _supply * ((1 + _amount / _reserveBalance) ^ (_reserveWeight / 1000000) - 1) + * + * @param _supply liquid token supply + * @param _reserveBalance reserve balance + * @param _reserveWeight reserve weight, represented in ppm (1-1000000) + * @param _amount amount of reserve tokens to get the target amount for + * + * @return target + */ + function purchaseTargetAmount( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveWeight, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); + + // special case for 0 deposit amount + if (_amount == 0) return 0; + + // special case if the weight = 100% + if (_reserveWeight == MAX_WEIGHT) return (_supply * _amount) / _reserveBalance; + + uint256 result; + uint8 precision; + uint256 baseN = _amount + _reserveBalance; + (result, precision) = power(baseN, _reserveBalance, _reserveWeight, MAX_WEIGHT); + uint256 temp = (_supply * result) >> precision; + return temp - _supply; + } + + /** + * @dev given a token supply, reserve balance, weight and a sell amount (in the main token), + * calculates the target amount for a given conversion (in the reserve token) + * + * Formula: + * return = _reserveBalance * (1 - (1 - _amount / _supply) ^ (MAX_WEIGHT / _reserveWeight)) + * + * @dev by MentoLabs: This function actually calculates a different formula that is equivalent to the one above. + * But ensures the base of the power function is larger than 1, which is required by the power function. + * The formula is: + * = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(MAX_WEIGHT/reserveRatio)) + * formula: amountOut = ---------------------------------------------------------------------------------- + * = (tokenSupply/(tokenSupply - amountIn ))^(MAX_WEIGHT/reserveRatio) + * + * + * @param _supply liquid token supply + * @param _reserveBalance reserve balance + * @param _reserveWeight reserve weight, represented in ppm (1-1000000) + * @param _amount amount of liquid tokens to get the target amount for + * + * @return reserve token amount + */ + function saleTargetAmount( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveWeight, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); + require(_amount <= _supply, "ERR_INVALID_AMOUNT"); + + // special case for 0 sell amount + if (_amount == 0) return 0; + + // special case for selling the entire supply + if (_amount == _supply) return _reserveBalance; + + // special case if the weight = 100% + if (_reserveWeight == MAX_WEIGHT) return (_reserveBalance * _amount) / _supply; + + uint256 result; + uint8 precision; + uint256 baseD = _supply - _amount; + (result, precision) = power(_supply, baseD, MAX_WEIGHT, _reserveWeight); + uint256 temp1 = _reserveBalance * result; + uint256 temp2 = _reserveBalance << precision; + return (temp1 - temp2) / result; + } + + /** + * @dev given a pool token supply, reserve balance, reserve ratio and an amount of requested pool tokens, + * calculates the amount of reserve tokens required for purchasing the given amount of pool tokens + * + * Formula: + * return = _reserveBalance * (((_supply + _amount) / _supply) ^ (MAX_WEIGHT / _reserveRatio) - 1) + * + * @param _supply pool token supply + * @param _reserveBalance reserve balance + * @param _reserveRatio reserve ratio, represented in ppm (2-2000000) + * @param _amount requested amount of pool tokens + * + * @return reserve token amount + */ + function fundCost( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveRatio, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveRatio > 1 && _reserveRatio <= MAX_WEIGHT * 2, "ERR_INVALID_RESERVE_RATIO"); + + // special case for 0 amount + if (_amount == 0) return 0; + + // special case if the reserve ratio = 100% + if (_reserveRatio == MAX_WEIGHT) return (_amount * _reserveBalance - 1) / _supply + 1; + + uint256 result; + uint8 precision; + uint256 baseN = _supply + _amount; + (result, precision) = power(baseN, _supply, MAX_WEIGHT, _reserveRatio); + uint256 temp = ((_reserveBalance * result - 1) >> precision) + 1; + return temp - _reserveBalance; + } + + /** + * Added by MentoLabs: + * @notice This function calculates the amount of tokens required to purchase a given amount of reserve tokens. + * @dev this formula was derived from the actual saleTargetAmount() function, and also ensures that the base of the power function is larger than 1. + * + * + * = tokenSupply * (-1 + (reserveBalance / (reserveBalance - amountOut) )^(reserveRatio/MAX_WEIGHT) ) + * Formula: amountIn = ------------------------------------------------------------------------------------------------ + * = (reserveBalance / (reserveBalance - amountOut) )^(reserveRatio/MAX_WEIGHT) + * + * + * @param _supply pool token supply + * @param _reserveBalance reserve balance + * @param _reserveWeight reserve weight, represented in ppm + * @param _amount amount of reserve tokens to get the target amount for + * + * @return reserve token amount + */ + function saleCost( + uint256 _supply, + uint256 _reserveBalance, + uint32 _reserveWeight, + uint256 _amount + ) internal view returns (uint256) { + // validate input + require(_supply > 0, "ERR_INVALID_SUPPLY"); + require(_reserveBalance > 0, "ERR_INVALID_RESERVE_BALANCE"); + require(_reserveWeight > 0 && _reserveWeight <= MAX_WEIGHT, "ERR_INVALID_RESERVE_WEIGHT"); + + require(_amount <= _reserveBalance, "ERR_INVALID_AMOUNT"); + + // special case for 0 sell amount + if (_amount == 0) return 0; + + // special case for selling the entire supply + if (_amount == _reserveBalance) return _supply; + + // special case if the weight = 100% + // base formula can be simplified to: + // Formula: amountIn = amountOut * supply / reserveBalance + // the +1 and -1 are to ensure that this function rounds up which is required to prevent protocol loss. + if (_reserveWeight == MAX_WEIGHT) return (_supply * _amount - 1) / _reserveBalance + 1; + + uint256 result; + uint8 precision; + uint256 baseD = _reserveBalance - _amount; + (result, precision) = power(_reserveBalance, baseD, _reserveWeight, MAX_WEIGHT); + uint256 temp1 = _supply * result; + uint256 temp2 = _supply << precision; + return (temp1 - temp2 - 1) / result + 1; + } + + /** + * @dev General Description: + * Determine a value of precision. + * Calculate an integer approximation of (_baseN / _baseD) ^ (_expN / _expD) * 2 ^ precision. + * Return the result along with the precision used. + * + * Detailed Description: + * Instead of calculating "base ^ exp", we calculate "e ^ (log(base) * exp)". + * The value of "log(base)" is represented with an integer slightly smaller than "log(base) * 2 ^ precision". + * The larger "precision" is, the more accurately this value represents the real value. + * However, the larger "precision" is, the more bits are required in order to store this value. + * And the exponentiation function, which takes "x" and calculates "e ^ x", is limited to a maximum exponent (maximum value of "x"). + * This maximum exponent depends on the "precision" used, and it is given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + * Hence we need to determine the highest precision which can be used for the given input, before calling the exponentiation function. + * This allows us to compute "base ^ exp" with maximum accuracy and without exceeding 256 bits in any of the intermediate computations. + * This functions assumes that "_expN < 2 ^ 256 / log(MAX_NUM - 1)", otherwise the multiplication should be replaced with a "safeMul". + * Since we rely on unsigned-integer arithmetic and "base < 1" ==> "log(base) < 0", this function does not support "_baseN < _baseD". + */ + function power(uint256 _baseN, uint256 _baseD, uint32 _expN, uint32 _expD) internal view returns (uint256, uint8) { + require(_baseN < MAX_NUM); + + uint256 baseLog; + uint256 base = (_baseN * FIXED_1) / _baseD; + if (base < OPT_LOG_MAX_VAL) { + baseLog = optimalLog(base); + } else { + baseLog = generalLog(base); + } + + uint256 baseLogTimesExp = (baseLog * _expN) / _expD; + if (baseLogTimesExp < OPT_EXP_MAX_VAL) { + return (optimalExp(baseLogTimesExp), MAX_PRECISION); + } else { + uint8 precision = findPositionInMaxExpArray(baseLogTimesExp); + return (generalExp(baseLogTimesExp >> (MAX_PRECISION - precision), precision), precision); + } + } + + /** + * @dev computes log(x / FIXED_1) * FIXED_1. + * This functions assumes that "x >= FIXED_1", because the output would be negative otherwise. + */ + function generalLog(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + // If x >= 2, then we compute the integer part of log2(x), which is larger than 0. + if (x >= FIXED_2) { + uint8 count = floorLog2(x / FIXED_1); + x >>= count; // now x < 2 + res = count * FIXED_1; + } + + // If x > 1, then we compute the fraction part of log2(x), which is larger than 0. + if (x > FIXED_1) { + for (uint8 i = MAX_PRECISION; i > 0; --i) { + x = (x * x) / FIXED_1; // now 1 < x < 4 + if (x >= FIXED_2) { + x >>= 1; // now 1 < x < 2 + res += ONE << (i - 1); + } + } + } + + return (res * LN2_NUMERATOR) / LN2_DENOMINATOR; + } + + /** + * @dev computes the largest integer smaller than or equal to the binary logarithm of the input. + */ + function floorLog2(uint256 _n) internal pure returns (uint8) { + uint8 res = 0; + + if (_n < 256) { + // At most 8 iterations + while (_n > 1) { + _n >>= 1; + res += 1; + } + } else { + // Exactly 8 iterations + for (uint8 s = 128; s > 0; s >>= 1) { + if (_n >= (ONE << s)) { + _n >>= s; + res |= s; + } + } + } + + return res; + } + + /** + * @dev the global "maxExpArray" is sorted in descending order, and therefore the following statements are equivalent: + * - This function finds the position of [the smallest value in "maxExpArray" larger than or equal to "x"] + * - This function finds the highest position of [a value in "maxExpArray" larger than or equal to "x"] + */ + function findPositionInMaxExpArray(uint256 _x) internal view returns (uint8 position) { + uint8 lo = MIN_PRECISION; + uint8 hi = MAX_PRECISION; + + while (lo + 1 < hi) { + uint8 mid = (lo + hi) / 2; + if (maxExpArray[mid] >= _x) lo = mid; + else hi = mid; + } + + if (maxExpArray[hi] >= _x) return hi; + if (maxExpArray[lo] >= _x) return lo; + + require(false); + } + + /** + * @dev this function can be auto-generated by the script 'PrintFunctionGeneralExp.py'. + * it approximates "e ^ x" via maclaurin summation: "(x^0)/0! + (x^1)/1! + ... + (x^n)/n!". + * it returns "e ^ (x / 2 ^ precision) * 2 ^ precision", that is, the result is upshifted for accuracy. + * the global "maxExpArray" maps each "precision" to "((maximumExponent + 1) << (MAX_PRECISION - precision)) - 1". + * the maximum permitted value for "x" is therefore given by "maxExpArray[precision] >> (MAX_PRECISION - precision)". + */ + function generalExp(uint256 _x, uint8 _precision) internal pure returns (uint256) { + uint256 xi = _x; + uint256 res = 0; + + xi = (xi * _x) >> _precision; + res += xi * 0x3442c4e6074a82f1797f72ac0000000; // add x^02 * (33! / 02!) + xi = (xi * _x) >> _precision; + res += xi * 0x116b96f757c380fb287fd0e40000000; // add x^03 * (33! / 03!) + xi = (xi * _x) >> _precision; + res += xi * 0x045ae5bdd5f0e03eca1ff4390000000; // add x^04 * (33! / 04!) + xi = (xi * _x) >> _precision; + res += xi * 0x00defabf91302cd95b9ffda50000000; // add x^05 * (33! / 05!) + xi = (xi * _x) >> _precision; + res += xi * 0x002529ca9832b22439efff9b8000000; // add x^06 * (33! / 06!) + xi = (xi * _x) >> _precision; + res += xi * 0x00054f1cf12bd04e516b6da88000000; // add x^07 * (33! / 07!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000a9e39e257a09ca2d6db51000000; // add x^08 * (33! / 08!) + xi = (xi * _x) >> _precision; + res += xi * 0x000012e066e7b839fa050c309000000; // add x^09 * (33! / 09!) + xi = (xi * _x) >> _precision; + res += xi * 0x000001e33d7d926c329a1ad1a800000; // add x^10 * (33! / 10!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000002bee513bdb4a6b19b5f800000; // add x^11 * (33! / 11!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000003a9316fa79b88eccf2a00000; // add x^12 * (33! / 12!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000048177ebe1fa812375200000; // add x^13 * (33! / 13!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000005263fe90242dcbacf00000; // add x^14 * (33! / 14!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000057e22099c030d94100000; // add x^15 * (33! / 15!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000057e22099c030d9410000; // add x^16 * (33! / 16!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000052b6b54569976310000; // add x^17 * (33! / 17!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000004985f67696bf748000; // add x^18 * (33! / 18!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000003dea12ea99e498000; // add x^19 * (33! / 19!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000031880f2214b6e000; // add x^20 * (33! / 20!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000025bcff56eb36000; // add x^21 * (33! / 21!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000001b722e10ab1000; // add x^22 * (33! / 22!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000001317c70077000; // add x^23 * (33! / 23!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000cba84aafa00; // add x^24 * (33! / 24!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000082573a0a00; // add x^25 * (33! / 25!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000005035ad900; // add x^26 * (33! / 26!) + xi = (xi * _x) >> _precision; + res += xi * 0x000000000000000000000002f881b00; // add x^27 * (33! / 27!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000001b29340; // add x^28 * (33! / 28!) + xi = (xi * _x) >> _precision; + res += xi * 0x00000000000000000000000000efc40; // add x^29 * (33! / 29!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000007fe0; // add x^30 * (33! / 30!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000420; // add x^31 * (33! / 31!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000021; // add x^32 * (33! / 32!) + xi = (xi * _x) >> _precision; + res += xi * 0x0000000000000000000000000000001; // add x^33 * (33! / 33!) + + return res / 0x688589cc0e9505e2f2fee5580000000 + _x + (ONE << _precision); // divide by 33! and then add x^1 / 1! + x^0 / 0! + } + + /** + * @dev computes log(x / FIXED_1) * FIXED_1 + * Input range: FIXED_1 <= x <= OPT_LOG_MAX_VAL - 1 + * Auto-generated via 'PrintFunctionOptimalLog.py' + * Detailed description: + * - Rewrite the input as a product of natural exponents and a single residual r, such that 1 < r < 2 + * - The natural logarithm of each (pre-calculated) exponent is the degree of the exponent + * - The natural logarithm of r is calculated via Taylor series for log(1 + x), where x = r - 1 + * - The natural logarithm of the input is calculated by summing up the intermediate results above + * - For example: log(250) = log(e^4 * e^1 * e^0.5 * 1.021692859) = 4 + 1 + 0.5 + log(1 + 0.021692859) + */ + // We're choosing to trust Bancor's audited Math + // slither-disable-start divide-before-multiply + function optimalLog(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + // slither false positive, y is initialized as z = y = ... + // slither-disable-next-line uninitialized-local + uint256 y; + uint256 z; + uint256 w; + + if (x >= 0xd3094c70f034de4b96ff7d5b6f99fcd8) { + res += 0x40000000000000000000000000000000; + x = (x * FIXED_1) / 0xd3094c70f034de4b96ff7d5b6f99fcd8; + } // add 1 / 2^1 + if (x >= 0xa45af1e1f40c333b3de1db4dd55f29a7) { + res += 0x20000000000000000000000000000000; + x = (x * FIXED_1) / 0xa45af1e1f40c333b3de1db4dd55f29a7; + } // add 1 / 2^2 + if (x >= 0x910b022db7ae67ce76b441c27035c6a1) { + res += 0x10000000000000000000000000000000; + x = (x * FIXED_1) / 0x910b022db7ae67ce76b441c27035c6a1; + } // add 1 / 2^3 + if (x >= 0x88415abbe9a76bead8d00cf112e4d4a8) { + res += 0x08000000000000000000000000000000; + x = (x * FIXED_1) / 0x88415abbe9a76bead8d00cf112e4d4a8; + } // add 1 / 2^4 + if (x >= 0x84102b00893f64c705e841d5d4064bd3) { + res += 0x04000000000000000000000000000000; + x = (x * FIXED_1) / 0x84102b00893f64c705e841d5d4064bd3; + } // add 1 / 2^5 + if (x >= 0x8204055aaef1c8bd5c3259f4822735a2) { + res += 0x02000000000000000000000000000000; + x = (x * FIXED_1) / 0x8204055aaef1c8bd5c3259f4822735a2; + } // add 1 / 2^6 + if (x >= 0x810100ab00222d861931c15e39b44e99) { + res += 0x01000000000000000000000000000000; + x = (x * FIXED_1) / 0x810100ab00222d861931c15e39b44e99; + } // add 1 / 2^7 + if (x >= 0x808040155aabbbe9451521693554f733) { + res += 0x00800000000000000000000000000000; + x = (x * FIXED_1) / 0x808040155aabbbe9451521693554f733; + } // add 1 / 2^8 + + z = y = x - FIXED_1; + w = (y * y) / FIXED_1; + res += (z * (0x100000000000000000000000000000000 - y)) / 0x100000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^01 / 01 - y^02 / 02 + res += (z * (0x0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - y)) / 0x200000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^03 / 03 - y^04 / 04 + res += (z * (0x099999999999999999999999999999999 - y)) / 0x300000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^05 / 05 - y^06 / 06 + res += (z * (0x092492492492492492492492492492492 - y)) / 0x400000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^07 / 07 - y^08 / 08 + res += (z * (0x08e38e38e38e38e38e38e38e38e38e38e - y)) / 0x500000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^09 / 09 - y^10 / 10 + res += (z * (0x08ba2e8ba2e8ba2e8ba2e8ba2e8ba2e8b - y)) / 0x600000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^11 / 11 - y^12 / 12 + res += (z * (0x089d89d89d89d89d89d89d89d89d89d89 - y)) / 0x700000000000000000000000000000000; + z = (z * w) / FIXED_1; // add y^13 / 13 - y^14 / 14 + res += (z * (0x088888888888888888888888888888888 - y)) / 0x800000000000000000000000000000000; // add y^15 / 15 - y^16 / 16 + + return res; + } + + /** + * @dev computes e ^ (x / FIXED_1) * FIXED_1 + * input range: 0 <= x <= OPT_EXP_MAX_VAL - 1 + * auto-generated via 'PrintFunctionOptimalExp.py' + * Detailed description: + * - Rewrite the input as a sum of binary exponents and a single residual r, as small as possible + * - The exponentiation of each binary exponent is given (pre-calculated) + * - The exponentiation of r is calculated via Taylor series for e^x, where x = r + * - The exponentiation of the input is calculated by multiplying the intermediate results above + * - For example: e^5.521692859 = e^(4 + 1 + 0.5 + 0.021692859) = e^4 * e^1 * e^0.5 * e^0.021692859 + */ + function optimalExp(uint256 x) internal pure returns (uint256) { + uint256 res = 0; + + // slither false positive, y is initialized as z = y = ... + // slither-disable-next-line uninitialized-local + uint256 y; + uint256 z; + + z = y = x % 0x10000000000000000000000000000000; // get the input modulo 2^(-3) + z = (z * y) / FIXED_1; + res += z * 0x10e1b3be415a0000; // add y^02 * (20! / 02!) + z = (z * y) / FIXED_1; + res += z * 0x05a0913f6b1e0000; // add y^03 * (20! / 03!) + z = (z * y) / FIXED_1; + res += z * 0x0168244fdac78000; // add y^04 * (20! / 04!) + z = (z * y) / FIXED_1; + res += z * 0x004807432bc18000; // add y^05 * (20! / 05!) + z = (z * y) / FIXED_1; + res += z * 0x000c0135dca04000; // add y^06 * (20! / 06!) + z = (z * y) / FIXED_1; + res += z * 0x0001b707b1cdc000; // add y^07 * (20! / 07!) + z = (z * y) / FIXED_1; + res += z * 0x000036e0f639b800; // add y^08 * (20! / 08!) + z = (z * y) / FIXED_1; + res += z * 0x00000618fee9f800; // add y^09 * (20! / 09!) + z = (z * y) / FIXED_1; + res += z * 0x0000009c197dcc00; // add y^10 * (20! / 10!) + z = (z * y) / FIXED_1; + res += z * 0x0000000e30dce400; // add y^11 * (20! / 11!) + z = (z * y) / FIXED_1; + res += z * 0x000000012ebd1300; // add y^12 * (20! / 12!) + z = (z * y) / FIXED_1; + res += z * 0x0000000017499f00; // add y^13 * (20! / 13!) + z = (z * y) / FIXED_1; + res += z * 0x0000000001a9d480; // add y^14 * (20! / 14!) + z = (z * y) / FIXED_1; + res += z * 0x00000000001c6380; // add y^15 * (20! / 15!) + z = (z * y) / FIXED_1; + res += z * 0x000000000001c638; // add y^16 * (20! / 16!) + z = (z * y) / FIXED_1; + res += z * 0x0000000000001ab8; // add y^17 * (20! / 17!) + z = (z * y) / FIXED_1; + res += z * 0x000000000000017c; // add y^18 * (20! / 18!) + z = (z * y) / FIXED_1; + res += z * 0x0000000000000014; // add y^19 * (20! / 19!) + z = (z * y) / FIXED_1; + res += z * 0x0000000000000001; // add y^20 * (20! / 20!) + res = res / 0x21c3677c82b40000 + y + FIXED_1; // divide by 20! and then add y^1 / 1! + y^0 / 0! + + if ((x & 0x010000000000000000000000000000000) != 0) + res = (res * 0x1c3d6a24ed82218787d624d3e5eba95f9) / 0x18ebef9eac820ae8682b9793ac6d1e776; // multiply by e^2^(-3) + if ((x & 0x020000000000000000000000000000000) != 0) + res = (res * 0x18ebef9eac820ae8682b9793ac6d1e778) / 0x1368b2fc6f9609fe7aceb46aa619baed4; // multiply by e^2^(-2) + if ((x & 0x040000000000000000000000000000000) != 0) + res = (res * 0x1368b2fc6f9609fe7aceb46aa619baed5) / 0x0bc5ab1b16779be3575bd8f0520a9f21f; // multiply by e^2^(-1) + if ((x & 0x080000000000000000000000000000000) != 0) + res = (res * 0x0bc5ab1b16779be3575bd8f0520a9f21e) / 0x0454aaa8efe072e7f6ddbab84b40a55c9; // multiply by e^2^(+0) + if ((x & 0x100000000000000000000000000000000) != 0) + res = (res * 0x0454aaa8efe072e7f6ddbab84b40a55c5) / 0x00960aadc109e7a3bf4578099615711ea; // multiply by e^2^(+1) + if ((x & 0x200000000000000000000000000000000) != 0) + res = (res * 0x00960aadc109e7a3bf4578099615711d7) / 0x0002bf84208204f5977f9a8cf01fdce3d; // multiply by e^2^(+2) + if ((x & 0x400000000000000000000000000000000) != 0) + res = (res * 0x0002bf84208204f5977f9a8cf01fdc307) / 0x0000003c6ab775dd0b95b4cbee7e65d11; // multiply by e^2^(+3) + + return res; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} +// slither-disable-end divide-before-multiply diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol new file mode 100644 index 00000000..b744fcc7 --- /dev/null +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; + +import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; +import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; + +import { BancorExchangeProvider } from "./BancorExchangeProvider.sol"; +import { UD60x18, unwrap, wrap } from "prb/math/UD60x18.sol"; + +/** + * @title GoodDollarExchangeProvider + * @notice Provides exchange functionality for the GoodDollar system. + */ +contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchangeProvider, PausableUpgradeable { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Address of the Expansion Controller contract. + IGoodDollarExpansionController public expansionController; + + // Address of the GoodDollar DAO contract. + // solhint-disable-next-line var-name-mixedcase + address public AVATAR; + + /* ===================================================== */ + /* ==================== Constructor ==================== */ + /* ===================================================== */ + + /** + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) BancorExchangeProvider(disable) {} + + /// @inheritdoc IGoodDollarExchangeProvider + function initialize( + address _broker, + address _reserve, + address _expansionController, + address _avatar + ) public initializer { + BancorExchangeProvider._initialize(_broker, _reserve); + __Pausable_init(); + + setExpansionController(_expansionController); + setAvatar(_avatar); + } + + /* =================================================== */ + /* ==================== Modifiers ==================== */ + /* =================================================== */ + + modifier onlyAvatar() { + require(msg.sender == AVATAR, "Only Avatar can call this function"); + _; + } + + modifier onlyExpansionController() { + require(msg.sender == address(expansionController), "Only ExpansionController can call this function"); + _; + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IGoodDollarExchangeProvider + function setAvatar(address _avatar) public onlyOwner { + require(_avatar != address(0), "Avatar address must be set"); + AVATAR = _avatar; + emit AvatarUpdated(_avatar); + } + + /// @inheritdoc IGoodDollarExchangeProvider + function setExpansionController(address _expansionController) public onlyOwner { + require(_expansionController != address(0), "ExpansionController address must be set"); + expansionController = IGoodDollarExpansionController(_expansionController); + emit ExpansionControllerUpdated(_expansionController); + } + + /** + * @inheritdoc BancorExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external override onlyAvatar { + return _setExitContribution(exchangeId, exitContribution); + } + + /** + * @inheritdoc BancorExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function createExchange(PoolExchange calldata _exchange) external override onlyAvatar returns (bytes32 exchangeId) { + return _createExchange(_exchange); + } + + /** + * @inheritdoc BancorExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function destroyExchange( + bytes32 exchangeId, + uint256 exchangeIdIndex + ) external override onlyAvatar returns (bool destroyed) { + return _destroyExchange(exchangeId, exchangeIdIndex); + } + + /// @inheritdoc BancorExchangeProvider + function swapIn( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountIn + ) public override onlyBroker whenNotPaused returns (uint256 amountOut) { + amountOut = BancorExchangeProvider.swapIn(exchangeId, tokenIn, tokenOut, amountIn); + } + + /// @inheritdoc BancorExchangeProvider + function swapOut( + bytes32 exchangeId, + address tokenIn, + address tokenOut, + uint256 amountOut + ) public override onlyBroker whenNotPaused returns (uint256 amountIn) { + amountIn = BancorExchangeProvider.swapOut(exchangeId, tokenIn, tokenOut, amountOut); + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Calculates the amount of G$ tokens that need to be minted as a result of the expansion + * while keeping the current price the same. + * calculation: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + */ + function mintFromExpansion( + bytes32 exchangeId, + uint256 reserveRatioScalar + ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { + require(reserveRatioScalar > 0, "Reserve ratio scalar must be greater than 0"); + PoolExchange memory exchange = getPoolExchange(exchangeId); + + UD60x18 scaledRatio = wrap(uint256(exchange.reserveRatio) * 1e10); + UD60x18 newRatio = scaledRatio.mul(wrap(reserveRatioScalar)); + + uint32 newRatioUint = uint32(unwrap(newRatio) / 1e10); + require(newRatioUint > 0, "New ratio must be greater than 0"); + + UD60x18 numerator = wrap(exchange.tokenSupply).mul(scaledRatio); + numerator = numerator.sub(wrap(exchange.tokenSupply).mul(newRatio)); + + uint256 scaledAmountToMint = unwrap(numerator.div(newRatio)); + + exchanges[exchangeId].reserveRatio = newRatioUint; + exchanges[exchangeId].tokenSupply += scaledAmountToMint; + + amountToMint = scaledAmountToMint / tokenPrecisionMultipliers[exchange.tokenAddress]; + emit ReserveRatioUpdated(exchangeId, newRatioUint); + + return amountToMint; + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Calculates the amount of G$ tokens that need to be minted as a result of the reserve interest + * flowing into the reserve while keeping the current price the same. + * calculation: amountToMint = reserveInterest * tokenSupply / reserveBalance + */ + function mintFromInterest( + bytes32 exchangeId, + uint256 reserveInterest + ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { + PoolExchange memory exchange = getPoolExchange(exchangeId); + + uint256 reserveinterestScaled = reserveInterest * tokenPrecisionMultipliers[exchange.reserveAsset]; + uint256 amountToMintScaled = unwrap( + wrap(reserveinterestScaled).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) + ); + amountToMint = amountToMintScaled / tokenPrecisionMultipliers[exchange.tokenAddress]; + + exchanges[exchangeId].tokenSupply += amountToMintScaled; + exchanges[exchangeId].reserveBalance += reserveinterestScaled; + + return amountToMint; + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Calculates the new reserve ratio needed to mint the G$ reward while keeping the current price the same. + * calculation: newRatio = reserveBalance / (tokenSupply + reward) * currentPrice + */ + function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { + PoolExchange memory exchange = getPoolExchange(exchangeId); + + uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; + uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; + + UD60x18 numerator = wrap(exchange.reserveBalance); + UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled)); + uint256 newRatioScaled = unwrap(numerator.div(denominator)); + + uint32 newRatioUint = uint32(newRatioScaled / 1e10); + exchanges[exchangeId].reserveRatio = newRatioUint; + exchanges[exchangeId].tokenSupply += rewardScaled; + + emit ReserveRatioUpdated(exchangeId, newRatioUint); + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function pause() external virtual onlyAvatar { + _pause(); + } + + /** + * @inheritdoc IGoodDollarExchangeProvider + * @dev Only callable by the GoodDollar DAO contract. + */ + function unpause() external virtual onlyAvatar { + _unpause(); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol new file mode 100644 index 00000000..f753d060 --- /dev/null +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; +import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; +import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; +import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; +import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; + +/** + * @title GoodDollarExpansionController + * @notice Provides functionality to expand the supply of GoodDollars. + */ +contract GoodDollarExpansionController is IGoodDollarExpansionController, PausableUpgradeable, OwnableUpgradeable { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // MAX_WEIGHT is the max rate that can be assigned to an exchange + uint256 public constant MAX_WEIGHT = 1e18; + + // Address of the distribution helper contract + IDistributionHelper public distributionHelper; + + // Address of reserve contract holding the GoodDollar reserve + address public reserve; + + // Address of the GoodDollar exchange provider + IGoodDollarExchangeProvider public goodDollarExchangeProvider; + + // Maps exchangeId to exchangeExpansionConfig + mapping(bytes32 exchangeId => ExchangeExpansionConfig) public exchangeExpansionConfigs; + + // Address of the GoodDollar DAO contract. + // solhint-disable-next-line var-name-mixedcase + address public AVATAR; + + /* ===================================================== */ + /* ==================== Constructor ==================== */ + /* ===================================================== */ + + /** + * @dev Should be called with disable=true in deployments when it's accessed through a Proxy. + * Call this with disable=false during testing, when used without a proxy. + * @param disable Set to true to run `_disableInitializers()` inherited from + * openzeppelin-contracts-upgradeable/Initializable.sol + */ + constructor(bool disable) { + if (disable) { + _disableInitializers(); + } + } + + /// @inheritdoc IGoodDollarExpansionController + function initialize( + address _goodDollarExchangeProvider, + address _distributionHelper, + address _reserve, + address _avatar + ) public initializer { + __Pausable_init(); + __Ownable_init(); + + setGoodDollarExchangeProvider(_goodDollarExchangeProvider); + _setDistributionHelper(_distributionHelper); + setReserve(_reserve); + setAvatar(_avatar); + } + + /* =================================================== */ + /* ==================== Modifiers ==================== */ + /* =================================================== */ + + modifier onlyAvatar() { + require(msg.sender == AVATAR, "Only Avatar can call this function"); + _; + } + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /// @inheritdoc IGoodDollarExpansionController + function getExpansionConfig(bytes32 exchangeId) public view returns (ExchangeExpansionConfig memory) { + require(exchangeExpansionConfigs[exchangeId].expansionRate > 0, "Expansion config not set"); + return exchangeExpansionConfigs[exchangeId]; + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IGoodDollarExpansionController + function setGoodDollarExchangeProvider(address _goodDollarExchangeProvider) public onlyOwner { + require(_goodDollarExchangeProvider != address(0), "GoodDollarExchangeProvider address must be set"); + goodDollarExchangeProvider = IGoodDollarExchangeProvider(_goodDollarExchangeProvider); + emit GoodDollarExchangeProviderUpdated(_goodDollarExchangeProvider); + } + + /// @inheritdoc IGoodDollarExpansionController + function setDistributionHelper(address _distributionHelper) public onlyAvatar { + return _setDistributionHelper(_distributionHelper); + } + + /// @inheritdoc IGoodDollarExpansionController + function setReserve(address _reserve) public onlyOwner { + require(_reserve != address(0), "Reserve address must be set"); + reserve = _reserve; + emit ReserveUpdated(_reserve); + } + + /// @inheritdoc IGoodDollarExpansionController + function setAvatar(address _avatar) public onlyOwner { + require(_avatar != address(0), "Avatar address must be set"); + AVATAR = _avatar; + emit AvatarUpdated(_avatar); + } + + /// @inheritdoc IGoodDollarExpansionController + function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external onlyAvatar { + require(expansionRate < MAX_WEIGHT, "Expansion rate must be less than 100%"); + require(expansionRate > 0, "Expansion rate must be greater than 0"); + require(expansionFrequency > 0, "Expansion frequency must be greater than 0"); + + exchangeExpansionConfigs[exchangeId].expansionRate = expansionRate; + exchangeExpansionConfigs[exchangeId].expansionFrequency = expansionFrequency; + + emit ExpansionConfigSet(exchangeId, expansionRate, expansionFrequency); + } + + /// @inheritdoc IGoodDollarExpansionController + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { + require(reserveInterest > 0, "Reserve interest must be greater than 0"); + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + + uint256 amountToMint = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterest); + + require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events + emit InterestUBIMinted(exchangeId, amountToMint); + } + + /// @inheritdoc IGoodDollarExpansionController + function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted) { + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + + uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve); + uint256 additionalReserveBalance = contractReserveBalance - exchange.reserveBalance; + if (additionalReserveBalance > 0) { + amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance); + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events + emit InterestUBIMinted(exchangeId, amountMinted); + } + } + + /// @inheritdoc IGoodDollarExpansionController + function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted) { + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); + + bool shouldExpand = block.timestamp > config.lastExpansion + config.expansionFrequency; + if (shouldExpand || config.lastExpansion == 0) { + uint256 reserveRatioScalar = _getReserveRatioScalar(config); + + exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); + amountMinted = goodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); + distributionHelper.onDistribution(amountMinted); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events + emit ExpansionUBIMinted(exchangeId, amountMinted); + } + } + + /// @inheritdoc IGoodDollarExpansionController + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { + require(to != address(0), "Recipient address must be set"); + require(amount > 0, "Amount must be greater than 0"); + IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) + .getPoolExchange(exchangeId); + + goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount); + IGoodDollar(exchange.tokenAddress).mint(to, amount); + + // Ignored, because contracts only interacts with trusted contracts and tokens + // slither-disable-next-line reentrancy-events + emit RewardMinted(exchangeId, to, amount); + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + /** + * @notice Sets the distribution helper address. + * @param _distributionHelper The address of the distribution helper contract. + */ + function _setDistributionHelper(address _distributionHelper) internal { + require(_distributionHelper != address(0), "Distribution helper address must be set"); + distributionHelper = IDistributionHelper(_distributionHelper); + emit DistributionHelperUpdated(_distributionHelper); + } + + /** + * @notice Calculates the reserve ratio scalar for the given expansion config. + * @param config The expansion config. + * @return reserveRatioScalar The reserve ratio scalar. + */ + function _getReserveRatioScalar(ExchangeExpansionConfig memory config) internal view returns (uint256) { + uint256 numberOfExpansions; + + // If there was no previous expansion, we expand once. + if (config.lastExpansion == 0) { + numberOfExpansions = 1; + } else { + numberOfExpansions = (block.timestamp - config.lastExpansion) / config.expansionFrequency; + } + + uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate; + return unwrap(powu(wrap(stepReserveRatioScalar), numberOfExpansions)); + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[50] private __gap; +} diff --git a/contracts/goodDollar/interfaces/IGoodProtocol.sol b/contracts/goodDollar/interfaces/IGoodProtocol.sol new file mode 100644 index 00000000..1c774079 --- /dev/null +++ b/contracts/goodDollar/interfaces/IGoodProtocol.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: SEE LICENSE IN LICENSE +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IGoodDollar { + function mint(address to, uint256 amount) external; + + function burn(uint256 amount) external; + + function safeTransferFrom(address from, address to, uint256 value) external; + + function addMinter(address _minter) external; + + function isMinter(address account) external view returns (bool); + + function balanceOf(address account) external view returns (uint256); + + // slither-disable-next-line erc721-interface + function approve(address spender, uint256 amount) external returns (bool); +} + +interface IDistributionHelper { + function onDistribution(uint256 _amount) external; +} diff --git a/contracts/import.sol b/contracts/import.sol index e74ef425..86143961 100644 --- a/contracts/import.sol +++ b/contracts/import.sol @@ -12,4 +12,3 @@ import "celo/contracts/common/Freezer.sol"; import "celo/contracts/stability/SortedOracles.sol"; import "test/utils/harnesses/WithThresholdHarness.sol"; import "test/utils/harnesses/WithCooldownHarness.sol"; -import "test/utils/harnesses/TradingLimitsHarness.sol"; diff --git a/contracts/interfaces/IBancorExchangeProvider.sol b/contracts/interfaces/IBancorExchangeProvider.sol new file mode 100644 index 00000000..9f491c97 --- /dev/null +++ b/contracts/interfaces/IBancorExchangeProvider.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IBancorExchangeProvider { + struct PoolExchange { + address reserveAsset; + address tokenAddress; + uint256 tokenSupply; + uint256 reserveBalance; + uint32 reserveRatio; + uint32 exitContribution; + } + + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ + + /** + * @notice Emitted when the broker address is updated. + * @param newBroker The address of the new broker. + */ + event BrokerUpdated(address indexed newBroker); + + /** + * @notice Emitted when the reserve contract is set. + * @param newReserve The address of the new reserve. + */ + event ReserveUpdated(address indexed newReserve); + + /** + * @notice Emitted when a new pool has been created. + * @param exchangeId The id of the new pool + * @param reserveAsset The address of the reserve asset + * @param tokenAddress The address of the token + */ + event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + /** + * @notice Emitted when a pool has been destroyed. + * @param exchangeId The id of the pool to destroy + * @param reserveAsset The address of the reserve asset + * @param tokenAddress The address of the token + */ + event ExchangeDestroyed(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + /** + * @notice Emitted when the exit contribution for a pool is set. + * @param exchangeId The id of the pool + * @param exitContribution The exit contribution + */ + event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /** + * @notice Allows the contract to be upgradable via the proxy. + * @param _broker The address of the broker contract. + * @param _reserve The address of the reserve contract. + */ + function initialize(address _broker, address _reserve) external; + + /** + * @notice Retrieves the pool with the specified exchangeId. + * @param exchangeId The ID of the pool to be retrieved. + * @return exchange The pool with that ID. + */ + function getPoolExchange(bytes32 exchangeId) external view returns (PoolExchange memory exchange); + + /** + * @notice Gets all pool IDs. + * @return exchangeIds List of the pool IDs. + */ + function getExchangeIds() external view returns (bytes32[] memory exchangeIds); + + /** + * @notice Gets the current price based of the Bancor formula + * @param exchangeId The ID of the pool to get the price for + * @return price The current continuous price of the pool + */ + function currentPrice(bytes32 exchangeId) external view returns (uint256 price); + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + /** + * @notice Sets the address of the broker contract. + * @param _broker The new address of the broker contract. + */ + function setBroker(address _broker) external; + + /** + * @notice Sets the address of the reserve contract. + * @param _reserve The new address of the reserve contract. + */ + function setReserve(address _reserve) external; + + /** + * @notice Sets the exit contribution for a given pool + * @param exchangeId The ID of the pool + * @param exitContribution The exit contribution to be set + */ + function setExitContribution(bytes32 exchangeId, uint32 exitContribution) external; + + /** + * @notice Creates a new pool with the given parameters. + * @param exchange The pool to be created. + * @return exchangeId The ID of the new pool. + */ + function createExchange(PoolExchange calldata exchange) external returns (bytes32 exchangeId); + + /** + * @notice Destroys a pool with the given parameters if it exists. + * @param exchangeId The ID of the pool to be destroyed. + * @param exchangeIdIndex The index of the pool in the exchangeIds array. + * @return destroyed A boolean indicating whether or not the exchange was successfully destroyed. + */ + function destroyExchange(bytes32 exchangeId, uint256 exchangeIdIndex) external returns (bool destroyed); +} diff --git a/contracts/interfaces/IBiPoolManager.sol b/contracts/interfaces/IBiPoolManager.sol index 0d17b9f8..56194d55 100644 --- a/contracts/interfaces/IBiPoolManager.sol +++ b/contracts/interfaces/IBiPoolManager.sol @@ -13,8 +13,7 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; /** * @title BiPool Manager interface - * @notice The two asset pool manager is responsible for - * managing the state of all two-asset virtual pools. + * @notice An exchange provider implementation managing the state of all two-asset virtual pools. */ interface IBiPoolManager { /** diff --git a/contracts/interfaces/IBroker.sol b/contracts/interfaces/IBroker.sol index 675713bd..e0d3089f 100644 --- a/contracts/interfaces/IBroker.sol +++ b/contracts/interfaces/IBroker.sol @@ -38,76 +38,124 @@ interface IBroker { event TradingLimitConfigured(bytes32 exchangeId, address token, ITradingLimits.Config config); /** - * @notice Execute a token swap with fixed amountIn. + * @notice Allows the contract to be upgradable via the proxy. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The address of the Reserve contract. + */ + function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external; + + /** + * @notice Set the reserves for the exchange providers. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The addresses of the Reserve contracts. + */ + function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; + + /** + * @notice Add an exchange provider to the list of providers. + * @param exchangeProvider The address of the exchange provider to add. + * @param reserve The address of the reserve used by the exchange provider. + * @return index The index of the newly added specified exchange provider. + */ + function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); + + /** + * @notice Remove an exchange provider from the list of providers. + * @param exchangeProvider The address of the exchange provider to remove. + * @param index The index of the exchange provider being removed. + */ + function removeExchangeProvider(address exchangeProvider, uint256 index) external; + + /** + * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. - * @param amountIn The amount of tokenIn to be sold. - * @param amountOutMin Minimum amountOut to be received - controls slippage. - * @return amountOut The amount of tokenOut to be bought. + * @param amountOut The amount of tokenOut to be bought. + * @return amountIn The amount of tokenIn to be sold. */ - function swapIn( + function getAmountIn( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountIn, - uint256 amountOutMin - ) external returns (uint256 amountOut); + uint256 amountOut + ) external view returns (uint256 amountIn); /** - * @notice Execute a token swap with fixed amountOut. + * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. - * @param amountOut The amount of tokenOut to be bought. - * @param amountInMax Maximum amount of tokenIn that can be traded. - * @return amountIn The amount of tokenIn to be sold. + * @param amountIn The amount of tokenIn to be sold. + * @return amountOut The amount of tokenOut to be bought. */ - function swapOut( + function getAmountOut( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountOut, - uint256 amountInMax - ) external returns (uint256 amountIn); + uint256 amountIn + ) external view returns (uint256 amountOut); /** - * @notice Calculate amountOut of tokenOut received for a given amountIn of tokenIn. + * @notice Execute a token swap with fixed amountIn. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. * @param amountIn The amount of tokenIn to be sold. + * @param amountOutMin Minimum amountOut to be received - controls slippage. * @return amountOut The amount of tokenOut to be bought. */ - function getAmountOut( + function swapIn( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountIn - ) external view returns (uint256 amountOut); + uint256 amountIn, + uint256 amountOutMin + ) external returns (uint256 amountOut); /** - * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut. + * @notice Execute a token swap with fixed amountOut. * @param exchangeProvider the address of the exchange provider for the pair. * @param exchangeId The id of the exchange to use. * @param tokenIn The token to be sold. * @param tokenOut The token to be bought. * @param amountOut The amount of tokenOut to be bought. + * @param amountInMax Maximum amount of tokenIn that can be traded. * @return amountIn The amount of tokenIn to be sold. */ - function getAmountIn( + function swapOut( address exchangeProvider, bytes32 exchangeId, address tokenIn, address tokenOut, - uint256 amountOut - ) external view returns (uint256 amountIn); + uint256 amountOut, + uint256 amountInMax + ) external returns (uint256 amountIn); + + /** + * @notice Permissionless way to burn stables from msg.sender directly. + * @param token The token getting burned. + * @param amount The amount of the token getting burned. + * @return True if transaction succeeds. + */ + function burnStableTokens(address token, uint256 amount) external returns (bool); + + /** + * @notice Configure trading limits for an (exchangeId, token) tuple. + * @dev Will revert if the configuration is not valid according to the TradingLimits library. + * Resets existing state according to the TradingLimits library logic. + * Can only be called by owner. + * @param exchangeId the exchangeId to target. + * @param token the token to target. + * @param config the new trading limits config. + */ + function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; /** * @notice Get the list of registered exchange providers. @@ -116,39 +164,19 @@ interface IBroker { */ function getExchangeProviders() external view returns (address[] memory); - function burnStableTokens(address token, uint256 amount) external returns (bool); - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserve The address of the Reserve contract. + * @notice Get the address of the exchange provider at a given index. + * @dev Auto-generated getter for the exchangeProviders array. + * @param index The index of the exchange provider. + * @return exchangeProvider The address of the exchange provider. */ - function initialize(address[] calldata _exchangeProviders, address _reserve) external; - - /// @notice IOwnable: - function transferOwnership(address newOwner) external; - - function renounceOwnership() external; - - function owner() external view returns (address); - - /// @notice Getters: - function reserve() external view returns (address); + function exchangeProviders(uint256 index) external view returns (address exchangeProvider); + /** + * @notice Check if a given address is an exchange provider. + * @dev Auto-generated getter for the isExchangeProvider mapping. + * @param exchangeProvider The address to check. + * @return isExchangeProvider True if the address is an exchange provider, false otherwise. + */ function isExchangeProvider(address exchangeProvider) external view returns (bool); - - /// @notice Setters: - function addExchangeProvider(address exchangeProvider) external returns (uint256 index); - - function removeExchangeProvider(address exchangeProvider, uint256 index) external; - - function setReserve(address _reserve) external; - - function configureTradingLimit(bytes32 exchangeId, address token, ITradingLimits.Config calldata config) external; - - function tradingLimitsConfig(bytes32 id) external view returns (ITradingLimits.Config memory); - - function tradingLimitsState(bytes32 id) external view returns (ITradingLimits.State memory); - - function exchangeProviders(uint256 i) external view returns (address); } diff --git a/contracts/interfaces/IBrokerAdmin.sol b/contracts/interfaces/IBrokerAdmin.sol index d0aba861..6cac3dd4 100644 --- a/contracts/interfaces/IBrokerAdmin.sol +++ b/contracts/interfaces/IBrokerAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity >=0.5.13 <0.8.19; /* * @title Broker Admin Interface @@ -38,11 +38,12 @@ interface IBrokerAdmin { * @param exchangeProvider The address of the ExchangeProvider to add. * @return index The index where the ExchangeProvider was inserted. */ - function addExchangeProvider(address exchangeProvider) external returns (uint256 index); + function addExchangeProvider(address exchangeProvider, address reserve) external returns (uint256 index); /** - * @notice Set the Mento reserve address. - * @param reserve The Mento reserve address. + * @notice Set the reserves for the exchange providers. + * @param _exchangeProviders The addresses of the ExchangeProvider contracts. + * @param _reserves The addresses of the Reserve contracts. */ - function setReserve(address reserve) external; + function setReserves(address[] calldata _exchangeProviders, address[] calldata _reserves) external; } diff --git a/contracts/interfaces/IExchangeProvider.sol b/contracts/interfaces/IExchangeProvider.sol index b6f956fa..79c9353f 100644 --- a/contracts/interfaces/IExchangeProvider.sol +++ b/contracts/interfaces/IExchangeProvider.sol @@ -76,7 +76,7 @@ interface IExchangeProvider { /** * @notice Calculate amountIn of tokenIn needed for a given amountOut of tokenOut - * @param exchangeId The id of the exchange to use + * @param exchangeId The ID of the pool to use * @param tokenIn The token to be sold * @param tokenOut The token to be bought * @param amountOut The amount of tokenOut to be bought diff --git a/contracts/interfaces/IGoodDollarExchangeProvider.sol b/contracts/interfaces/IGoodDollarExchangeProvider.sol new file mode 100644 index 00000000..54328aeb --- /dev/null +++ b/contracts/interfaces/IGoodDollarExchangeProvider.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IGoodDollarExchangeProvider { + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ + + /** + * @notice Emitted when the ExpansionController address is updated. + * @param expansionController The address of the ExpansionController contract. + */ + event ExpansionControllerUpdated(address indexed expansionController); + + /** + * @notice Emitted when the GoodDollar DAO address is updated. + * @param AVATAR The address of the GoodDollar DAO contract. + */ + // solhint-disable-next-line var-name-mixedcase + event AvatarUpdated(address indexed AVATAR); + + /** + * @notice Emitted when the reserve ratio for a pool is updated. + * @param exchangeId The id of the pool. + * @param reserveRatio The new reserve ratio. + */ + event ReserveRatioUpdated(bytes32 indexed exchangeId, uint32 reserveRatio); + + /* =========================================== */ + /* ================ Functions ================ */ + /* =========================================== */ + + /** + * @notice Initializes the contract with the given parameters. + * @param _broker The address of the Broker contract. + * @param _reserve The address of the Reserve contract. + * @param _expansionController The address of the ExpansionController contract. + * @param _avatar The address of the GoodDollar DAO contract. + */ + function initialize(address _broker, address _reserve, address _expansionController, address _avatar) external; + + /** + * @notice Sets the address of the GoodDollar DAO contract. + * @param _avatar The address of the DAO contract. + */ + function setAvatar(address _avatar) external; + + /** + * @notice Sets the address of the Expansion Controller contract. + * @param _expansionController The address of the Expansion Controller contract. + */ + function setExpansionController(address _expansionController) external; + + /** + * @notice Calculates the amount of G$ tokens to be minted as a result of the expansion. + * @param exchangeId The ID of the pool to calculate the expansion for. + * @param reserveRatioScalar Scaler for calculating the new reserve ratio. + * @return amountToMint Amount of G$ tokens to be minted as a result of the expansion. + */ + function mintFromExpansion(bytes32 exchangeId, uint256 reserveRatioScalar) external returns (uint256 amountToMint); + + /** + * @notice Calculates the amount of G$ tokens to be minted as a result of the collected reserve interest. + * @param exchangeId The ID of the pool the collected reserve interest is added to. + * @param reserveInterest The amount of reserve asset tokens collected from interest. + * @return amountToMint The amount of G$ tokens to be minted as a result of the collected reserve interest. + */ + function mintFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountToMint); + + /** + * @notice Calculates the reserve ratio needed to mint the given G$ reward. + * @param exchangeId The ID of the pool the G$ reward is minted from. + * @param reward The amount of G$ tokens to be minted as a reward. + */ + function updateRatioForReward(bytes32 exchangeId, uint256 reward) external; + + /** + * @notice Pauses the Exchange, disabling minting. + */ + function pause() external; + + /** + * @notice Unpauses the Exchange, enabling minting again. + */ + function unpause() external; +} diff --git a/contracts/interfaces/IGoodDollarExpansionController.sol b/contracts/interfaces/IGoodDollarExpansionController.sol new file mode 100644 index 00000000..2268ef97 --- /dev/null +++ b/contracts/interfaces/IGoodDollarExpansionController.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17 <0.8.19; +pragma experimental ABIEncoderV2; + +interface IGoodDollarExpansionController { + /** + * @notice Struct holding the configuration for the expansion of an exchange. + * @param expansionRate The rate of expansion in percentage with 1e18 being 100%. + * @param expansionFrequency The frequency of expansion in seconds. + * @param lastExpansion The timestamp of the last prior expansion. + */ + struct ExchangeExpansionConfig { + uint64 expansionRate; + uint32 expansionFrequency; + uint32 lastExpansion; + } + + /* ------- Events ------- */ + + /** + * @notice Emitted when the GoodDollarExchangeProvider is updated. + * @param exchangeProvider The address of the new GoodDollarExchangeProvider. + */ + event GoodDollarExchangeProviderUpdated(address indexed exchangeProvider); + + /** + * @notice Emitted when the distribution helper is updated. + * @param distributionHelper The address of the new distribution helper. + */ + event DistributionHelperUpdated(address indexed distributionHelper); + + /** + * @notice Emitted when the Reserve address is updated. + * @param reserve The address of the new Reserve. + */ + event ReserveUpdated(address indexed reserve); + + /** + * @notice Emitted when the GoodDollar DAO address is updated. + * @param avatar The new address of the GoodDollar DAO. + */ + event AvatarUpdated(address indexed avatar); + + /** + * @notice Emitted when the expansion config is set for an pool. + * @param exchangeId The ID of the pool. + * @param expansionRate The rate of expansion. + * @param expansionFrequency The frequency of expansion. + */ + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); + + /** + * @notice Emitted when a G$ reward is minted. + * @param exchangeId The ID of the pool. + * @param to The address of the recipient. + * @param amount The amount of G$ tokens minted. + */ + event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); + + /** + * @notice Emitted when UBI is minted through collecting reserve interest. + * @param exchangeId The ID of the pool. + * @param amount The amount of G$ tokens minted. + */ + event InterestUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + /** + * @notice Emitted when UBI is minted through expansion. + * @param exchangeId The ID of the pool. + * @param amount The amount of G$ tokens minted. + */ + event ExpansionUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + /* ------- Functions ------- */ + + /** + * @notice Initializes the contract with the given parameters. + * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. + * @param _distributionHelper The address of the distribution helper contract. + * @param _reserve The address of the Reserve contract. + * @param _avatar The address of the GoodDollar DAO contract. + */ + function initialize( + address _goodDollarExchangeProvider, + address _distributionHelper, + address _reserve, + address _avatar + ) external; + + /** + * @notice Returns the expansion config for the given exchange. + * @param exchangeId The id of the exchange to get the expansion config for. + * @return config The expansion config. + */ + function getExpansionConfig(bytes32 exchangeId) external returns (ExchangeExpansionConfig memory); + + /** + * @notice Sets the GoodDollarExchangeProvider address. + * @param _goodDollarExchangeProvider The address of the GoodDollarExchangeProvider contract. + */ + function setGoodDollarExchangeProvider(address _goodDollarExchangeProvider) external; + + /** + * @notice Sets the distribution helper address. + * @param _distributionHelper The address of the distribution helper contract. + */ + function setDistributionHelper(address _distributionHelper) external; + + /** + * @notice Sets the reserve address. + * @param _reserve The address of the reserve contract. + */ + function setReserve(address _reserve) external; + + /** + * @notice Sets the AVATAR address. + * @param _avatar The address of the AVATAR contract. + */ + function setAvatar(address _avatar) external; + + /** + * @notice Sets the expansion config for the given pool. + * @param exchangeId The ID of the pool to set the expansion config for. + * @param expansionRate The rate of expansion. + * @param expansionFrequency The frequency of expansion. + */ + function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external; + + /** + * @notice Mints UBI as G$ tokens for a given pool from collected reserve interest. + * @param exchangeId The ID of the pool to mint UBI for. + * @param reserveInterest The amount of reserve tokens collected from interest. + */ + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external; + + /** + * @notice Mints UBI as G$ tokens for a given pool by comparing the contract's reserve balance to the virtual balance. + * @param exchangeId The ID of the pool to mint UBI for. + * @return amountMinted The amount of G$ tokens minted. + */ + function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted); + + /** + * @notice Mints UBI as G$ tokens for a given pool by calculating the expansion rate. + * @param exchangeId The ID of the pool to mint UBI for. + * @return amountMinted The amount of G$ tokens minted. + */ + function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted); + + /** + * @notice Mints a reward of G$ tokens for a given pool. + * @param exchangeId The ID of the pool to mint a G$ reward for. + * @param to The address of the recipient. + * @param amount The amount of G$ tokens to mint. + */ + function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; +} diff --git a/contracts/interfaces/IStableTokenV2.sol b/contracts/interfaces/IStableTokenV2.sol index cbf49418..10d2a077 100644 --- a/contracts/interfaces/IStableTokenV2.sol +++ b/contracts/interfaces/IStableTokenV2.sol @@ -70,9 +70,7 @@ interface IStableTokenV2 { */ function initializeV2(address _broker, address _validators, address _exchange) external; - /** - * @notice Gets the address of the Broker contract. - */ + /// @notice Gets the address of the Broker contract. function broker() external returns (address); /** diff --git a/contracts/libraries/TradingLimits.sol b/contracts/libraries/TradingLimits.sol index 66ad2cfd..14e59549 100644 --- a/contracts/libraries/TradingLimits.sol +++ b/contracts/libraries/TradingLimits.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; @@ -44,7 +44,7 @@ library TradingLimits { uint8 private constant L0 = 1; // 0b001 Limit0 uint8 private constant L1 = 2; // 0b010 Limit1 uint8 private constant LG = 4; // 0b100 LimitGlobal - int48 private constant MAX_INT48 = int48(uint48(-1) / 2); + int48 private constant MAX_INT48 = type(int48).max; /** * @notice Validate a trading limit configuration. @@ -129,7 +129,11 @@ library TradingLimits { ) internal view returns (ITradingLimits.State memory) { int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); - int48 deltaFlowUnits = _deltaFlowUnits == 0 ? 1 : int48(_deltaFlowUnits); + + int48 deltaFlowUnits = int48(_deltaFlowUnits); + if (deltaFlowUnits == 0) { + deltaFlowUnits = _deltaFlow > 0 ? int48(1) : int48(-1); + } if (config.flags & L0 > 0) { if (block.timestamp > self.lastUpdated0 + config.timestep0) { diff --git a/contracts/swap/Broker.sol b/contracts/swap/Broker.sol index 234172c0..24cdd43e 100644 --- a/contracts/swap/Broker.sol +++ b/contracts/swap/Broker.sol @@ -1,22 +1,20 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; -import { Ownable } from "openzeppelin-solidity/contracts/ownership/Ownable.sol"; -import { SafeERC20 } from "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; -import { IERC20 } from "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; -import { SafeMath } from "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import { Ownable } from "openzeppelin-contracts-next/contracts/access/Ownable.sol"; +import { SafeERC20MintableBurnable } from "contracts/common/SafeERC20MintableBurnable.sol"; +import { IERC20MintableBurnable as IERC20 } from "contracts/common/IERC20MintableBurnable.sol"; import { IExchangeProvider } from "../interfaces/IExchangeProvider.sol"; import { IBroker } from "../interfaces/IBroker.sol"; import { IBrokerAdmin } from "../interfaces/IBrokerAdmin.sol"; import { IReserve } from "../interfaces/IReserve.sol"; -import { IStableTokenV2 } from "../interfaces/IStableTokenV2.sol"; import { ITradingLimits } from "../interfaces/ITradingLimits.sol"; import { TradingLimits } from "../libraries/TradingLimits.sol"; import { Initializable } from "celo/contracts/common/Initializable.sol"; -import { ReentrancyGuard } from "celo/contracts/common/libraries/ReentrancyGuard.sol"; +import { ReentrancyGuard } from "openzeppelin-contracts-next/contracts/security/ReentrancyGuard.sol"; interface IERC20Metadata { /** @@ -32,90 +30,86 @@ interface IERC20Metadata { contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuard { using TradingLimits for ITradingLimits.State; using TradingLimits for ITradingLimits.Config; - using SafeERC20 for IERC20; - using SafeMath for uint256; + using SafeERC20MintableBurnable for IERC20; /* ==================== State Variables ==================== */ + /// @inheritdoc IBroker address[] public exchangeProviders; + + /// @inheritdoc IBroker mapping(address => bool) public isExchangeProvider; mapping(bytes32 => ITradingLimits.State) public tradingLimitsState; mapping(bytes32 => ITradingLimits.Config) public tradingLimitsConfig; - // Address of the reserve. - IReserve public reserve; + // Deprecated address of the reserve. Kept to keep storage layout consistent with previous versions. + // slither-disable-next-line constable-states + uint256 public __deprecated0; // prev: IReserve public reserve; - uint256 private constant MAX_INT256 = uint256(-1) / 2; + uint256 private constant MAX_INT256 = uint256(type(int256).max); + mapping(address => address) public exchangeReserve; /* ==================== Constructor ==================== */ /** * @notice Sets initialized == true on implementation contracts. * @param test Set to true to skip implementation initialization. */ - constructor(bool test) public Initializable(test) {} + constructor(bool test) Initializable(test) {} - /** - * @notice Allows the contract to be upgradable via the proxy. - * @param _exchangeProviders The addresses of the ExchangeProvider contracts. - * @param _reserve The address of the Reserve contract. - */ - function initialize(address[] calldata _exchangeProviders, address _reserve) external initializer { + /// @inheritdoc IBroker + function initialize(address[] calldata _exchangeProviders, address[] calldata _reserves) external initializer { _transferOwnership(msg.sender); for (uint256 i = 0; i < _exchangeProviders.length; i++) { - addExchangeProvider(_exchangeProviders[i]); + addExchangeProvider(_exchangeProviders[i], _reserves[i]); + } + } + + /// @inheritdoc IBroker + function setReserves( + address[] calldata _exchangeProviders, + address[] calldata _reserves + ) external override(IBroker, IBrokerAdmin) onlyOwner { + for (uint256 i = 0; i < _exchangeProviders.length; i++) { + require(isExchangeProvider[_exchangeProviders[i]], "ExchangeProvider does not exist"); + require(_reserves[i] != address(0), "Reserve address can't be 0"); + exchangeReserve[_exchangeProviders[i]] = _reserves[i]; + emit ReserveSet(_exchangeProviders[i], _reserves[i]); } - setReserve(_reserve); } /* ==================== Mutative Functions ==================== */ - /** - * @notice Add an exchange provider to the list of providers. - * @param exchangeProvider The address of the exchange provider to add. - * @return index The index of the newly added specified exchange provider. - */ - function addExchangeProvider(address exchangeProvider) public onlyOwner returns (uint256 index) { + /// @inheritdoc IBroker + function addExchangeProvider( + address exchangeProvider, + address reserve + ) public override(IBroker, IBrokerAdmin) onlyOwner returns (uint256 index) { require(!isExchangeProvider[exchangeProvider], "ExchangeProvider already exists in the list"); require(exchangeProvider != address(0), "ExchangeProvider address can't be 0"); + require(reserve != address(0), "Reserve address can't be 0"); exchangeProviders.push(exchangeProvider); isExchangeProvider[exchangeProvider] = true; + exchangeReserve[exchangeProvider] = reserve; emit ExchangeProviderAdded(exchangeProvider); - index = exchangeProviders.length.sub(1); + emit ReserveSet(exchangeProvider, reserve); + index = exchangeProviders.length - 1; } - /** - * @notice Remove an exchange provider from the list of providers. - * @param exchangeProvider The address of the exchange provider to remove. - * @param index The index of the exchange provider being removed. - */ - function removeExchangeProvider(address exchangeProvider, uint256 index) public onlyOwner { + /// @inheritdoc IBroker + function removeExchangeProvider( + address exchangeProvider, + uint256 index + ) public override(IBroker, IBrokerAdmin) onlyOwner { require(exchangeProviders[index] == exchangeProvider, "index doesn't match provider"); - exchangeProviders[index] = exchangeProviders[exchangeProviders.length.sub(1)]; + exchangeProviders[index] = exchangeProviders[exchangeProviders.length - 1]; exchangeProviders.pop(); delete isExchangeProvider[exchangeProvider]; + delete exchangeReserve[exchangeProvider]; emit ExchangeProviderRemoved(exchangeProvider); } - /** - * @notice Set the Mento reserve address. - * @param _reserve The Mento reserve address. - */ - function setReserve(address _reserve) public onlyOwner { - require(_reserve != address(0), "Reserve address must be set"); - emit ReserveSet(_reserve, address(reserve)); - reserve = IReserve(_reserve); - } - - /** - * @notice Calculate the amount of tokenIn to be sold for a given amountOut of tokenOut - * @param exchangeProvider the address of the exchange manager for the pair - * @param exchangeId The id of the exchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountOut The amount of tokenOut to be bought - * @return amountIn The amount of tokenIn to be sold - */ + /// @inheritdoc IBroker function getAmountIn( address exchangeProvider, bytes32 exchangeId, @@ -124,18 +118,14 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar uint256 amountOut ) external view returns (uint256 amountIn) { require(isExchangeProvider[exchangeProvider], "ExchangeProvider does not exist"); + address reserve = exchangeReserve[exchangeProvider]; + if (IReserve(reserve).isCollateralAsset(tokenOut)) { + require(IERC20(tokenOut).balanceOf(reserve) >= amountOut, "Insufficient balance in reserve"); + } amountIn = IExchangeProvider(exchangeProvider).getAmountIn(exchangeId, tokenIn, tokenOut, amountOut); } - /** - * @notice Calculate the amount of tokenOut to be bought for a given amount of tokenIn to be sold - * @param exchangeProvider the address of the exchange manager for the pair - * @param exchangeId The id of the exchange to use - * @param tokenIn The token to be sold - * @param tokenOut The token to be bought - * @param amountIn The amount of tokenIn to be sold - * @return amountOut The amount of tokenOut to be bought - */ + /// @inheritdoc IBroker function getAmountOut( address exchangeProvider, bytes32 exchangeId, @@ -145,18 +135,13 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar ) external view returns (uint256 amountOut) { require(isExchangeProvider[exchangeProvider], "ExchangeProvider does not exist"); amountOut = IExchangeProvider(exchangeProvider).getAmountOut(exchangeId, tokenIn, tokenOut, amountIn); + address reserve = exchangeReserve[exchangeProvider]; + if (IReserve(reserve).isCollateralAsset(tokenOut)) { + require(IERC20(tokenOut).balanceOf(reserve) >= amountOut, "Insufficient balance in reserve"); + } } - /** - * @notice Execute a token swap with fixed amountIn. - * @param exchangeProvider the address of the exchange provider for the pair. - * @param exchangeId The id of the exchange to use. - * @param tokenIn The token to be sold. - * @param tokenOut The token to be bought. - * @param amountIn The amount of tokenIn to be sold. - * @param amountOutMin Minimum amountOut to be received - controls slippage. - * @return amountOut The amount of tokenOut to be bought. - */ + /// @inheritdoc IBroker function swapIn( address exchangeProvider, bytes32 exchangeId, @@ -170,21 +155,14 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar amountOut = IExchangeProvider(exchangeProvider).swapIn(exchangeId, tokenIn, tokenOut, amountIn); require(amountOut >= amountOutMin, "amountOutMin not met"); guardTradingLimits(exchangeId, tokenIn, amountIn, tokenOut, amountOut); - transferIn(msg.sender, tokenIn, amountIn); - transferOut(msg.sender, tokenOut, amountOut); + + address reserve = exchangeReserve[exchangeProvider]; + transferIn(payable(msg.sender), tokenIn, amountIn, reserve); + transferOut(payable(msg.sender), tokenOut, amountOut, reserve); emit Swap(exchangeProvider, exchangeId, msg.sender, tokenIn, tokenOut, amountIn, amountOut); } - /** - * @notice Execute a token swap with fixed amountOut. - * @param exchangeProvider the address of the exchange provider for the pair. - * @param exchangeId The id of the exchange to use. - * @param tokenIn The token to be sold. - * @param tokenOut The token to be bought. - * @param amountOut The amount of tokenOut to be bought. - * @param amountInMax Maximum amount of tokenIn that can be traded. - * @return amountIn The amount of tokenIn to be sold. - */ + /// @inheritdoc IBroker function swapOut( address exchangeProvider, bytes32 exchangeId, @@ -198,41 +176,26 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar amountIn = IExchangeProvider(exchangeProvider).swapOut(exchangeId, tokenIn, tokenOut, amountOut); require(amountIn <= amountInMax, "amountInMax exceeded"); guardTradingLimits(exchangeId, tokenIn, amountIn, tokenOut, amountOut); - transferIn(msg.sender, tokenIn, amountIn); - transferOut(msg.sender, tokenOut, amountOut); + + address reserve = exchangeReserve[exchangeProvider]; + transferIn(payable(msg.sender), tokenIn, amountIn, reserve); + transferOut(payable(msg.sender), tokenOut, amountOut, reserve); emit Swap(exchangeProvider, exchangeId, msg.sender, tokenIn, tokenOut, amountIn, amountOut); } - /** - * @notice Permissionless way to burn stables from msg.sender directly. - * @param token The token getting burned. - * @param amount The amount of the token getting burned. - * @return True if transaction succeeds. - */ + /// @inheritdoc IBroker function burnStableTokens(address token, uint256 amount) public returns (bool) { - require(reserve.isStableAsset(token), "Token must be a reserve stable asset"); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); - require(IStableTokenV2(token).burn(amount), "Burning of the stable asset failed"); + IERC20(token).safeBurn(amount); return true; } - /** - * @notice Configure trading limits for an (exchangeId, token) touple. - * @dev Will revert if the configuration is not valid according to the - * TradingLimits library. - * Resets existing state according to the TradingLimits library logic. - * Can only be called by owner. - * @param exchangeId the exchangeId to target. - * @param token the token to target. - * @param config the new trading limits config. - */ - // TODO: Make this external with next update. - // slither-disable-next-line external-function + /// @inheritdoc IBroker function configureTradingLimit( bytes32 exchangeId, address token, ITradingLimits.Config memory config - ) public onlyOwner { + ) external onlyOwner { config.validate(); bytes32 limitId = exchangeId ^ bytes32(uint256(uint160(token))); @@ -250,10 +213,12 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param to The address receiving the asset. * @param token The asset to transfer. * @param amount The amount of `token` to be transferred. + * @param _reserve The address of the corresponding reserve. */ - function transferOut(address payable to, address token, uint256 amount) internal { + function transferOut(address payable to, address token, uint256 amount, address _reserve) internal { + IReserve reserve = IReserve(_reserve); if (reserve.isStableAsset(token)) { - require(IStableTokenV2(token).mint(to, amount), "Minting of the stable asset failed"); + IERC20(token).safeMint(to, amount); } else if (reserve.isCollateralAsset(token)) { require(reserve.transferExchangeCollateralAsset(token, to, amount), "Transfer of the collateral asset failed"); } else { @@ -268,11 +233,13 @@ contract Broker is IBroker, IBrokerAdmin, Initializable, Ownable, ReentrancyGuar * @param from The address to transfer the asset from. * @param token The asset to transfer. * @param amount The amount of `token` to be transferred. + * @param _reserve The address of the corresponding reserve. */ - function transferIn(address payable from, address token, uint256 amount) internal { + function transferIn(address payable from, address token, uint256 amount, address _reserve) internal { + IReserve reserve = IReserve(_reserve); if (reserve.isStableAsset(token)) { IERC20(token).safeTransferFrom(from, address(this), amount); - require(IStableTokenV2(token).burn(amount), "Burning of the stable asset failed"); + IERC20(token).safeBurn(amount); } else if (reserve.isCollateralAsset(token)) { IERC20(token).safeTransferFrom(from, address(reserve), amount); } else { diff --git a/foundry.toml b/foundry.toml index d1bc4ab2..89127769 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,13 +14,9 @@ no_match_contract = "ForkTest" gas_limit = 9223372036854775807 -allow_paths = [ - "node_modules/@celo" -] +allow_paths = ["node_modules/@celo"] -fs_permissions = [ - { access = "read", path = "out" } -] +fs_permissions = [{ access = "read", path = "out" }] [profile.ci] fuzz_runs = 1_000 @@ -35,6 +31,5 @@ no_match_contract = "_random" # in order to reset the no_match_contract match_contract = "ForkTest" [rpc_endpoints] -celo="${CELO_MAINNET_RPC_URL}" -baklava="${BAKLAVA_RPC_URL}" -alfajores="${ALFAJORES_RPC_URL}" +celo = "${CELO_MAINNET_RPC_URL}" +alfajores = "${ALFAJORES_RPC_URL}" diff --git a/lib/forge-std b/lib/forge-std index 1714bee7..035de35f 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit 035de35f5e366c8d6ed142aec4ccb57fe2dd87d4 diff --git a/package.json b/package.json index 01572f79..7d828e81 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,15 @@ "prettier:check": "prettier --config \"./.prettierrc.yml\" --check \"**/*.{json,md,sol,yml}\"", "solhint": "yarn solhint:contracts && yarn solhint:tests", "solhint:contracts": "solhint --config \"./.solhint.json\" \"contracts/**/*.sol\" -w 0", - "solhint:tests": "solhint --config \"./.solhint.test.json\" \"test/**/*.sol\" -w 0", + "solhint:tests": "solhint --config \"./test/.solhint.json\" \"test/**/*.sol\" -w 0", "test": "forge test", "fork-test": "env FOUNDRY_PROFILE=fork-tests forge test", - "fork-test:baklava": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Baklava", "fork-test:alfajores": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores", "fork-test:celo-mainnet": "env FOUNDRY_PROFILE=fork-tests forge test --match-contract Celo", "check-no-ir": "./bin/check-contracts.sh", - "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip \"test/**/*\"" + "check-contract-sizes": "env FOUNDRY_PROFILE=optimized forge build --sizes --skip \"test/**/*\"", + "slither": "slither .", + "todo": "git ls-files -c --exclude-standard | grep -v \"package.json\" | xargs -I {} sh -c 'grep -H -n -i --color \"TODO:\\|FIXME:\" \"{}\" 2>/dev/null || true'" }, "dependencies": { "@celo/contracts": "^11.0.0" diff --git a/.solhint.test.json b/test/.solhint.json similarity index 66% rename from .solhint.test.json rename to test/.solhint.json index 6d67cf31..de2b87b7 100644 --- a/.solhint.test.json +++ b/test/.solhint.json @@ -2,39 +2,24 @@ "extends": "solhint:recommended", "plugins": ["prettier"], "rules": { - "one-contract-per-file": "off", - "no-global-import": "off", - "no-console": "off", "code-complexity": ["error", 8], "compiler-version": ["error", ">=0.5.13"], - "func-visibility": [ - "error", - { - "ignoreConstructors": true - } - ], - "max-line-length": ["error", 121], - "not-rely-on-time": "off", + "const-name-snakecase": "off", + "contract-name-camelcase": "off", + "func-name-mixedcase": "off", + "func-visibility": ["error", { "ignoreConstructors": true }], "function-max-lines": ["error", 121], "gas-custom-errors": "off", + "max-line-length": ["error", 121], "max-states-count": "off", - "var-name-mixedcase": "off", - "func-name-mixedcase": "off", - "state-visibility": "off", - "const-name-snakecase": "off", - "contract-name-camelcase": "off", + "no-console": "off", "no-empty-blocks": "off", - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ], - "reason-string": [ - "warn", - { - "maxLength": 64 - } - ] + "no-global-import": "off", + "not-rely-on-time": "off", + "one-contract-per-file": "off", + "prettier/prettier": ["error", { "endOfLine": "auto" }], + "reason-string": ["warn", { "maxLength": 64 }], + "state-visibility": "off", + "var-name-mixedcase": "off" } } diff --git a/test/fork/BancorExchangeProviderForkTest.sol b/test/fork/BancorExchangeProviderForkTest.sol new file mode 100644 index 00000000..ad2c4d71 --- /dev/null +++ b/test/fork/BancorExchangeProviderForkTest.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; + +// Contracts +import { BaseForkTest } from "./BaseForkTest.sol"; +import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + +contract BancorExchangeProviderForkTest is BaseForkTest { + address ownerAddress; + address brokerAddress; + address reserveAddress; + ERC20 reserveToken; + ERC20 swapToken; + + BancorExchangeProvider bancorExchangeProvider; + IBancorExchangeProvider.PoolExchange poolExchange; + bytes32 exchangeId; + + constructor(uint256 _chainId) BaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + ownerAddress = makeAddr("owner"); + brokerAddress = address(this.broker()); + reserveAddress = address(mentoReserve); + reserveToken = ERC20(address(mentoReserve.collateralAssets(0))); // == CELO + swapToken = ERC20(this.lookup("StableToken")); // == cUSD + + // Deploy and initialize BancorExchangeProvider (includes BancorFormula as part of init) + setUpBancorExchangeProvider(); + } + + function setUpBancorExchangeProvider() public { + vm.startPrank(ownerAddress); + bancorExchangeProvider = new BancorExchangeProvider(false); + bancorExchangeProvider.initialize(brokerAddress, reserveAddress); + vm.stopPrank(); + + poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(swapToken), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 0.2 * 1e8, + exitContribution: 0.01 * 1e8 + }); + + vm.prank(ownerAddress); + exchangeId = bancorExchangeProvider.createExchange(poolExchange); + } + + function test_init_isDeployedAndInitializedCorrectly() public view { + assertEq(bancorExchangeProvider.owner(), ownerAddress); + assertEq(bancorExchangeProvider.broker(), brokerAddress); + assertEq(address(bancorExchangeProvider.reserve()), reserveAddress); + + IBancorExchangeProvider.PoolExchange memory _poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(_poolExchange.reserveAsset, _poolExchange.reserveAsset); + assertEq(_poolExchange.tokenAddress, _poolExchange.tokenAddress); + assertEq(_poolExchange.tokenSupply, _poolExchange.tokenSupply); + assertEq(_poolExchange.reserveBalance, _poolExchange.reserveBalance); + assertEq(_poolExchange.reserveRatio, _poolExchange.reserveRatio); + assertEq(_poolExchange.exitContribution, _poolExchange.exitContribution); + } + + function test_swapIn_whenTokenInIsReserveToken_shouldSwapIn() public { + uint256 amountIn = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(swapToken), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(swapToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore + amountOut); + } + + function test_swapIn_whenTokenInIsSwapToken_shouldSwapIn() public { + uint256 amountIn = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut( + exchangeId, + address(swapToken), + address(reserveToken), + amountIn + ); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn({ + exchangeId: exchangeId, + tokenIn: address(swapToken), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore - amountIn); + } + + function test_swapOut_whenTokenInIsReserveToken_shouldSwapOut() public { + uint256 amountOut = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn( + exchangeId, + address(reserveToken), + address(swapToken), + amountOut + ); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(swapToken), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore + amountOut); + } + + function test_swapOut_whenTokenInIsSwapToken_shouldSwapOut() public { + uint256 amountOut = 1e18; + uint256 reserveBalanceBefore = poolExchange.reserveBalance; + uint256 swapTokenSupplyBefore = poolExchange.tokenSupply; + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn( + exchangeId, + address(swapToken), + address(reserveToken), + amountOut + ); + vm.prank(brokerAddress); + + uint256 amountIn = bancorExchangeProvider.swapOut({ + exchangeId: exchangeId, + tokenIn: address(swapToken), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 swapTokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(swapTokenSupplyAfter, swapTokenSupplyBefore - amountIn); + } +} diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol index fe97e8a0..3bda5146 100644 --- a/test/fork/BaseForkTest.sol +++ b/test/fork/BaseForkTest.sol @@ -1,18 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; +// Libraries import { Test } from "mento-std/Test.sol"; import { CELO_REGISTRY_ADDRESS } from "mento-std/Constants.sol"; - import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; -import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; +// Interfaces +import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; +import { IOwnable } from "contracts/interfaces/IOwnable.sol"; +import { IRegistry } from "celo/contracts/common/interfaces/IRegistry.sol"; import { IReserve } from "contracts/interfaces/IReserve.sol"; import { ISortedOracles } from "contracts/interfaces/ISortedOracles.sol"; import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; + +// Contracts & Utils +import { Broker } from "contracts/swap/Broker.sol"; +import { TradingLimitsHarness } from "test/utils/harnesses/TradingLimitsHarness.sol"; import { toRateFeed } from "./helpers/misc.sol"; interface IMint { @@ -22,12 +29,11 @@ interface IMint { /** * @title BaseForkTest * @notice Fork tests for Mento! - * This test suite tests invariantes on a fork of a live Mento environemnts. - * The philosophy is to test in accordance with how the target fork is configured, - * therfore it doesn't make assumptions about the systems, nor tries to configure - * the system to test specific scenarios. - * However, it should be exausitve in testing invariants across all tradable pairs - * in the system, therefore each test should. + * This test suite tests invariants on a fork of a live Mento environments. + * The philosophy is to test in accordance with how the target fork is configured. + * Therefore, it doesn't make assumptions about the systems, nor tries to configure + * the system to test specific scenarios. However, it should be exhaustive in testing + * invariants across all tradable pairs in the system. */ abstract contract BaseForkTest is Test { using FixidityLib for FixidityLib.Fraction; @@ -36,9 +42,10 @@ abstract contract BaseForkTest is Test { address governance; IBroker public broker; + IBiPoolManager biPoolManager; IBreakerBox public breakerBox; ISortedOracles public sortedOracles; - IReserve public reserve; + IReserve public mentoReserve; ITradingLimitsHarness public tradingLimits; address public trader; @@ -65,30 +72,51 @@ abstract contract BaseForkTest is Test { function setUp() public virtual { fork(targetChainId); - // @dev Updaing the target fork block every 200 blocks, about ~8 min. - // This means that when running locally RPC calls will be cached. + /// @dev Updating the target fork block every 200 blocks, about ~8 min. + /// This means that, when running locally, RPC calls will be cached. fork(targetChainId, (block.number / 100) * 100); // The precompile handler needs to be reinitialized after forking. __CeloPrecompiles_init(); - tradingLimits = ITradingLimitsHarness(deployCode("TradingLimitsHarness")); + tradingLimits = new TradingLimitsHarness(); + broker = IBroker(lookup("Broker")); + biPoolManager = IBiPoolManager(broker.exchangeProviders(0)); sortedOracles = ISortedOracles(lookup("SortedOracles")); governance = lookup("Governance"); breakerBox = IBreakerBox(address(sortedOracles.breakerBox())); vm.label(address(breakerBox), "BreakerBox"); trader = makeAddr("trader"); - reserve = IReserve(broker.reserve()); + mentoReserve = IReserve(lookup("Reserve")); + + setUpBroker(); - /// @dev Hardcoded number of dependencies for each ratefeed. - /// Should be updated when they change, there is a test that will - /// validate that. + /// @dev Hardcoded number of dependencies for each rate feed. + /// Should be updated when they change, there is a test that + /// will validate that. rateFeedDependenciesCount[lookup("StableTokenXOF")] = 2; rateFeedDependenciesCount[toRateFeed("EUROCXOF")] = 2; rateFeedDependenciesCount[toRateFeed("USDCEUR")] = 1; rateFeedDependenciesCount[toRateFeed("USDCBRL")] = 1; } + // TODO: Broker setup can be removed after the Broker changes have been deployed to Mainnet + function setUpBroker() internal { + Broker newBrokerImplementation = new Broker(false); + vm.prank(IOwnable(address(broker)).owner()); + ICeloProxy(address(broker))._setImplementation(address(newBrokerImplementation)); + address brokerImplAddressAfterUpgrade = ICeloProxy(address(broker))._getImplementation(); + assert(address(newBrokerImplementation) == brokerImplAddressAfterUpgrade); + + address[] memory exchangeProviders = new address[](1); + exchangeProviders[0] = address(biPoolManager); + address[] memory reserves = new address[](1); + reserves[0] = address(mentoReserve); + + vm.prank(IOwnable(address(broker)).owner()); + broker.setReserves(exchangeProviders, reserves); + } + function mint(address asset, address to, uint256 amount, bool updateSupply) public { if (asset == lookup("GoldToken")) { if (!updateSupply) { diff --git a/test/fork/ChainForkTest.sol b/test/fork/ChainForkTest.sol index c6f4c321..b57c7479 100644 --- a/test/fork/ChainForkTest.sol +++ b/test/fork/ChainForkTest.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; import { console } from "forge-std/console.sol"; @@ -28,12 +27,12 @@ abstract contract ChainForkTest is BaseForkTest { IBiPoolManager biPoolManager = IBiPoolManager(broker.getExchangeProviders()[0]); vm.expectRevert("contract already initialized"); - biPoolManager.initialize(address(broker), reserve, sortedOracles, breakerBox); + biPoolManager.initialize(address(broker), mentoReserve, sortedOracles, breakerBox); } function test_brokerCanNotBeReinitialized() public { vm.expectRevert("contract already initialized"); - broker.initialize(new address[](0), address(reserve)); + broker.initialize(new address[](0), new address[](0)); } function test_sortedOraclesCanNotBeReinitialized() public { @@ -43,7 +42,7 @@ abstract contract ChainForkTest is BaseForkTest { function test_reserveCanNotBeReinitialized() public { vm.expectRevert("contract already initialized"); - reserve.initialize( + mentoReserve.initialize( address(10), 0, 0, @@ -83,10 +82,10 @@ abstract contract ChainForkTest is BaseForkTest { function test_numberCollateralAssetsCount() public { address collateral; for (uint256 i = 0; i < COLLATERAL_ASSETS_COUNT; i++) { - collateral = reserve.collateralAssets(i); + collateral = mentoReserve.collateralAssets(i); } vm.expectRevert(); - reserve.collateralAssets(COLLATERAL_ASSETS_COUNT); + mentoReserve.collateralAssets(COLLATERAL_ASSETS_COUNT); } function test_stableTokensCanNotBeReinitialized() public { diff --git a/test/fork/ExchangeForkTest.sol b/test/fork/ExchangeForkTest.sol index 61ec0d77..e462f045 100644 --- a/test/fork/ExchangeForkTest.sol +++ b/test/fork/ExchangeForkTest.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, const-name-snakecase, max-states-count pragma solidity ^0.8; import { console } from "forge-std/console.sol"; @@ -49,11 +48,16 @@ abstract contract ExchangeForkTest is SwapAssertions, CircuitBreakerAssertions, super.setUp(); loadExchange(); - console.log("%s | %s | %s", this.ticker(), exchangeProviderAddr, vm.toString(exchangeId)); + console.log( + "%s | ExchangeProvider: %s | ExchangeID: %s", + this.ticker(), + exchangeProviderAddr, + vm.toString(exchangeId) + ); for (uint256 i = 0; i < COLLATERAL_ASSETS_COUNT; i++) { - address collateralAsset = reserve.collateralAssets(i); + address collateralAsset = mentoReserve.collateralAssets(i); vm.label(collateralAsset, IERC20(collateralAsset).symbol()); - mint(collateralAsset, address(reserve), uint256(25_000_000).toSubunits(collateralAsset), true); + mint(collateralAsset, address(mentoReserve), uint256(25_000_000).toSubunits(collateralAsset), true); } } diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol index 0aaf6373..d3ab0b48 100644 --- a/test/fork/ForkTests.t.sol +++ b/test/fork/ForkTests.t.sol @@ -1,22 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// solhint-disable func-name-mixedcase, max-line-length pragma solidity ^0.8; /** @dev Fork tests for Mento! -This test suite tests invariants on a fork of a live Mento environemnts. +This test suite tests invariants on a fork of a live Mento environment. -Thare are two types of tests contracts: -- ChainForkTests: Tests that are specific to the chain, such as the number of exchanges, the number of collateral assets, contract initialization state, etc. +Thare are two types of test contracts: +- ChainForkTests: Tests that are specific to the chain, such as the number of exchanges, the number of collateral + assets, contract initialization state, etc. - ExchangeForkTests: Tests that are specific to the exchange, such as trading limits, swaps, circuit breakers, etc. To make it easier to debug and develop, we have one ChainForkTest for each chain (Alfajores, Celo) and one ExchangeForkTest for each exchange provider and exchange pair. -The ChainFork tests are instantiated with: +The ChainForkTests are instantiated with: - Chain ID. - Expected number of exchange providers. - Expected number of exchanges per exchange provider. -If any of this assertions fail then the ChainTest will fail and that's the queue to update this file +If any of these assertions fail, then the ChainForkTest will fail and that's the cue to update this file and add additional ExchangeForkTests. The ExchangeForkTests are instantiated with: @@ -24,22 +24,26 @@ The ExchangeForkTests are instantiated with: - Exchange Provider Index. - Exchange Index. -And the naming convetion for them is: -${ChainName}_P${ExchangeProviderIndex}E${ExchangeIndex}_ExchangeForkTest -e.g. Alfajores_P0E00_ExchangeForkTest (Alfajores, Exchange Provider 0, Exchange 0) -The Exchange Index is 0 padded to make them align nicely in the file, exchange provider counts shouldn't -exceed 10, if they do, then we need to update the naming convention. +And the naming convention for them is: +- ${ChainName}_P${ExchangeProviderIndex}E${ExchangeIndex}_ExchangeForkTest +- e.g. "Alfajores_P0E00_ExchangeForkTest (Alfajores, Exchange Provider 0, Exchange 0)" +The Exchange Index is 0 padded to make them align nicely in the file. +Exchange provider counts shouldn't exceed 10. If they do, then we need to update the naming convention. This makes it easy to drill into which exchange is failing and debug it like: -$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract CELO_P0E12 +- `$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract CELO_P0E12` or run all tests for a chain: -$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores +- `$ env FOUNDRY_PROFILE=fork-tests forge test --match-contract Alfajores` */ +import { CELO_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; import { uints } from "mento-std/Array.sol"; import { ChainForkTest } from "./ChainForkTest.sol"; import { ExchangeForkTest } from "./ExchangeForkTest.sol"; -import { CELO_ID, ALFAJORES_ID } from "mento-std/Constants.sol"; +import { BancorExchangeProviderForkTest } from "./BancorExchangeProviderForkTest.sol"; +import { GoodDollarTradingLimitsForkTest } from "./GoodDollar/TradingLimitsForkTest.sol"; +import { GoodDollarSwapForkTest } from "./GoodDollar/SwapForkTest.sol"; +import { GoodDollarExpansionForkTest } from "./GoodDollar/ExpansionForkTest.sol"; contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(15)) {} @@ -104,3 +108,11 @@ contract Celo_P0E12_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 12) {} contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} contract Celo_P0E14_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 14) {} + +contract Celo_BancorExchangeProviderForkTest is BancorExchangeProviderForkTest(CELO_ID) {} + +contract Celo_GoodDollarTradingLimitsForkTest is GoodDollarTradingLimitsForkTest(CELO_ID) {} + +contract Celo_GoodDollarSwapForkTest is GoodDollarSwapForkTest(CELO_ID) {} + +contract Celo_GoodDollarExpansionForkTest is GoodDollarExpansionForkTest(CELO_ID) {} diff --git a/test/fork/GoodDollar/ExpansionForkTest.sol b/test/fork/GoodDollar/ExpansionForkTest.sol new file mode 100644 index 00000000..c1f50e8f --- /dev/null +++ b/test/fork/GoodDollar/ExpansionForkTest.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; + +// Interfaces +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +// Contracts +import { GoodDollarBaseForkTest } from "./GoodDollarBaseForkTest.sol"; + +contract GoodDollarExpansionForkTest is GoodDollarBaseForkTest { + using TradingLimitHelpers for *; + using TokenHelpers for *; + + constructor(uint256 _chainId) GoodDollarBaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + } + + function test_mintFromExpansion() public { + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 distributionHelperBalanceBefore = goodDollarToken.balanceOf(distributionHelperAddress); + + vm.mockCall( + distributionHelperAddress, + abi.encodeWithSelector(IDistributionHelper(distributionHelperAddress).onDistribution.selector), + abi.encode(true) + ); + + skip(2 days + 1 seconds); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + assertApproxEqAbs(priceBefore, priceAfter, 1e11); + assertEq(goodDollarToken.balanceOf(distributionHelperAddress), amountMinted + distributionHelperBalanceBefore); + } + + function test_mintFromInterest() public { + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + address reserveInterestCollector = makeAddr("reserveInterestCollector"); + uint256 reserveInterest = 1000 * 1e18; + deal(address(reserveToken), reserveInterestCollector, reserveInterest); + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 interestCollectorBalanceBefore = reserveToken.balanceOf(reserveInterestCollector); + uint256 distributionHelperBalanceBefore = goodDollarToken.balanceOf(distributionHelperAddress); + + vm.startPrank(reserveInterestCollector); + reserveToken.approve(address(expansionController), reserveInterest); + expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + vm.stopPrank(); + + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 interestCollectorBalanceAfter = reserveToken.balanceOf(reserveInterestCollector); + uint256 distributionHelperBalanceAfter = goodDollarToken.balanceOf(distributionHelperAddress); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + reserveInterest); + assertEq(interestCollectorBalanceAfter, interestCollectorBalanceBefore - reserveInterest); + assertTrue(distributionHelperBalanceBefore < distributionHelperBalanceAfter); + assertEq(priceBefore, priceAfter); + } +} diff --git a/test/fork/GoodDollar/GoodDollarBaseForkTest.sol b/test/fork/GoodDollar/GoodDollarBaseForkTest.sol new file mode 100644 index 00000000..02fc09ad --- /dev/null +++ b/test/fork/GoodDollar/GoodDollarBaseForkTest.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries / Helpers / Utils +import { console } from "forge-std/console.sol"; +import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { L0, L1, LG, min } from "../helpers/misc.sol"; + +// Interfaces +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; +import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +// Contracts +import { BaseForkTest } from "../BaseForkTest.sol"; +import { Broker } from "contracts/swap/Broker.sol"; +import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; +import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; + +contract GoodDollarBaseForkTest is BaseForkTest { + using FixidityLib for FixidityLib.Fraction; + using TokenHelpers for *; + + // Addresses + address constant AVATAR_ADDRESS = 0x495d133B938596C9984d462F007B676bDc57eCEC; + address constant CUSD_ADDRESS = 0x765DE816845861e75A25fCA122bb6898B8B1282a; + address constant GOOD_DOLLAR_ADDRESS = 0x62B8B11039FcfE5aB0C56E502b1C372A3d2a9c7A; + address constant REGISTRY_ADDRESS = 0x000000000000000000000000000000000000ce10; + address ownerAddress; + address distributionHelperAddress = makeAddr("distributionHelper"); + + // GoodDollar Relaunch Config + uint256 constant INITIAL_RESERVE_BALANCE = 200_000 * 1e18; + uint256 constant INITIAL_GOOD_DOLLAR_TOKEN_SUPPLY = 7_000_000_000 * 1e18; + uint32 constant INITIAL_RESERVE_RATIO = 0.28571428 * 1e8; + uint32 constant INITIAL_EXIT_CONTRIBUTION = 0.1 * 1e8; + uint64 constant INITIAL_EXPANSION_RATE = uint64(288617289022312); // == ~10% per year (assuming daily expansion) + uint32 constant INITIAL_EXPANSION_FREQUENCY = uint32(1 days); // Daily expansion + + // Tokens + IStableTokenV2 reserveToken; + IGoodDollar goodDollarToken; + + // Contracts + IReserve public goodDollarReserve; + GoodDollarExchangeProvider goodDollarExchangeProvider; + GoodDollarExpansionController expansionController; + + IBancorExchangeProvider.PoolExchange public poolExchange; + bytes32 exchangeId; + + constructor(uint256 _chainId) BaseForkTest(_chainId) {} + + /* ======================================== */ + /* ================ Set Up ================ */ + /* ======================================== */ + + function setUp() public virtual override { + super.setUp(); + // Tokens + reserveToken = IStableTokenV2(CUSD_ADDRESS); + goodDollarToken = IGoodDollar(GOOD_DOLLAR_ADDRESS); + + // Contracts + goodDollarExchangeProvider = new GoodDollarExchangeProvider(false); + expansionController = new GoodDollarExpansionController(false); + // deployCode() hack to deploy solidity v0.5 reserve contract from a v0.8 contract + goodDollarReserve = IReserve(deployCode("Reserve", abi.encode(true))); + + // Addresses + ownerAddress = makeAddr("owner"); + + // Initialize GoodDollarExchangeProvider + configureReserve(); + configureBroker(); + configureGoodDollarExchangeProvider(); + configureTokens(); + configureExpansionController(); + configureTradingLimits(); + } + + function configureReserve() public { + bytes32[] memory initialAssetAllocationSymbols = new bytes32[](2); + initialAssetAllocationSymbols[0] = bytes32("cGLD"); + initialAssetAllocationSymbols[1] = bytes32("cUSD"); + + uint256[] memory initialAssetAllocationWeights = new uint256[](2); + initialAssetAllocationWeights[0] = FixidityLib.newFixedFraction(1, 2).unwrap(); + initialAssetAllocationWeights[1] = FixidityLib.newFixedFraction(1, 2).unwrap(); + + uint256 tobinTax = FixidityLib.newFixedFraction(5, 1000).unwrap(); + uint256 tobinTaxReserveRatio = FixidityLib.newFixedFraction(2, 1).unwrap(); + + address[] memory collateralAssets = new address[](1); + collateralAssets[0] = address(reserveToken); + + uint256[] memory collateralAssetDailySpendingRatios = new uint256[](1); + collateralAssetDailySpendingRatios[0] = 1e24; + + vm.startPrank(ownerAddress); + goodDollarReserve.initialize({ + registryAddress: REGISTRY_ADDRESS, + _tobinTaxStalenessThreshold: 600, // deprecated + _spendingRatioForCelo: 1000000000000000000000000, + _frozenGold: 0, + _frozenDays: 0, + _assetAllocationSymbols: initialAssetAllocationSymbols, + _assetAllocationWeights: initialAssetAllocationWeights, + _tobinTax: tobinTax, + _tobinTaxReserveRatio: tobinTaxReserveRatio, + _collateralAssets: collateralAssets, + _collateralAssetDailySpendingRatios: collateralAssetDailySpendingRatios + }); + + goodDollarReserve.addToken(address(goodDollarToken)); + goodDollarReserve.addExchangeSpender(address(broker)); + vm.stopPrank(); + require( + goodDollarReserve.isStableAsset(address(goodDollarToken)), + "GoodDollar is not a stable token in the reserve" + ); + require( + goodDollarReserve.isCollateralAsset(address(reserveToken)), + "ReserveToken is not a collateral asset in the reserve" + ); + } + + function configureTokens() public { + vm.startPrank(AVATAR_ADDRESS); + goodDollarToken.addMinter(address(broker)); + goodDollarToken.addMinter(address(expansionController)); + vm.stopPrank(); + + deal({ token: address(reserveToken), to: address(goodDollarReserve), give: INITIAL_RESERVE_BALANCE }); + + uint256 initialReserveGoodDollarBalanceInWei = (INITIAL_RESERVE_BALANCE / + goodDollarExchangeProvider.currentPrice(exchangeId)) * 1e18; + mintGoodDollar({ amount: initialReserveGoodDollarBalanceInWei, to: address(goodDollarReserve) }); + } + + function configureBroker() public { + vm.prank(Broker(address(broker)).owner()); + broker.addExchangeProvider(address(goodDollarExchangeProvider), address(goodDollarReserve)); + + require( + broker.isExchangeProvider(address(goodDollarExchangeProvider)), + "ExchangeProvider is not registered in the broker" + ); + require( + Broker(address(broker)).exchangeReserve(address(goodDollarExchangeProvider)) == address(goodDollarReserve), + "Reserve is not registered in the broker" + ); + } + + function configureGoodDollarExchangeProvider() public { + vm.prank(ownerAddress); + goodDollarExchangeProvider.initialize( + address(broker), + address(goodDollarReserve), + address(expansionController), + AVATAR_ADDRESS + ); + + poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(goodDollarToken), + tokenSupply: INITIAL_GOOD_DOLLAR_TOKEN_SUPPLY, + reserveBalance: INITIAL_RESERVE_BALANCE, + reserveRatio: INITIAL_RESERVE_RATIO, + exitContribution: INITIAL_EXIT_CONTRIBUTION + }); + + vm.prank(AVATAR_ADDRESS); + exchangeId = IBancorExchangeProvider(address(goodDollarExchangeProvider)).createExchange(poolExchange); + } + + function configureExpansionController() public { + vm.prank(ownerAddress); + expansionController.initialize({ + _goodDollarExchangeProvider: address(goodDollarExchangeProvider), + _distributionHelper: distributionHelperAddress, + _reserve: address(goodDollarReserve), + _avatar: AVATAR_ADDRESS + }); + + vm.prank(AVATAR_ADDRESS); + expansionController.setExpansionConfig({ + exchangeId: exchangeId, + expansionRate: INITIAL_EXPANSION_RATE, + expansionFrequency: INITIAL_EXPANSION_FREQUENCY + }); + } + + function configureTradingLimits() internal { + ITradingLimits.Config memory config = ITradingLimits.Config({ + // No more than 5,000 cUSD outflow within 5 minutes + timestep0: 300, + limit0: 5_000, + // No more than 50,000 cUSD outflow within 1 day + timestep1: 86_400, + limit1: 50_000, + // No more than 100,000 cUSD outflow in total + limitGlobal: 100_000, + flags: 1 | 2 | 4 // L0 = 1, L1 = 2, LG = 4 + }); + + vm.prank(Broker(address(broker)).owner()); + broker.configureTradingLimit(exchangeId, address(reserveToken), config); + } + + /** + * @notice Manual deal helper because foundry's vm.deal() crashes + * on the GoodDollar contract with "panic: assertion failed (0x01)" + */ + function mintGoodDollar(uint256 amount, address to) public { + vm.prank(AVATAR_ADDRESS); + goodDollarToken.mint(to, amount); + } + + function logHeader() internal view { + string memory ticker = string( + abi.encodePacked(IERC20(address(reserveToken)).symbol(), "/", IERC20(address(goodDollarToken)).symbol()) + ); + console.log("========================================"); + console.log(unicode"🔦 Testing pair:", ticker); + console.log("========================================"); + } + + function getTradingLimitId(address tokenAddress) public view returns (bytes32 limitId) { + bytes32 tokenInBytes32 = bytes32(uint256(uint160(tokenAddress))); + bytes32 _limitId = exchangeId ^ tokenInBytes32; + return _limitId; + } + + function getTradingLimitsConfig(address tokenAddress) public view returns (ITradingLimits.Config memory config) { + bytes32 limitId = getTradingLimitId(tokenAddress); + ITradingLimits.Config memory _config; + (_config.timestep0, _config.timestep1, _config.limit0, _config.limit1, _config.limitGlobal, _config.flags) = Broker( + address(broker) + ).tradingLimitsConfig(limitId); + + return _config; + } + + function getTradingLimitsState(address tokenAddress) public view returns (ITradingLimits.State memory state) { + bytes32 limitId = getTradingLimitId(tokenAddress); + ITradingLimits.State memory _state; + (_state.lastUpdated0, _state.lastUpdated1, _state.netflow0, _state.netflow1, _state.netflowGlobal) = Broker( + address(broker) + ).tradingLimitsState(limitId); + + return _state; + } + + function getRefreshedTradingLimitsState( + address tokenAddress + ) public view returns (ITradingLimits.State memory state) { + ITradingLimits.Config memory config = getTradingLimitsConfig(tokenAddress); + // Netflow might be outdated because of a skip(...) call. + // By doing an update(-1) and then update(1 ) we refresh the state without changing the state. + // The reason we can't just update(0) is that 0 would be cast to -1 in the update function. + state = tradingLimits.update(getTradingLimitsState(tokenAddress), config, -2, 1); + state = tradingLimits.update(state, config, 1, 0); + } + + function maxOutflow(address tokenAddress) internal view returns (int48) { + ITradingLimits.Config memory config = getTradingLimitsConfig(tokenAddress); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(tokenAddress); + int48 maxOutflowL0 = config.limit0 + state.netflow0; + int48 maxOutflowL1 = config.limit1 + state.netflow1; + int48 maxOutflowLG = config.limitGlobal + state.netflowGlobal; + + if (config.flags == L0 | L1 | LG) { + return min(maxOutflowL0, maxOutflowL1, maxOutflowLG); + } else if (config.flags == L0 | LG) { + return min(maxOutflowL0, maxOutflowLG); + } else if (config.flags == L0 | L1) { + return min(maxOutflowL0, maxOutflowL1); + } else if (config.flags == L0) { + return maxOutflowL0; + } else { + revert("Unexpected limit config"); + } + } + + function test_init_isDeployedAndInitializedCorrectly() public view { + assertEq(goodDollarExchangeProvider.owner(), ownerAddress); + assertEq(goodDollarExchangeProvider.broker(), address(broker)); + assertEq(address(goodDollarExchangeProvider.reserve()), address(goodDollarReserve)); + + IBancorExchangeProvider.PoolExchange memory _poolExchange = goodDollarExchangeProvider.getPoolExchange(exchangeId); + assertEq(_poolExchange.reserveAsset, _poolExchange.reserveAsset); + assertEq(_poolExchange.tokenAddress, _poolExchange.tokenAddress); + assertEq(_poolExchange.tokenSupply, _poolExchange.tokenSupply); + assertEq(_poolExchange.reserveBalance, _poolExchange.reserveBalance); + assertEq(_poolExchange.reserveRatio, _poolExchange.reserveRatio); + assertEq(_poolExchange.exitContribution, _poolExchange.exitContribution); + } +} diff --git a/test/fork/GoodDollar/SwapForkTest.sol b/test/fork/GoodDollar/SwapForkTest.sol new file mode 100644 index 00000000..b8f36b68 --- /dev/null +++ b/test/fork/GoodDollar/SwapForkTest.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; + +// Interfaces +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + +// Contracts +import { GoodDollarBaseForkTest } from "./GoodDollarBaseForkTest.sol"; + +contract GoodDollarSwapForkTest is GoodDollarBaseForkTest { + using TradingLimitHelpers for *; + using TokenHelpers for *; + + constructor(uint256 _chainId) GoodDollarBaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + } + + function test_swapIn_reserveTokenToGoodDollar() public { + uint256 amountIn = 1000 * 1e18; + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + + // Calculate the expected amount of G$ to receive for `amountIn` cUSD + uint256 expectedAmountOut = broker.getAmountOut( + address(goodDollarExchangeProvider), + exchangeId, + address(reserveToken), + address(goodDollarToken), + amountIn + ); + + // Give trader required amount of cUSD to swap + deal({ token: address(reserveToken), to: trader, give: amountIn }); + + vm.startPrank(trader); + // Trader approves the broker to spend their cUSD + reserveToken.approve({ spender: address(broker), amount: amountIn }); + + // Broker swaps `amountIn` of trader's cUSD for G$ + broker.swapIn({ + exchangeProvider: address(goodDollarExchangeProvider), + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(goodDollarToken), + amountIn: amountIn, + amountOutMin: expectedAmountOut + }); + vm.stopPrank(); + + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(expectedAmountOut, goodDollarToken.balanceOf(trader)); + assertEq(reserveBalanceBefore + amountIn, reserveBalanceAfter); + assertTrue(priceBefore < priceAfter); + } + + function test_swapIn_goodDollarToReserveToken() public { + uint256 amountIn = 1000 * 1e18; + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 expectedAmountOut = broker.getAmountOut( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountIn + ); + + mintGoodDollar(amountIn, trader); + + vm.startPrank(trader); + goodDollarToken.approve(address(broker), amountIn); + broker.swapIn( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountIn, + expectedAmountOut + ); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(expectedAmountOut, reserveToken.balanceOf(trader)); + assertEq(reserveBalanceBefore - expectedAmountOut, reserveBalanceAfter); + assertTrue(priceAfter < priceBefore); + } + + function test_swapOut_reserveTokenToGoodDollar() public { + uint256 amountOut = 1000 * 1e18; + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 expectedAmountIn = broker.getAmountIn( + address(goodDollarExchangeProvider), + exchangeId, + address(reserveToken), + address(goodDollarToken), + amountOut + ); + + deal(address(reserveToken), trader, expectedAmountIn); + + vm.startPrank(trader); + reserveToken.approve(address(broker), expectedAmountIn); + broker.swapOut( + address(goodDollarExchangeProvider), + exchangeId, + address(reserveToken), + address(goodDollarToken), + amountOut, + expectedAmountIn + ); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(amountOut, goodDollarToken.balanceOf(trader)); + assertEq(reserveBalanceBefore + expectedAmountIn, reserveBalanceAfter); + assertTrue(priceBefore < priceAfter); + } + + function test_swapOut_goodDollarToReserveToken() public { + uint256 amountOut = 1000 * 1e18; + + uint256 reserveBalanceBefore = reserveToken.balanceOf(address(goodDollarReserve)); + uint256 priceBefore = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 expectedAmountIn = broker.getAmountIn( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountOut + ); + + mintGoodDollar(expectedAmountIn, trader); + + vm.startPrank(trader); + goodDollarToken.approve(address(broker), expectedAmountIn); + broker.swapOut( + address(goodDollarExchangeProvider), + exchangeId, + address(goodDollarToken), + address(reserveToken), + amountOut, + expectedAmountIn + ); + uint256 priceAfter = IBancorExchangeProvider(address(goodDollarExchangeProvider)).currentPrice(exchangeId); + uint256 reserveBalanceAfter = reserveToken.balanceOf(address(goodDollarReserve)); + + assertEq(amountOut, reserveToken.balanceOf(trader)); + assertEq(reserveBalanceBefore - amountOut, reserveBalanceAfter); + assertTrue(priceAfter < priceBefore); + } +} diff --git a/test/fork/GoodDollar/TradingLimitsForkTest.sol b/test/fork/GoodDollar/TradingLimitsForkTest.sol new file mode 100644 index 00000000..ac8865b6 --- /dev/null +++ b/test/fork/GoodDollar/TradingLimitsForkTest.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8; + +// Libraries +import { console } from "forge-std/console.sol"; +import { L0, L1, LG } from "../helpers/misc.sol"; +import { TokenHelpers } from "../helpers/TokenHelpers.sol"; +import { TradingLimitHelpers } from "../helpers/TradingLimitHelpers.sol"; + +// Interfaces +import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; + +// Contracts +import { GoodDollarBaseForkTest } from "./GoodDollarBaseForkTest.sol"; + +contract GoodDollarTradingLimitsForkTest is GoodDollarBaseForkTest { + using TradingLimitHelpers for *; + using TokenHelpers for *; + + constructor(uint256 _chainId) GoodDollarBaseForkTest(_chainId) {} + + function setUp() public override { + super.setUp(); + } + + function test_tradingLimitsAreConfiguredForReserveToken() public view { + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + bool reserveAssetLimitConfigured = config.flags > uint8(0); + require(reserveAssetLimitConfigured, "Limit not configured"); + } + + function test_tradingLimitsAreEnforced_reserveTokenOutflowLimit0() public { + _swapUntilReserveTokenLimit0_onOutflow(); + _swapGoodDollarForReserveToken(bytes(L0.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenOutflowLimit1() public { + _swapUntilReserveTokenLimit1_onOutflow(); + _swapGoodDollarForReserveToken(bytes(L1.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenOutflowLimitGlobal() public { + _swapUntilReserveTokenGlobalLimit_onOutflow(); + _swapGoodDollarForReserveToken(bytes(LG.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenInflowLimit0() public { + _swapUntilReserveTokenLimit0_onInflow(); + _swapReserveTokenForGoodDollar(bytes(L0.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenInflowLimit1() public { + _swapUntilReserveTokenLimit1_onInflow(); + _swapReserveTokenForGoodDollar(bytes(L1.revertReason())); + } + + function test_tradingLimitsAreEnforced_reserveTokenInflowGlobalLimit() public { + _swapUntilReserveTokenGlobalLimit_onInflow(); + _swapReserveTokenForGoodDollar(bytes(LG.revertReason())); + } + + /** + * @notice Swaps G$ for cUSD with the maximum amount allowed per swap + * @param revertReason An optional revert reason to expect, if swap should revert. + * @dev Pass an empty string when not expecting a revert. + */ + function _swapGoodDollarForReserveToken(bytes memory revertReason) internal { + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + + // Get the max amount we can swap in a single transaction before we hit L0 + uint256 maxPerSwapInWei = uint256(uint48(config.limit0)) * 1e18; + uint256 inflowRequiredForAmountOut = goodDollarExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(goodDollarToken), + tokenOut: address(reserveToken), + amountOut: maxPerSwapInWei + }); + + mintGoodDollar(inflowRequiredForAmountOut, trader); + + vm.startPrank(trader); + goodDollarToken.approve(address(broker), inflowRequiredForAmountOut); + + // If a revertReason was provided, expect a revert with that reason + if (revertReason.length > 0) { + vm.expectRevert(revertReason); + } + broker.swapOut({ + exchangeProvider: address(goodDollarExchangeProvider), + exchangeId: exchangeId, + tokenIn: address(goodDollarToken), + tokenOut: address(reserveToken), + amountOut: maxPerSwapInWei, + amountInMax: type(uint256).max + }); + vm.stopPrank(); + } + + function _swapUntilReserveTokenLimit0_onOutflow() internal { + _swapGoodDollarForReserveToken({ revertReason: "" }); + } + + function _swapUntilReserveTokenLimit1_onOutflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until L1=%d on outflow", block.timestamp, uint48(config.limit1)); + + // Get the max amount we can swap in a single transaction before we hit L0 + int48 maxPerSwap = config.limit0; + + // Swap until right before we would hit the L1 limit. + // We swap in `maxPerSwap` increments and timewarp + // by `timestep0 + 1` seconds so we avoid hitting L0. + uint256 i; + while (state.netflow1 - maxPerSwap >= -1 * config.limit1) { + skip(config.timestep0 + 1); + // Check that there's still outflow to trade as sometimes we hit LG while + // still having a bit of L1 left, which causes an infinite loop. + if (maxOutflow(address(reserveToken)) == 0) { + break; + } + + _swapUntilReserveTokenLimit0_onOutflow(); + + config = getTradingLimitsConfig(address(reserveToken)); + state = getTradingLimitsState(address(reserveToken)); + + i++; + require(i <= 10, "possible infinite loopL more than 10 iterations"); + } + skip(config.timestep0 + 1); + } + + function _swapUntilReserveTokenGlobalLimit_onOutflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until LG=%d on outflow", block.timestamp, uint48(config.limitGlobal)); + + int48 maxPerSwap = config.limit0; + uint256 i; + while (state.netflowGlobal - maxPerSwap >= config.limitGlobal * -1) { + skip(config.timestep1 + 1); + _swapUntilReserveTokenLimit1_onOutflow(); + state = getRefreshedTradingLimitsState(address(reserveToken)); + i++; + require(i <= 50, "possible infinite loop: more than 50 iterations"); + } + skip(config.timestep1 + 1); + } + + /** + * @notice Swaps cUSD for G$ with the maximum amount allowed per swap + * @param revertReason An optional revert reason to expect, if swap should revert. + * @dev Pass an empty string when not expecting a revert. + */ + function _swapReserveTokenForGoodDollar(bytes memory revertReason) internal { + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + + // Get the max amount we can swap in a single transaction before we hit L0 + uint256 maxPerSwapInWei = uint256(uint48(config.limit0)) * 1e18; + deal({ token: address(reserveToken), to: trader, give: maxPerSwapInWei }); + + vm.startPrank(trader); + reserveToken.approve(address(broker), maxPerSwapInWei); + + // If a revertReason was provided, expect a revert with that reason + if (revertReason.length > 0) { + vm.expectRevert(revertReason); + } + broker.swapIn({ + exchangeProvider: address(goodDollarExchangeProvider), + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(goodDollarToken), + amountIn: maxPerSwapInWei, + amountOutMin: 0 + }); + vm.stopPrank(); + } + + function _swapUntilReserveTokenLimit0_onInflow() internal { + _swapReserveTokenForGoodDollar({ revertReason: "" }); + } + + function _swapUntilReserveTokenLimit1_onInflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until L1=%d on inflow", block.timestamp, uint48(config.limit1)); + + // Get the max amount we can swap in a single transaction before we hit L0 + int48 maxPerSwap = config.limit0; + + // Swap until right before we would hit the L1 limit. + // We swap in `maxPerSwap` increments and timewarp + // by `timestep0 + 1` seconds so we avoid hitting L0. + while (state.netflow1 + maxPerSwap <= config.limit1) { + skip(config.timestep0 + 1); + _swapUntilReserveTokenLimit0_onInflow(); + config = getTradingLimitsConfig(address(reserveToken)); + state = getTradingLimitsState(address(reserveToken)); + + if (state.netflowGlobal == config.limitGlobal) { + console.log(unicode"🚨 LG reached during L1 inflow"); + break; + } + } + skip(config.timestep0 + 1); + } + + function _swapUntilReserveTokenGlobalLimit_onInflow() internal { + // Get the trading limits config and state for the reserve token + ITradingLimits.Config memory config = getTradingLimitsConfig(address(reserveToken)); + ITradingLimits.State memory state = getRefreshedTradingLimitsState(address(reserveToken)); + console.log(unicode"🏷️ [%d] Swap until LG=%d on inflow", block.timestamp, uint48(config.limitGlobal)); + + int48 maxPerSwap = config.limit0; + uint256 i; + while (state.netflowGlobal + maxPerSwap <= config.limitGlobal) { + skip(config.timestep1 + 1); + _swapUntilReserveTokenLimit1_onInflow(); + state = getRefreshedTradingLimitsState(address(reserveToken)); + i++; + require(i <= 50, "possible infinite loop: more than 50 iterations"); + } + skip(config.timestep1 + 1); + } +} diff --git a/test/fork/actions/SwapActions.sol b/test/fork/actions/SwapActions.sol index ce4160cb..9225249e 100644 --- a/test/fork/actions/SwapActions.sol +++ b/test/fork/actions/SwapActions.sol @@ -252,7 +252,7 @@ contract SwapActions is StdCheats { /* * from -> LG[to] * This function will do valid swaps until just before LG is hit - * during outflow on `to`, therfore we check the negative end + * during outflow on `to`, therefore we check the negative end * of the limit because `to` flows out of the reserve. */ ITradingLimits.Config memory limitConfig = ctx.tradingLimitsConfig(to); @@ -265,7 +265,7 @@ contract SwapActions is StdCheats { skip(limitConfig.timestep1 + 1); swapUntilL1_onOutflow(from, to); limitConfig = ctx.tradingLimitsConfig(to); - // Triger an update to reset netflows + // Trigger an update to reset netflows limitState = ctx.tradingLimitsState(to); } skip(limitConfig.timestep1 + 1); @@ -275,7 +275,7 @@ contract SwapActions is StdCheats { skip(limitConfig.timestep0 + 1); swapUntilL0_onOutflow(from, to); limitConfig = ctx.tradingLimitsConfig(to); - // Triger an update to reset netflows + // Trigger an update to reset netflows limitState = ctx.tradingLimitsState(to); } skip(limitConfig.timestep0 + 1); diff --git a/test/fork/assertions/SwapAssertions.sol b/test/fork/assertions/SwapAssertions.sol index 2108fbba..4e29a1e6 100644 --- a/test/fork/assertions/SwapAssertions.sol +++ b/test/fork/assertions/SwapAssertions.sol @@ -38,10 +38,10 @@ contract SwapAssertions is StdAssertions, Actions { ctx.logLimits(from); ctx.logLimits(to); if (ctx.atInflowLimit(from, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global inflow limit is reached on %s", from); + console.log(unicode"🚨 Cannot test swap because the global inflow limit is reached on %s", from); return; } else if (ctx.atOutflowLimit(to, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global outflow limit is reached on %s", to); + console.log(unicode"🚨 Cannot test swap because the global outflow limit is reached on %s", to); return; } @@ -74,10 +74,10 @@ contract SwapAssertions is StdAssertions, Actions { ctx.logLimits(from); ctx.logLimits(to); if (ctx.atInflowLimit(from, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global inflow limit is reached on %s", from); + console.log(unicode"🚨 Cannot test swap because the global inflow limit is reached on %s", from); return; } else if (ctx.atOutflowLimit(to, LG)) { - console.log(unicode"🚨 Cannot test swap beacause the global outflow limit is reached on %s", to); + console.log(unicode"🚨 Cannot test swap because the global outflow limit is reached on %s", to); return; } diff --git a/test/fork/helpers/TradingLimitHelpers.sol b/test/fork/helpers/TradingLimitHelpers.sol index 5f47adb4..cc108b55 100644 --- a/test/fork/helpers/TradingLimitHelpers.sol +++ b/test/fork/helpers/TradingLimitHelpers.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8; import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; +import { Broker } from "contracts/swap/Broker.sol"; import { ExchangeForkTest } from "../ExchangeForkTest.sol"; import { OracleHelpers } from "./OracleHelpers.sol"; @@ -14,7 +15,15 @@ library TradingLimitHelpers { using OracleHelpers for *; function isLimitConfigured(ExchangeForkTest ctx, bytes32 limitId) public view returns (bool) { - ITradingLimits.Config memory limitConfig = ctx.broker().tradingLimitsConfig(limitId); + ITradingLimits.Config memory limitConfig; + ( + limitConfig.timestep0, + limitConfig.timestep1, + limitConfig.limit0, + limitConfig.limit1, + limitConfig.limitGlobal, + limitConfig.flags + ) = Broker(address(ctx.broker())).tradingLimitsConfig(limitId); return limitConfig.flags > uint8(0); } @@ -22,21 +31,59 @@ library TradingLimitHelpers { ExchangeForkTest ctx, bytes32 limitId ) public view returns (ITradingLimits.Config memory) { - return ctx.broker().tradingLimitsConfig(limitId); + ITradingLimits.Config memory limitConfig; + ( + limitConfig.timestep0, + limitConfig.timestep1, + limitConfig.limit0, + limitConfig.limit1, + limitConfig.limitGlobal, + limitConfig.flags + ) = Broker(address(ctx.broker())).tradingLimitsConfig(limitId); + + return limitConfig; } function tradingLimitsState(ExchangeForkTest ctx, bytes32 limitId) public view returns (ITradingLimits.State memory) { - return ctx.broker().tradingLimitsState(limitId); + ITradingLimits.State memory limitState; + ( + limitState.lastUpdated0, + limitState.lastUpdated1, + limitState.netflow0, + limitState.netflow1, + limitState.netflowGlobal + ) = Broker(address(ctx.broker())).tradingLimitsState(limitId); + return limitState; } function tradingLimitsConfig(ExchangeForkTest ctx, address asset) public view returns (ITradingLimits.Config memory) { + ITradingLimits.Config memory limitConfig; bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.broker().tradingLimitsConfig(ctx.exchangeId() ^ assetBytes32); + bytes32 limitId = ctx.exchangeId() ^ assetBytes32; + + ( + limitConfig.timestep0, + limitConfig.timestep1, + limitConfig.limit0, + limitConfig.limit1, + limitConfig.limitGlobal, + limitConfig.flags + ) = Broker(address(ctx.broker())).tradingLimitsConfig(limitId); + return limitConfig; } function tradingLimitsState(ExchangeForkTest ctx, address asset) public view returns (ITradingLimits.State memory) { + ITradingLimits.State memory limitState; bytes32 assetBytes32 = bytes32(uint256(uint160(asset))); - return ctx.broker().tradingLimitsState(ctx.exchangeId() ^ assetBytes32); + bytes32 limitId = ctx.exchangeId() ^ assetBytes32; + ( + limitState.lastUpdated0, + limitState.lastUpdated1, + limitState.netflow0, + limitState.netflow1, + limitState.netflowGlobal + ) = Broker(address(ctx.broker())).tradingLimitsState(limitId); + return limitState; } function refreshedTradingLimitsState( @@ -44,13 +91,11 @@ library TradingLimitHelpers { address asset ) public view returns (ITradingLimits.State memory state) { ITradingLimits.Config memory config = tradingLimitsConfig(ctx, asset); - // Netflow might be outdated because of a skip(...) call and doing - // an update(0) would reset the netflow if enough time has passed. - state = ctx.tradingLimits().update(tradingLimitsState(ctx, asset), config, 0, 0); - // XXX: There's a bug in our current TradingLimits library implementation where - // an update with 0 netflow will round to 1. So we do another update with -1 netflow - // to get it back to the actual value. - state = ctx.tradingLimits().update(state, config, -1, 0); + // Netflow might be outdated because of a skip(...) call. + // By doing an update(-1) and then update(1 ) we refresh the state without changing the state. + // The reason we can't just update(0) is that 0 would be cast to -1 in the update function. + state = ctx.tradingLimits().update(tradingLimitsState(ctx, asset), config, -1, 1); + state = ctx.tradingLimits().update(state, config, 1, 0); } function isLimitEnabled(ITradingLimits.Config memory config, uint8 limit) internal pure returns (bool) { diff --git a/test/integration/protocol/ProtocolTest.sol b/test/integration/protocol/ProtocolTest.sol index 04058559..668eda84 100644 --- a/test/integration/protocol/ProtocolTest.sol +++ b/test/integration/protocol/ProtocolTest.sol @@ -360,7 +360,10 @@ contract ProtocolTest is Test, WithRegistry { address[] memory exchangeProviders = new address[](1); exchangeProviders[0] = address(biPoolManager); - broker.initialize(exchangeProviders, address(reserve)); + address[] memory reserves = new address[](1); + reserves[0] = address(reserve); + + broker.initialize(exchangeProviders, reserves); registry.setAddressFor("Broker", address(broker)); reserve.addExchangeSpender(address(broker)); biPoolManager.setPricingModules(pricingModuleIdentifiers, pricingModules); diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol new file mode 100644 index 00000000..e3d19b53 --- /dev/null +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -0,0 +1,1693 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility, max-line-length +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase + +import { Test } from "forge-std/Test.sol"; +import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; +import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; + +contract BancorExchangeProviderTest is Test { + /* ------- Events from IBancorExchangeProvider ------- */ + + event BrokerUpdated(address indexed newBroker); + + event ReserveUpdated(address indexed newReserve); + + event PowerUpdated(address indexed newPower); + + event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + event ExchangeDestroyed(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); + + /* ------------------------------------------- */ + + ERC20 public reserveToken; + ERC20 public token; + ERC20 public token2; + + address public reserveAddress; + address public brokerAddress; + IBancorExchangeProvider.PoolExchange public poolExchange1; + IBancorExchangeProvider.PoolExchange public poolExchange2; + + function setUp() public virtual { + reserveToken = new ERC20("cUSD", "cUSD"); + token = new ERC20("Good$", "G$"); + token2 = new ERC20("Good2$", "G2$"); + + brokerAddress = makeAddr("Broker"); + reserveAddress = makeAddr("Reserve"); + + poolExchange1 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange2 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token2), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(true) + ); + } + + function initializeBancorExchangeProvider() internal returns (BancorExchangeProvider) { + BancorExchangeProvider bancorExchangeProvider = new BancorExchangeProvider(false); + + bancorExchangeProvider.initialize(brokerAddress, reserveAddress); + return bancorExchangeProvider; + } +} + +contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + /* ---------- Initializer ---------- */ + function test_initialize_shouldSetOwner() public view { + assertEq(bancorExchangeProvider.owner(), address(this)); + } + + function test_initialize_shouldSetBroker() public view { + assertEq(bancorExchangeProvider.broker(), brokerAddress); + } + + function test_initialize_shouldSetReserve() public view { + assertEq(address(bancorExchangeProvider.reserve()), reserveAddress); + } + + /* ---------- Setters ---------- */ + function test_setBroker_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.setBroker(makeAddr("NewBroker")); + } + + function test_setBroker_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Broker address must be set"); + bancorExchangeProvider.setBroker(address(0)); + } + + function test_setBroker_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newBroker = makeAddr("NewBroker"); + + vm.expectEmit(true, true, true, true); + emit BrokerUpdated(newBroker); + bancorExchangeProvider.setBroker(newBroker); + + assertEq(bancorExchangeProvider.broker(), newBroker); + } + + function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.setReserve(makeAddr("NewReserve")); + } + + function test_setReserve_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Reserve address must be set"); + bancorExchangeProvider.setReserve(address(0)); + } + + function test_setReserve_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newReserve = makeAddr("NewReserve"); + + vm.expectEmit(true, true, true, true); + emit ReserveUpdated(newReserve); + bancorExchangeProvider.setReserve(newReserve); + + assertEq(address(bancorExchangeProvider.reserve()), newReserve); + } + + function test_setExitContribution_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bytes32 exchangeId = "0xexchangeId"; + bancorExchangeProvider.setExitContribution(exchangeId, 1e5); + } + + function test_setExitContribution_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.setExitContribution(exchangeId, 1e5); + } + + function test_setExitContribution_whenExitContributionAbove100Percent_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); + vm.expectRevert("Exit contribution is too high"); + bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); + } + + function test_setExitContribution_whenSenderIsOwner_shouldUpdateAndEmit() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint32 newExitContribution = 1e3; + vm.expectEmit(true, true, true, true); + emit ExitContributionSet(exchangeId, newExitContribution); + bancorExchangeProvider.setExitContribution(exchangeId, newExitContribution); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.exitContribution, newExitContribution); + } + + /* ---------- Getters ---------- */ + + function test_getPoolExchange_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.getPoolExchange(exchangeId); + } + + function test_getPoolExchange_whenPoolExists_shouldReturnPool() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.reserveAsset, poolExchange1.reserveAsset); + assertEq(poolExchange.tokenAddress, poolExchange1.tokenAddress); + assertEq(poolExchange.tokenSupply, poolExchange1.tokenSupply); + assertEq(poolExchange.reserveBalance, poolExchange1.reserveBalance); + assertEq(poolExchange.reserveRatio, poolExchange1.reserveRatio); + assertEq(poolExchange.exitContribution, poolExchange1.exitContribution); + } + + function test_getExchangeIds_whenNoExchanges_shouldReturnEmptyArray() public view { + bytes32[] memory exchangeIds = bancorExchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 0); + } + + function test_getExchangeIds_whenExchangesExist_shouldReturnExchangeIds() public { + bytes32 exchangeId1 = bancorExchangeProvider.createExchange(poolExchange1); + + bytes32[] memory exchangeIds = bancorExchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 1); + assertEq(exchangeIds[0], exchangeId1); + } + + function test_getExchanges_whenNoExchanges_shouldReturnEmptyArray() public view { + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 0); + } + + function test_getExchanges_whenExchangesExist_shouldReturnExchange() public { + bytes32 exchangeId1 = bancorExchangeProvider.createExchange(poolExchange1); + + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId1); + assertEq(exchanges[0].assets[0], poolExchange1.reserveAsset); + assertEq(exchanges[0].assets[1], poolExchange1.tokenAddress); + } +} + +contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_createExchange_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveAssetIsZero_shouldRevert() public { + poolExchange1.reserveAsset = address(0); + vm.expectRevert("Invalid reserve asset"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveAssetIsNotCollateral_shouldRevert() public { + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(false) + ); + vm.expectRevert("Reserve asset must be a collateral registered with the reserve"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenTokenAddressIsZero_shouldRevert() public { + poolExchange1.tokenAddress = address(0); + vm.expectRevert("Invalid token address"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenTokenAddressIsNotStable_shouldRevert() public { + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(false) + ); + vm.expectRevert("Token must be a stable registered with the reserve"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveRatioIsSmaller2_shouldRevert() public { + poolExchange1.reserveRatio = 0; + vm.expectRevert("Reserve ratio is too low"); + bancorExchangeProvider.createExchange(poolExchange1); + poolExchange1.reserveRatio = 1; + vm.expectRevert("Reserve ratio is too low"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenReserveRatioAbove100Percent_shouldRevert() public { + poolExchange1.reserveRatio = bancorExchangeProvider.MAX_WEIGHT() + 1; + vm.expectRevert("Reserve ratio is too high"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenExitContributionAbove100Percent_shouldRevert() public { + poolExchange1.exitContribution = bancorExchangeProvider.MAX_WEIGHT() + 1; + vm.expectRevert("Exit contribution is too high"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenExchangeAlreadyExists_shouldRevert() public { + bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("Exchange already exists"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchanges_whenReserveTokenHasMoreDecimalsThan18_shouldRevert() public { + vm.mockCall(address(reserveToken), abi.encodeWithSelector(reserveToken.decimals.selector), abi.encode(19)); + vm.expectRevert("Reserve asset decimals must be <= 18"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenTokenHasMoreDecimalsThan18_shouldRevert() public { + vm.mockCall(address(token), abi.encodeWithSelector(token.decimals.selector), abi.encode(19)); + vm.expectRevert("Token decimals must be <= 18"); + bancorExchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenExchangeDoesNotExist_shouldCreateExchangeAndEmit() public { + vm.expectEmit(true, true, true, true); + bytes32 expectedExchangeId = keccak256(abi.encodePacked(reserveToken.symbol(), token.symbol())); + emit ExchangeCreated(expectedExchangeId, address(reserveToken), address(token)); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + assertEq(exchangeId, expectedExchangeId); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.reserveAsset, poolExchange1.reserveAsset); + assertEq(poolExchange.tokenAddress, poolExchange1.tokenAddress); + assertEq(poolExchange.tokenSupply, poolExchange1.tokenSupply); + assertEq(poolExchange.reserveBalance, poolExchange1.reserveBalance); + assertEq(poolExchange.reserveRatio, poolExchange1.reserveRatio); + assertEq(poolExchange.exitContribution, poolExchange1.exitContribution); + + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId); + + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(token)), 1); + } +} + +contract BancorExchangeProviderTest_destroyExchange is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_destroyExchange_whenSenderIsNotOwner_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + } + + function test_destroyExchange_whenIndexOutOfRange_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("exchangeIdIndex not in range"); + bancorExchangeProvider.destroyExchange(exchangeId, 10); + } + + function test_destroyExchange_whenExchangeIdAndIndexDontMatch_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("exchangeId at index doesn't match"); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + } + + function test_destroyExchange_whenExchangeExists_shouldDestroyExchangeAndEmit() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); + + vm.expectEmit(true, true, true, true); + emit ExchangeDestroyed(exchangeId, poolExchange1.reserveAsset, poolExchange1.tokenAddress); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + + bytes32[] memory exchangeIds = bancorExchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 1); + + IExchangeProvider.Exchange[] memory exchanges = bancorExchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId2); + } +} + +contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_getAmountIn_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInNotInExchange_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token2), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenOutNotInExchange_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token2), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInEqualsTokenOut_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInIsTokenAndTokenSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInIsTokenAndAmountOutLargerThanReserveBalance_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_AMOUNT"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: poolExchange1.reserveBalance + 1 + }); + } + + function test_getAmountIn_whenTokenInIsTokenAndAmountOutZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 0 + }); + assertEq(amountIn, 0); + } + + function test_getAmountIn_whenTokenInIsTokenAndAmountOutEqualReserveBalance_shouldReturnSupply() public { + // need to set exit contribution to 0 to make the formula work otherwise amountOut would need to be adjusted + // to be equal to reserveBalance after exit contribution is applied + poolExchange1.exitContribution = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 expectedAmountIn = poolExchange1.tokenSupply; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: poolExchange1.reserveBalance + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 12e18; + // formula: amountIn = (amountOut / (1-e)) * tokenSupply / reserveBalance + // calculation: (12 / 0.99) * 300_000 / 60_000 = 60.60606060606060606060606060606060606060 + uint256 expectedAmountIn = 60606060606060606060; + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); + bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 0 + }); + assertEq(amountIn, 0); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 12e18; + // formula: amountIn = (amountOut * reserveBalance) / supply + // calculation: (12 * 60_000) / 300_000 = 2.4 + uint256 expectedAmountIn = 1e18 * 2.4; + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAsset_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: amountIn = reserveBalance * (( (tokenSupply + amountOut) / tokenSupply) ^ (1/reserveRatio) - 1) + // calculation: 60_000 * ((300_001/300_000)^(1/0.2) - 1) ≈ 1.000006666688888926 + uint256 expectedAmountIn = 1000006666688888926; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: 1e18 + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsToken_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) + // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line + // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + + // calculation: (300000 * ( -1 + (60000 / (60000-(1/0.99)))^0.2))/(60000 / (60000-(1/0.99)))^0.2 = 1.010107812196722301 + uint256 expectedAmountIn = 1010107812196722302; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: 1e18 + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 1e12; // 0.000001 token + // formula: amountIn = reserveBalance * ((amountOut/tokenSupply + 1)^(1/reserveRatio) - 1) + // calculation: 60_000 * ((0.000001/300_000 + 1)^(1/0.2) - 1) ≈ 0.00000100000000000666666 + uint256 expectedAmountIn = 1000000000007; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 1e12; // 0.000001 token + // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) + // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line + // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + + // calculation: (300000 * ( -1 + (60000 / (60000-(0.000001/0.99)))^0.2))/(60000 / (60000-(0.000001/0.99)))^0.2 ≈ 0.000001010101010107 + uint256 expectedAmountIn = 1010101010108; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 1_000_000e18; + // formula: amountIn = reserveBalance * ((amountOut/tokenSupply + 1)^(1/reserveRatio) - 1) + // calculation: 60_000 * ((1_000_000/300_000 + 1)^(1/0.2) - 1) ≈ 91617283.9506172839506172839 + // 1 wei difference due to precision loss + uint256 expectedAmountIn = 91617283950617283950617284; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + } + + function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = 59000e18; // 59_000 since total reserve is 60k + // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) + // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line + // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + + // calculation: (300000 * ( -1 + (60000 / (60000-(59000/0.99)))^0.2))/(60000 / (60000-(59000/0.99)))^0.2 = 189649.078540006525698460 + uint256 expectedAmountIn = 189649078540006525698460; + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + // we allow up to 1% difference due to precision loss + assertApproxEqRel(amountIn, expectedAmountIn, 1e18 * 0.01); + } + + function test_getAmountIn_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount() public { + // Set exit contribution to 1% (1e6 out of 1e8) for exchange 1 and 0 for exchange 2 + // all other parameters are the same + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + bancorExchangeProvider.setExitContribution(exchangeId, 1e6); + bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); + bancorExchangeProvider.setExitContribution(exchangeId2, 0); + + uint256 amountOut = 116e18; + // formula: amountIn = (tokenSupply * (( (amountOut + reserveBalance) / reserveBalance) ^ (reserveRatio) - 1)) / exitContribution + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // exit contribution is 1% + uint256 amountOut2 = (amountOut * 100) / 99; + assertTrue(amountOut < amountOut2); + + uint256 amountIn2 = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId2, + tokenIn: address(token2), + tokenOut: address(reserveToken), + amountOut: amountOut2 + }); + assertEq(amountIn, amountIn2); + } + + function test_getAmountIn_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { + // Create new tokens with different decimals + ERC20 reserveToken6 = new ERC20("Reserve6", "RSV6"); + ERC20 stableToken18 = new ERC20("Stable18", "STB18"); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(stableToken18)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6)), + abi.encode(true) + ); + + // Mock decimals for these tokens + vm.mockCall(address(reserveToken6), abi.encodeWithSelector(reserveToken6.decimals.selector), abi.encode(6)); + vm.mockCall(address(stableToken18), abi.encodeWithSelector(stableToken18.decimals.selector), abi.encode(18)); + + IBancorExchangeProvider.PoolExchange memory newPoolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6), + tokenAddress: address(stableToken18), + tokenSupply: 100_000 * 1e18, // 100,000 + reserveBalance: 50_000 * 1e18, // 50,000 + reserveRatio: 1e8 * 0.5, // 50% + exitContribution: 0 + }); + + bytes32 newExchangeId = bancorExchangeProvider.createExchange(newPoolExchange); + + uint256 amountOut = 1e18; // 1 StableToken out + + // Formula: reserveBalance * ((amountOut/tokenSupply + 1) ^ (1/reserveRatio) - 1) + // calculation: 50_000 * ((1/100_000 + 1) ^ (1/0.5) - 1) = 1.000005 in 6 decimals = 1000005 + uint256 expectedAmountIn = 1000005; + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountOut: amountOut + }); + assertEq(amountIn, expectedAmountIn); + // 100_000 * ((1 + 1.000005/50000)^0.5 - 1) = 1.000005 in 18 decimals = 1000005000000000000 + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountIn: amountIn + }); + // we allow a 10 wei difference due to rounding errors + assertApproxEqAbs(amountOut, reversedAmountOut, 10); + } + + function test_getAmountIn_whenTokenInIsReserveAsset_fuzz(uint256 amountOut) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountOut range between 1 and 10_000_000 tokens + amountOut = bound(amountOut, 1e18, 10_000_000 * 1e18); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + // Verify the reverse swap + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); + } + + function test_getAmountIn_whenTokenInIsToken_fuzz(uint256 amountOut) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // reserve balance is 200_000 and you can't get more than 90% of it because of the exit contribution + amountOut = bound(amountOut, 1e18, (200_000 * 1e18 * 90) / 100); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(0 < amountIn, "Amount in should be positive"); + + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); + } + + function test_getAmountIn_whenTokenInIsToken_fullFuzz( + uint256 amountOut, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio, + uint256 exitContribution + ) public { + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 100_000_000 * 1e18); + // tokenSupply range between 100 tokens and 100_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 100_000_000 * 1e18); + // reserveRatio range between 1% and 100% + reserveRatio = bound(reserveRatio, 1e6, 1e8); + // exitContribution range between 0% and 20% + exitContribution = bound(exitContribution, 0, 2e7); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: uint32(exitContribution) + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountOut range between 0.0001 tokens and 70% of reserveBalance + amountOut = bound(amountOut, 0.0001e18, (reserveBalance * 7) / 10); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.0001); + } + + function test_getAmountIn_whenTokenInIsReserveAsset_fullFuzz( + uint256 amountOut, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio + ) public { + // tokenSupply range between 100 tokens and 10_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 10_000_000 * 1e18); + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 10_000_000 * 1e18); + // reserveRatio range between 5% and 100% + reserveRatio = bound(reserveRatio, 5e6, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: 0 // no exit contribution because reserveToken in + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountOut range between 0.0001 tokens and 3 times the current tokenSupply + amountOut = bound(amountOut, 0.0001e18, tokenSupply * 3); + + uint256 amountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + uint256 reversedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.01); + } +} + +contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_getAmountOut_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInNotInExchange_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token2), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenOutNotInExchange_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token2), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInEqualTokenOut_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndTokenSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 0 + }); + assertEq(amountOut, 0); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e18; + // formula: amountOut = tokenSupply * amountIn / reserveBalance + // calculation: 300_000 * 1 / 60_000 = 5 + uint256 expectedAmountOut = 5e18; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsTokenAndSupplyIsZero_shouldRevert() public { + poolExchange1.tokenSupply = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_SUPPLY"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 1e18 + }); + } + + function test_getAmountOut_whenTokenInIsTokenAndAmountLargerSupply_shouldRevert() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.expectRevert("ERR_INVALID_AMOUNT"); + bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: poolExchange1.tokenSupply + 1 + }); + } + + function test_getAmountOut_whenTokenInIsTokenAndAmountIsZero_shouldReturnZero() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 0 + }); + assertEq(amountOut, 0); + } + + function test_getAmountOut_whenTokenInIsTokenAndAmountIsSupply_shouldReturnReserveBalanceMinusExitContribution() + public + { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: poolExchange1.tokenSupply + }); + assertEq(amountOut, (poolExchange1.reserveBalance * (1e8 - poolExchange1.exitContribution)) / 1e8); + } + + function test_getAmountOut_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { + poolExchange1.reserveRatio = 1e8; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e18; + // formula: amountOut = (reserveBalance * amountIn / tokenSupply) * (1-e) + // calculation: (60_000 * 1 / 300_000) * 0.99 = 0.198 + uint256 expectedAmountOut = 198000000000000000; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsReserveAsset_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) + // calculation: 300_000 * ((1 + 1 / 60_000) ^ 0.2 - 1) ≈ 0.999993333399999222 + uint256 expectedAmountOut = 999993333399999222; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: 1e18 + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsToken_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) + // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + + // calculation: ((60_000 *(-1+(300_000/(300_000-1))^5) ) / (300_000/(300_000-1))^5)*0.99 = 0.989993400021999963 + // 1 wei difference due to precision loss + uint256 expectedAmountOut = 989993400021999962; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 1e18 + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e12; // 0.000001 reserve token + // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) + // calculation: 300_000 * ((1 + 0.000001 / 60_000) ^ 0.2 - 1) ≈ 0.00000099999999999333 + uint256 expectedAmountOut = 999999999993; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1e12; // 0.000001 token + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) + // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + + // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001))^5) )/(300_000/(300_000-0.000001))^5)*0.99 ≈ 0.0000009899999999934 + uint256 expectedAmountOut = 989999999993; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 1_000_000e18; + // formula: amountOut = tokenSupply * ((1 + amountIn / reserveBalance) ^ reserveRatio - 1) + // calculation: 300_000 * ((1 + 1_000_000 / 60_000) ^ 0.2 - 1) ≈ 232785.231205449318288038 + uint256 expectedAmountOut = 232785231205449318288038; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + } + + function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLarge_shouldReturnCorrectAmount() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 amountIn = 299_000 * 1e18; // 299,000 tokens only 300k supply + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) + // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + + // calculation: ((60_000 *(-1+(300_000/(300_000-299_000))^5) ) / (300_000/(300_000-299_000))^5)*0.99 ≈ 59399.999999975555555555 + uint256 expectedAmountOut = 59399999999975555555555; + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.01); + } + + function test_getAmountOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount( + uint256 amountIn + ) public { + // Set exit contribution to 1% (1e6 out of 1e8) for exchange 1 and 0 for exchange 2 + // all other parameters are the same + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + bancorExchangeProvider.setExitContribution(exchangeId, 1e6); + bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); + bancorExchangeProvider.setExitContribution(exchangeId2, 0); + + amountIn = bound(amountIn, 100, 299_000 * 1e18); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + uint256 amountOut2 = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId2, + tokenIn: address(token2), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + assertEq(amountOut, (amountOut2 * 99) / 100); + } + + function test_getAmountOut_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { + // Create new tokens with different decimals + ERC20 reserveToken6 = new ERC20("Reserve6", "RSV6"); + ERC20 stableToken18 = new ERC20("Stable18", "STB18"); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(stableToken18)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6)), + abi.encode(true) + ); + + // Mock decimals for these tokens + vm.mockCall(address(reserveToken6), abi.encodeWithSelector(reserveToken6.decimals.selector), abi.encode(6)); + vm.mockCall(address(stableToken18), abi.encodeWithSelector(stableToken18.decimals.selector), abi.encode(18)); + + IBancorExchangeProvider.PoolExchange memory newPoolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6), + tokenAddress: address(stableToken18), + tokenSupply: 100_000 * 1e18, // 100,000 + reserveBalance: 50_000 * 1e18, // 50,000 + reserveRatio: 1e8 * 0.5, // 50% + exitContribution: 0 + }); + + bytes32 newExchangeId = bancorExchangeProvider.createExchange(newPoolExchange); + + uint256 amountIn = 1000000; // 1 ReserveToken in 6 decimals + + // formula: amountOut = tokenSupply * (-1 + (1 + (amountIn/reserveBalance))^reserveRatio) + // calculation: 100_000 * (-1 + (1+ (1 / 50_000))^0.5) ≈ 0.999995000049999375 in 18 decimals + uint256 expectedAmountOut = 999995000049999375; + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountIn: amountIn + }); + assertEq(amountOut, expectedAmountOut); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: newExchangeId, + tokenIn: address(reserveToken6), + tokenOut: address(stableToken18), + amountOut: amountOut + }); + // we allow a 1 wei difference due to precision loss + assertApproxEqAbs(amountIn, reversedAmountIn, 1); + } + + function test_getAmountOut_whenTokenInIsReserveAsset_fuzz(uint256 amountIn) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 1 and 10_000_000 tokens + amountIn = bound(amountIn, 1e18, 10_000_000 * 1e18); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // Basic sanity checks + assertTrue(0 < amountOut, "Amount out should be positive"); + + // Verify the reverse swap + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + // we allow up to 10 wei due to precision loss + assertApproxEqAbs(reversedAmountIn, amountIn, 10, "Reversed swap should approximately equal original amount in"); + } + + function test_getAmountOut_whenTokenInIsToken_fuzz(uint256 amountIn) public { + // these values are closed to the ones in the real exchange will be initialized with + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: uint32(28571428), + exitContribution: 1e7 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 10_000wei and 3_500_000_000tokens + amountIn = bound(amountIn, 1e18, (poolExchange.tokenSupply * 5) / 10); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + // Basic sanity checks + assertTrue(amountOut > 0, "Amount out should be positive"); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // we allow up to 0.1% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.001); + } + + function test_getAmountOut_whenTokenInIsReserveAsset_fullFuzz( + uint256 amountIn, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio + ) public { + // tokenSupply range between 100 tokens and 10_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 10_000_000 * 1e18); + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 10_000_000 * 1e18); + // reserveRatio range between 1% and 100% + reserveRatio = bound(reserveRatio, 1e6, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: 0 // no exit contribution because reserveToken in + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 0.0001 tokens and 1_000_000 tokens + amountIn = bound(amountIn, 0.0001e18, 1_000_000 * 1e18); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + // Basic sanity checks + assertTrue(amountOut > 0, "Amount out should be positive"); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + // we allow up to 0.01% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.0001); + } + + function test_getAmountOut_whenTokenInIsToken_fullFuzz( + uint256 amountIn, + uint256 reserveBalance, + uint256 tokenSupply, + uint256 reserveRatio, + uint256 exitContribution + ) public { + // reserveBalance range between 100 tokens and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 100e18, 10_000_000 * 1e18); + // tokenSupply range between 100 tokens and 100_000_000 tokens + tokenSupply = bound(tokenSupply, 100e18, 10_000_000 * 1e18); + // reserveRatio range between 5% and 100% + reserveRatio = bound(reserveRatio, 5e6, 1e8); + // exitContribution range between 0% and 20% + exitContribution = bound(exitContribution, 0, 2e7); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: uint32(exitContribution) + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + // amountIn range between 0.0001 tokens and 80% of tokenSupply + // if we would allow 100% of the tokenSupply, the precision loss can get higher + amountIn = bound(amountIn, 0.0001e18, (tokenSupply * 8) / 10); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + // Basic sanity checks + assertTrue(amountIn > 0, "Amount in should be positive"); + + uint256 reversedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + // we allow up to 1% difference due to precision loss + assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.01); + } +} + +contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { + BancorExchangeProvider bancorExchangeProvider; + + function setUp() public override { + super.setUp(); + bancorExchangeProvider = initializeBancorExchangeProvider(); + } + + function test_currentPrice_whenExchangeDoesNotExist_shouldRevert() public { + bytes32 exchangeId = "0xexchangeId"; + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.currentPrice(exchangeId); + } + + function test_currentPrice_whenExchangeExists_shouldReturnCorrectPrice() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + // formula: price = reserveBalance / tokenSupply * reserveRatio + // calculation: 60_000 / 300_000 * 0.2 = 1 + uint256 expectedPrice = 1e18; + uint256 price = bancorExchangeProvider.currentPrice(exchangeId); + assertEq(price, expectedPrice); + } + + function test_currentPrice_fuzz(uint256 reserveBalance, uint256 tokenSupply, uint256 reserveRatio) public { + // reserveBalance range between 1 token and 10_000_000 tokens + reserveBalance = bound(reserveBalance, 1e18, 10_000_000 * 1e18); + // tokenSupply range between 1 token and 10_000_000 tokens + tokenSupply = bound(tokenSupply, 1e18, 10_000_000 * 1e18); + // reserveRatio range between 1% and 100% + reserveRatio = bound(reserveRatio, 1e6, 1e8); + + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: tokenSupply, + reserveBalance: reserveBalance, + reserveRatio: uint32(reserveRatio), + exitContribution: 0 + }); + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange); + + uint256 price = bancorExchangeProvider.currentPrice(exchangeId); + assertTrue(0 < price, "Price should be positive"); + } +} + +contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { + function test_swapIn_whenCallerIsNotBroker_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(makeAddr("NotBroker")); + vm.expectRevert("Caller is not the Broker"); + bancorExchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), 1e18); + } + + function test_swapIn_whenExchangeDoesNotExist_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + vm.prank(brokerAddress); + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.swapIn("0xexchangeId", address(reserveToken), address(token), 1e18); + } + + function test_swapIn_whenTokenInNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapIn(exchangeId, address(token2), address(token), 1e18); + } + + function test_swapIn_whenTokenOutNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapIn(exchangeId, address(token), address(token2), 1e18); + } + + function test_swapIn_whenTokenInEqualsTokenOut_itReverts() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapIn(exchangeId, address(token), address(token), 1e18); + } + + function test_swapIn_whenTokenInIsReserveAsset_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), amountIn); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + + function test_swapIn_whenTokenInIsToken_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn(exchangeId, address(token), address(reserveToken), amountIn); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); + } +} + +contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { + function test_swapOut_whenCallerIsNotBroker_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(makeAddr("NotBroker")); + vm.expectRevert("Caller is not the Broker"); + bancorExchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), 1e18); + } + + function test_swapOut_whenExchangeDoesNotExist_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + vm.prank(brokerAddress); + vm.expectRevert("Exchange does not exist"); + bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); + } + + function test_swapOut_whenTokenInNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapOut(exchangeId, address(token2), address(token), 1e18); + } + + function test_swapOut_whenTokenOutNotInexchange_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapOut(exchangeId, address(token), address(token2), 1e18); + } + + function test_swapOut_whenTokenInEqualsTokenOut_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("tokenIn and tokenOut must match exchange"); + bancorExchangeProvider.swapOut(exchangeId, address(token), address(token), 1e18); + } + + function test_swapOut_whenTokenInIsReserveAsset_shouldSwapOut() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), amountOut); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + + function test_swapOut_whenTokenInIsToken_shouldSwapOut() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + uint256 reserveBalanceBefore = poolExchange1.reserveBalance; + uint256 tokenSupplyBefore = poolExchange1.tokenSupply; + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), amountOut); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); + } +} diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol new file mode 100644 index 00000000..c87f5418 --- /dev/null +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -0,0 +1,896 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase + +import { Test } from "forge-std/Test.sol"; +import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; +import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; + +import { IReserve } from "contracts/interfaces/IReserve.sol"; +import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; + +contract GoodDollarExchangeProviderTest is Test { + /* ------- Events from IGoodDollarExchangeProvider ------- */ + + event ExpansionControllerUpdated(address indexed expansionController); + + event AvatarUpdated(address indexed AVATAR); + + event ReserveRatioUpdated(bytes32 indexed exchangeId, uint32 reserveRatio); + + event ExchangeCreated(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + event ExchangeDestroyed(bytes32 indexed exchangeId, address indexed reserveAsset, address indexed tokenAddress); + + event ExitContributionSet(bytes32 indexed exchangeId, uint256 exitContribution); + + /* ------------------------------------------- */ + + ERC20 public reserveToken; + ERC20 public token; + ERC20 public token2; + + address public reserveAddress; + address public brokerAddress; + address public avatarAddress; + address public expansionControllerAddress; + + IBancorExchangeProvider.PoolExchange public poolExchange1; + IBancorExchangeProvider.PoolExchange public poolExchange2; + IBancorExchangeProvider.PoolExchange public poolExchange; + + function setUp() public virtual { + reserveToken = new ERC20("cUSD", "cUSD"); + token = new ERC20("Good$", "G$"); + token2 = new ERC20("Good2$", "G2$"); + + reserveAddress = makeAddr("Reserve"); + brokerAddress = makeAddr("Broker"); + avatarAddress = makeAddr("Avatar"); + expansionControllerAddress = makeAddr("ExpansionController"); + + poolExchange1 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 0.2 * 1e8, + exitContribution: 0.01 * 1e8 + }); + + poolExchange2 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token2), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7_000_000_000 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: 1e8 * 0.28571428, + exitContribution: 1e8 * 0.1 + }); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), + abi.encode(true) + ); + } + + function initializeGoodDollarExchangeProvider() internal returns (GoodDollarExchangeProvider) { + GoodDollarExchangeProvider exchangeProvider = new GoodDollarExchangeProvider(false); + + exchangeProvider.initialize(brokerAddress, reserveAddress, expansionControllerAddress, avatarAddress); + return exchangeProvider; + } +} + +contract GoodDollarExchangeProviderTest_initializerSettersGetters is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + } + + /* ---------- Initilizer ---------- */ + + function test_initializer() public view { + assertEq(exchangeProvider.owner(), address(this)); + assertEq(exchangeProvider.broker(), brokerAddress); + assertEq(address(exchangeProvider.reserve()), reserveAddress); + assertEq(address(exchangeProvider.expansionController()), expansionControllerAddress); + assertEq(exchangeProvider.AVATAR(), avatarAddress); + } + + /* ---------- Setters ---------- */ + + function test_setAvatar_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + exchangeProvider.setAvatar(makeAddr("NewAvatar")); + } + + function test_setAvatar_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Avatar address must be set"); + exchangeProvider.setAvatar(address(0)); + } + + function test_setAvatar_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newAvatar = makeAddr("NewAvatar"); + vm.expectEmit(true, true, true, true); + emit AvatarUpdated(newAvatar); + exchangeProvider.setAvatar(newAvatar); + + assertEq(exchangeProvider.AVATAR(), newAvatar); + } + + function test_setExpansionController_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + exchangeProvider.setExpansionController(makeAddr("NewExpansionController")); + } + + function test_setExpansionController_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("ExpansionController address must be set"); + exchangeProvider.setExpansionController(address(0)); + } + + function test_setExpansionController_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newExpansionController = makeAddr("NewExpansionController"); + vm.expectEmit(true, true, true, true); + emit ExpansionControllerUpdated(newExpansionController); + exchangeProvider.setExpansionController(newExpansionController); + + assertEq(address(exchangeProvider.expansionController()), newExpansionController); + } + + /* ---------- setExitContribution ---------- */ + /* Focuses only on access control, implementation details are covered in BancorExchangeProvider tests */ + function test_setExitContribution_whenSenderIsOwner_shouldRevert() public { + vm.expectRevert("Only Avatar can call this function"); + bytes32 exchangeId = "0xexchangeId"; + exchangeProvider.setExitContribution(exchangeId, 1e5); + } + + function test_setExitContribution_whenSenderIsNotAvatar_shouldRevert() public { + vm.startPrank(makeAddr("NotAvatarAndNotOwner")); + vm.expectRevert("Only Avatar can call this function"); + bytes32 exchangeId = "0xexchangeId"; + exchangeProvider.setExitContribution(exchangeId, 1e5); + vm.stopPrank(); + } + + function test_setExitContribution_whenSenderIsAvatar_shouldUpdateAndEmit() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + uint32 newExitContribution = 1e3; + vm.expectEmit(true, true, true, true); + emit ExitContributionSet(exchangeId, newExitContribution); + exchangeProvider.setExitContribution(exchangeId, newExitContribution); + + IBancorExchangeProvider.PoolExchange memory poolExchange = exchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.exitContribution, newExitContribution); + vm.stopPrank(); + } + /* ---------- setExitContribution end ---------- */ +} + +/** + * @notice createExchange tests + * @dev These tests focus only on access control. The implementation details + * are covered in the BancorExchangeProvider tests. + */ +contract GoodDollarExchangeProviderTest_createExchange is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + } + + function test_createExchange_whenSenderIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenSenderIsOwner_shouldRevert() public { + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.createExchange(poolExchange1); + } + + function test_createExchange_whenSenderIsAvatar_shouldCreateExchangeAndEmit() public { + vm.startPrank(avatarAddress); + vm.expectEmit(true, true, true, true); + bytes32 expectedExchangeId = keccak256(abi.encodePacked(reserveToken.symbol(), token.symbol())); + emit ExchangeCreated(expectedExchangeId, address(reserveToken), address(token)); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + assertEq(exchangeId, expectedExchangeId); + + IBancorExchangeProvider.PoolExchange memory poolExchange = exchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.reserveAsset, poolExchange1.reserveAsset); + assertEq(poolExchange.tokenAddress, poolExchange1.tokenAddress); + assertEq(poolExchange.tokenSupply, poolExchange1.tokenSupply); + assertEq(poolExchange.reserveBalance, poolExchange1.reserveBalance); + assertEq(poolExchange.reserveRatio, poolExchange1.reserveRatio); + assertEq(poolExchange.exitContribution, poolExchange1.exitContribution); + + IExchangeProvider.Exchange[] memory exchanges = exchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId); + + assertEq(exchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); + assertEq(exchangeProvider.tokenPrecisionMultipliers(address(token)), 1); + vm.stopPrank(); + } +} + +/** + * @notice destroyExchange tests + * @dev These tests focus only on access control. The implementation details + * are covered in the BancorExchangeProvider tests. + */ +contract GoodDollarExchangeProviderTest_destroyExchange is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + } + + function test_destroyExchange_whenSenderIsOwner_shouldRevert() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + vm.stopPrank(); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.destroyExchange(exchangeId, 0); + } + + function test_destroyExchange_whenSenderIsNotAvatar_shouldRevert() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + vm.stopPrank(); + + vm.startPrank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.destroyExchange(exchangeId, 0); + vm.stopPrank(); + } + + function test_destroyExchange_whenExchangeExists_shouldDestroyExchangeAndEmit() public { + vm.startPrank(avatarAddress); + bytes32 exchangeId = exchangeProvider.createExchange(poolExchange1); + bytes32 exchangeId2 = exchangeProvider.createExchange(poolExchange2); + vm.stopPrank(); + + vm.startPrank(avatarAddress); + vm.expectEmit(true, true, true, true); + emit ExchangeDestroyed(exchangeId, poolExchange1.reserveAsset, poolExchange1.tokenAddress); + exchangeProvider.destroyExchange(exchangeId, 0); + + bytes32[] memory exchangeIds = exchangeProvider.getExchangeIds(); + assertEq(exchangeIds.length, 1); + + IExchangeProvider.Exchange[] memory exchanges = exchangeProvider.getExchanges(); + assertEq(exchanges.length, 1); + assertEq(exchanges[0].exchangeId, exchangeId2); + vm.stopPrank(); + } +} + +contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + uint256 expansionRate; + uint256 reserveRatioScalar; + + function setUp() public override { + super.setUp(); + // based on a yearly expansion rate of 10% the daily rate is: + // (1-x)^365 = 0.9 -> x = 1 - 0.9^(1/365) = 0.00028861728902231263... + expansionRate = 288617289022312; + reserveRatioScalar = 1e18 - expansionRate; + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange); + } + + function test_mintFromExpansion_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); + exchangeProvider.mintFromExpansion(exchangeId, expansionRate); + } + + function test_mintFromExpansionRate_whenReserveRatioScalarIs0_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Reserve ratio scalar must be greater than 0"); + exchangeProvider.mintFromExpansion(exchangeId, 0); + } + + function test_mintFromExpansion_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); + exchangeProvider.mintFromExpansion(bytes32(0), expansionRate); + } + + function test_mintFromExpansion_whenNewRatioIsZero_shouldRevert() public { + uint256 verySmallReserveRatioScalar = 1; + + vm.expectRevert("New ratio must be greater than 0"); + vm.prank(expansionControllerAddress); + exchangeProvider.mintFromExpansion(exchangeId, verySmallReserveRatioScalar); + } + + function test_mintFromExpansion_whenReserveRatioScalarIs100Percent_shouldReturn0() public { + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, 1e18); + assertEq(amountToMint, 0, "Minted amount should be 0"); + } + + function test_mintFromExpansion_whenValidReserveRatioScalar_shouldReturnCorrectAmountAndEmit() public { + // reserveRatioScalar is (1-0.000288617289022312) based of 10% yearly expansion rate + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) = 0.285631817919071438 + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285631817919071438) / 0.285631817919071438 + // ≈ 2_020_904,291074052815139287 + uint32 expectedReserveRatio = 28563181; + uint256 expectedAmountToMint = 2020904291074052815139287; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_mintFromExpansion_withSmallReserveRatioScalar_shouldReturnCorrectAmount() public { + uint256 smallReserveRatioScalar = 1e18 * 0.00001; // 0.001% + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.0000028571428 + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.0000028571428) /0.0000028571428 + // amountToMint ≈ 699993000000000 + uint32 expectedReserveRatio = 285; + uint256 expectedAmountToMint = 699993000000000 * 1e18; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, smallReserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + // 1% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + } + + function test_mintFromExpansion_withLargeReserveRatioScalar_shouldReturnCorrectAmount() public { + uint256 largeReserveRatioScalar = 1e18 - 1; // Just below 100% + // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.285714279999999999 + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285714279999999999) /0.285714279999999999 + // amountToMint ≈ 0.00000002450000049000 + uint32 expectedReserveRatio = 28571427; + uint256 expectedAmountToMint = 24500000490; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, largeReserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_mintFromExpansion_withMultipleConsecutiveExpansions_shouldMintCorrectly() public { + uint256 totalMinted = 0; + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint256 initialPrice = exchangeProvider.currentPrice(exchangeId); + + vm.startPrank(expansionControllerAddress); + for (uint256 i = 0; i < 5; i++) { + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + totalMinted += amountToMint; + assertGt(amountToMint, 0, "Amount minted should be greater than 0"); + } + vm.stopPrank(); + + // Calculate expected reserve ratio + // daily Scalar is applied 5 times newRatio = initialReserveRatio * (dailyScalar ** 5) + // newRatio = 0.28571428 * (0.999711382710977688 ** 5) ≈ 0.2853022075264986 + uint256 expectedReserveRatio = 28530220; + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + totalMinted, + "Token supply should increase by total minted amount" + ); + assertLt(poolExchangeAfter.reserveRatio, initialReserveRatio, "Reserve ratio should decrease"); + assertEq(poolExchangeAfter.reserveBalance, initialReserveBalance, "Reserve balance should remain unchanged"); + assertApproxEqRel( + poolExchangeAfter.reserveRatio, + uint32(expectedReserveRatio), + 1e18 * 0.0001, // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated + "Reserve ratio should be updated correctly within 0.01% tolerance" + ); + assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function testFuzz_mintFromExpansion(uint256 reserveRatioScalar) public { + // 0.001% to 100% + reserveRatioScalar = bound(reserveRatioScalar, 1e18 * 0.00001, 1e18); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + uint256 expectedReserveRatio = (uint256(initialReserveRatio) * reserveRatioScalar) / 1e18; + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, uint32(expectedReserveRatio)); + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertGe(amountToMint, 0, "Minted amount should be greater or equal than 0"); + assertGe(initialReserveRatio, poolExchangeAfter.reserveRatio, "Reserve ratio should decrease"); + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + // 1% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + } +} + +contract GoodDollarExchangeProviderTest_mintFromInterest is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + uint256 reserveInterest; + + function setUp() public override { + super.setUp(); + reserveInterest = 1000 * 1e18; + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange); + } + + function test_mintFromInterest_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); + exchangeProvider.mintFromInterest(exchangeId, reserveInterest); + } + + function test_mintFromInterest_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); + exchangeProvider.mintFromInterest(bytes32(0), reserveInterest); + } + + function test_mintFromInterest_whenInterestIs0_shouldReturn0() public { + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, 0); + assertEq(amountToMint, 0, "Minted amount should be 0"); + } + + function test_mintFromInterest_whenInterestLarger0_shouldReturnCorrectAmount() public { + uint256 interest = 1_000 * 1e18; + // formula: amountToMint = reserveInterest * tokenSupply / reserveBalance + // amountToMint = 1_000 * 7_000_000_000 / 200_000 = 35_000_000 + uint256 expectedAmountToMint = 35_000_000 * 1e18; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, interest); + + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + interest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromInterest_whenInterestIsSmall_shouldReturnCorrectAmount() public { + uint256 interest = 100; // 100wei + // formula: amountToMint = reserveInterest * tokenSupply / reserveBalance + // amountToMint = (100/1e18) * 7_000_000_000 / 200_000 = 0.000000000003500000 + uint256 expectedAmountToMint = 3_500_000; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, interest); + + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + interest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromInterest_whenInterestIsLarge_shouldReturnCorrectAmount() public { + // 1_000_000 reserve tokens 5 times current reserve balance + uint256 interest = 1_000_000 * 1e18; + // formula: amountToMint = reserveInterest * tokenSupply / reserveBalance + // amountToMint = 1_000_000 * 7_000_000_000 / 200_000 = 35_000_000_000 + uint256 expectedAmountToMint = 35_000_000_000 * 1e18; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, interest); + + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + + assertEq(amountToMint, expectedAmountToMint, "Minted amount should be correct"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + interest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function test_mintFromInterest_withMultipleConsecutiveInterests_shouldMintCorrectly() public { + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.startPrank(expansionControllerAddress); + uint256 totalMinted = 0; + for (uint256 i = 0; i < 5; i++) { + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, reserveInterest); + totalMinted += amountToMint; + } + vm.stopPrank(); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + totalMinted, + "Token supply should increase by total minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + poolExchange.reserveBalance + reserveInterest * 5, + "Reserve balance should increase by total interest" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } + + function testFuzz_mintFromInterest(uint256 fuzzedInterest) public { + fuzzedInterest = bound(fuzzedInterest, 1, type(uint256).max / poolExchange.tokenSupply); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + uint256 amountToMint = exchangeProvider.mintFromInterest(exchangeId, fuzzedInterest); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertGt(amountToMint, 0, "Minted amount should be greater than 0"); + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + amountToMint, + "Token supply should increase by minted amount" + ); + assertEq( + poolExchangeAfter.reserveBalance, + initialReserveBalance + fuzzedInterest, + "Reserve balance should increase by interest amount" + ); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); + } +} + +contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + uint256 reward; + + function setUp() public override { + super.setUp(); + reward = 1000 * 1e18; + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange); + } + + function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public { + vm.prank(makeAddr("NotExpansionController")); + vm.expectRevert("Only ExpansionController can call this function"); + exchangeProvider.updateRatioForReward(exchangeId, reward); + } + + function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { + vm.prank(expansionControllerAddress); + vm.expectRevert("Exchange does not exist"); + exchangeProvider.updateRatioForReward(bytes32(0), reward); + } + + function test_updateRatioForReward_whenRewardLarger0_shouldReturnCorrectRatioAndEmit() public { + // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) + // reserveRatio = 200_000 / ((7_000_000_000 + 1_000) * 0.000100000002) ≈ 0.28571423... + uint32 expectedReserveRatio = 28571423; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, reward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + reward, + "Token supply should increase by reward amount" + ); + // 1% relative error tolerance because of precision loss when new reserve ratio is calculated + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public { + uint256 reward = 1e18; // 1 token + // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) + // reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799 + uint32 expectedReserveRatio = 28571427; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, reward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + reward, + "Token supply should increase by reward amount" + ); + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public { + uint256 reward = 1_000_000_000 * 1e18; // 1 billion tokens + // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) + // reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000... + + uint32 expectedReserveRatio = 24999999; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, reward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); + assertEq( + poolExchangeAfter.tokenSupply, + poolExchange.tokenSupply + reward, + "Token supply should increase by reward amount" + ); + assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + } + + function test_updateRatioForReward_withMultipleConsecutiveRewards() public { + uint256 totalReward = 0; + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 initialPrice = exchangeProvider.currentPrice(exchangeId); + + vm.startPrank(expansionControllerAddress); + for (uint256 i = 0; i < 5; i++) { + exchangeProvider.updateRatioForReward(exchangeId, reward); + totalReward += reward; + } + vm.stopPrank(); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + totalReward, + "Token supply should increase by total reward" + ); + assertEq(poolExchangeAfter.reserveBalance, initialReserveBalance, "Reserve balance should remain unchanged"); + assertLt(poolExchangeAfter.reserveRatio, initialReserveRatio, "Reserve ratio should decrease"); + assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.001, "Price should remain within 0.1% of initial price"); + } + + function testFuzz_updateRatioForReward(uint256 fuzzedReward) public { + // 1 to 100 trillion tokens + fuzzedReward = bound(fuzzedReward, 1, 100_000_000_000_000 * 1e18); + + uint256 initialTokenSupply = poolExchange.tokenSupply; + uint256 initialReserveBalance = poolExchange.reserveBalance; + uint32 initialReserveRatio = poolExchange.reserveRatio; + uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); + + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward); + + IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); + uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); + + assertEq( + poolExchangeAfter.tokenSupply, + initialTokenSupply + fuzzedReward, + "Token supply should increase by reward amount" + ); + assertEq(poolExchangeAfter.reserveBalance, initialReserveBalance, "Reserve balance should remain unchanged"); + assertLe(poolExchangeAfter.reserveRatio, initialReserveRatio, "Reserve ratio should stay the same or decrease"); + assertApproxEqRel( + priceBefore, + priceAfter, + 1e18 * 0.001, + "Price should remain unchanged, with a max relative error of 0.1%" + ); + } +} + +contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTest { + GoodDollarExchangeProvider exchangeProvider; + bytes32 exchangeId; + + function setUp() public override { + super.setUp(); + exchangeProvider = initializeGoodDollarExchangeProvider(); + vm.prank(avatarAddress); + exchangeId = exchangeProvider.createExchange(poolExchange1); + } + + function test_pause_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.pause(); + } + + function test_unpause_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + exchangeProvider.unpause(); + } + + function test_pause_whenCallerIsAvatar_shouldPauseAndDisableExchange() public { + vm.prank(avatarAddress); + exchangeProvider.pause(); + + assert(exchangeProvider.paused()); + + vm.startPrank(brokerAddress); + vm.expectRevert("Pausable: paused"); + exchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), 1e18); + + vm.expectRevert("Pausable: paused"); + exchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), 1e18); + + vm.startPrank(expansionControllerAddress); + vm.expectRevert("Pausable: paused"); + exchangeProvider.mintFromExpansion(exchangeId, 1e18); + + vm.expectRevert("Pausable: paused"); + exchangeProvider.mintFromInterest(exchangeId, 1e18); + + vm.expectRevert("Pausable: paused"); + exchangeProvider.updateRatioForReward(exchangeId, 1e18); + } + + function test_unpause_whenCallerIsAvatar_shouldUnpauseAndEnableExchange() public { + vm.prank(avatarAddress); + exchangeProvider.pause(); + + vm.prank(avatarAddress); + exchangeProvider.unpause(); + + assert(exchangeProvider.paused() == false); + + vm.startPrank(brokerAddress); + + exchangeProvider.swapIn(exchangeId, address(reserveToken), address(token), 1e18); + exchangeProvider.swapOut(exchangeId, address(reserveToken), address(token), 1e18); + + vm.startPrank(expansionControllerAddress); + + exchangeProvider.mintFromExpansion(exchangeId, 1e18); + exchangeProvider.mintFromInterest(exchangeId, 1e18); + exchangeProvider.updateRatioForReward(exchangeId, 1e18); + } +} diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol new file mode 100644 index 00000000..4e46e620 --- /dev/null +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; +// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility +// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase + +import { Test } from "forge-std/Test.sol"; +import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; +import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; + +import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; +import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; +import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; +import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; + +import { GoodDollarExpansionControllerHarness } from "test/utils/harnesses/GoodDollarExpansionControllerHarness.sol"; + +contract GoodDollarExpansionControllerTest is Test { + /* ------- Events from IGoodDollarExpansionController ------- */ + + event GoodDollarExchangeProviderUpdated(address indexed exchangeProvider); + + event DistributionHelperUpdated(address indexed distributionHelper); + + event ReserveUpdated(address indexed reserve); + + event AvatarUpdated(address indexed avatar); + + event ExpansionConfigSet(bytes32 indexed exchangeId, uint64 expansionRate, uint32 expansionFrequency); + + event RewardMinted(bytes32 indexed exchangeId, address indexed to, uint256 amount); + + event InterestUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + event ExpansionUBIMinted(bytes32 indexed exchangeId, uint256 amount); + + /* ------------------------------------------- */ + + ERC20Mock public reserveToken; + ERC20Mock public token; + + address public exchangeProvider; + address public distributionHelper; + address public reserveAddress; + address public avatarAddress; + + bytes32 exchangeId = "ExchangeId"; + + uint64 expansionRate = 1e18 * 0.01; + uint32 expansionFrequency = uint32(1 days); + + IBancorExchangeProvider.PoolExchange pool; + + function setUp() public virtual { + reserveToken = new ERC20Mock("cUSD", "cUSD", address(this), 1); + token = new ERC20Mock("Good$", "G$", address(this), 1); + + exchangeProvider = makeAddr("ExchangeProvider"); + distributionHelper = makeAddr("DistributionHelper"); + reserveAddress = makeAddr("Reserve"); + avatarAddress = makeAddr("Avatar"); + + pool = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e18, + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IBancorExchangeProvider(exchangeProvider).getPoolExchange.selector), + abi.encode(pool) + ); + } + + function initializeGoodDollarExpansionController() internal returns (GoodDollarExpansionController) { + GoodDollarExpansionController expansionController = new GoodDollarExpansionController(false); + + expansionController.initialize(exchangeProvider, distributionHelper, reserveAddress, avatarAddress); + + return expansionController; + } +} + +contract GoodDollarExpansionControllerTest_initializerSettersGetters is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + } + + /* ---------- Initilizer ---------- */ + + function test_initializer() public view { + assertEq(address(expansionController.distributionHelper()), distributionHelper); + assertEq(expansionController.reserve(), reserveAddress); + assertEq(address(expansionController.goodDollarExchangeProvider()), exchangeProvider); + assertEq(expansionController.AVATAR(), avatarAddress); + } + + /* ---------- Getters ---------- */ + + function test_getExpansionConfig_whenConfigIsNotSet_shouldRevert() public { + vm.expectRevert("Expansion config not set"); + expansionController.getExpansionConfig("NotSetExchangeId"); + } + + function test_getExpansionConfig_whenConfigIsSet_shouldReturnConfig() public { + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(config.expansionRate, expansionRate); + assertEq(config.expansionFrequency, expansionFrequency); + assertEq(config.lastExpansion, 0); + } + + /* ---------- Setters ---------- */ + + function test_setGoodDollarExchangeProvider_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + expansionController.setGoodDollarExchangeProvider(makeAddr("NewExchangeProvider")); + } + + function test_setGoodDollarExchangeProvider_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("GoodDollarExchangeProvider address must be set"); + expansionController.setGoodDollarExchangeProvider(address(0)); + } + + function test_setGoodDollarExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { + address newExchangeProvider = makeAddr("NewExchangeProvider"); + vm.expectEmit(true, true, true, true); + emit GoodDollarExchangeProviderUpdated(newExchangeProvider); + expansionController.setGoodDollarExchangeProvider(newExchangeProvider); + + assertEq(address(expansionController.goodDollarExchangeProvider()), newExchangeProvider); + } + + function test_setDistributionHelper_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); + } + + function test_setDistributionHelper_whenCallerIsOwner_shouldRevert() public { + vm.expectRevert("Only Avatar can call this function"); + expansionController.setDistributionHelper(makeAddr("NewDistributionHelper")); + } + + function test_setDistributionHelper_whenAddressIsZero_shouldRevert() public { + vm.startPrank(avatarAddress); + vm.expectRevert("Distribution helper address must be set"); + expansionController.setDistributionHelper(address(0)); + vm.stopPrank(); + } + + function test_setDistributionHelper_whenCallerIsAvatar_shouldUpdateAndEmit() public { + vm.startPrank(avatarAddress); + address newDistributionHelper = makeAddr("NewDistributionHelper"); + vm.expectEmit(true, true, true, true); + emit DistributionHelperUpdated(newDistributionHelper); + expansionController.setDistributionHelper(newDistributionHelper); + + assertEq(address(expansionController.distributionHelper()), newDistributionHelper); + vm.stopPrank(); + } + + function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + expansionController.setReserve(makeAddr("NewReserve")); + } + + function test_setReserve_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Reserve address must be set"); + expansionController.setReserve(address(0)); + } + + function test_setReserve_whenCallerIsOwner_shouldUpdateAndEmit() public { + address newReserve = makeAddr("NewReserve"); + vm.expectEmit(true, true, true, true); + emit ReserveUpdated(newReserve); + expansionController.setReserve(newReserve); + + assertEq(expansionController.reserve(), newReserve); + } + + function test_setAvatar_whenSenderIsNotOwner_shouldRevert() public { + vm.prank(makeAddr("NotOwner")); + vm.expectRevert("Ownable: caller is not the owner"); + expansionController.setAvatar(makeAddr("NewAvatar")); + } + + function test_setAvatar_whenAddressIsZero_shouldRevert() public { + vm.expectRevert("Avatar address must be set"); + expansionController.setAvatar(address(0)); + } + + function test_setAvatar_whenCallerIsOwner_shouldUpdateAndEmit() public { + address newAvatar = makeAddr("NewAvatar"); + vm.expectEmit(true, true, true, true); + emit AvatarUpdated(newAvatar); + expansionController.setAvatar(newAvatar); + + assertEq(expansionController.AVATAR(), newAvatar); + } + + function test_setExpansionConfig_whenSenderIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenExpansionRateIsLargerOrEqualToOne_shouldRevert() public { + expansionRate = 1e18; + + vm.prank(avatarAddress); + vm.expectRevert("Expansion rate must be less than 100%"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenExpansionRateIsZero_shouldRevert() public { + expansionRate = 0; + + vm.prank(avatarAddress); + vm.expectRevert("Expansion rate must be greater than 0"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenExpansionFrequencyIsZero_shouldRevert() public { + expansionFrequency = 0; + + vm.prank(avatarAddress); + vm.expectRevert("Expansion frequency must be greater than 0"); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + } + + function test_setExpansionConfig_whenCallerIsAvatar_shouldUpdateAndEmit() public { + vm.prank(avatarAddress); + vm.expectEmit(true, true, true, true); + emit ExpansionConfigSet(exchangeId, expansionRate, expansionFrequency); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(config.expansionRate, expansionRate); + assertEq(config.expansionFrequency, expansionFrequency); + assertEq(config.lastExpansion, 0); + } +} + +contract GoodDollarExpansionControllerTest_mintUBIFromInterest is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector), + abi.encode(1000e18) + ); + } + + function test_mintUBIFromInterest_whenReserveInterestIs0_shouldRevert() public { + vm.expectRevert("Reserve interest must be greater than 0"); + expansionController.mintUBIFromInterest(exchangeId, 0); + } + + function test_mintUBIFromInterest_whenAmountToMintIsLargerThan0_shouldMintTransferAndEmit() public { + uint256 reserveInterest = 1000e18; + uint256 amountToMint = 1000e18; + address interestCollector = makeAddr("InterestCollector"); + + deal(address(reserveToken), interestCollector, reserveInterest); + assertEq(reserveToken.balanceOf(interestCollector), reserveInterest); + + uint256 interestCollectorBalanceBefore = reserveToken.balanceOf(interestCollector); + uint256 reserveBalanceBefore = reserveToken.balanceOf(reserveAddress); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.startPrank(interestCollector); + reserveToken.approve(address(expansionController), reserveInterest); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + + assertEq(reserveToken.balanceOf(reserveAddress), reserveBalanceBefore + reserveInterest); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(reserveToken.balanceOf(interestCollector), interestCollectorBalanceBefore - reserveInterest); + } +} + +contract GoodDollarExpansionControllerTest_mintUBIFromReserveBalance is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector), + abi.encode(1000e18) + ); + } + + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIs0_shouldReturn0() public { + deal(address(reserveToken), reserveAddress, pool.reserveBalance); + uint256 amountMinted = expansionController.mintUBIFromReserveBalance(exchangeId); + assertEq(amountMinted, 0); + } + + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIsLargerThan0_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + uint256 additionalReserveBalance = 1000e18; + + deal(address(reserveToken), reserveAddress, pool.reserveBalance + additionalReserveBalance); + + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = expansionController.mintUBIFromReserveBalance(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + } +} + +contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector), + abi.encode(1000e18) + ); + + vm.mockCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector), + abi.encode(true) + ); + } + + function test_mintUBIFromExpansion_whenExpansionConfigIsNotSet_shouldRevert() public { + vm.expectRevert("Expansion config not set"); + expansionController.mintUBIFromExpansion("NotSetExchangeId"); + } + + function test_mintUBIFromExpansion_whenShouldNotExpand_shouldNotExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + uint256 lastExpansion = config.lastExpansion; + skip(lastExpansion + config.expansionFrequency - 1); + + assertEq(expansionController.mintUBIFromExpansion(exchangeId), 0); + } + + function test_mintUBIFromExpansion_whenFirstExpansionAndLessThanExpansionFrequencyPassed_shouldExpand1Time() public { + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + assert(block.timestamp < config.lastExpansion + config.expansionFrequency); + uint256 reserveRatioScalar = 1e18 * 0.99; + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + config = expansionController.getExpansionConfig(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } + + function test_mintUBIFromExpansion_whenFirstExpansionAndMultipleExpansionFrequenciesPassed_shouldExpand1Time() + public + { + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + skip(config.expansionFrequency * 3 + 1); + assert(block.timestamp > config.lastExpansion + config.expansionFrequency * 3); + uint256 reserveRatioScalar = 1e18 * 0.99; + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + config = expansionController.getExpansionConfig(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } + + function test_mintUBIFromExpansion_when1DayPassed_shouldCalculateCorrectRateAndExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); + + // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^1 = 0.99 + uint256 reserveRatioScalar = 1e18 * 0.99; + skip(expansionFrequency + 1); + + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } + + function test_mintUBIFromExpansion_whenMultipleDaysPassed_shouldCalculateCorrectRateAndExpand() public { + // doing one initial expansion to not be first expansion + // since on first expansion the expansion is always applied once. + expansionController.mintUBIFromExpansion(exchangeId); + + // 3 days have passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider + // should be 0.99^3 = 0.970299 + uint256 reserveRatioScalar = 1e18 * 0.970299; + + skip(3 * expansionFrequency + 1); + + uint256 amountToMint = 1000e18; + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + + vm.expectCall( + exchangeProvider, + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromExpansion.selector, + exchangeId, + reserveRatioScalar + ) + ); + vm.expectCall( + distributionHelper, + abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) + ); + + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( + exchangeId + ); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(config.lastExpansion, block.timestamp); + } +} + +contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpansionControllerTest { + GoodDollarExpansionControllerHarness expansionController; + + function setUp() public override { + super.setUp(); + expansionController = new GoodDollarExpansionControllerHarness(false); + } + + function test_getExpansionScaler_whenExpansionRateIs0_shouldReturn1e18() public { + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController + .ExchangeExpansionConfig(0, 1, 0); + assertEq(expansionController.exposed_getReserveRatioScalar(config), 1e18); + } + + function test_getExpansionScaler_whenExpansionRateIs1_shouldReturn1() public { + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController + .ExchangeExpansionConfig(1e18 - 1, 1, 0); + assertEq(expansionController.exposed_getReserveRatioScalar(config), 1); + } + + function testFuzz_getExpansionScaler( + uint256 _expansionRate, + uint256 _expansionFrequency, + uint256 _lastExpansion, + uint256 _timeDelta + ) public { + uint64 expansionRate = uint64(bound(_expansionRate, 1, 1e18 - 1)); + uint32 expansionFrequency = uint32(bound(_expansionFrequency, 1, 1e6)); + uint32 lastExpansion = uint32(bound(_lastExpansion, 0, 1e6)); + uint32 timeDelta = uint32(bound(_timeDelta, 0, 1e6)); + + skip(lastExpansion + timeDelta); + + IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController + .ExchangeExpansionConfig(expansionRate, expansionFrequency, lastExpansion); + uint256 scaler = expansionController.exposed_getReserveRatioScalar(config); + assert(scaler >= 0 && scaler <= 1e18); + } +} + +contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDollarExpansionControllerTest { + GoodDollarExpansionController expansionController; + + function setUp() public override { + super.setUp(); + expansionController = initializeGoodDollarExpansionController(); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + + vm.mockCall( + exchangeProvider, + abi.encodeWithSelector(IGoodDollarExchangeProvider(exchangeProvider).updateRatioForReward.selector), + abi.encode(true) + ); + } + + function test_mintRewardFromReserveRatio_whenCallerIsNotAvatar_shouldRevert() public { + vm.prank(makeAddr("NotAvatar")); + vm.expectRevert("Only Avatar can call this function"); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18); + } + + function test_mintRewardFromReserveRatio_whenToIsZero_shouldRevert() public { + vm.prank(avatarAddress); + vm.expectRevert("Recipient address must be set"); + expansionController.mintRewardFromReserveRatio(exchangeId, address(0), 1000e18); + } + + function test_mintRewardFromReserveRatio_whenAmountIs0_shouldRevert() public { + vm.prank(avatarAddress); + vm.expectRevert("Amount must be greater than 0"); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); + } + + function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + address to = makeAddr("To"); + uint256 toBalanceBefore = token.balanceOf(to); + + vm.expectEmit(true, true, true, true); + emit RewardMinted(exchangeId, to, amountToMint); + + vm.prank(avatarAddress); + expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint); + + assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); + } +} diff --git a/test/unit/libraries/TradingLimits.t.sol b/test/unit/libraries/TradingLimits.t.sol index 136b2bc2..878fdf24 100644 --- a/test/unit/libraries/TradingLimits.t.sol +++ b/test/unit/libraries/TradingLimits.t.sol @@ -6,7 +6,7 @@ pragma solidity ^0.8; import { Test } from "mento-std/Test.sol"; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; -import { ITradingLimitsHarness } from "test/utils/harnesses/ITradingLimitsHarness.sol"; +import { TradingLimitsHarness } from "test/utils/harnesses/TradingLimitsHarness.sol"; // forge test --match-contract TradingLimits -vvv contract TradingLimitsTest is Test { @@ -15,7 +15,7 @@ contract TradingLimitsTest is Test { uint8 private constant LG = 4; // 0b100 ITradingLimits.State private state; - ITradingLimitsHarness private harness; + TradingLimitsHarness private harness; function configEmpty() internal pure returns (ITradingLimits.Config memory config) {} @@ -87,7 +87,7 @@ contract TradingLimitsTest is Test { } function setUp() public { - harness = ITradingLimitsHarness(deployCode("TradingLimitsHarness")); + harness = new TradingLimitsHarness(); } /* ==================== Config#validate ==================== */ @@ -286,11 +286,16 @@ contract TradingLimitsTest is Test { assertEq(state.netflowGlobal, 100); } - function test_update_withSubUnitAmounts_updatesAs1() public { + function test_update_withPositiveSubUnitAmounts_updatesAs1() public { state = harness.update(state, configLG(500000), 1e6, 18); assertEq(state.netflowGlobal, 1); } + function test_update_withNegativeSubUnitAmounts_updatesAsMinus1() public { + state = harness.update(state, configLG(500000), -1e6, 18); + assertEq(state.netflowGlobal, -1); + } + function test_update_withTooLargeAmount_reverts() public { vm.expectRevert(bytes("dFlow too large")); state = harness.update(state, configLG(500000), 3 * 10e32, 18); diff --git a/test/unit/swap/Broker.t.sol b/test/unit/swap/Broker.t.sol index 06ef2d80..98a57893 100644 --- a/test/unit/swap/Broker.t.sol +++ b/test/unit/swap/Broker.t.sol @@ -12,7 +12,7 @@ import { TestERC20 } from "test/utils/mocks/TestERC20.sol"; import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; import { IStableTokenV2 } from "contracts/interfaces/IStableTokenV2.sol"; -import { IBroker } from "contracts/interfaces/IBroker.sol"; +import { Broker } from "contracts/swap/Broker.sol"; import { ITradingLimits } from "contracts/interfaces/ITradingLimits.sol"; // forge test --match-contract Broker -vvv @@ -40,24 +40,27 @@ contract BrokerTest is Test { address randomAsset = makeAddr("randomAsset"); MockReserve reserve; + MockReserve reserve1; TestERC20 stableAsset; TestERC20 collateralAsset; - IBroker broker; + Broker broker; MockExchangeProvider exchangeProvider; address exchangeProvider1 = makeAddr("exchangeProvider1"); address exchangeProvider2 = makeAddr("exchangeProvider2"); address[] public exchangeProviders; + address[] public reserves; function setUp() public virtual { /* Dependencies and makeAddrs */ reserve = new MockReserve(); + reserve1 = new MockReserve(); collateralAsset = new TestERC20("Collateral", "CL"); stableAsset = new TestERC20("StableAsset", "SA0"); randomAsset = makeAddr("randomAsset"); - broker = IBroker(deployCode("Broker", abi.encode(true))); + broker = new Broker(true); exchangeProvider = new MockExchangeProvider(); reserve.addToken(address(stableAsset)); @@ -66,7 +69,12 @@ contract BrokerTest is Test { exchangeProviders.push(exchangeProvider1); exchangeProviders.push(exchangeProvider2); exchangeProviders.push((address(exchangeProvider))); - broker.initialize(exchangeProviders, address(reserve)); + reserves.push(address(reserve)); + reserves.push(address(reserve)); + reserves.push(address(reserve)); + + vm.prank(deployer); + broker.initialize(exchangeProviders, reserves); } } @@ -74,15 +82,16 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { /* ---------- Initilizer ---------- */ function test_initilize_shouldSetOwner() public view { - assertEq(broker.owner(), address(this)); + assertEq(broker.owner(), deployer); } function test_initilize_shouldSetExchangeProviderAddresseses() public view { assertEq(broker.getExchangeProviders(), exchangeProviders); } - - function test_initilize_shouldSetReserve() public view { - assertEq(address(broker.reserve()), address(reserve)); + function test_initilize_shouldSetReserves() public view { + assertEq(address(broker.exchangeReserve(exchangeProvider1)), address(reserve)); + assertEq(address(broker.exchangeReserve(exchangeProvider2)), address(reserve)); + assertEq(address(broker.exchangeReserve(address(exchangeProvider))), address(reserve)); } /* ---------- Setters ---------- */ @@ -90,43 +99,62 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { function test_addExchangeProvider_whenSenderIsNotOwner_shouldRevert() public { vm.expectRevert("Ownable: caller is not the owner"); vm.prank(notDeployer); - broker.addExchangeProvider(address(0)); + broker.addExchangeProvider(address(0), address(0)); } - function test_addExchangeProvider_whenAddressIsZero_shouldRevert() public { + function test_addExchangeProvider_whenExchangeProviderAddressIsZero_shouldRevert() public { vm.expectRevert("ExchangeProvider address can't be 0"); - broker.addExchangeProvider(address(0)); + vm.prank(deployer); + broker.addExchangeProvider(address(0), address(reserve)); + } + + function test_addExchangeProvider_whenReserveAddressIsZero_shouldRevert() public { + changePrank(deployer); + vm.expectRevert("Reserve address can't be 0"); + broker.addExchangeProvider(makeAddr("newExchangeProvider"), address(0)); } function test_addExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { address newExchangeProvider = makeAddr("newExchangeProvider"); - vm.expectEmit(true, false, false, false); + + vm.expectEmit(true, true, true, true); emit ExchangeProviderAdded(newExchangeProvider); - broker.addExchangeProvider(newExchangeProvider); + vm.expectEmit(true, true, true, true); + emit ReserveSet(newExchangeProvider, address(reserve1)); + + vm.prank(deployer); + broker.addExchangeProvider(newExchangeProvider, address(reserve1)); + address[] memory updatedExchangeProviders = broker.getExchangeProviders(); assertEq(updatedExchangeProviders[updatedExchangeProviders.length - 1], newExchangeProvider); assertEq(broker.isExchangeProvider(newExchangeProvider), true); + assertEq(broker.exchangeReserve(newExchangeProvider), address(reserve1)); } function test_addExchangeProvider_whenAlreadyAdded_shouldRevert() public { vm.expectRevert("ExchangeProvider already exists in the list"); - broker.addExchangeProvider(address(exchangeProvider)); + vm.prank(deployer); + broker.addExchangeProvider(address(exchangeProvider), address(reserve1)); } function test_removeExchangeProvider_whenSenderIsOwner_shouldUpdateAndEmit() public { vm.expectEmit(true, true, true, true); emit ExchangeProviderRemoved(exchangeProvider1); + vm.prank(deployer); broker.removeExchangeProvider(exchangeProvider1, 0); assert(broker.getExchangeProviders()[0] != exchangeProvider1); + assertEq(broker.exchangeReserve(exchangeProvider1), address(0)); } function test_removeExchangeProvider_whenAddressDoesNotExist_shouldRevert() public { vm.expectRevert("index doesn't match provider"); + vm.prank(deployer); broker.removeExchangeProvider(notDeployer, 1); } function test_removeExchangeProvider_whenIndexOutOfRange_shouldRevert() public { vm.expectRevert("index doesn't match provider"); + vm.prank(deployer); broker.removeExchangeProvider(exchangeProvider1, 1); } @@ -136,24 +164,48 @@ contract BrokerTest_initilizerAndSetters is BrokerTest { broker.removeExchangeProvider(exchangeProvider1, 0); } - function test_setReserve_whenSenderIsNotOwner_shouldRevert() public { + function test_setReserves_whenSenderIsNotOwner_shouldRevert() public { vm.prank(notDeployer); vm.expectRevert("Ownable: caller is not the owner"); - broker.setReserve(address(0)); + broker.setReserves(new address[](0), new address[](0)); + } + + function test_setReserves_whenExchangeProviderIsNotAdded_shouldRevert() public { + address[] memory exchangeProviders = new address[](1); + exchangeProviders[0] = makeAddr("newExchangeProvider"); + address[] memory reserves = new address[](1); + reserves[0] = makeAddr("newReserve"); + changePrank(deployer); + vm.expectRevert("ExchangeProvider does not exist"); + broker.setReserves(exchangeProviders, reserves); } - function test_setReserve_whenAddressIsZero_shouldRevert() public { - vm.expectRevert("Reserve address must be set"); - broker.setReserve(address(0)); + function test_setReserves_whenReserveAddressIsZero_shouldRevert() public { + address[] memory exchangeProviders = new address[](1); + exchangeProviders[0] = exchangeProvider1; + address[] memory reserves = new address[](1); + reserves[0] = address(0); + changePrank(deployer); + vm.expectRevert("Reserve address can't be 0"); + broker.setReserves(exchangeProviders, reserves); } - function test_setReserve_whenSenderIsOwner_shouldUpdateAndEmit() public { - address newReserve = makeAddr("newReserve"); - vm.expectEmit(true, false, false, false); - emit ReserveSet(newReserve, address(reserve)); + function test_setReserves_whenSenderIsOwner_shouldUpdateAndEmit() public { + address[] memory exchangeProviders = new address[](2); + exchangeProviders[0] = exchangeProvider1; + exchangeProviders[1] = exchangeProvider2; - broker.setReserve(newReserve); - assertEq(address(broker.reserve()), newReserve); + address[] memory reserves = new address[](2); + reserves[0] = makeAddr("newReserve"); + reserves[1] = makeAddr("newReserve2"); + changePrank(deployer); + vm.expectEmit(true, true, true, true); + emit ReserveSet(exchangeProvider1, reserves[0]); + vm.expectEmit(true, true, true, true); + emit ReserveSet(exchangeProvider2, reserves[1]); + broker.setReserves(exchangeProviders, reserves); + assertEq(address(broker.exchangeReserve(address(exchangeProvider1))), reserves[0]); + assertEq(address(broker.exchangeReserve(address(exchangeProvider2))), reserves[1]); } } @@ -173,27 +225,135 @@ contract BrokerTest_getAmounts is BrokerTest { function test_getAmountIn_whenExchangeProviderWasNotSet_shouldRevert() public { vm.expectRevert("ExchangeProvider does not exist"); - broker.getAmountIn(randomExchangeProvider, exchangeId, address(stableAsset), address(collateralAsset), 1e24); + broker.getAmountIn({ + exchangeProvider: randomExchangeProvider, + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: 1e24 + }); + } + + function test_getAmountIn_whenReserveBalanceIsLessThanAmountOut_shouldRevert() public { + assertEq(collateralAsset.balanceOf(address(reserve)), 0); + vm.expectRevert("Insufficient balance in reserve"); + broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: 1e24 + }); + } + + function test_getAmountIn_whenReserveBalanceIsEqualToAmountOut_shouldReturnAmountIn() public { + uint256 amountOut = 1e18; + collateralAsset.mint(address(reserve), amountOut); + + uint256 amountIn = broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: amountOut + }); + + assertEq(amountIn, 25e17); } - function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public view { - uint256 amountIn = broker.getAmountIn( + function test_getAmountIn_whenReserveBalanceIsLargerThanAmountOut_shouldReturnAmountIn() public { + uint256 amountOut = 1e18; + collateralAsset.mint(address(reserve), 1000e18); + + uint256 amountIn = broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: amountOut + }); + + assertEq(amountIn, 25e17); + } + + function test_getAmountIn_whenExchangeProviderIsSet_shouldReceiveCall() public { + collateralAsset.mint(address(reserve), 1000e18); + vm.expectCall( address(exchangeProvider), - exchangeId, - address(stableAsset), - address(collateralAsset), - 1e18 + abi.encodeWithSelector( + exchangeProvider.getAmountIn.selector, + exchangeId, + address(stableAsset), + address(collateralAsset), + 1e18 + ) ); + uint256 amountIn = broker.getAmountIn({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountOut: 1e18 + }); assertEq(amountIn, 25e17); } function test_getAmountOut_whenExchangeProviderWasNotSet_shouldRevert() public { vm.expectRevert("ExchangeProvider does not exist"); - broker.getAmountOut(randomExchangeProvider, exchangeId, randomAsset, randomAsset, 1e24); + broker.getAmountOut({ + exchangeProvider: randomExchangeProvider, + exchangeId: exchangeId, + tokenIn: randomAsset, + tokenOut: randomAsset, + amountIn: 1e24 + }); + } + + function test_getAmountOut_whenReserveBalanceIsLessThanAmountOut_shouldRevert() public { + assertEq(collateralAsset.balanceOf(address(reserve)), 0); + vm.expectRevert("Insufficient balance in reserve"); + broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: 1e24 + }); + } + + function test_getAmountOut_whenReserveBalanceIsEqualAmountOut_shouldReturnAmountIn() public { + uint256 amountIn = 1e18; + collateralAsset.mint(address(reserve), amountIn); + + uint256 amountOut = broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: amountIn + }); + + assertEq(amountOut, 4e17); + } + + function test_getAmountOut_whenReserveBalanceIsLargerThanAmountOut_shouldReturnAmountIn() public { + uint256 amountIn = 1e18; + collateralAsset.mint(address(reserve), 1000e18); + + uint256 amountOut = broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: amountIn + }); + + assertEq(amountOut, 4e17); } function test_getAmountOut_whenExchangeProviderIsSet_shouldReceiveCall() public { + collateralAsset.mint(address(reserve), 1000e18); vm.expectCall( address(exchangeProvider), abi.encodeWithSelector( @@ -205,13 +365,13 @@ contract BrokerTest_getAmounts is BrokerTest { ) ); - uint256 amountOut = broker.getAmountOut( - address(exchangeProvider), - exchangeId, - address(stableAsset), - address(collateralAsset), - 1e18 - ); + uint256 amountOut = broker.getAmountOut({ + exchangeProvider: address(exchangeProvider), + exchangeId: exchangeId, + tokenIn: address(stableAsset), + tokenOut: address(collateralAsset), + amountIn: 1e18 + }); assertEq(amountOut, 4e17); } } @@ -219,12 +379,6 @@ contract BrokerTest_getAmounts is BrokerTest { contract BrokerTest_BurnStableTokens is BrokerTest { uint256 burnAmount = 1; - function test_burnStableTokens_whenTokenIsNotReserveStable_shouldRevert() public { - vm.prank(notDeployer); - vm.expectRevert("Token must be a reserve stable asset"); - broker.burnStableTokens(randomAsset, 2); - } - function test_burnStableTokens_whenTokenIsAReserveStable_shouldBurnAndEmit() public { stableAsset.mint(notDeployer, 2); vm.prank(notDeployer); @@ -488,6 +642,7 @@ contract BrokerTest_swap is BrokerTest { vm.expectEmit(true, true, true, true); emit TradingLimitConfigured(exchangeId, address(stableAsset), config); + vm.prank(deployer); broker.configureTradingLimit(exchangeId, address(stableAsset), config); vm.prank(trader); @@ -504,6 +659,7 @@ contract BrokerTest_swap is BrokerTest { vm.expectEmit(true, true, true, true); emit TradingLimitConfigured(exchangeId, address(stableAsset), config); + vm.prank(deployer); broker.configureTradingLimit(exchangeId, address(stableAsset), config); vm.expectRevert(bytes("L0 Exceeded")); diff --git a/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol b/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol new file mode 100644 index 00000000..ad6f080f --- /dev/null +++ b/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; +// solhint-disable func-name-mixedcase + +import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; + +contract GoodDollarExpansionControllerHarness is GoodDollarExpansionController { + constructor(bool disabled) GoodDollarExpansionController(disabled) {} + + function exposed_getReserveRatioScalar(ExchangeExpansionConfig calldata config) external returns (uint256) { + return _getReserveRatioScalar(config); + } +} diff --git a/test/utils/harnesses/TradingLimitsHarness.sol b/test/utils/harnesses/TradingLimitsHarness.sol index 723ac7ae..047a0f22 100644 --- a/test/utils/harnesses/TradingLimitsHarness.sol +++ b/test/utils/harnesses/TradingLimitsHarness.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.5.13; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; import { TradingLimits } from "contracts/libraries/TradingLimits.sol"; @@ -10,18 +10,18 @@ contract TradingLimitsHarness is ITradingLimitsHarness { using TradingLimits for ITradingLimits.State; using TradingLimits for ITradingLimits.Config; - function validate(ITradingLimits.Config memory config) public view { + function validate(ITradingLimits.Config memory config) public pure { return config.validate(); } - function verify(ITradingLimits.State memory state, ITradingLimits.Config memory config) public view { + function verify(ITradingLimits.State memory state, ITradingLimits.Config memory config) public pure { return state.verify(config); } function reset( ITradingLimits.State memory state, ITradingLimits.Config memory config - ) public view returns (ITradingLimits.State memory) { + ) public pure returns (ITradingLimits.State memory) { return state.reset(config); } From d198a380622ae4ec752e919e0d83c534d0c0b36e Mon Sep 17 00:00:00 2001 From: Nelson Taveras <4562733+nvtaveras@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:50:08 +0100 Subject: [PATCH 20/38] feat: add cCOP fork tests (#537) --- test/fork/BaseForkTest.sol | 13 +++++++----- test/fork/ForkTests.t.sol | 8 ++++++-- .../GoodDollarExchangeProvider.t.sol | 20 +++++++++---------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/test/fork/BaseForkTest.sol b/test/fork/BaseForkTest.sol index 3bda5146..51f804ad 100644 --- a/test/fork/BaseForkTest.sol +++ b/test/fork/BaseForkTest.sol @@ -9,6 +9,7 @@ import { FixidityLib } from "celo/contracts/common/FixidityLib.sol"; // Interfaces import { IBiPoolManager } from "contracts/interfaces/IBiPoolManager.sol"; import { IBreakerBox } from "contracts/interfaces/IBreakerBox.sol"; +import { IERC20 } from "contracts/interfaces/IERC20.sol"; import { IBroker } from "contracts/interfaces/IBroker.sol"; import { ICeloProxy } from "contracts/interfaces/ICeloProxy.sol"; import { IOwnable } from "contracts/interfaces/IOwnable.sol"; @@ -119,14 +120,16 @@ abstract contract BaseForkTest is Test { function mint(address asset, address to, uint256 amount, bool updateSupply) public { if (asset == lookup("GoldToken")) { - if (!updateSupply) { - revert("BaseForkTest: can't mint GoldToken without updating supply"); - } - vm.prank(address(0)); - IMint(asset).mint(to, amount); + // with L2 Celo, we need to transfer GoldToken to the user manually from the reserve + transferCeloFromReserve(to, amount); return; } deal(asset, to, amount, updateSupply); } + + function transferCeloFromReserve(address to, uint256 amount) internal { + vm.prank(address(mentoReserve)); + IERC20(lookup("GoldToken")).transfer(to, amount); + } } diff --git a/test/fork/ForkTests.t.sol b/test/fork/ForkTests.t.sol index d3ab0b48..c47de510 100644 --- a/test/fork/ForkTests.t.sol +++ b/test/fork/ForkTests.t.sol @@ -45,7 +45,7 @@ import { GoodDollarTradingLimitsForkTest } from "./GoodDollar/TradingLimitsForkT import { GoodDollarSwapForkTest } from "./GoodDollar/SwapForkTest.sol"; import { GoodDollarExpansionForkTest } from "./GoodDollar/ExpansionForkTest.sol"; -contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(15)) {} +contract Alfajores_ChainForkTest is ChainForkTest(ALFAJORES_ID, 1, uints(16)) {} contract Alfajores_P0E00_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 0) {} @@ -77,7 +77,9 @@ contract Alfajores_P0E13_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 1 contract Alfajores_P0E14_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 14) {} -contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(15)) {} +contract Alfajores_P0E15_ExchangeForkTest is ExchangeForkTest(ALFAJORES_ID, 0, 15) {} + +contract Celo_ChainForkTest is ChainForkTest(CELO_ID, 1, uints(16)) {} contract Celo_P0E00_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 0) {} @@ -109,6 +111,8 @@ contract Celo_P0E13_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 13) {} contract Celo_P0E14_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 14) {} +contract Celo_P0E15_ExchangeForkTest is ExchangeForkTest(CELO_ID, 0, 15) {} + contract Celo_BancorExchangeProviderForkTest is BancorExchangeProviderForkTest(CELO_ID) {} contract Celo_GoodDollarTradingLimitsForkTest is GoodDollarTradingLimitsForkTest(CELO_ID) {} diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index c87f5418..96eb9780 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -472,20 +472,20 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); } - function testFuzz_mintFromExpansion(uint256 reserveRatioScalar) public { + function testFuzz_mintFromExpansion(uint256 _reserveRatioScalar) public { // 0.001% to 100% - reserveRatioScalar = bound(reserveRatioScalar, 1e18 * 0.00001, 1e18); + _reserveRatioScalar = bound(_reserveRatioScalar, 1e18 * 0.00001, 1e18); uint256 initialTokenSupply = poolExchange.tokenSupply; uint32 initialReserveRatio = poolExchange.reserveRatio; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); - uint256 expectedReserveRatio = (uint256(initialReserveRatio) * reserveRatioScalar) / 1e18; + uint256 expectedReserveRatio = (uint256(initialReserveRatio) * _reserveRatioScalar) / 1e18; vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, uint32(expectedReserveRatio)); vm.prank(expansionControllerAddress); - uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); + uint256 amountToMint = exchangeProvider.mintFromExpansion(exchangeId, _reserveRatioScalar); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -720,7 +720,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan } function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public { - uint256 reward = 1e18; // 1 token + uint256 _reward = 1e18; // 1 token // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) // reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799 uint32 expectedReserveRatio = 28571427; @@ -729,7 +729,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, _reward); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -737,14 +737,14 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); assertEq( poolExchangeAfter.tokenSupply, - poolExchange.tokenSupply + reward, + poolExchange.tokenSupply + _reward, "Token supply should increase by reward amount" ); assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); } function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public { - uint256 reward = 1_000_000_000 * 1e18; // 1 billion tokens + uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) // reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000... @@ -754,7 +754,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, _reward); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -762,7 +762,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); assertEq( poolExchangeAfter.tokenSupply, - poolExchange.tokenSupply + reward, + poolExchange.tokenSupply + _reward, "Token supply should increase by reward amount" ); assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); From c9e2d5eab26de848ee70a422190a0da0c19f7045 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:16:38 +0100 Subject: [PATCH 21/38] chore: add missing getter function from Reserve to interface (#541) --- contracts/interfaces/IReserve.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/interfaces/IReserve.sol b/contracts/interfaces/IReserve.sol index 553e28b8..8dd31f07 100644 --- a/contracts/interfaces/IReserve.sol +++ b/contracts/interfaces/IReserve.sol @@ -125,4 +125,6 @@ interface IReserve { function removeOtherReserveAddress(address otherReserveAddress, uint256 index) external returns (bool); function collateralAssets(uint256 index) external view returns (address); + + function collateralAssetLastSpendingDay(address collateralAsset) external view returns (uint256); } From 84d9d42894cafa23a444e9da74a155aa154a2872 Mon Sep 17 00:00:00 2001 From: baroooo Date: Fri, 22 Nov 2024 16:54:51 +0100 Subject: [PATCH 22/38] feat: fix add missing min check (#547) ### Description Fixes the audit finding https://audits.sherlock.xyz/contests/598/voting/44?dashboard_id=14327c9c7bc843a43c46c4efd899f4c1 ### Other changes no ### Tested Unit test ### Related issues - Fixes [#597](https://github.com/mento-protocol/mento-general/issues/597) --- contracts/libraries/TradingLimits.sol | 2 ++ test/unit/libraries/TradingLimits.t.sol | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/contracts/libraries/TradingLimits.sol b/contracts/libraries/TradingLimits.sol index 14e59549..dfde4520 100644 --- a/contracts/libraries/TradingLimits.sol +++ b/contracts/libraries/TradingLimits.sol @@ -45,6 +45,7 @@ library TradingLimits { uint8 private constant L1 = 2; // 0b010 Limit1 uint8 private constant LG = 4; // 0b100 LimitGlobal int48 private constant MAX_INT48 = type(int48).max; + int48 private constant MIN_INT48 = type(int48).min; /** * @notice Validate a trading limit configuration. @@ -129,6 +130,7 @@ library TradingLimits { ) internal view returns (ITradingLimits.State memory) { int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); + require(_deltaFlowUnits >= MIN_INT48, "dFlow too small"); int48 deltaFlowUnits = int48(_deltaFlowUnits); if (deltaFlowUnits == 0) { diff --git a/test/unit/libraries/TradingLimits.t.sol b/test/unit/libraries/TradingLimits.t.sol index 878fdf24..fc99aa2d 100644 --- a/test/unit/libraries/TradingLimits.t.sol +++ b/test/unit/libraries/TradingLimits.t.sol @@ -301,6 +301,12 @@ contract TradingLimitsTest is Test { state = harness.update(state, configLG(500000), 3 * 10e32, 18); } + function test_update_withTooSmallAmount_reverts() public { + int256 tooSmall = (type(int48).min - int256(1)) * 1e18; + vm.expectRevert(bytes("dFlow too small")); + state = harness.update(state, configLG(500000), tooSmall, 18); + } + function test_update_withOverflowOnAdd_reverts() public { ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); int256 maxFlow = int256(uint256(type(uint48).max / 2)); From 9f7ee3c4d286da9422a23e92cf22845a1eb3726d Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:55:40 +0100 Subject: [PATCH 23/38] feat: ensure exitContribution is less than 100% (#549) --- contracts/goodDollar/BancorExchangeProvider.sol | 2 +- test/unit/goodDollar/BancorExchangeProvider.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 95391152..930e2ab5 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -262,7 +262,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B function _setExitContribution(bytes32 exchangeId, uint32 exitContribution) internal { require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); - require(exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); + require(exitContribution < MAX_WEIGHT, "Exit contribution is too high"); PoolExchange storage exchange = exchanges[exchangeId]; exchange.exitContribution = exitContribution; diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index e3d19b53..3781e191 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -164,12 +164,12 @@ contract BancorExchangeProviderTest_initilizerSettersGetters is BancorExchangePr bancorExchangeProvider.setExitContribution(exchangeId, 1e5); } - function test_setExitContribution_whenExitContributionAbove100Percent_shouldRevert() public { + function test_setExitContribution_whenExitContributionIsNotLessThan100Percent_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint32 maxWeight = bancorExchangeProvider.MAX_WEIGHT(); vm.expectRevert("Exit contribution is too high"); - bancorExchangeProvider.setExitContribution(exchangeId, maxWeight + 1); + bancorExchangeProvider.setExitContribution(exchangeId, maxWeight); } function test_setExitContribution_whenSenderIsOwner_shouldUpdateAndEmit() public { From f3219158316f81e65011dabb0e8ef3d2b2371f1b Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:56:41 +0100 Subject: [PATCH 24/38] chore: remove Pausable (#552) --- contracts/goodDollar/GoodDollarExpansionController.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index f753d060..44d12977 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -8,7 +8,6 @@ import { IERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/IERC20 import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; -import { PausableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/security/PausableUpgradeable.sol"; import { OwnableUpgradeable } from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; @@ -16,7 +15,7 @@ import { unwrap, wrap, powu } from "prb/math/UD60x18.sol"; * @title GoodDollarExpansionController * @notice Provides functionality to expand the supply of GoodDollars. */ -contract GoodDollarExpansionController is IGoodDollarExpansionController, PausableUpgradeable, OwnableUpgradeable { +contract GoodDollarExpansionController is IGoodDollarExpansionController, OwnableUpgradeable { /* ========================================================= */ /* ==================== State Variables ==================== */ /* ========================================================= */ @@ -63,7 +62,6 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Pausab address _reserve, address _avatar ) public initializer { - __Pausable_init(); __Ownable_init(); setGoodDollarExchangeProvider(_goodDollarExchangeProvider); From d51019c8ce9685f0b8273a1cf40dcd5d326fa8d9 Mon Sep 17 00:00:00 2001 From: Ryan Noble Date: Fri, 22 Nov 2024 16:58:16 +0100 Subject: [PATCH 25/38] fix: Removed redundant require (#546) --- contracts/goodDollar/BancorExchangeProvider.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 930e2ab5..069ec197 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -144,7 +144,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B /// @inheritdoc IBancorExchangeProvider function currentPrice(bytes32 exchangeId) public view returns (uint256 price) { // calculates: reserveBalance / (tokenSupply * reserveRatio) - require(exchanges[exchangeId].reserveAsset != address(0), "Exchange does not exist"); PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); From b14b3e6f0e429d727cde6ae1321bfeeadac561e5 Mon Sep 17 00:00:00 2001 From: Ryan Noble Date: Mon, 25 Nov 2024 16:33:13 +0100 Subject: [PATCH 26/38] fix: $GD resolved large reward issue (#544) --- contracts/goodDollar/GoodDollarExchangeProvider.sol | 2 ++ test/unit/goodDollar/GoodDollarExchangeProvider.t.sol | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index b744fcc7..af6b3e0c 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -203,6 +203,8 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan uint256 newRatioScaled = unwrap(numerator.div(denominator)); uint32 newRatioUint = uint32(newRatioScaled / 1e10); + require(newRatioUint > 0, "New ratio must be greater than 0"); + exchanges[exchangeId].reserveRatio = newRatioUint; exchanges[exchangeId].tokenSupply += rewardScaled; diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index 96eb9780..eb075cab 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -683,6 +683,15 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan exchangeId = exchangeProvider.createExchange(poolExchange); } + function test_updateRatioForReward_whenNewRatioIsZero_shouldRevert() public { + // Use a very large reward that will make the denominator massive compared to numerator + uint256 veryLargeReward = type(uint256).max / 1e20; // Large but not large enough to overflow + + vm.expectRevert("New ratio must be greater than 0"); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward); + } + function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public { vm.prank(makeAddr("NotExpansionController")); vm.expectRevert("Only ExpansionController can call this function"); From 6295656d4f7727c061696e9056dc678229fe6237 Mon Sep 17 00:00:00 2001 From: Ryan Noble Date: Mon, 25 Nov 2024 16:34:42 +0100 Subject: [PATCH 27/38] fix: Typo redundant casting (#545) --- contracts/goodDollar/BancorExchangeProvider.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 069ec197..30863f27 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -164,9 +164,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B /// @inheritdoc IBancorExchangeProvider function setReserve(address _reserve) public onlyOwner { - require(address(_reserve) != address(0), "Reserve address must be set"); + require(_reserve != address(0), "Reserve address must be set"); reserve = IReserve(_reserve); - emit ReserveUpdated(address(_reserve)); + emit ReserveUpdated(_reserve); } /// @inheritdoc IBancorExchangeProvider From ea5d779dada37bddc24f70f024a3055db7cb67e0 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:35:14 +0100 Subject: [PATCH 28/38] feat: fix expansion rounding issue (#548) --- .../goodDollar/GoodDollarExchangeProvider.sol | 7 +++- .../GoodDollarExchangeProvider.t.sol | 39 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index af6b3e0c..5b62537c 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -144,7 +144,12 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan PoolExchange memory exchange = getPoolExchange(exchangeId); UD60x18 scaledRatio = wrap(uint256(exchange.reserveRatio) * 1e10); - UD60x18 newRatio = scaledRatio.mul(wrap(reserveRatioScalar)); + + // The division and multiplication by 1e10 here ensures that the new ratio used for calculating the amount to mint + // is the same as the one set in the exchange but only scaled to 18 decimals. + // Ignored, because the division and multiplication by 1e10 is needed see comment above. + // slither-disable-next-line divide-before-multiply + UD60x18 newRatio = wrap((unwrap(scaledRatio.mul(wrap(reserveRatioScalar))) / 1e10) * 1e10); uint32 newRatioUint = uint32(unwrap(newRatio) / 1e10); require(newRatioUint > 0, "New ratio must be greater than 0"); diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index eb075cab..6fceeaa2 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -349,11 +349,12 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP function test_mintFromExpansion_whenValidReserveRatioScalar_shouldReturnCorrectAmountAndEmit() public { // reserveRatioScalar is (1-0.000288617289022312) based of 10% yearly expansion rate // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) = 0.285631817919071438 - // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285631817919071438) / 0.285631817919071438 - // ≈ 2_020_904,291074052815139287 + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1-0.000288617289022312) + // newRatio = 0.28563181 (only 8 decimals) + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.28563181) / 0.28563181 + // ≈ 2_021_098,420375517698816528 uint32 expectedReserveRatio = 28563181; - uint256 expectedAmountToMint = 2020904291074052815139287; + uint256 expectedAmountToMint = 2021098420375517698816528; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); @@ -371,18 +372,17 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP "Token supply should increase by minted amount" ); assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); - // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } function test_mintFromExpansion_withSmallReserveRatioScalar_shouldReturnCorrectAmount() public { uint256 smallReserveRatioScalar = 1e18 * 0.00001; // 0.001% // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.0000028571428 - // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.0000028571428) /0.0000028571428 - // amountToMint ≈ 699993000000000 + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * 1e13/1e18 = 0.00000285 (only 8 decimals) + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.00000285) /0.00000285 + // amountToMint ≈ 701.747.371.929.824,561403508771929824 uint32 expectedReserveRatio = 285; - uint256 expectedAmountToMint = 699993000000000 * 1e18; + uint256 expectedAmountToMint = 701747371929824561403508771929824; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); @@ -400,18 +400,17 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP "Token supply should increase by minted amount" ); assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); - // 1% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } function test_mintFromExpansion_withLargeReserveRatioScalar_shouldReturnCorrectAmount() public { uint256 largeReserveRatioScalar = 1e18 - 1; // Just below 100% // Formula: amountToMint = (tokenSupply * reserveRatio - tokenSupply * newRatio) / newRatio - // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.285714279999999999 - // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.285714279999999999) /0.285714279999999999 - // amountToMint ≈ 0.00000002450000049000 + // newRatio = reserveRatio * reserveRatioScalar = 0.28571428 * (1e18 -1)/1e18 ≈ 0.28571427 (only 8 decimals) + // amountToMint = (7_000_000_000 * 0.28571428 - 7_000_000_000 * 0.28571427) /0.28571427 + // amountToMint ≈ 245.00001347500074112504 uint32 expectedReserveRatio = 28571427; - uint256 expectedAmountToMint = 24500000490; + uint256 expectedAmountToMint = 245000013475000741125; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); @@ -429,8 +428,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP "Token supply should increase by minted amount" ); assertEq(poolExchangeAfter.reserveRatio, expectedReserveRatio, "Reserve ratio should be updated correctly"); - // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } function test_mintFromExpansion_withMultipleConsecutiveExpansions_shouldMintCorrectly() public { @@ -469,7 +467,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP 1e18 * 0.0001, // 0.01% relative error tolerance because of precision loss when new reserve ratio is calculated "Reserve ratio should be updated correctly within 0.01% tolerance" ); - assertApproxEqRel(initialPrice, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); + assertEq(initialPrice, priceAfter, "Price should remain unchanged"); } function testFuzz_mintFromExpansion(uint256 _reserveRatioScalar) public { @@ -497,8 +495,7 @@ contract GoodDollarExchangeProviderTest_mintFromExpansion is GoodDollarExchangeP initialTokenSupply + amountToMint, "Token supply should increase by minted amount" ); - // 1% relative error tolerance because of precision loss when new reserve ratio is calculated - assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.01, "Price should remain within 1% of initial price"); + assertEq(priceBefore, priceAfter, "Price should remain unchanged"); } } From 161ec082dcfc4468d0da8ac084f2dd8ec5c54ad6 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:37:26 +0100 Subject: [PATCH 29/38] fix: ensure 0 deltaFlow doesn't update state (#550) --- contracts/libraries/TradingLimits.sol | 4 ++++ test/unit/libraries/TradingLimits.t.sol | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/contracts/libraries/TradingLimits.sol b/contracts/libraries/TradingLimits.sol index dfde4520..557d23a6 100644 --- a/contracts/libraries/TradingLimits.sol +++ b/contracts/libraries/TradingLimits.sol @@ -128,6 +128,10 @@ library TradingLimits { int256 _deltaFlow, uint8 decimals ) internal view returns (ITradingLimits.State memory) { + if (_deltaFlow == 0) { + return self; + } + int256 _deltaFlowUnits = _deltaFlow / int256((10 ** uint256(decimals))); require(_deltaFlowUnits <= MAX_INT48, "dFlow too large"); require(_deltaFlowUnits >= MIN_INT48, "dFlow too small"); diff --git a/test/unit/libraries/TradingLimits.t.sol b/test/unit/libraries/TradingLimits.t.sol index fc99aa2d..66432d69 100644 --- a/test/unit/libraries/TradingLimits.t.sol +++ b/test/unit/libraries/TradingLimits.t.sol @@ -259,6 +259,13 @@ contract TradingLimitsTest is Test { assertEq(state.netflowGlobal, 0); } + function test_update_withZeroDeltaFlow_doesNotUpdate() public { + state = harness.update(state, configL0L1LG(300, 1000, 1 days, 10000, 1000000), 0, 18); + assertEq(state.netflow0, 0); + assertEq(state.netflow1, 0); + assertEq(state.netflowGlobal, 0); + } + function test_update_withL0_updatesActive() public { state = harness.update(state, configL0(500, 1000), 100 * 1e18, 18); assertEq(state.netflow0, 100); From e840fa38d6fe1a08d7d2d6fd87a3e919d2cb73ac Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:15:37 +0100 Subject: [PATCH 30/38] fix: ensure scaling for different reserveAsset token decimals (#551) ### Description This PR adds scaling to the reserve balance read in mintUBIFromReserveBalance in Case the token has less than 18 decimals. ### Other changes ### Tested - added test verifying expected behavior for token with 6 decimals ### Related issues ### Backwards compatibility ### Documentation --- .../GoodDollarExpansionController.sol | 5 ++- .../GoodDollarExpansionController.t.sol | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index 44d12977..402e3343 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -5,6 +5,7 @@ import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollar import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; import { IERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "openzeppelin-contracts-next/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IGoodDollar } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; @@ -152,7 +153,9 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); - uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve); + uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve) * + (10 ** (18 - IERC20Metadata(exchange.reserveAsset).decimals())); + uint256 additionalReserveBalance = contractReserveBalance - exchange.reserveBalance; if (additionalReserveBalance > 0) { amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance); diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol index 4e46e620..974843a5 100644 --- a/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -5,6 +5,7 @@ pragma solidity 0.8.18; import { Test } from "forge-std/Test.sol"; import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; +import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; @@ -326,6 +327,37 @@ contract GoodDollarExpansionControllerTest_mintUBIFromReserveBalance is GoodDoll assertEq(amountMinted, 0); } + function test_mintUBIFromReserveBalance_whenReserveAssetDecimalsIsLessThan18_shouldScaleCorrectly() public { + ERC20DecimalsMock reserveToken6DecimalsMock = new ERC20DecimalsMock("Reserve Token", "RES", 6); + IBancorExchangeProvider.PoolExchange memory pool2 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6DecimalsMock), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e18, // internally scaled to 18 decimals + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + uint256 reserveInterest = 1000e6; + deal(address(reserveToken6DecimalsMock), reserveAddress, 200_000 * 1e6 + reserveInterest); + + vm.mockCall( + address(exchangeProvider), + abi.encodeWithSelector(IBancorExchangeProvider(exchangeProvider).getPoolExchange.selector, exchangeId), + abi.encode(pool2) + ); + + vm.expectCall( + address(exchangeProvider), + abi.encodeWithSelector( + IGoodDollarExchangeProvider(exchangeProvider).mintFromInterest.selector, + exchangeId, + reserveInterest * 1e12 + ) + ); + expansionController.mintUBIFromReserveBalance(exchangeId); + } + function test_mintUBIFromReserveBalance_whenAdditionalReserveBalanceIsLargerThan0_shouldMintAndEmit() public { uint256 amountToMint = 1000e18; uint256 additionalReserveBalance = 1000e18; From 86053dfa46cdde052a14fbde9e30ac6cbb99af69 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Tue, 26 Nov 2024 12:32:55 +0100 Subject: [PATCH 31/38] fix: setting of last expansion timestamp (#553) ### Description fixes issue in how lastUpdated was set: before if 3.5 days passed and a daily expansion was configured, 3 expansions were calculated and the last expansion timestamp was updated to now essentially losing half a day. Over time this could have resulted in the yearly expansion rate not being achieved. ### Other changes ### Tested - added tests for this behavior ### Related issues https://github.com/mento-protocol/mento-general/issues/592 ### Backwards compatibility ### Documentation --- .../GoodDollarExpansionController.sol | 15 ++++-- .../GoodDollarExpansionController.t.sol | 48 ++++++++++--------- .../GoodDollarExpansionControllerHarness.sol | 8 +++- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index 402e3343..6790b996 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -173,11 +173,10 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl .getPoolExchange(exchangeId); ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); - bool shouldExpand = block.timestamp > config.lastExpansion + config.expansionFrequency; + bool shouldExpand = block.timestamp >= config.lastExpansion + config.expansionFrequency; if (shouldExpand || config.lastExpansion == 0) { - uint256 reserveRatioScalar = _getReserveRatioScalar(config); + uint256 reserveRatioScalar = _getReserveRatioScalar(exchangeId); - exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); amountMinted = goodDollarExchangeProvider.mintFromExpansion(exchangeId, reserveRatioScalar); IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); @@ -220,17 +219,23 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl /** * @notice Calculates the reserve ratio scalar for the given expansion config. - * @param config The expansion config. + * @param exchangeId The ID of the exchange. * @return reserveRatioScalar The reserve ratio scalar. */ - function _getReserveRatioScalar(ExchangeExpansionConfig memory config) internal view returns (uint256) { + function _getReserveRatioScalar(bytes32 exchangeId) internal returns (uint256) { + ExchangeExpansionConfig memory config = getExpansionConfig(exchangeId); uint256 numberOfExpansions; // If there was no previous expansion, we expand once. if (config.lastExpansion == 0) { numberOfExpansions = 1; + exchangeExpansionConfigs[exchangeId].lastExpansion = uint32(block.timestamp); } else { numberOfExpansions = (block.timestamp - config.lastExpansion) / config.expansionFrequency; + // slither-disable-next-line divide-before-multiply + exchangeExpansionConfigs[exchangeId].lastExpansion = uint32( + config.lastExpansion + numberOfExpansions * config.expansionFrequency + ); } uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate; diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol index 974843a5..65d3cb8d 100644 --- a/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -402,7 +402,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp expansionController.mintUBIFromExpansion("NotSetExchangeId"); } - function test_mintUBIFromExpansion_whenShouldNotExpand_shouldNotExpand() public { + function test_mintUBIFromExpansion_whenLessThanExpansionFrequencyPassed_shouldNotExpand() public { // doing one initial expansion to not be first expansion // since on first expansion the expansion is always applied once. expansionController.mintUBIFromExpansion(exchangeId); @@ -410,8 +410,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( exchangeId ); - uint256 lastExpansion = config.lastExpansion; - skip(lastExpansion + config.expansionFrequency - 1); + skip(config.expansionFrequency - 1); assertEq(expansionController.mintUBIFromExpansion(exchangeId), 0); } @@ -497,14 +496,11 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp // 1 day has passed since last expansion and expansion rate is 1% so the rate passed to the exchangeProvider // should be 0.99^1 = 0.99 uint256 reserveRatioScalar = 1e18 * 0.99; - skip(expansionFrequency + 1); + skip(expansionFrequency); uint256 amountToMint = 1000e18; uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); - vm.expectEmit(true, true, true, true); - emit ExpansionUBIMinted(exchangeId, amountToMint); - vm.expectCall( exchangeProvider, abi.encodeWithSelector( @@ -518,6 +514,9 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp abi.encodeWithSelector(IDistributionHelper(distributionHelper).onDistribution.selector, amountToMint) ); + vm.expectEmit(true, true, true, true); + emit ExpansionUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = expansionController.mintUBIFromExpansion(exchangeId); IGoodDollarExpansionController.ExchangeExpansionConfig memory config = expansionController.getExpansionConfig( exchangeId @@ -528,7 +527,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp assertEq(config.lastExpansion, block.timestamp); } - function test_mintUBIFromExpansion_whenMultipleDaysPassed_shouldCalculateCorrectRateAndExpand() public { + function test_mintUBIFromExpansion_whenThreeAndAHalfDaysPassed_shouldMintCorrectAmountAndSetLastExpansion() public { // doing one initial expansion to not be first expansion // since on first expansion the expansion is always applied once. expansionController.mintUBIFromExpansion(exchangeId); @@ -537,7 +536,12 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp // should be 0.99^3 = 0.970299 uint256 reserveRatioScalar = 1e18 * 0.970299; - skip(3 * expansionFrequency + 1); + IGoodDollarExpansionController.ExchangeExpansionConfig memory stateBefore = expansionController.getExpansionConfig( + exchangeId + ); + + // 3.5 days have passed since last expansion + skip((7 * expansionFrequency) / 2); uint256 amountToMint = 1000e18; uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); @@ -565,7 +569,7 @@ contract GoodDollarExpansionControllerTest_mintUBIFromExpansion is GoodDollarExp assertEq(amountMinted, amountToMint); assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); - assertEq(config.lastExpansion, block.timestamp); + assertEq(config.lastExpansion, stateBefore.lastExpansion + expansionFrequency * 3); } } @@ -575,18 +579,14 @@ contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpan function setUp() public override { super.setUp(); expansionController = new GoodDollarExpansionControllerHarness(false); + expansionController.initialize(exchangeProvider, distributionHelper, reserveAddress, avatarAddress); } - function test_getExpansionScaler_whenExpansionRateIs0_shouldReturn1e18() public { - IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController - .ExchangeExpansionConfig(0, 1, 0); - assertEq(expansionController.exposed_getReserveRatioScalar(config), 1e18); - } - - function test_getExpansionScaler_whenExpansionRateIs1_shouldReturn1() public { - IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController - .ExchangeExpansionConfig(1e18 - 1, 1, 0); - assertEq(expansionController.exposed_getReserveRatioScalar(config), 1); + function test_getExpansionScaler_whenStepReserveRatioScalerIs1_shouldReturn1() public { + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, 1e18 - 1, 1); + // stepReserveRatioScalar is 1e18 - expansionRate = 1e18 - (1e18 - 1) = 1 + assertEq(expansionController.exposed_getReserveRatioScalar(exchangeId), 1); } function testFuzz_getExpansionScaler( @@ -602,9 +602,11 @@ contract GoodDollarExpansionControllerTest_getExpansionScalar is GoodDollarExpan skip(lastExpansion + timeDelta); - IGoodDollarExpansionController.ExchangeExpansionConfig memory config = IGoodDollarExpansionController - .ExchangeExpansionConfig(expansionRate, expansionFrequency, lastExpansion); - uint256 scaler = expansionController.exposed_getReserveRatioScalar(config); + vm.prank(avatarAddress); + expansionController.setExpansionConfig(exchangeId, expansionRate, expansionFrequency); + expansionController.setLastExpansion(exchangeId, lastExpansion); + uint256 scaler = expansionController.exposed_getReserveRatioScalar(exchangeId); + assert(scaler >= 0 && scaler <= 1e18); } } diff --git a/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol b/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol index ad6f080f..aef0837f 100644 --- a/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol +++ b/test/utils/harnesses/GoodDollarExpansionControllerHarness.sol @@ -7,7 +7,11 @@ import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarEx contract GoodDollarExpansionControllerHarness is GoodDollarExpansionController { constructor(bool disabled) GoodDollarExpansionController(disabled) {} - function exposed_getReserveRatioScalar(ExchangeExpansionConfig calldata config) external returns (uint256) { - return _getReserveRatioScalar(config); + function exposed_getReserveRatioScalar(bytes32 exchangeId) external returns (uint256) { + return _getReserveRatioScalar(exchangeId); + } + + function setLastExpansion(bytes32 exchangeId, uint32 lastExpansion) external { + exchangeExpansionConfigs[exchangeId].lastExpansion = lastExpansion; } } From 9653183cc69b0ba6b11d388c3c40f99d791d0a7f Mon Sep 17 00:00:00 2001 From: baroooo Date: Tue, 26 Nov 2024 15:49:45 +0100 Subject: [PATCH 32/38] Feat: slippage protection for reserve ratio (#554) ### Description Adds slippage protection to `updateRatioForReward()` ### Other changes Simplifies the new ratio calculation formula ### Tested Unit tests ### Related issues - Fixes [#594](https://github.com/mento-protocol/mento-general/issues/594) --- .../goodDollar/GoodDollarExchangeProvider.sol | 26 ++++++---- .../GoodDollarExpansionController.sol | 25 +++++++-- .../IGoodDollarExchangeProvider.sol | 3 +- .../IGoodDollarExpansionController.sol | 16 +++++- .../GoodDollarExchangeProvider.t.sol | 51 +++++++++++++------ .../GoodDollarExpansionController.t.sol | 20 ++++++++ 6 files changed, 109 insertions(+), 32 deletions(-) diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index 5b62537c..b75170d2 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -195,23 +195,31 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan /** * @inheritdoc IGoodDollarExchangeProvider * @dev Calculates the new reserve ratio needed to mint the G$ reward while keeping the current price the same. - * calculation: newRatio = reserveBalance / (tokenSupply + reward) * currentPrice + * calculation: newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) */ - function updateRatioForReward(bytes32 exchangeId, uint256 reward) external onlyExpansionController whenNotPaused { + function updateRatioForReward( + bytes32 exchangeId, + uint256 reward, + uint256 maxSlippagePercentage + ) external onlyExpansionController whenNotPaused { PoolExchange memory exchange = getPoolExchange(exchangeId); - uint256 currentPriceScaled = currentPrice(exchangeId) * tokenPrecisionMultipliers[exchange.reserveAsset]; - uint256 rewardScaled = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; + uint256 scaledRatio = uint256(exchange.reserveRatio) * 1e10; + uint256 scaledReward = reward * tokenPrecisionMultipliers[exchange.tokenAddress]; + + UD60x18 numerator = wrap(exchange.tokenSupply).mul(wrap(scaledRatio)); + UD60x18 denominator = wrap(exchange.tokenSupply).add(wrap(scaledReward)); + uint256 newScaledRatio = unwrap(numerator.div(denominator)); - UD60x18 numerator = wrap(exchange.reserveBalance); - UD60x18 denominator = wrap(exchange.tokenSupply + rewardScaled).mul(wrap(currentPriceScaled)); - uint256 newRatioScaled = unwrap(numerator.div(denominator)); + uint32 newRatioUint = uint32(newScaledRatio / 1e10); - uint32 newRatioUint = uint32(newRatioScaled / 1e10); require(newRatioUint > 0, "New ratio must be greater than 0"); + uint256 allowedSlippage = (exchange.reserveRatio * maxSlippagePercentage) / MAX_WEIGHT; + require(exchange.reserveRatio - newRatioUint <= allowedSlippage, "Slippage exceeded"); + exchanges[exchangeId].reserveRatio = newRatioUint; - exchanges[exchangeId].tokenSupply += rewardScaled; + exchanges[exchangeId].tokenSupply += scaledReward; emit ReserveRatioUpdated(exchangeId, newRatioUint); } diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index 6790b996..5d64a505 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -21,8 +21,11 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl /* ==================== State Variables ==================== */ /* ========================================================= */ - // MAX_WEIGHT is the max rate that can be assigned to an exchange - uint256 public constant MAX_WEIGHT = 1e18; + // EXPANSION_MAX_WEIGHT is the max rate that can be assigned to an exchange + uint256 public constant EXPANSION_MAX_WEIGHT = 1e18; + + // BANCOR_MAX_WEIGHT is used for BPS calculations in GoodDollarExchangeProvider + uint32 public constant BANCOR_MAX_WEIGHT = 1e8; // Address of the distribution helper contract IDistributionHelper public distributionHelper; @@ -122,7 +125,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl /// @inheritdoc IGoodDollarExpansionController function setExpansionConfig(bytes32 exchangeId, uint64 expansionRate, uint32 expansionFrequency) external onlyAvatar { - require(expansionRate < MAX_WEIGHT, "Expansion rate must be less than 100%"); + require(expansionRate < EXPANSION_MAX_WEIGHT, "Expansion rate must be less than 100%"); require(expansionRate > 0, "Expansion rate must be greater than 0"); require(expansionFrequency > 0, "Expansion frequency must be greater than 0"); @@ -190,12 +193,24 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl /// @inheritdoc IGoodDollarExpansionController function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external onlyAvatar { + // Defaults to no slippage protection + mintRewardFromReserveRatio(exchangeId, to, amount, BANCOR_MAX_WEIGHT); + } + + /// @inheritdoc IGoodDollarExpansionController + function mintRewardFromReserveRatio( + bytes32 exchangeId, + address to, + uint256 amount, + uint256 maxSlippagePercentage + ) public onlyAvatar { require(to != address(0), "Recipient address must be set"); require(amount > 0, "Amount must be greater than 0"); + require(maxSlippagePercentage <= BANCOR_MAX_WEIGHT, "Max slippage percentage cannot be greater than 100%"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); - goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount); + goodDollarExchangeProvider.updateRatioForReward(exchangeId, amount, maxSlippagePercentage); IGoodDollar(exchange.tokenAddress).mint(to, amount); // Ignored, because contracts only interacts with trusted contracts and tokens @@ -238,7 +253,7 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl ); } - uint256 stepReserveRatioScalar = MAX_WEIGHT - config.expansionRate; + uint256 stepReserveRatioScalar = EXPANSION_MAX_WEIGHT - config.expansionRate; return unwrap(powu(wrap(stepReserveRatioScalar), numberOfExpansions)); } diff --git a/contracts/interfaces/IGoodDollarExchangeProvider.sol b/contracts/interfaces/IGoodDollarExchangeProvider.sol index 54328aeb..6ad4751f 100644 --- a/contracts/interfaces/IGoodDollarExchangeProvider.sol +++ b/contracts/interfaces/IGoodDollarExchangeProvider.sol @@ -72,8 +72,9 @@ interface IGoodDollarExchangeProvider { * @notice Calculates the reserve ratio needed to mint the given G$ reward. * @param exchangeId The ID of the pool the G$ reward is minted from. * @param reward The amount of G$ tokens to be minted as a reward. + * @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-1e8). */ - function updateRatioForReward(bytes32 exchangeId, uint256 reward) external; + function updateRatioForReward(bytes32 exchangeId, uint256 reward, uint256 maxSlippagePercentage) external; /** * @notice Pauses the Exchange, disabling minting. diff --git a/contracts/interfaces/IGoodDollarExpansionController.sol b/contracts/interfaces/IGoodDollarExpansionController.sol index 2268ef97..c09874d5 100644 --- a/contracts/interfaces/IGoodDollarExpansionController.sol +++ b/contracts/interfaces/IGoodDollarExpansionController.sol @@ -148,10 +148,24 @@ interface IGoodDollarExpansionController { function mintUBIFromExpansion(bytes32 exchangeId) external returns (uint256 amountMinted); /** - * @notice Mints a reward of G$ tokens for a given pool. + * @notice Mints a reward of G$ tokens for a given pool. Defaults to no slippage protection. * @param exchangeId The ID of the pool to mint a G$ reward for. * @param to The address of the recipient. * @param amount The amount of G$ tokens to mint. */ function mintRewardFromReserveRatio(bytes32 exchangeId, address to, uint256 amount) external; + + /** + * @notice Mints a reward of G$ tokens for a given pool. + * @param exchangeId The ID of the pool to mint a G$ reward for. + * @param to The address of the recipient. + * @param amount The amount of G$ tokens to mint. + * @param maxSlippagePercentage Maximum allowed percentage difference between new and old reserve ratio (0-100). + */ + function mintRewardFromReserveRatio( + bytes32 exchangeId, + address to, + uint256 amount, + uint256 maxSlippagePercentage + ) external; } diff --git a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol index 6fceeaa2..1a30aa2a 100644 --- a/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol +++ b/test/unit/goodDollar/GoodDollarExchangeProvider.t.sol @@ -686,31 +686,31 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.expectRevert("New ratio must be greater than 0"); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward); + exchangeProvider.updateRatioForReward(exchangeId, veryLargeReward, 1e8); } function test_updateRatioForReward_whenCallerIsNotExpansionController_shouldRevert() public { vm.prank(makeAddr("NotExpansionController")); vm.expectRevert("Only ExpansionController can call this function"); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); } function test_updateRatioForReward_whenExchangeIdIsInvalid_shouldRevert() public { vm.prank(expansionControllerAddress); vm.expectRevert("Exchange does not exist"); - exchangeProvider.updateRatioForReward(bytes32(0), reward); + exchangeProvider.updateRatioForReward(bytes32(0), reward, 1e8); } function test_updateRatioForReward_whenRewardLarger0_shouldReturnCorrectRatioAndEmit() public { - // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) - // reserveRatio = 200_000 / ((7_000_000_000 + 1_000) * 0.000100000002) ≈ 0.28571423... + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000) = 0.28571423 uint32 expectedReserveRatio = 28571423; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -727,15 +727,16 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan function test_updateRatioForReward_whenRewardIsSmall_shouldReturnCorrectRatioAndEmit() public { uint256 _reward = 1e18; // 1 token - // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) - // reserveRatio = 200_000 / ((7_000_000_000 + 1) * 0.000100000002) ≈ 0.2857142799 + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1) = 0.28571427 + uint32 expectedReserveRatio = 28571427; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, _reward); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -751,8 +752,8 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan function test_updateRatioForReward_whenRewardIsLarge_shouldReturnCorrectRatioAndEmit() public { uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens - // formula: newRatio = reserveBalance / ((tokenSupply + reward) * currentPrice) - // reserveRatio = 200_000 / ((7_000_000_000 + 1_000_000_000) * 0.000100000002) ≈ 0.2499999950000... + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995 uint32 expectedReserveRatio = 24999999; uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); @@ -760,7 +761,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.expectEmit(true, true, true, true); emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, _reward); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -774,6 +775,24 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan assertApproxEqRel(priceBefore, priceAfter, 1e18 * 0.0001, "Price should remain within 0.01% of initial price"); } + function test_updateRatioForReward_whenSlippageIsHigherThanAccepted_shouldRevert() public { + uint256 _reward = 1_000_000_000 * 1e18; // 1 billion tokens + // formula newRatio = (tokenSupply * reserveRatio) / (tokenSupply + reward) + // formula: newRatio = (7_000_000_000 * 0.28571428) / (7_000_000_000 + 1_000_000_000) = 0.249999995 + // slippage = (newRatio - reserveRatio) / reserveRatio = (0.249999995 - 0.28571428) / 0.28571428 ~= -0.125 + + uint32 expectedReserveRatio = 24999999; + + vm.prank(expansionControllerAddress); + vm.expectRevert("Slippage exceeded"); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 12 * 1e6); + + vm.expectEmit(true, true, true, true); + emit ReserveRatioUpdated(exchangeId, expectedReserveRatio); + vm.prank(expansionControllerAddress); + exchangeProvider.updateRatioForReward(exchangeId, _reward, 13 * 1e6); + } + function test_updateRatioForReward_withMultipleConsecutiveRewards() public { uint256 totalReward = 0; uint256 initialTokenSupply = poolExchange.tokenSupply; @@ -783,7 +802,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan vm.startPrank(expansionControllerAddress); for (uint256 i = 0; i < 5; i++) { - exchangeProvider.updateRatioForReward(exchangeId, reward); + exchangeProvider.updateRatioForReward(exchangeId, reward, 1e8); totalReward += reward; } vm.stopPrank(); @@ -811,7 +830,7 @@ contract GoodDollarExchangeProviderTest_updateRatioForReward is GoodDollarExchan uint256 priceBefore = exchangeProvider.currentPrice(exchangeId); vm.prank(expansionControllerAddress); - exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward); + exchangeProvider.updateRatioForReward(exchangeId, fuzzedReward, 1e8); IBancorExchangeProvider.PoolExchange memory poolExchangeAfter = exchangeProvider.getPoolExchange(exchangeId); uint256 priceAfter = exchangeProvider.currentPrice(exchangeId); @@ -876,7 +895,7 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe exchangeProvider.mintFromInterest(exchangeId, 1e18); vm.expectRevert("Pausable: paused"); - exchangeProvider.updateRatioForReward(exchangeId, 1e18); + exchangeProvider.updateRatioForReward(exchangeId, 1e18, 100); } function test_unpause_whenCallerIsAvatar_shouldUnpauseAndEnableExchange() public { @@ -897,6 +916,6 @@ contract GoodDollarExchangeProviderTest_pausable is GoodDollarExchangeProviderTe exchangeProvider.mintFromExpansion(exchangeId, 1e18); exchangeProvider.mintFromInterest(exchangeId, 1e18); - exchangeProvider.updateRatioForReward(exchangeId, 1e18); + exchangeProvider.updateRatioForReward(exchangeId, 1e18, 1e8); } } diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol index 65d3cb8d..d763a768 100644 --- a/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -645,6 +645,12 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 0); } + function test_mintRewardFromReserveRatio_whenSlippageIsGreaterThan100_shouldRevert() public { + vm.prank(avatarAddress); + vm.expectRevert("Max slippage percentage cannot be greater than 100%"); + expansionController.mintRewardFromReserveRatio(exchangeId, makeAddr("To"), 1000e18, 1e8 + 1); + } + function test_mintRewardFromReserveRatio_whenCallerIsAvatar_shouldMintAndEmit() public { uint256 amountToMint = 1000e18; address to = makeAddr("To"); @@ -658,4 +664,18 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); } + + function test_mintRewardFromReserveRatio_whenCustomSlippage_shouldMintAndEmit() public { + uint256 amountToMint = 1000e18; + address to = makeAddr("To"); + uint256 toBalanceBefore = token.balanceOf(to); + + vm.expectEmit(true, true, true, true); + emit RewardMinted(exchangeId, to, amountToMint); + + vm.prank(avatarAddress); + expansionController.mintRewardFromReserveRatio(exchangeId, to, amountToMint, 1); + + assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); + } } From 8236d656a060b0a44e0979cb4251d0e984a8189d Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 27 Nov 2024 10:26:30 +0100 Subject: [PATCH 33/38] Fix/use correct min for int48 (#556) ### Description Fixes Sherlock issue 46 by using `type(int48).min` ### Other changes updates test for withOverflowOnAdd to test the correct limit ### Tested unit tests ### Related issues - Fixes [#598](https://github.com/mento-protocol/mento-general/issues/598) --- contracts/libraries/TradingLimits.sol | 2 +- test/unit/libraries/TradingLimits.t.sol | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/contracts/libraries/TradingLimits.sol b/contracts/libraries/TradingLimits.sol index 557d23a6..e9c410d6 100644 --- a/contracts/libraries/TradingLimits.sol +++ b/contracts/libraries/TradingLimits.sol @@ -172,7 +172,7 @@ library TradingLimits { */ function safeINT48Add(int48 a, int48 b) internal pure returns (int48) { int256 c = int256(a) + int256(b); - require(c >= -1 * MAX_INT48 && c <= MAX_INT48, "int48 addition overflow"); + require(c >= MIN_INT48 && c <= MAX_INT48, "int48 addition overflow"); return int48(c); } } diff --git a/test/unit/libraries/TradingLimits.t.sol b/test/unit/libraries/TradingLimits.t.sol index 66432d69..e9681f7f 100644 --- a/test/unit/libraries/TradingLimits.t.sol +++ b/test/unit/libraries/TradingLimits.t.sol @@ -316,10 +316,23 @@ contract TradingLimitsTest is Test { function test_update_withOverflowOnAdd_reverts() public { ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); - int256 maxFlow = int256(uint256(type(uint48).max / 2)); + int256 maxFlow = int256(type(int48).max); state = harness.update(state, config, (maxFlow - 1000) * 1e18, 18); + state = harness.update(state, config, 1000 * 1e18, 18); + + vm.expectRevert(bytes("int48 addition overflow")); + state = harness.update(state, config, 1 * 1e18, 18); + } + + function test_update_withUnderflowOnAdd_reverts() public { + ITradingLimits.Config memory config = configLG(int48(uint48(2 ** 47))); + int256 minFlow = int256(type(int48).min); + + state = harness.update(state, config, (minFlow + 1000) * 1e18, 18); + state = harness.update(state, config, -1000 * 1e18, 18); + vm.expectRevert(bytes("int48 addition overflow")); - state = harness.update(state, config, 1002 * 10e18, 18); + state = harness.update(state, config, -1 * 1e18, 18); } } From 1e78e2cb5772613ff7b1ae8f0b51ccf6102fba3a Mon Sep 17 00:00:00 2001 From: Ryan Noble Date: Thu, 28 Nov 2024 13:53:01 +0100 Subject: [PATCH 34/38] fix: Exchange created with zero reserve (#558) --- .../goodDollar/BancorExchangeProvider.sol | 1 + .../goodDollar/BancorExchangeProvider.t.sol | 56 ++----------------- 2 files changed, 7 insertions(+), 50 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 30863f27..b90c6bd8 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -361,6 +361,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B require(exchange.reserveRatio > 1, "Reserve ratio is too low"); require(exchange.reserveRatio <= MAX_WEIGHT, "Reserve ratio is too high"); require(exchange.exitContribution <= MAX_WEIGHT, "Exit contribution is too high"); + require(exchange.reserveBalance > 0, "Reserve balance must be greater than 0"); } /** diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 3781e191..4d2867cf 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -253,6 +253,12 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest bancorExchangeProvider.createExchange(poolExchange1); } + function test_createExchange_whenReserveBalanceIsZero_shouldRevert() public { + poolExchange1.reserveBalance = 0; + vm.expectRevert("Reserve balance must be greater than 0"); + bancorExchangeProvider.createExchange(poolExchange1); + } + function test_createExchange_whenReserveAssetIsNotCollateral_shouldRevert() public { vm.mockCall( reserveAddress, @@ -452,19 +458,6 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { }); } - function test_getAmountIn_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountIn({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountOut: 1e18 - }); - } - function test_getAmountIn_whenTokenInIsTokenAndAmountOutLargerThanReserveBalance_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("ERR_INVALID_AMOUNT"); @@ -533,19 +526,6 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { }); } - function test_getAmountIn_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountIn({ - exchangeId: exchangeId, - tokenIn: address(reserveToken), - tokenOut: address(token), - amountOut: 1e18 - }); - } - function test_getAmountIn_whenTokenInIsReserveAssetAndAmountOutIsZero_shouldReturnZero() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = bancorExchangeProvider.getAmountIn({ @@ -998,18 +978,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { }); } - function test_getAmountOut_whenTokenInIsReserveAssetAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(reserveToken), - tokenOut: address(token), - amountIn: 1e18 - }); - } - function test_getAmountOut_whenTokenInIsReserveAssetAndAmountInIsZero_shouldReturnZero() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = bancorExchangeProvider.getAmountOut({ @@ -1049,18 +1017,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { }); } - function test_getAmountOut_whenTokenInIsTokenAndReserveBalanceIsZero_shouldRevert() public { - poolExchange1.reserveBalance = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_RESERVE_BALANCE"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountIn: 1e18 - }); - } - function test_getAmountOut_whenTokenInIsTokenAndAmountLargerSupply_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.expectRevert("ERR_INVALID_AMOUNT"); From 98290ba865448ae489d665b92decd529e0350fc3 Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:54:55 +0100 Subject: [PATCH 35/38] Test/new exit contribution (#557) ### Description This PR tries to solve the discontinued curve problem that was causing the same sell amount when executed in two swaps resulting in a larger amountOut than when sold in one swap. The idea is to apply the exitContribution on the G$ amount being sold and burning that amount. In order to not move on the curve by this burn we increase the reserve ratio in order to keep the price before the exitContribution burn equal to the price after. ### Other changes Added requires that ensure the token Supply always stays at a minimum of 1 wei ### Tested - new tests added ensuring the difference in splitting sells into multiple swaps stays super small compared to the gas costs ### Related issues https://github.com/mento-protocol/mento-general/issues/596 https://github.com/mento-protocol/mento-core/issues/555 ### Backwards compatibility ### Documentation --------- Co-authored-by: baroooo --- .../goodDollar/BancorExchangeProvider.sol | 71 +++++- .../goodDollar/BancorExchangeProvider.t.sol | 205 +++++++++++------- 2 files changed, 191 insertions(+), 85 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index b90c6bd8..83e131a3 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -122,6 +122,13 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) external view virtual returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + + if (tokenIn == exchange.tokenAddress) { + require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); + // apply exit contribution + scaledAmountIn = (scaledAmountIn * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; + } + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; @@ -137,6 +144,13 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + + if (tokenIn == exchange.tokenAddress) { + // apply exit contribution + scaledAmountIn = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); + } + amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; return amountIn; } @@ -196,8 +210,21 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B ) public virtual onlyBroker returns (uint256 amountOut) { PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountIn = amountIn * tokenPrecisionMultipliers[tokenIn]; + uint256 exitContribution = 0; + + if (tokenIn == exchange.tokenAddress) { + require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); + // apply exit contribution + exitContribution = (scaledAmountIn * exchange.exitContribution) / MAX_WEIGHT; + scaledAmountIn -= exitContribution; + } + uint256 scaledAmountOut = _getScaledAmountOut(exchange, tokenIn, tokenOut, scaledAmountIn); + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + if (exitContribution > 0) { + _accountExitContribution(exchangeId, exitContribution); + } amountOut = scaledAmountOut / tokenPrecisionMultipliers[tokenOut]; return amountOut; @@ -213,9 +240,26 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledAmountOut = amountOut * tokenPrecisionMultipliers[tokenOut]; uint256 scaledAmountIn = _getScaledAmountIn(exchange, tokenIn, tokenOut, scaledAmountOut); + + uint256 exitContribution = 0; + uint256 scaledAmountInWithExitContribution = scaledAmountIn; + + if (tokenIn == exchange.tokenAddress) { + // apply exit contribution + scaledAmountInWithExitContribution = (scaledAmountIn * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); + require( + scaledAmountInWithExitContribution < exchange.tokenSupply, + "amountIn required is greater than tokenSupply" + ); + exitContribution = scaledAmountInWithExitContribution - scaledAmountIn; + } + executeSwap(exchangeId, tokenIn, scaledAmountIn, scaledAmountOut); + if (exitContribution > 0) { + _accountExitContribution(exchangeId, exitContribution); + } - amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + amountIn = scaledAmountInWithExitContribution / tokenPrecisionMultipliers[tokenIn]; return amountIn; } @@ -289,6 +333,27 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B exchanges[exchangeId].tokenSupply = exchange.tokenSupply; } + /** + * @notice Accounting of exit contribution on a swap. + * @dev Accounting of exit contribution without changing the current price of an exchange. + * this is done by updating the reserve ratio and subtracting the exit contribution from the token supply. + * Formula: newRatio = (Supply * oldRatio) / (Supply - exitContribution) + * @param exchangeId The ID of the pool + * @param exitContribution The amount of the token to be removed from the pool, scaled to 18 decimals + */ + function _accountExitContribution(bytes32 exchangeId, uint256 exitContribution) internal { + PoolExchange memory exchange = getPoolExchange(exchangeId); + uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; + UD60x18 nominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); + UD60x18 denominator = wrap(exchange.tokenSupply - exitContribution); + UD60x18 newRatioScaled = nominator.div(denominator); + + uint256 newRatio = unwrap(newRatioScaled) / 1e10; + + exchanges[exchangeId].reserveRatio = uint32(newRatio); + exchanges[exchangeId].tokenSupply -= exitContribution; + } + /** * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut * @param exchange The pool exchange to operate on @@ -306,8 +371,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B if (tokenIn == exchange.reserveAsset) { scaledAmountIn = fundCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); } else { - // apply exit contribution - scaledAmountOut = (scaledAmountOut * MAX_WEIGHT) / (MAX_WEIGHT - exchange.exitContribution); scaledAmountIn = saleCost(exchange.tokenSupply, exchange.reserveBalance, exchange.reserveRatio, scaledAmountOut); } } @@ -340,8 +403,6 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B exchange.reserveRatio, scaledAmountIn ); - // apply exit contribution - scaledAmountOut = (scaledAmountOut * (MAX_WEIGHT - exchange.exitContribution)) / MAX_WEIGHT; } } diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 4d2867cf..9cd8fa66 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -480,19 +480,15 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { assertEq(amountIn, 0); } - function test_getAmountIn_whenTokenInIsTokenAndAmountOutEqualReserveBalance_shouldReturnSupply() public { - // need to set exit contribution to 0 to make the formula work otherwise amountOut would need to be adjusted - // to be equal to reserveBalance after exit contribution is applied - poolExchange1.exitContribution = 0; + function test_getAmountIn_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - uint256 expectedAmountIn = poolExchange1.tokenSupply; - uint256 amountIn = bancorExchangeProvider.getAmountIn({ + vm.expectRevert("amountIn is greater than tokenSupply"); + bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), tokenOut: address(reserveToken), amountOut: poolExchange1.reserveBalance }); - assertEq(amountIn, expectedAmountIn); } function test_getAmountIn_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { @@ -571,12 +567,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsToken_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) - // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line - // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) + // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) + // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio - // calculation: (300000 * ( -1 + (60000 / (60000-(1/0.99)))^0.2))/(60000 / (60000-(1/0.99)))^0.2 = 1.010107812196722301 - uint256 expectedAmountIn = 1010107812196722302; + // calculation: (300000 * ( -1 + (60000 / (60000-1))^0.2))/(60000 / (60000-1))^0.2 ÷ 0.99 = 1.010107744175084961 + uint256 expectedAmountIn = 1010107744175084961; uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), @@ -604,11 +600,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = 1e12; // 0.000001 token - // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) - // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line - // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) + // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) + // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio - // calculation: (300000 * ( -1 + (60000 / (60000-(0.000001/0.99)))^0.2))/(60000 / (60000-(0.000001/0.99)))^0.2 ≈ 0.000001010101010107 + // calculation: (300000 * ( -1 + (60000 / (60000-0.000001))^0.2))/(60000 / (60000-0.000001))^0.2 ÷ 0.99 ≈ 0.000001010101010107 + // 1 wei difference due to precision loss uint256 expectedAmountIn = 1010101010108; uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, @@ -638,12 +635,12 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { function test_getAmountIn_whenTokenInIsTokenAndAmountOutIsLarge_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = 59000e18; // 59_000 since total reserve is 60k - // formula: = tokenSupply * (-1 + (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio ) - // formula: amountIn = ------------------------------------------------------------------------------------------------ this is a fractional line - // formula: = (reserveBalance / (reserveBalance - (amountOut/(1-e))) )^reserveRatio + // formula: = tokenSupply * (-1 + (reserveBalance/(reserveBalance - amountOut))^reserveRatio ) + // formula: amountIn = -------------------------------------------------------------------------------- ÷ (1-e) + // formula: = (reserveBalance / (reserveBalance - amountOut) )^reserveRatio - // calculation: (300000 * ( -1 + (60000 / (60000-(59000/0.99)))^0.2))/(60000 / (60000-(59000/0.99)))^0.2 = 189649.078540006525698460 - uint256 expectedAmountIn = 189649078540006525698460; + // calculation: (300000 * ( -1 + (60000 / (60000-59000))^0.2))/(60000 / (60000-59000))^0.2 ÷ 0.99 ≈ 169415.120269436288420151 + uint256 expectedAmountIn = 169415120269436288420151; uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), @@ -661,9 +658,8 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { bancorExchangeProvider.setExitContribution(exchangeId, 1e6); bytes32 exchangeId2 = bancorExchangeProvider.createExchange(poolExchange2); bancorExchangeProvider.setExitContribution(exchangeId2, 0); - uint256 amountOut = 116e18; - // formula: amountIn = (tokenSupply * (( (amountOut + reserveBalance) / reserveBalance) ^ (reserveRatio) - 1)) / exitContribution + uint256 amountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, tokenIn: address(token), @@ -671,17 +667,13 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { amountOut: amountOut }); - // exit contribution is 1% - uint256 amountOut2 = (amountOut * 100) / 99; - assertTrue(amountOut < amountOut2); - uint256 amountIn2 = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId2, tokenIn: address(token2), tokenOut: address(reserveToken), - amountOut: amountOut2 + amountOut: amountOut }); - assertEq(amountIn, amountIn2); + assertEq(amountIn, (amountIn2 * 100) / 99); } function test_getAmountIn_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { @@ -1005,29 +997,6 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { assertEq(amountOut, expectedAmountOut); } - function test_getAmountOut_whenTokenInIsTokenAndSupplyIsZero_shouldRevert() public { - poolExchange1.tokenSupply = 0; - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_SUPPLY"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountIn: 1e18 - }); - } - - function test_getAmountOut_whenTokenInIsTokenAndAmountLargerSupply_shouldRevert() public { - bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - vm.expectRevert("ERR_INVALID_AMOUNT"); - bancorExchangeProvider.getAmountOut({ - exchangeId: exchangeId, - tokenIn: address(token), - tokenOut: address(reserveToken), - amountIn: poolExchange1.tokenSupply + 1 - }); - } - function test_getAmountOut_whenTokenInIsTokenAndAmountIsZero_shouldReturnZero() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountOut = bancorExchangeProvider.getAmountOut({ @@ -1039,25 +1008,23 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { assertEq(amountOut, 0); } - function test_getAmountOut_whenTokenInIsTokenAndAmountIsSupply_shouldReturnReserveBalanceMinusExitContribution() - public - { + function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - uint256 amountOut = bancorExchangeProvider.getAmountOut({ + vm.expectRevert("amountIn is greater than tokenSupply"); + bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), tokenOut: address(reserveToken), - amountIn: poolExchange1.tokenSupply + amountIn: (poolExchange1.tokenSupply) }); - assertEq(amountOut, (poolExchange1.reserveBalance * (1e8 - poolExchange1.exitContribution)) / 1e8); } function test_getAmountOut_whenTokenInIsTokenAndReserveRatioIs100Percent_shouldReturnCorrectAmount() public { poolExchange1.reserveRatio = 1e8; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = 1e18; - // formula: amountOut = (reserveBalance * amountIn / tokenSupply) * (1-e) - // calculation: (60_000 * 1 / 300_000) * 0.99 = 0.198 + // formula: amountOut = (reserveBalance * amountIn * (1-e)) / tokenSupply + // calculation: (60_000 * 1 * 0.99) / 300_000 = 0.198 uint256 expectedAmountOut = 198000000000000000; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, @@ -1065,6 +1032,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { tokenOut: address(reserveToken), amountIn: amountIn }); + assertEq(amountOut, expectedAmountOut); } @@ -1084,13 +1052,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsToken_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); - // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) - // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) - // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn*(1-e)))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- + // formula: = (tokenSupply/(tokenSupply - amountIn*(1-e)))^(1/reserveRatio) - // calculation: ((60_000 *(-1+(300_000/(300_000-1))^5) ) / (300_000/(300_000-1))^5)*0.99 = 0.989993400021999963 + // calculation: ((60_000 *(-1+(300_000/(300_000-1*0.99))^5) ) / (300_000/(300_000-1*0.99))^5) = 0.989993466021562164 // 1 wei difference due to precision loss - uint256 expectedAmountOut = 989993400021999962; + uint256 expectedAmountOut = 989993466021562164; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), @@ -1118,11 +1086,11 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsTokenAndAmountOutIsSmall_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = 1e12; // 0.000001 token - // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) - // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) - // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- + // formula: = (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio) - // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001))^5) )/(300_000/(300_000-0.000001))^5)*0.99 ≈ 0.0000009899999999934 + // calculation: ((60_000 *(-1+(300_000/(300_000-0.000001*0.99))^5) )/(300_000/(300_000-0.000001*0.99))^5) = 0.0000009899999999934 uint256 expectedAmountOut = 989999999993; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, @@ -1151,12 +1119,13 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { function test_getAmountOut_whenTokenInIsTokenAndAmountInIsLarge_shouldReturnCorrectAmount() public { bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); uint256 amountIn = 299_000 * 1e18; // 299,000 tokens only 300k supply - // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio)) - // formula: amountOut = ---------------------------------------------------------------------------------- * (1 - e) - // formula: = (tokenSupply/(tokenSupply - amountIn ))^(1/reserveRatio) + // formula: = reserveBalance * ( -1 + (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio)) + // formula: amountOut = ---------------------------------------------------------------------------------- + // formula: = (tokenSupply/(tokenSupply - amountIn * (1-e)))^(1/reserveRatio) - // calculation: ((60_000 *(-1+(300_000/(300_000-299_000))^5) ) / (300_000/(300_000-299_000))^5)*0.99 ≈ 59399.999999975555555555 - uint256 expectedAmountOut = 59399999999975555555555; + // calculation: ((60_000 *(-1+(300_000/(300_000-299_000*0.99))^5) ) / (300_000/(300_000-299_000 *0.99))^5) = 59999999975030522464200 + // 1 wei difference due to precision loss + uint256 expectedAmountOut = 59999999975030522464200; uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), @@ -1164,8 +1133,8 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { amountIn: amountIn }); - // we allow up to 1% difference due to precision loss - assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.01); + // we allow up to 0.1% difference due to precision loss + assertApproxEqRel(amountOut, expectedAmountOut, 1e18 * 0.001); } function test_getAmountOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnCorrectAmount( @@ -1179,6 +1148,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { bancorExchangeProvider.setExitContribution(exchangeId2, 0); amountIn = bound(amountIn, 100, 299_000 * 1e18); + uint256 amountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, tokenIn: address(token), @@ -1189,9 +1159,9 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { exchangeId: exchangeId2, tokenIn: address(token2), tokenOut: address(reserveToken), - amountIn: amountIn + amountIn: (amountIn * 99) / 100 }); - assertEq(amountOut, (amountOut2 * 99) / 100); + assertEq(amountOut, amountOut2); } function test_getAmountOut_whenDifferentTokenDecimals_shouldReturnCorrectAmount() public { @@ -1510,6 +1480,14 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { bancorExchangeProvider.swapIn(exchangeId, address(token), address(token), 1e18); } + function test_swapIn_whenTokenInIsTokenAndAmountIsLargerOrEqualSupply_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("amountIn is greater than tokenSupply"); + bancorExchangeProvider.swapIn(exchangeId, address(token), address(reserveToken), poolExchange1.tokenSupply); + } + function test_swapIn_whenTokenInIsReserveAsset_shouldSwapIn() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountIn = 1e18; @@ -1557,6 +1535,39 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); } + + function test_swapIn_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() + public + { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountToSell = 100_000 * 1e18; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountOutInOneSell = bancorExchangeProvider.swapIn( + exchangeId, + address(token), + address(reserveToken), + amountToSell + ); + + // destroy and recreate the exchange to reset everything + vm.prank(bancorExchangeProvider.owner()); + bancorExchangeProvider.destroyExchange(exchangeId, 0); + exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOutInMultipleSells; + for (uint256 i = 0; i < 100_000; i++) { + vm.prank(brokerAddress); + amountOutInMultipleSells += bancorExchangeProvider.swapIn( + exchangeId, + address(token), + address(reserveToken), + 1e18 + ); + } + // we allow up to 0.1% difference due to precision loss on exitContribution accounting + assertApproxEqRel(amountOutInOneSell, amountOutInMultipleSells, 1e18 * 0.001); + } } contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { @@ -1575,7 +1586,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { bancorExchangeProvider.swapOut("0xexchangeId", address(reserveToken), address(token), 1e18); } - function test_swapOut_whenTokenInNotInexchange_shouldRevert() public { + function test_swapOut_whenTokenInNotInExchange_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.prank(brokerAddress); @@ -1583,7 +1594,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { bancorExchangeProvider.swapOut(exchangeId, address(token2), address(token), 1e18); } - function test_swapOut_whenTokenOutNotInexchange_shouldRevert() public { + function test_swapOut_whenTokenOutNotInExchange_shouldRevert() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); vm.prank(brokerAddress); @@ -1599,6 +1610,14 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { bancorExchangeProvider.swapOut(exchangeId, address(token), address(token), 1e18); } + function test_swapOut_whenTokenInIsTokenAndAmountInIsLargerOrEqualSupply_shouldRevert() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + vm.expectRevert("amountIn required is greater than tokenSupply"); + bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), poolExchange1.reserveBalance); + } + function test_swapOut_whenTokenInIsReserveAsset_shouldSwapOut() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountOut = 1e18; @@ -1646,4 +1665,30 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); } + + function test_swapOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() + public + { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountToBuy = 50_000 * 1e18; + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountInInOneBuy = bancorExchangeProvider.swapOut( + exchangeId, + address(token), + address(reserveToken), + amountToBuy + ); + + bancorExchangeProvider.destroyExchange(exchangeId, 0); + exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountInInMultipleBuys; + for (uint256 i = 0; i < 50_000; i++) { + vm.prank(brokerAddress); + amountInInMultipleBuys += bancorExchangeProvider.swapOut(exchangeId, address(token), address(reserveToken), 1e18); + } + // we allow up to 0.1% difference due to precision loss on exitContribution accounting + assertApproxEqRel(amountInInOneBuy, amountInInMultipleBuys, 1e18 * 0.001); + } } From f4e3b950b4beea4f94fd0dcd0b7170c4e86f9cf7 Mon Sep 17 00:00:00 2001 From: baroooo Date: Mon, 2 Dec 2024 13:20:47 +0100 Subject: [PATCH 36/38] fix: update currentPrice to return correct decimals (#560) ### Description Scales down the current price to correct decimals dividing it by the reserve asset precision multiplier ### Other changes added some extra tests for swapIn and swapOut with non standard token decimals ### Tested Unit test ### Related issues - Fixes #559 --- .../goodDollar/BancorExchangeProvider.sol | 6 +- .../goodDollar/BancorExchangeProvider.t.sol | 161 ++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 83e131a3..cc79b1f6 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -160,9 +160,11 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B // calculates: reserveBalance / (tokenSupply * reserveRatio) PoolExchange memory exchange = getPoolExchange(exchangeId); uint256 scaledReserveRatio = uint256(exchange.reserveRatio) * 1e10; + UD60x18 denominator = wrap(exchange.tokenSupply).mul(wrap(scaledReserveRatio)); - price = unwrap(wrap(exchange.reserveBalance).div(denominator)); - return price; + uint256 priceScaled = unwrap(wrap(exchange.reserveBalance).div(denominator)); + + price = priceScaled / tokenPrecisionMultipliers[exchange.reserveAsset]; } /* ============================================================ */ diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 9cd8fa66..1e56a65d 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -5,6 +5,8 @@ pragma solidity 0.8.18; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "openzeppelin-contracts-next/contracts/token/ERC20/ERC20.sol"; +import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; + import { BancorExchangeProvider } from "contracts/goodDollar/BancorExchangeProvider.sol"; import { IExchangeProvider } from "contracts/interfaces/IExchangeProvider.sol"; import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; @@ -30,16 +32,22 @@ contract BancorExchangeProviderTest is Test { ERC20 public reserveToken; ERC20 public token; ERC20 public token2; + ERC20DecimalsMock public reserveTokenWith6Decimals; + ERC20DecimalsMock public tokenWith6Decimals; address public reserveAddress; address public brokerAddress; IBancorExchangeProvider.PoolExchange public poolExchange1; IBancorExchangeProvider.PoolExchange public poolExchange2; + IBancorExchangeProvider.PoolExchange public poolExchange3; + IBancorExchangeProvider.PoolExchange public poolExchange4; function setUp() public virtual { reserveToken = new ERC20("cUSD", "cUSD"); token = new ERC20("Good$", "G$"); token2 = new ERC20("Good2$", "G2$"); + reserveTokenWith6Decimals = new ERC20DecimalsMock("Reserve Token", "RES", 6); + tokenWith6Decimals = new ERC20DecimalsMock("Token", "TKN", 6); brokerAddress = makeAddr("Broker"); reserveAddress = makeAddr("Reserve"); @@ -62,6 +70,24 @@ contract BancorExchangeProviderTest is Test { exitContribution: 1e8 * 0.01 }); + poolExchange3 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveTokenWith6Decimals), + tokenAddress: address(token), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + + poolExchange4 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken), + tokenAddress: address(tokenWith6Decimals), + tokenSupply: 300_000 * 1e18, + reserveBalance: 60_000 * 1e18, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), @@ -72,11 +98,22 @@ contract BancorExchangeProviderTest is Test { abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token2)), abi.encode(true) ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(tokenWith6Decimals)), + abi.encode(true) + ); vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken)), abi.encode(true) ); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveTokenWith6Decimals)), + abi.encode(true) + ); } function initializeBancorExchangeProvider() internal returns (BancorExchangeProvider) { @@ -1416,6 +1453,15 @@ contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { assertEq(price, expectedPrice); } + function test_currentPrice_whenReserveTokenHasLessThan18Decimals_shouldReturnCorrectPrice() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + // formula: price = reserveBalance / tokenSupply * reserveRatio + // calculation: 60_000 / 300_000 * 0.2 = 1 + uint256 expectedPrice = 1e6; + uint256 price = bancorExchangeProvider.currentPrice(exchangeId); + assertEq(price, expectedPrice); + } + function test_currentPrice_fuzz(uint256 reserveBalance, uint256 tokenSupply, uint256 reserveRatio) public { // reserveBalance range between 1 token and 10_000_000 tokens reserveBalance = bound(reserveBalance, 1e18, 10_000_000 * 1e18); @@ -1512,6 +1558,35 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); } + function test_swapIn_whenTokenInIsReserveAssetWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e6; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + uint256 reserveBalanceBefore = poolExchange3.reserveBalance; + uint256 tokenSupplyBefore = poolExchange3.tokenSupply; + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn( + exchangeId, + address(reserveTokenWith6Decimals), + address(token), + amountIn + ); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn * 1e12); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + function test_swapIn_whenTokenInIsToken_shouldSwapIn() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountIn = 1e18; @@ -1536,6 +1611,35 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); } + function test_swapIn_whenTokenIsTokenWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); + uint256 reserveBalanceBefore = poolExchange4.reserveBalance; + uint256 tokenSupplyBefore = poolExchange4.tokenSupply; + + uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(reserveToken), + tokenOut: address(tokenWith6Decimals), + amountIn: amountIn + }); + vm.prank(brokerAddress); + uint256 amountOut = bancorExchangeProvider.swapIn( + exchangeId, + address(reserveToken), + address(tokenWith6Decimals), + amountIn + ); + assertEq(amountOut, expectedAmountOut); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore + amountIn); + assertApproxEqRel(tokenSupplyAfter, tokenSupplyBefore + amountOut * 1e12, 1e18 * 0.0001); + } + function test_swapIn_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() public { @@ -1642,6 +1746,35 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); } + function test_swapOut_whenTokenInIsReserveAssetWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); + uint256 reserveBalanceBefore = poolExchange3.reserveBalance; + uint256 tokenSupplyBefore = poolExchange3.tokenSupply; + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut( + exchangeId, + address(reserveTokenWith6Decimals), + address(token), + amountOut + ); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertApproxEqRel(reserveBalanceAfter, reserveBalanceBefore + amountIn * 1e12, 1e18 * 0.0001); + assertEq(tokenSupplyAfter, tokenSupplyBefore + amountOut); + } + function test_swapOut_whenTokenInIsToken_shouldSwapOut() public { BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); uint256 amountOut = 1e18; @@ -1666,6 +1799,34 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { assertEq(tokenSupplyAfter, tokenSupplyBefore - amountIn); } + function test_swapOut_whenTokenInIsTokenWith6Decimals_shouldSwapIn() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 1e18; + + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); + uint256 reserveBalanceBefore = poolExchange4.reserveBalance; + uint256 tokenSupplyBefore = poolExchange4.tokenSupply; + + uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(tokenWith6Decimals), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + vm.prank(brokerAddress); + uint256 amountIn = bancorExchangeProvider.swapOut( + exchangeId, + address(tokenWith6Decimals), + address(reserveToken), + amountOut + ); + assertEq(amountIn, expectedAmountIn); + + (, , uint256 tokenSupplyAfter, uint256 reserveBalanceAfter, , ) = bancorExchangeProvider.exchanges(exchangeId); + + assertEq(reserveBalanceAfter, reserveBalanceBefore - amountOut); + assertApproxEqRel(tokenSupplyAfter, tokenSupplyBefore - amountIn * 1e12, 1e18 * 0.0001); + } function test_swapOut_whenTokenInIsTokenAndExitContributionIsNonZero_shouldReturnSameAmountWhenSellIsDoneInMultipleSteps() public { From 06b02cc0de7e82d0a9e6d0d299fc75ff4e5277d8 Mon Sep 17 00:00:00 2001 From: baroooo Date: Wed, 4 Dec 2024 15:55:22 +0100 Subject: [PATCH 37/38] Fix/scaling issues for tokens with less than 18 decimals (#561) --- .../goodDollar/BancorExchangeProvider.sol | 3 + .../goodDollar/GoodDollarExchangeProvider.sol | 5 +- .../GoodDollarExpansionController.sol | 12 +-- .../IGoodDollarExpansionController.sol | 3 +- .../goodDollar/BancorExchangeProvider.t.sol | 47 ++++++++--- .../GoodDollarExpansionController.t.sol | 83 +++++++++++++++++++ 6 files changed, 132 insertions(+), 21 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index cc79b1f6..041148c0 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -287,6 +287,9 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B tokenPrecisionMultipliers[exchange.reserveAsset] = 10 ** (18 - uint256(reserveAssetDecimals)); tokenPrecisionMultipliers[exchange.tokenAddress] = 10 ** (18 - uint256(tokenDecimals)); + exchange.reserveBalance = exchange.reserveBalance * tokenPrecisionMultipliers[exchange.reserveAsset]; + exchange.tokenSupply = exchange.tokenSupply * tokenPrecisionMultipliers[exchange.tokenAddress]; + exchanges[exchangeId] = exchange; exchangeIds.push(exchangeId); emit ExchangeCreated(exchangeId, exchange.reserveAsset, exchange.tokenAddress); diff --git a/contracts/goodDollar/GoodDollarExchangeProvider.sol b/contracts/goodDollar/GoodDollarExchangeProvider.sol index b75170d2..088e7702 100644 --- a/contracts/goodDollar/GoodDollarExchangeProvider.sol +++ b/contracts/goodDollar/GoodDollarExchangeProvider.sol @@ -180,14 +180,13 @@ contract GoodDollarExchangeProvider is IGoodDollarExchangeProvider, BancorExchan ) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) { PoolExchange memory exchange = getPoolExchange(exchangeId); - uint256 reserveinterestScaled = reserveInterest * tokenPrecisionMultipliers[exchange.reserveAsset]; uint256 amountToMintScaled = unwrap( - wrap(reserveinterestScaled).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) + wrap(reserveInterest).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance)) ); amountToMint = amountToMintScaled / tokenPrecisionMultipliers[exchange.tokenAddress]; exchanges[exchangeId].tokenSupply += amountToMintScaled; - exchanges[exchangeId].reserveBalance += reserveinterestScaled; + exchanges[exchangeId].reserveBalance += reserveInterest; return amountToMint; } diff --git a/contracts/goodDollar/GoodDollarExpansionController.sol b/contracts/goodDollar/GoodDollarExpansionController.sol index 5d64a505..a78c1cb2 100644 --- a/contracts/goodDollar/GoodDollarExpansionController.sol +++ b/contracts/goodDollar/GoodDollarExpansionController.sol @@ -136,19 +136,21 @@ contract GoodDollarExpansionController is IGoodDollarExpansionController, Ownabl } /// @inheritdoc IGoodDollarExpansionController - function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external { + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountMinted) { require(reserveInterest > 0, "Reserve interest must be greater than 0"); IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider)) .getPoolExchange(exchangeId); - uint256 amountToMint = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterest); - require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); - IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint); + + uint256 reserveInterestScaled = reserveInterest * (10 ** (18 - IERC20Metadata(exchange.reserveAsset).decimals())); + amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterestScaled); + + IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted); // Ignored, because contracts only interacts with trusted contracts and tokens // slither-disable-next-line reentrancy-events - emit InterestUBIMinted(exchangeId, amountToMint); + emit InterestUBIMinted(exchangeId, amountMinted); } /// @inheritdoc IGoodDollarExpansionController diff --git a/contracts/interfaces/IGoodDollarExpansionController.sol b/contracts/interfaces/IGoodDollarExpansionController.sol index c09874d5..2f482288 100644 --- a/contracts/interfaces/IGoodDollarExpansionController.sol +++ b/contracts/interfaces/IGoodDollarExpansionController.sol @@ -130,8 +130,9 @@ interface IGoodDollarExpansionController { * @notice Mints UBI as G$ tokens for a given pool from collected reserve interest. * @param exchangeId The ID of the pool to mint UBI for. * @param reserveInterest The amount of reserve tokens collected from interest. + * @return amountMinted The amount of G$ tokens minted. */ - function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external; + function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external returns (uint256 amountMinted); /** * @notice Mints UBI as G$ tokens for a given pool by comparing the contract's reserve balance to the virtual balance. diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index 1e56a65d..cb602b04 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -41,6 +41,7 @@ contract BancorExchangeProviderTest is Test { IBancorExchangeProvider.PoolExchange public poolExchange2; IBancorExchangeProvider.PoolExchange public poolExchange3; IBancorExchangeProvider.PoolExchange public poolExchange4; + IBancorExchangeProvider.PoolExchange public poolExchange5; function setUp() public virtual { reserveToken = new ERC20("cUSD", "cUSD"); @@ -74,7 +75,7 @@ contract BancorExchangeProviderTest is Test { reserveAsset: address(reserveTokenWith6Decimals), tokenAddress: address(token), tokenSupply: 300_000 * 1e18, - reserveBalance: 60_000 * 1e18, + reserveBalance: 60_000 * 1e6, reserveRatio: 1e8 * 0.2, exitContribution: 1e8 * 0.01 }); @@ -82,12 +83,21 @@ contract BancorExchangeProviderTest is Test { poolExchange4 = IBancorExchangeProvider.PoolExchange({ reserveAsset: address(reserveToken), tokenAddress: address(tokenWith6Decimals), - tokenSupply: 300_000 * 1e18, + tokenSupply: 300_000 * 1e6, reserveBalance: 60_000 * 1e18, reserveRatio: 1e8 * 0.2, exitContribution: 1e8 * 0.01 }); + poolExchange5 = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveTokenWith6Decimals), + tokenAddress: address(tokenWith6Decimals), + tokenSupply: 300_000 * 1e6, + reserveBalance: 60_000 * 1e6, + reserveRatio: 1e8 * 0.2, + exitContribution: 1e8 * 0.01 + }); + vm.mockCall( reserveAddress, abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), @@ -383,6 +393,23 @@ contract BancorExchangeProviderTest_createExchange is BancorExchangeProviderTest assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveToken)), 1); assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(token)), 1); } + + function test_createExchange_whenTokensHasLessThan18Decimals_shouldCreateExchangeWithCorrectSupplyAndBalance() + public + { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange5); + + IBancorExchangeProvider.PoolExchange memory poolExchange = bancorExchangeProvider.getPoolExchange(exchangeId); + assertEq(poolExchange.reserveAsset, poolExchange5.reserveAsset); + assertEq(poolExchange.tokenAddress, poolExchange5.tokenAddress); + assertEq(poolExchange.tokenSupply, poolExchange5.tokenSupply * 1e12); + assertEq(poolExchange.reserveBalance, poolExchange5.reserveBalance * 1e12); + assertEq(poolExchange.reserveRatio, poolExchange5.reserveRatio); + assertEq(poolExchange.exitContribution, poolExchange5.exitContribution); + + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(reserveTokenWith6Decimals)), 1e12); + assertEq(bancorExchangeProvider.tokenPrecisionMultipliers(address(tokenWith6Decimals)), 1e12); + } } contract BancorExchangeProviderTest_destroyExchange is BancorExchangeProviderTest { @@ -737,7 +764,7 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { reserveAsset: address(reserveToken6), tokenAddress: address(stableToken18), tokenSupply: 100_000 * 1e18, // 100,000 - reserveBalance: 50_000 * 1e18, // 50,000 + reserveBalance: 50_000 * 1e6, // 50,000 reserveRatio: 1e8 * 0.5, // 50% exitContribution: 0 }); @@ -1225,7 +1252,7 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { reserveAsset: address(reserveToken6), tokenAddress: address(stableToken18), tokenSupply: 100_000 * 1e18, // 100,000 - reserveBalance: 50_000 * 1e18, // 50,000 + reserveBalance: 50_000 * 1e6, // 50,000 reserveRatio: 1e8 * 0.5, // 50% exitContribution: 0 }); @@ -1563,8 +1590,7 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { uint256 amountIn = 1e6; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); - uint256 reserveBalanceBefore = poolExchange3.reserveBalance; - uint256 tokenSupplyBefore = poolExchange3.tokenSupply; + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, @@ -1616,8 +1642,7 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { uint256 amountIn = 1e18; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); - uint256 reserveBalanceBefore = poolExchange4.reserveBalance; - uint256 tokenSupplyBefore = poolExchange4.tokenSupply; + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); uint256 expectedAmountOut = bancorExchangeProvider.getAmountOut({ exchangeId: exchangeId, @@ -1751,8 +1776,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { uint256 amountOut = 1e18; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange3); - uint256 reserveBalanceBefore = poolExchange3.reserveBalance; - uint256 tokenSupplyBefore = poolExchange3.tokenSupply; + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, @@ -1804,8 +1828,7 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { uint256 amountOut = 1e18; bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange4); - uint256 reserveBalanceBefore = poolExchange4.reserveBalance; - uint256 tokenSupplyBefore = poolExchange4.tokenSupply; + (, , uint256 tokenSupplyBefore, uint256 reserveBalanceBefore, , ) = bancorExchangeProvider.exchanges(exchangeId); uint256 expectedAmountIn = bancorExchangeProvider.getAmountIn({ exchangeId: exchangeId, diff --git a/test/unit/goodDollar/GoodDollarExpansionController.t.sol b/test/unit/goodDollar/GoodDollarExpansionController.t.sol index d763a768..23366d94 100644 --- a/test/unit/goodDollar/GoodDollarExpansionController.t.sol +++ b/test/unit/goodDollar/GoodDollarExpansionController.t.sol @@ -7,11 +7,13 @@ import { Test } from "forge-std/Test.sol"; import { ERC20Mock } from "openzeppelin-contracts-next/contracts/mocks/ERC20Mock.sol"; import { ERC20DecimalsMock } from "openzeppelin-contracts-next/contracts/mocks/ERC20DecimalsMock.sol"; import { GoodDollarExpansionController } from "contracts/goodDollar/GoodDollarExpansionController.sol"; +import { GoodDollarExchangeProvider } from "contracts/goodDollar/GoodDollarExchangeProvider.sol"; import { IGoodDollarExpansionController } from "contracts/interfaces/IGoodDollarExpansionController.sol"; import { IGoodDollarExchangeProvider } from "contracts/interfaces/IGoodDollarExchangeProvider.sol"; import { IBancorExchangeProvider } from "contracts/interfaces/IBancorExchangeProvider.sol"; import { IDistributionHelper } from "contracts/goodDollar/interfaces/IGoodProtocol.sol"; +import { IReserve } from "contracts/interfaces/IReserve.sol"; import { GoodDollarExpansionControllerHarness } from "test/utils/harnesses/GoodDollarExpansionControllerHarness.sol"; @@ -679,3 +681,84 @@ contract GoodDollarExpansionControllerTest_mintRewardFromReserveRatio is GoodDol assertEq(token.balanceOf(to), toBalanceBefore + amountToMint); } } + +contract GoodDollarExpansionControllerIntegrationTest is GoodDollarExpansionControllerTest { + address brokerAddress = makeAddr("Broker"); + GoodDollarExpansionController _expansionController; + GoodDollarExchangeProvider _exchangeProvider; + ERC20DecimalsMock reserveToken6DecimalsMock; + + function setUp() public override { + super.setUp(); + _exchangeProvider = new GoodDollarExchangeProvider(false); + _expansionController = new GoodDollarExpansionController(false); + + _expansionController.initialize(address(_exchangeProvider), distributionHelper, reserveAddress, avatarAddress); + _exchangeProvider.initialize(brokerAddress, reserveAddress, address(_expansionController), avatarAddress); + + reserveToken6DecimalsMock = new ERC20DecimalsMock("Reserve Token", "RES", 6); + IBancorExchangeProvider.PoolExchange memory poolExchange = IBancorExchangeProvider.PoolExchange({ + reserveAsset: address(reserveToken6DecimalsMock), + tokenAddress: address(token), + tokenSupply: 7 * 1e9 * 1e18, + reserveBalance: 200_000 * 1e6, + reserveRatio: 0.2 * 1e8, // 20% + exitContribution: 0.1 * 1e8 // 10% + }); + + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isStableAsset.selector, address(token)), + abi.encode(true) + ); + vm.mockCall( + reserveAddress, + abi.encodeWithSelector(IReserve(reserveAddress).isCollateralAsset.selector, address(reserveToken6DecimalsMock)), + abi.encode(true) + ); + vm.prank(avatarAddress); + exchangeId = _exchangeProvider.createExchange(poolExchange); + } + + function test_mintUBIFromReserveBalance_whenReserveTokenHas6Decimals_shouldMintAndEmit() public { + uint256 reserveInterest = 1000e6; + // amountToMint = reserveInterest * tokenSupply / reserveBalance + uint256 amountToMint = 35_000_000e18; + + deal(address(reserveToken6DecimalsMock), reserveAddress, 200_000 * 1e6 + reserveInterest); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = _expansionController.mintUBIFromReserveBalance(exchangeId); + + assertEq(amountMinted, amountToMint); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + } + + function test_mintUBIFromInterest_whenReserveTokenHas6Decimals_shouldMintAndEmit() public { + uint256 reserveInterest = 1000e6; + // amountToMint = reserveInterest * tokenSupply / reserveBalance + uint256 amountToMint = 35_000_000e18; + address interestCollector = makeAddr("InterestCollector"); + + deal(address(reserveToken6DecimalsMock), interestCollector, reserveInterest); + + vm.startPrank(interestCollector); + reserveToken6DecimalsMock.approve(address(_expansionController), reserveInterest); + + uint256 interestCollectorBalanceBefore = reserveToken6DecimalsMock.balanceOf(interestCollector); + uint256 reserveBalanceBefore = reserveToken6DecimalsMock.balanceOf(reserveAddress); + uint256 distributionHelperBalanceBefore = token.balanceOf(distributionHelper); + + vm.expectEmit(true, true, true, true); + emit InterestUBIMinted(exchangeId, amountToMint); + uint256 amountMinted = _expansionController.mintUBIFromInterest(exchangeId, reserveInterest); + + assertEq(amountMinted, amountToMint); + + assertEq(reserveToken6DecimalsMock.balanceOf(reserveAddress), reserveBalanceBefore + reserveInterest); + assertEq(token.balanceOf(distributionHelper), distributionHelperBalanceBefore + amountToMint); + assertEq(reserveToken6DecimalsMock.balanceOf(interestCollector), interestCollectorBalanceBefore - reserveInterest); + } +} From 20fc515c055dcf44f68c0bbbb3dec223be6bea2a Mon Sep 17 00:00:00 2001 From: philbow61 <80156619+philbow61@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:55:40 +0100 Subject: [PATCH 38/38] feat: ensure amountOut's are rounded down and amountIn's are roundedUp (#562) --- .../goodDollar/BancorExchangeProvider.sol | 14 +- .../goodDollar/BancorExchangeProvider.t.sol | 275 ++++++++++++++++++ 2 files changed, 287 insertions(+), 2 deletions(-) diff --git a/contracts/goodDollar/BancorExchangeProvider.sol b/contracts/goodDollar/BancorExchangeProvider.sol index 041148c0..6f2ebdeb 100644 --- a/contracts/goodDollar/BancorExchangeProvider.sol +++ b/contracts/goodDollar/BancorExchangeProvider.sol @@ -151,7 +151,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B require(scaledAmountIn < exchange.tokenSupply, "amountIn is greater than tokenSupply"); } - amountIn = scaledAmountIn / tokenPrecisionMultipliers[tokenIn]; + amountIn = divAndRoundUp(scaledAmountIn, tokenPrecisionMultipliers[tokenIn]); return amountIn; } @@ -261,7 +261,7 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B _accountExitContribution(exchangeId, exitContribution); } - amountIn = scaledAmountInWithExitContribution / tokenPrecisionMultipliers[tokenIn]; + amountIn = divAndRoundUp(scaledAmountInWithExitContribution, tokenPrecisionMultipliers[tokenIn]); return amountIn; } @@ -359,6 +359,16 @@ contract BancorExchangeProvider is IExchangeProvider, IBancorExchangeProvider, B exchanges[exchangeId].tokenSupply -= exitContribution; } + /** + * @notice Division and rounding up if there is a remainder + * @param a The dividend + * @param b The divisor + * @return The result of the division rounded up + */ + function divAndRoundUp(uint256 a, uint256 b) internal pure returns (uint256) { + return (a / b) + (a % b > 0 ? 1 : 0); + } + /** * @notice Calculate the scaledAmountIn of tokenIn for a given scaledAmountOut of tokenOut * @param exchange The pool exchange to operate on diff --git a/test/unit/goodDollar/BancorExchangeProvider.t.sol b/test/unit/goodDollar/BancorExchangeProvider.t.sol index cb602b04..d39a90d1 100644 --- a/test/unit/goodDollar/BancorExchangeProvider.t.sol +++ b/test/unit/goodDollar/BancorExchangeProvider.t.sol @@ -968,6 +968,88 @@ contract BancorExchangeProviderTest_getAmountIn is BancorExchangeProviderTest { // we allow up to 1% difference due to precision loss assertApproxEqRel(reversedAmountOut, amountOut, 1e18 * 0.01); } + + function test_getAmountIn_whenTokenInIsTokenWith6TokenDecimals_shouldRoundUpInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + + uint256 amountOut = 55e18; + uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId6Decimals, + tokenIn: address(tokenWith6Decimals), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId18Decimals, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertTrue(amountIn18Decimals <= amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_getAmountIn_whenTokenInIsReserveAssetWith6TokenDecimals_shouldRoundUpInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + + uint256 amountOut = 55e18; + uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId6Decimals, + tokenIn: address(reserveTokenWith6Decimals), + tokenOut: address(token), + amountOut: amountOut + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId18Decimals, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountOut: amountOut + }); + + assertTrue(amountIn18Decimals <= amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_getAmountIn_whenTokenInHas6DecimalsButNoRoundingNeeded_shouldNotRoundUp() public { + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 15e17 + }); + + uint256 amountIn18Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertEq(amountIn18Decimals, 15e17); + + bancorExchangeProvider.destroyExchange(exchangeId, 0); + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + + uint256 amountIn6Decimals = bancorExchangeProvider.getAmountIn({ + exchangeId: exchangeId6Decimals, + tokenIn: address(tokenWith6Decimals), + tokenOut: address(reserveToken), + amountOut: amountOut + }); + + assertEq(amountIn6Decimals, amountIn18Decimals / 1e12); + } } contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { @@ -1455,6 +1537,56 @@ contract BancorExchangeProviderTest_getAmountOut is BancorExchangeProviderTest { // we allow up to 1% difference due to precision loss assertApproxEqRel(reversedAmountIn, amountIn, 1e18 * 0.01); } + + function test_getAmountOut_whenTokenOutIsTokenWith6TokenDecimals_shouldRoundDownInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + + uint256 amountIn = 55e18; + uint256 amountOut6Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId6Decimals, + tokenIn: address(reserveToken), + tokenOut: address(tokenWith6Decimals), + amountIn: amountIn + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut18Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId18Decimals, + tokenIn: address(reserveToken), + tokenOut: address(token), + amountIn: amountIn + }); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } + + function test_getAmountOut_whenTokenOutIsReserveAssetWith6TokenDecimals_shouldRoundDownInFavorOfReserve() public { + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + + uint256 amountIn = 55e18; + uint256 amountOut6Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId6Decimals, + tokenIn: address(token), + tokenOut: address(reserveTokenWith6Decimals), + amountIn: amountIn + }); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut18Decimals = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId18Decimals, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: amountIn + }); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } } contract BancorExchangeProviderTest_currentPrice is BancorExchangeProviderTest { @@ -1697,6 +1829,60 @@ contract BancorExchangeProviderTest_swapIn is BancorExchangeProviderTest { // we allow up to 0.1% difference due to precision loss on exitContribution accounting assertApproxEqRel(amountOutInOneSell, amountOutInMultipleSells, 1e18 * 0.001); } + + function test_swapIn_whenTokenOutIsTokenWith6Decimals_shouldRoundDownInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + vm.prank(brokerAddress); + uint256 amountOut6Decimals = bancorExchangeProvider.swapIn( + exchangeId6Decimals, + address(reserveToken), + address(tokenWith6Decimals), + amountIn + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountOut18Decimals = bancorExchangeProvider.swapIn( + exchangeId18Decimals, + address(reserveToken), + address(token), + amountIn + ); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } + + function test_swapIn_whenTokenOutIsReserveAssetWith6Decimals_shouldRoundDownInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountIn = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + vm.prank(brokerAddress); + uint256 amountOut6Decimals = bancorExchangeProvider.swapIn( + exchangeId6Decimals, + address(token), + address(reserveTokenWith6Decimals), + amountIn + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountOut18Decimals = bancorExchangeProvider.swapIn( + exchangeId18Decimals, + address(token), + address(reserveToken), + amountIn + ); + + assertTrue(amountOut6Decimals * 1e12 < amountOut18Decimals); + assertEq(amountOut6Decimals, (amountOut18Decimals / 1e12)); + } } contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { @@ -1875,4 +2061,93 @@ contract BancorExchangeProviderTest_swapOut is BancorExchangeProviderTest { // we allow up to 0.1% difference due to precision loss on exitContribution accounting assertApproxEqRel(amountInInOneBuy, amountInInMultipleBuys, 1e18 * 0.001); } + + function test_swapOut_whenTokenInIsTokenWith6Decimals_shouldRoundUpInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange4); + vm.prank(brokerAddress); + uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( + exchangeId6Decimals, + address(tokenWith6Decimals), + address(reserveToken), + amountOut + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( + exchangeId18Decimals, + address(token), + address(reserveToken), + amountOut + ); + + assertTrue(amountIn18Decimals < amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_swapOut_whenTokenInIsReserveAssetWith6Decimals_shouldRoundUpInFavorOfReserve() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + uint256 amountOut = 55e18; + + bytes32 exchangeId6Decimals = bancorExchangeProvider.createExchange(poolExchange3); + vm.prank(brokerAddress); + uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( + exchangeId6Decimals, + address(reserveTokenWith6Decimals), + address(token), + amountOut + ); + + bancorExchangeProvider.destroyExchange(exchangeId6Decimals, 0); + bytes32 exchangeId18Decimals = bancorExchangeProvider.createExchange(poolExchange1); + vm.prank(brokerAddress); + uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( + exchangeId18Decimals, + address(reserveToken), + address(token), + amountOut + ); + + assertTrue(amountIn18Decimals < amountIn6Decimals * 1e12); + assertEq(amountIn6Decimals, (amountIn18Decimals / 1e12) + 1); + } + + function test_swapOut_whenTokenInHas6DecimalsButNoRoundingNeeded_shouldNotRoundUp() public { + BancorExchangeProvider bancorExchangeProvider = initializeBancorExchangeProvider(); + bytes32 exchangeId = bancorExchangeProvider.createExchange(poolExchange1); + + uint256 amountOut = bancorExchangeProvider.getAmountOut({ + exchangeId: exchangeId, + tokenIn: address(token), + tokenOut: address(reserveToken), + amountIn: 15e17 + }); + + vm.prank(brokerAddress); + uint256 amountIn18Decimals = bancorExchangeProvider.swapOut( + exchangeId, + address(token), + address(reserveToken), + amountOut + ); + + assertEq(amountIn18Decimals, 15e17); + + bancorExchangeProvider.destroyExchange(exchangeId, 0); + exchangeId = bancorExchangeProvider.createExchange(poolExchange4); + + vm.prank(brokerAddress); + uint256 amountIn6Decimals = bancorExchangeProvider.swapOut( + exchangeId, + address(tokenWith6Decimals), + address(reserveToken), + amountOut + ); + + assertEq(amountIn6Decimals, amountIn18Decimals / 1e12); + } }