-
-
Save zzzitron/74dfbc0249151a682061cd6532628d87 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: Unlicense | |
pragma solidity >=0.8.0; | |
import {Test} from "forge-std/Test.sol"; | |
import {console2} from "forge-std/console2.sol"; | |
import {UserFactory} from "test/lib/UserFactory.sol"; | |
import {BondFixedTermCDA} from "test/lib/bonds/BondFixedTermCDA.sol"; | |
import {BondAggregator} from "test/lib/bonds/BondAggregator.sol"; | |
import {BondFixedTermTeller} from "test/lib/bonds/BondFixedTermTeller.sol"; | |
import {RolesAuthority, Authority as SolmateAuthority} from "solmate/auth/authorities/RolesAuthority.sol"; | |
import {MockERC20, ERC20} from "solmate/test/utils/mocks/MockERC20.sol"; | |
import {MockPrice} from "test/mocks/MockPrice.sol"; | |
import {IBondAuctioneer} from "interfaces/IBondAuctioneer.sol"; | |
import {IBondAggregator} from "interfaces/IBondAggregator.sol"; | |
import {FullMath} from "libraries/FullMath.sol"; | |
import "src/Kernel.sol"; | |
import {OlympusRange} from "modules/RANGE.sol"; | |
import {OlympusTreasury} from "modules/TRSRY.sol"; | |
import {OlympusMinter, OHM} from "modules/MINTR.sol"; | |
import {Operator} from "policies/Operator.sol"; | |
import {BondCallback} from "policies/BondCallback.sol"; | |
contract MockOhm is ERC20 { | |
constructor( | |
string memory _name, | |
string memory _symbol, | |
uint8 _decimals | |
) ERC20(_name, _symbol, _decimals) {} | |
function mint(address to, uint256 value) public virtual { | |
_mint(to, value); | |
} | |
function burnFrom(address from, uint256 value) public virtual { | |
_burn(from, value); | |
} | |
} | |
// solhint-disable-next-line max-states-count | |
contract OperatorTest is Test { | |
using FullMath for uint256; | |
UserFactory public userCreator; | |
address internal alice; | |
address internal bob; | |
address internal guardian; | |
address internal policy; | |
address internal heart; | |
RolesAuthority internal auth; | |
BondAggregator internal aggregator; | |
BondFixedTermTeller internal teller; | |
BondFixedTermCDA internal auctioneer; | |
MockOhm internal ohm; | |
MockERC20 internal reserve; | |
Kernel internal kernel; | |
MockPrice internal price; | |
OlympusRange internal range; | |
OlympusTreasury internal treasury; | |
OlympusMinter internal minter; | |
Operator internal operator; | |
BondCallback internal callback; | |
function setUp() public { | |
vm.warp(51 * 365 * 24 * 60 * 60); // Set timestamp at roughly Jan 1, 2021 (51 years since Unix epoch) | |
userCreator = new UserFactory(); | |
{ | |
/// Deploy bond system to test against | |
address[] memory users = userCreator.create(5); | |
alice = users[0]; | |
bob = users[1]; | |
guardian = users[2]; | |
policy = users[3]; | |
heart = users[4]; | |
auth = new RolesAuthority(guardian, SolmateAuthority(address(0))); | |
/// Deploy the bond system | |
aggregator = new BondAggregator(guardian, auth); | |
teller = new BondFixedTermTeller(guardian, aggregator, guardian, auth); | |
auctioneer = new BondFixedTermCDA(teller, aggregator, guardian, auth); | |
/// Register auctioneer on the bond system | |
vm.prank(guardian); | |
aggregator.registerAuctioneer(auctioneer); | |
} | |
{ | |
/// Deploy mock tokens | |
ohm = new MockOhm("Olympus", "OHM", 9); | |
reserve = new MockERC20("Reserve", "RSV", 18); | |
} | |
{ | |
/// Deploy kernel | |
kernel = new Kernel(); // this contract will be the executor | |
/// Deploy modules (some mocks) | |
price = new MockPrice(kernel, uint48(8 hours)); | |
range = new OlympusRange( | |
kernel, | |
[ERC20(ohm), ERC20(reserve)], | |
[uint256(100), uint256(1000), uint256(2000)] | |
); | |
treasury = new OlympusTreasury(kernel); | |
minter = new OlympusMinter(kernel, address(ohm)); | |
/// Configure mocks | |
price.setMovingAverage(100 * 1e18); | |
price.setLastPrice(100 * 1e18); | |
price.setDecimals(18); | |
} | |
{ | |
/// Deploy bond callback | |
callback = new BondCallback(kernel, IBondAggregator(address(aggregator)), ohm); | |
/// Deploy operator | |
operator = new Operator( | |
kernel, | |
IBondAuctioneer(address(auctioneer)), | |
callback, | |
[ERC20(ohm), ERC20(reserve)], | |
[ | |
uint32(2000), // cushionFactor | |
uint32(5 days), // duration | |
uint32(100_000), // debtBuffer | |
uint32(1 hours), // depositInterval | |
uint32(1000), // reserveFactor | |
uint32(1 hours), // regenWait | |
uint32(5), // regenThreshold | |
uint32(7) // regenObserve | |
] | |
); | |
/// Registor operator to create bond markets with a callback | |
vm.prank(guardian); | |
auctioneer.setCallbackAuthStatus(address(operator), true); | |
} | |
{ | |
/// Initialize system and kernel | |
/// Install modules | |
kernel.executeAction(Actions.InstallModule, address(price)); | |
kernel.executeAction(Actions.InstallModule, address(range)); | |
kernel.executeAction(Actions.InstallModule, address(treasury)); | |
kernel.executeAction(Actions.InstallModule, address(minter)); | |
/// Approve policies | |
kernel.executeAction(Actions.ActivatePolicy, address(operator)); | |
kernel.executeAction(Actions.ActivatePolicy, address(callback)); | |
} | |
{ | |
/// Configure access control | |
/// Operator roles | |
kernel.grantRole(toRole("operator_operate"), address(heart)); | |
kernel.grantRole(toRole("operator_operate"), guardian); | |
kernel.grantRole(toRole("operator_reporter"), address(callback)); | |
kernel.grantRole(toRole("operator_policy"), policy); | |
kernel.grantRole(toRole("operator_admin"), guardian); | |
/// Bond callback roles | |
kernel.grantRole(toRole("callback_whitelist"), address(operator)); | |
kernel.grantRole(toRole("callback_whitelist"), guardian); | |
kernel.grantRole(toRole("callback_admin"), guardian); | |
} | |
/// Set operator on the callback | |
vm.prank(guardian); | |
callback.setOperator(operator); | |
// Mint tokens to users and treasury for testing | |
uint256 testOhm = 1_000_000 * 1e9; | |
uint256 testReserve = 1_000_000 * 1e18; | |
ohm.mint(alice, testOhm * 20); | |
reserve.mint(alice, testReserve * 20); | |
reserve.mint(address(treasury), testReserve * 100); | |
// Approve the operator and bond teller for the tokens to swap | |
vm.prank(alice); | |
ohm.approve(address(operator), testOhm * 20); | |
vm.prank(alice); | |
reserve.approve(address(operator), testReserve * 20); | |
vm.prank(alice); | |
ohm.approve(address(teller), testOhm * 20); | |
vm.prank(alice); | |
reserve.approve(address(teller), testReserve * 20); | |
} | |
/* ========== HELPER FUNCTIONS ========== */ | |
function knockDownWall(bool high_) internal returns (uint256 amountIn, uint256 amountOut) { | |
if (high_) { | |
/// Get current capacity of the high wall | |
/// Set amount in to put capacity 1 below the threshold for shutting down the wall | |
uint256 startCapacity = range.capacity(true); | |
uint256 highWallPrice = range.price(true, true); | |
amountIn = startCapacity.mulDiv(highWallPrice, 1e9).mulDiv(9999, 10000) + 1; | |
uint256 expAmountOut = operator.getAmountOut(reserve, amountIn); | |
/// Swap at the high wall | |
vm.prank(alice); | |
amountOut = operator.swap(reserve, amountIn, expAmountOut); | |
} else { | |
/// Get current capacity of the low wall | |
/// Set amount in to put capacity 1 below the threshold for shutting down the wall | |
uint256 startCapacity = range.capacity(false); | |
uint256 lowWallPrice = range.price(true, false); | |
amountIn = startCapacity.mulDiv(1e9, lowWallPrice).mulDiv(9999, 10000) + 1; | |
uint256 expAmountOut = operator.getAmountOut(ohm, amountIn); | |
/// Swap at the low wall | |
vm.prank(alice); | |
amountOut = operator.swap(ohm, amountIn, expAmountOut); | |
} | |
} | |
/* ========== WALL TESTS ========== */ | |
/// DONE | |
/// [X] Able to swap when walls are up | |
/// [X] Splippage check when swapping | |
/// [X] Wall breaks when capacity drops below the configured threshold | |
/// [X] Not able to swap at the walls when they are down | |
function testCorrectness_swapHighWall() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get current capacity of the high wall and starting balance for user | |
uint256 startCapacity = range.capacity(true); | |
uint256 amountIn = 100 * 1e18; | |
uint256 ohmBalance = ohm.balanceOf(alice); | |
uint256 reserveBalance = reserve.balanceOf(alice); | |
/// Calculate expected difference | |
uint256 highWallPrice = range.price(true, true); | |
uint256 expAmountOut = amountIn.mulDiv(1e9 * 1e18, 1e18 * highWallPrice); | |
/// Swap at the high wall | |
vm.prank(alice); | |
uint256 amountOut = operator.swap(reserve, amountIn, expAmountOut); | |
/// Get updated capacity of the high wall | |
uint256 endCapacity = range.capacity(true); | |
assertEq(amountOut, expAmountOut); | |
assertEq(endCapacity, startCapacity - amountOut); | |
assertEq(ohm.balanceOf(alice), ohmBalance + amountOut); | |
assertEq(reserve.balanceOf(alice), reserveBalance - amountIn); | |
} | |
function testCorrectness_swapLowWall() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get current capacity of the high wall and starting balance for user | |
uint256 startCapacity = range.capacity(false); | |
uint256 amountIn = 100 * 1e9; | |
uint256 ohmBalance = ohm.balanceOf(alice); | |
uint256 reserveBalance = reserve.balanceOf(alice); | |
/// Calculate expected difference | |
uint256 lowWallPrice = range.price(true, false); | |
uint256 expAmountOut = amountIn.mulDiv(1e18 * lowWallPrice, 1e9 * 1e18); | |
/// Swap at the high wall | |
vm.prank(alice); | |
uint256 amountOut = operator.swap(ohm, amountIn, expAmountOut); | |
/// Get updated capacity of the high wall | |
uint256 endCapacity = range.capacity(false); | |
assertEq(amountOut, expAmountOut); | |
assertEq(endCapacity, startCapacity - amountOut); | |
assertEq(ohm.balanceOf(alice), ohmBalance - amountIn); | |
assertEq(reserve.balanceOf(alice), reserveBalance + amountOut); | |
} | |
function testCorrectness_highWallBreaksAtThreshold() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Get initial balances and capacity | |
uint256 ohmBalance = ohm.balanceOf(alice); | |
uint256 reserveBalance = reserve.balanceOf(alice); | |
uint256 startCapacity = range.capacity(true); | |
/// Take down wall with helper function | |
(uint256 amountIn, uint256 amountOut) = knockDownWall(true); | |
/// Get updated capacity of the high wall | |
uint256 endCapacity = range.capacity(true); | |
/// Confirm the wall is down | |
assertTrue(!range.active(true)); | |
/// Check that capacity and balances are correct | |
assertEq(endCapacity, startCapacity - amountOut); | |
assertEq(ohm.balanceOf(alice), ohmBalance + amountOut); | |
assertEq(reserve.balanceOf(alice), reserveBalance - amountIn); | |
} | |
function testCorrectness_lowWallBreaksAtThreshold() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Get initial balances and capacity | |
uint256 ohmBalance = ohm.balanceOf(alice); | |
uint256 reserveBalance = reserve.balanceOf(alice); | |
uint256 startCapacity = range.capacity(false); | |
/// Take down wall with helper function | |
(uint256 amountIn, uint256 amountOut) = knockDownWall(false); | |
/// Get updated capacity of the high wall | |
uint256 endCapacity = range.capacity(false); | |
/// Confirm the wall is down | |
assertTrue(!range.active(false)); | |
/// Check that capacity and balances are correct | |
assertEq(endCapacity, startCapacity - amountOut); | |
assertEq(ohm.balanceOf(alice), ohmBalance - amountIn); | |
assertEq(reserve.balanceOf(alice), reserveBalance + amountOut); | |
} | |
function testCorrectness_cannotSwapHighWallWhenDown() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Take down wall with helper function | |
knockDownWall(true); | |
/// Try to swap, expect to fail | |
uint256 amountIn = 100 * 1e18; | |
uint256 expAmountOut = amountIn.mulDiv(1e9 * 1e18, 1e18 * range.price(true, true)); | |
bytes memory err = abi.encodeWithSignature("Operator_WallDown()"); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(reserve, amountIn, expAmountOut); | |
} | |
function testCorrectness_cannotSwapLowWallWhenDown() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Take down wall with helper function | |
knockDownWall(false); | |
/// Try to swap, expect to fail | |
uint256 amountIn = 100 * 1e9; | |
uint256 expAmountOut = amountIn.mulDiv(1e18 * range.price(true, false), 1e9 * 1e18); | |
bytes memory err = abi.encodeWithSignature("Operator_WallDown()"); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(ohm, amountIn, expAmountOut); | |
} | |
function testCorrectness_swapRevertsOnSlippage() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm walls are up | |
assertTrue(range.active(true)); | |
assertTrue(range.active(false)); | |
/// Set amounts for high wall swap with minAmountOut greater than expAmountOut | |
uint256 amountIn = 100 * 1e18; | |
uint256 expAmountOut = amountIn.mulDiv(1e9 * 1e18, 1e18 * range.price(true, true)); | |
uint256 minAmountOut = expAmountOut + 1; | |
/// Try to swap at low wall, expect to fail | |
bytes memory err = abi.encodeWithSignature( | |
"Operator_AmountLessThanMinimum(uint256,uint256)", | |
expAmountOut, | |
minAmountOut | |
); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(reserve, amountIn, minAmountOut); | |
/// Set amounts for low wall swap with minAmountOut greater than expAmountOut | |
amountIn = 100 * 1e9; | |
expAmountOut = amountIn.mulDiv(1e18 * range.price(true, false), 1e9 * 1e18); | |
minAmountOut = expAmountOut + 1; | |
/// Try to swap at low wall, expect to fail | |
err = abi.encodeWithSignature( | |
"Operator_AmountLessThanMinimum(uint256,uint256)", | |
expAmountOut, | |
minAmountOut | |
); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(ohm, amountIn, minAmountOut); | |
} | |
function testCorrectness_swapRevertsWithInvalidToken() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm walls are up | |
assertTrue(range.active(true)); | |
assertTrue(range.active(false)); | |
/// Try to swap with invalid token, expect to fail | |
uint256 amountIn = 100 * 1e18; | |
uint256 minAmountOut = 100 * 1e18; | |
ERC20 token = ERC20(bob); | |
bytes memory err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(token, amountIn, minAmountOut); | |
} | |
/* ========== CUSHION TESTS ========== */ | |
/// DONE | |
/// [X] Cushions deployed when price set in the range and operate triggered | |
/// [X] Cushions deactivated when price out of range and operate triggered or when wall goes down | |
/// [X] Cushion doesn't deploy when wall is down | |
/// [X] Bond purchases update capacity | |
function test_poc__lowCushionDeployWithWallspread10000Reverts() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// if the wallspread is 10000 the operate might revert | |
vm.prank(policy); | |
operator.setSpreads(7000, 10000); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
/// Set price on mock oracle into the lower cushion | |
/// given the lower wallspread is 10000 | |
/// when the lower market should be deployed the operate reverts | |
price.setLastPrice(20 * 1e18); | |
/// Trigger the operate function manually | |
/// The operate will revert Error with "Division or modulo by 0" | |
/// But I could not figure out to catch with `expectRevert` | |
/// so just commenting out the assert | |
// vm.prank(guardian); | |
// /// vm.expectRevert(bytes("Division or module by 0")); // this cannot catch the revert... | |
// operator.operate(); | |
} | |
function testCorrectness_highCushionDeployedInSpread() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed and capacity is set to the correct amount | |
uint256 marketId = range.market(true); | |
assertTrue(auctioneer.isLive(marketId)); | |
Operator.Config memory config = operator.config(); | |
uint256 marketCapacity = auctioneer.currentCapacity(marketId); | |
// console2.log("capacity", marketCapacity); | |
assertEq(marketCapacity, range.capacity(true).mulDiv(config.cushionFactor, 1e4)); | |
/// Check that the price is set correctly | |
// (, , , , , , , , , , , uint256 scale) = auctioneer.markets(marketId); | |
// uint256 price = auctioneer.marketPrice(marketId); | |
// console2.log("price", price); | |
// console2.log("scale", scale); | |
uint256 payout = auctioneer.payoutFor(120 * 1e18, marketId, alice); | |
assertEq(payout, 1e9); | |
} | |
function testCorrectness_highCushionClosedBelowSpread() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed and capacity is set to the correct amount | |
assertTrue(auctioneer.isLive(range.market(true))); | |
Operator.Config memory config = operator.config(); | |
uint256 marketCapacity = auctioneer.currentCapacity(range.market(true)); | |
assertEq(marketCapacity, range.capacity(true).mulDiv(config.cushionFactor, 1e4)); | |
/// Set price on mock oracle below the high cushion | |
price.setLastPrice(105 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is closed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
marketCapacity = auctioneer.currentCapacity(range.market(true)); | |
assertEq(marketCapacity, 0); | |
} | |
function testCorrectness_highCushionClosedAboveSpread() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed and capacity is set to the correct amount | |
assertTrue(auctioneer.isLive(range.market(true))); | |
Operator.Config memory config = operator.config(); | |
uint256 marketCapacity = auctioneer.currentCapacity(range.market(true)); | |
assertEq(marketCapacity, range.capacity(true).mulDiv(config.cushionFactor, 1e4)); | |
/// Set price on mock oracle below the high cushion | |
price.setLastPrice(130 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is closed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
marketCapacity = auctioneer.currentCapacity(range.market(true)); | |
assertEq(marketCapacity, 0); | |
} | |
function testCorrectness_highCushionClosedWhenWallDown() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed | |
assertTrue(auctioneer.isLive(range.market(true))); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Check that the cushion is closed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
} | |
function testCorrectness_highCushionNotDeployedWhenWallDown() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Set price on mock oracle into the low cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
} | |
function testCorrectness_lowCushionDeployedInSpread() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(89 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed and capacity is set to the correct amount | |
uint256 marketId = range.market(false); | |
assertTrue(auctioneer.isLive(marketId)); | |
Operator.Config memory config = operator.config(); | |
uint256 marketCapacity = auctioneer.currentCapacity(marketId); | |
assertEq(marketCapacity, range.capacity(false).mulDiv(config.cushionFactor, 1e4)); | |
/// Check that the price is set correctly | |
// (, , , , , , , , , , , uint256 scale) = auctioneer.markets(marketId); | |
// uint256 price = auctioneer.marketPrice(marketId); | |
// console2.log("price", price); | |
// console2.log("scale", scale); | |
uint256 payout = auctioneer.payoutFor(1e9, marketId, alice); | |
assertEq(payout, 80 * 1e18); | |
} | |
function testCorrectness_lowCushionClosedBelowSpread() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(89 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed and capacity is set to the correct amount | |
assertTrue(auctioneer.isLive(range.market(false))); | |
Operator.Config memory config = operator.config(); | |
uint256 marketCapacity = auctioneer.currentCapacity(range.market(false)); | |
assertEq(marketCapacity, range.capacity(false).mulDiv(config.cushionFactor, 1e4)); | |
/// Set price on mock oracle below the high cushion | |
price.setLastPrice(79 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is closed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
marketCapacity = auctioneer.currentCapacity(range.market(false)); | |
assertEq(marketCapacity, 0); | |
} | |
function testCorrectness_lowCushionClosedAboveSpread() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(89 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed and capacity is set to the correct amount | |
assertTrue(auctioneer.isLive(range.market(false))); | |
Operator.Config memory config = operator.config(); | |
uint256 marketCapacity = auctioneer.currentCapacity(range.market(false)); | |
assertEq(marketCapacity, range.capacity(false).mulDiv(config.cushionFactor, 1e4)); | |
/// Set price on mock oracle below the high cushion | |
price.setLastPrice(91 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is closed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
marketCapacity = auctioneer.currentCapacity(range.market(false)); | |
assertEq(marketCapacity, 0); | |
} | |
function testCorrectness_lowCushionClosedWhenWallDown() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
/// Set price on mock oracle into the low cushion | |
price.setLastPrice(89 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed | |
assertTrue(auctioneer.isLive(range.market(false))); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Check that the cushion is closed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
} | |
function testCorrectness_lowCushionNotDeployedWhenWallDown() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Set price on mock oracle into the low cushion | |
price.setLastPrice(89 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is not deployed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
} | |
function test_marketClosesAsExpected1() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Assert high wall is up | |
assertTrue(range.active(true)); | |
/// Set price below the moving average to almost regenerate high wall | |
price.setLastPrice(99 * 1e18); | |
/// Trigger the operator function enough times to almost regenerate the high wall | |
for (uint256 i; i < 4; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Ensure market not live yet | |
uint256 currentMarket = range.market(true); | |
assertEq(type(uint256).max, currentMarket); | |
/// Cause price to spike to trigger high cushion | |
uint256 cushionPrice = range.price(false, true); | |
price.setLastPrice(cushionPrice + 500); | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check market is live | |
currentMarket = range.market(true); | |
assertTrue(type(uint256).max != currentMarket); | |
assertTrue(auctioneer.isLive(currentMarket)); | |
/// Cause price to go back down to moving average | |
/// Move time forward past the regen period to trigger high wall regeneration | |
vm.warp(block.timestamp + 1 hours); | |
/// Will trigger regeneration of high wall | |
/// Will set the operator market on high side to type(uint256).max | |
/// However, the prior market will still be live when it's supposed to be deactivated | |
price.setLastPrice(95 * 1e18); | |
vm.prank(guardian); | |
operator.operate(); | |
/// Get latest market | |
uint256 newMarket = range.market(true); | |
/// Check market has been updated to non existent market | |
assertTrue(type(uint256).max == newMarket); | |
/// And, the previous market is closed | |
assertTrue(!auctioneer.isLive(currentMarket)); | |
} | |
function test_marketClosesAsExpected2() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Assert high wall is up | |
assertTrue(range.active(true)); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Get current market | |
uint256 currentMarket = range.market(true); | |
/// Check market has been updated and is live | |
assertTrue(type(uint256).max != currentMarket); | |
assertTrue(auctioneer.isLive(currentMarket)); | |
/// Take down wall | |
knockDownWall(true); | |
/// Get latest market | |
uint256 newMarket = range.market(true); | |
/// Check market has been updated to non existent market | |
assertTrue(type(uint256).max == newMarket); | |
/// And the previous market is closed | |
assertTrue(!auctioneer.isLive(currentMarket)); | |
} | |
function testCorrectness_highCushionPurchasesReduceCapacity() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get the start capacity of the high side | |
uint256 startCapacity = range.capacity(true); | |
/// Set price on mock oracle into the high cushion | |
price.setLastPrice(111 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed | |
uint256 id = range.market(true); | |
assertTrue(auctioneer.isLive(id)); | |
/// Set amount to purchase from cushion (which will be at wall price initially) | |
uint256 amountIn = auctioneer.maxAmountAccepted(id, guardian) / 2; | |
uint256 minAmountOut = auctioneer.payoutFor(amountIn, id, guardian); | |
/// Purchase from cushion | |
vm.prank(alice); | |
(uint256 payout, ) = teller.purchase(alice, guardian, id, amountIn, minAmountOut); | |
/// Check that the side capacity has been reduced by the amount of the payout | |
assertEq(range.capacity(true), startCapacity - payout); | |
} | |
function testCorrectness_lowCushionPurchasesReduceCapacity() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get the start capacity of the low side | |
uint256 startCapacity = range.capacity(false); | |
/// Set price on mock oracle into the low cushion | |
price.setLastPrice(89 * 1e18); | |
/// Trigger the operate function manually | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed | |
uint256 id = range.market(false); | |
assertTrue(auctioneer.isLive(id)); | |
/// Set amount to purchase from cushion (which will be at wall price initially) | |
uint256 amountIn = auctioneer.maxAmountAccepted(id, guardian) / 2; | |
uint256 minAmountOut = auctioneer.payoutFor(amountIn, id, guardian); | |
/// Purchase from cushion | |
vm.prank(alice); | |
(uint256 payout, ) = teller.purchase(alice, guardian, id, amountIn, minAmountOut); | |
/// Check that the side capacity has been reduced by the amount of the payout | |
assertEq(range.capacity(false), startCapacity - payout); | |
} | |
/* ========== REGENERATION TESTS ========== */ | |
/// DONE | |
/// [X] Wall regenerates when price on other side of MA for enough observations | |
/// [X] Wrap around logic works for counting observations | |
/// [X] Regen period enforces a minimum time to wait for regeneration | |
function testCorrectness_lowWallRegenA() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests the simplest case of regen | |
/// Takes down wall, moves price in regen range, | |
/// and hits regen count required with consequtive calls | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Check that the wall is down | |
assertTrue(!range.active(false)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the low wall and verify under threshold | |
uint256 startCapacity = range.capacity(false); | |
uint256 fullCapacity = operator.fullCapacity(false); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price above the moving average to regenerate low wall | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 5; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is up | |
assertTrue(range.active(false)); | |
/// Check that the capacity has regenerated | |
uint256 endCapacity = range.capacity(false); | |
assertEq(endCapacity, fullCapacity); | |
} | |
function testCorrectness_lowWallRegenB() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests wrap around logic of regen | |
/// Takes down wall, calls operate a few times with price not in regen range, | |
/// moves price into regen range, and hits regen count required with consequtive calls | |
/// that wrap around the count array | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Check that the wall is down | |
assertTrue(!range.active(false)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the low wall and verify under threshold | |
uint256 startCapacity = range.capacity(false); | |
uint256 fullCapacity = operator.fullCapacity(false); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price below the moving average so regeneration doesn't start | |
price.setLastPrice(98 * 1e18); | |
/// Trigger the operator function with negative | |
for (uint256 i; i < 8; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(false)); | |
/// Set price above the moving average to regenerate low wall | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 5; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is up | |
assertTrue(range.active(false)); | |
/// Check that the capacity has regenerated | |
uint256 endCapacity = range.capacity(false); | |
assertEq(endCapacity, fullCapacity); | |
} | |
function testCorrectness_lowWallRegenC() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that wall does not regenerate before the required count is reached | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Check that the wall is down | |
assertTrue(!range.active(false)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the low wall and verify under threshold | |
uint256 startCapacity = range.capacity(false); | |
uint256 fullCapacity = operator.fullCapacity(false); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price above the moving average to regenerate low wall | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 4; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(false)); | |
/// Check that the capacity hasn't regenerated | |
uint256 endCapacity = range.capacity(false); | |
assertEq(endCapacity, startCapacity); | |
} | |
function testCorrectness_lowWallRegenD() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that wall does not regenerate before the required count is reached | |
/// Use more complex logic to ensure wrap around logic is working and | |
/// that positive checks outside the moving window aren't counted | |
/// observations should be: +, -, -, -, +, +, +, + | |
/// last observation wraps around to first and therefore only 4/7 of the observations are counted | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Check that the wall is down | |
assertTrue(!range.active(false)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the low wall and verify under threshold | |
uint256 startCapacity = range.capacity(false); | |
uint256 fullCapacity = operator.fullCapacity(false); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price above the moving average to regenerate low wall | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator once to get a positive check | |
vm.prank(guardian); | |
operator.operate(); | |
/// Set price below the moving average to get negative checks | |
price.setLastPrice(99 * 1e18); | |
/// Trigger the operator function several times with negative checks | |
for (uint256 i; i < 3; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Set price above the moving average to regenerate low wall | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function several times with positive checks | |
for (uint256 i; i < 4; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(false)); | |
/// Check that the capacity hasn't regenerated | |
uint256 endCapacity = range.capacity(false); | |
assertEq(endCapacity, startCapacity); | |
} | |
function testCorrectness_lowWallRegenTime() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that the wall won't regenerate before the required time has passed, | |
/// even with enough observations | |
/// Takes down wall, moves price in regen range, | |
/// and hits regen count required with consequtive calls | |
/// Confirm wall is up | |
assertTrue(range.active(false)); | |
/// Take down the wall | |
knockDownWall(false); | |
/// Check that the wall is down | |
assertTrue(!range.active(false)); | |
/// Don't move time forward past the regen period so it won't regen | |
/// Get capacity of the low wall and verify under threshold | |
uint256 startCapacity = range.capacity(false); | |
uint256 fullCapacity = operator.fullCapacity(false); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price above the moving average to regenerate low wall | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 5; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(false)); | |
/// Check that the capacity hasn't regenerated | |
uint256 endCapacity = range.capacity(false); | |
assertEq(endCapacity, startCapacity); | |
} | |
function testCorrectness_lowCushionClosedOnRegen() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that a manually regenerated wall will close the cushion that is deployed currently | |
/// Trigger a cushion | |
price.setLastPrice(89 * 1e18); | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed | |
assertTrue(auctioneer.isLive(range.market(false))); | |
assertEq(range.market(false), 0); | |
/// Regenerate the wall manually, expect market to close | |
vm.prank(guardian); | |
operator.regenerate(false); | |
/// Check that the market is closed | |
assertTrue(!auctioneer.isLive(range.market(false))); | |
assertEq(range.market(false), type(uint256).max); | |
} | |
function testCorrectness_highWallRegenA() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests the simplest case of regen | |
/// Takes down wall, moves price in regen range, | |
/// and hits regen count required with consequtive calls | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Check that the wall is down | |
assertTrue(!range.active(true)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the high wall and verify under threshold | |
uint256 startCapacity = range.capacity(true); | |
uint256 fullCapacity = operator.fullCapacity(true); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price below the moving average to regenerate high wall | |
price.setLastPrice(99 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 5; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is up | |
assertTrue(range.active(true)); | |
/// Check that the capacity has regenerated | |
uint256 endCapacity = range.capacity(true); | |
assertEq(endCapacity, fullCapacity); | |
} | |
function testCorrectness_highWallRegenB() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests wrap around logic of regen | |
/// Takes down wall, calls operate a few times with price not in regen range, | |
/// moves price into regen range, and hits regen count required with consequtive calls | |
/// that wrap around the count array | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Check that the wall is down | |
assertTrue(!range.active(true)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the high wall and verify under threshold | |
uint256 startCapacity = range.capacity(true); | |
uint256 fullCapacity = operator.fullCapacity(true); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price above the moving average so regeneration doesn't start | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function with negative | |
for (uint256 i; i < 8; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(true)); | |
/// Set price below the moving average to regenerate high wall | |
price.setLastPrice(98 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 5; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is up | |
assertTrue(range.active(true)); | |
/// Check that the capacity has regenerated | |
uint256 endCapacity = range.capacity(true); | |
assertEq(endCapacity, fullCapacity); | |
} | |
function testCorrectness_highWallRegenC() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that wall does not regenerate before the required count is reached | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Check that the wall is down | |
assertTrue(!range.active(true)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the high wall and verify under threshold | |
uint256 startCapacity = range.capacity(true); | |
uint256 fullCapacity = operator.fullCapacity(true); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price below the moving average to regenerate high wall | |
price.setLastPrice(98 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 4; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(true)); | |
/// Check that the capacity hasn't regenerated | |
uint256 endCapacity = range.capacity(true); | |
assertEq(endCapacity, startCapacity); | |
} | |
function testCorrectness_highWallRegenD() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that wall does not regenerate before the required count is reached | |
/// Use more complex logic to ensure wrap around logic is working and | |
/// that positive checks outside the moving window aren't counted | |
/// observations should be: +, -, -, -, +, +, +, + | |
/// last observation wraps around to first and therefore only 4/7 of the observations are counted | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Check that the wall is down | |
assertTrue(!range.active(true)); | |
/// Move time forward past the regen period | |
vm.warp(block.timestamp + 1 hours); | |
/// Get capacity of the high wall and verify under threshold | |
uint256 startCapacity = range.capacity(true); | |
uint256 fullCapacity = operator.fullCapacity(true); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price below the moving average to regenerate low wall | |
price.setLastPrice(98 * 1e18); | |
/// Trigger the operator once to get a positive check | |
vm.prank(guardian); | |
operator.operate(); | |
/// Set price above the moving average to get negative checks | |
price.setLastPrice(101 * 1e18); | |
/// Trigger the operator function several times with negative checks | |
for (uint256 i; i < 3; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Set price below the moving average to regenerate high wall | |
price.setLastPrice(98 * 1e18); | |
/// Trigger the operator function several times with positive checks | |
for (uint256 i; i < 4; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(true)); | |
/// Check that the capacity hasn't regenerated | |
uint256 endCapacity = range.capacity(true); | |
assertEq(endCapacity, startCapacity); | |
} | |
function testCorrectness_highWallRegenTime() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that the wall won't regenerate before the required time has passed, | |
/// even with enough observations | |
/// Takes down wall, moves price in regen range, | |
/// and hits regen count required with consequtive calls | |
/// Confirm wall is up | |
assertTrue(range.active(true)); | |
/// Take down the wall | |
knockDownWall(true); | |
/// Check that the wall is down | |
assertTrue(!range.active(true)); | |
/// Don't move time forward past the regen period so it won't regen | |
/// Get capacity of the high wall and verify under threshold | |
uint256 startCapacity = range.capacity(true); | |
uint256 fullCapacity = operator.fullCapacity(true); | |
assertLe(startCapacity, fullCapacity.mulDiv(range.thresholdFactor(), 1e4)); | |
/// Set price below the moving average to regenerate high wall | |
price.setLastPrice(99 * 1e18); | |
/// Trigger the operator function enough times to regenerate the wall | |
for (uint256 i; i < 5; ++i) { | |
vm.prank(guardian); | |
operator.operate(); | |
} | |
/// Check that the wall is still down | |
assertTrue(!range.active(true)); | |
/// Check that the capacity hasn't regenerated | |
uint256 endCapacity = range.capacity(true); | |
assertEq(endCapacity, startCapacity); | |
} | |
function testCorrectness_highCushionClosedOnRegen() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Tests that a manually regenerated wall will close the cushion that is deployed currently | |
/// Trigger a cushion | |
price.setLastPrice(111 * 1e18); | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the cushion is deployed | |
assertTrue(auctioneer.isLive(range.market(true))); | |
assertEq(range.market(true), 0); | |
/// Regenerate the wall manually, expect market to close | |
vm.prank(guardian); | |
operator.regenerate(true); | |
/// Check that the market is closed | |
assertTrue(!auctioneer.isLive(range.market(true))); | |
assertEq(range.market(true), type(uint256).max); | |
} | |
/* ========== ACCESS CONTROL TESTS ========== */ | |
/// DONE | |
/// [X] operate only callable by heart or guardian | |
/// [X] admin configuration functions only callable by policy or guardian (negative here, positive in ADMIN TESTS sections) | |
function testCorrectness_onlyHeartOrGovernanceCanOperate() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Call operate as heart contract | |
vm.prank(heart); | |
operator.operate(); | |
/// Call operate as governance | |
vm.prank(guardian); | |
operator.operate(); | |
/// Try to call operate as anyone else | |
bytes memory err = abi.encodeWithSelector( | |
Policy_OnlyRole.selector, | |
toRole("operator_operate") | |
); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.operate(); | |
} | |
function testCorrectness_cannotOperatorIfNotInitialized() public { | |
/// Toggle operator to active manually erroneously (so it will not revert with inactive) | |
vm.prank(guardian); | |
operator.toggleActive(); | |
/// Call operate as heart contract and expect to revert | |
bytes memory err = abi.encodeWithSignature("Operator_NotInitialized()"); | |
vm.expectRevert(err); | |
vm.prank(heart); | |
operator.operate(); | |
} | |
function testCorrectness_nonPolicyCannotSetConfig() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Try to set spreads as random user, expect revert | |
bytes memory err = abi.encodeWithSelector( | |
Policy_OnlyRole.selector, | |
toRole("operator_policy") | |
); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.setSpreads(1500, 3000); | |
/// Try to set cushionFactor as random user, expect revert | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.setCushionFactor(1500); | |
/// Try to set cushionDuration as random user, expect revert | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.setCushionParams(uint32(6 hours), uint32(50_000), uint32(4 hours)); | |
/// Try to set cushionFactor as random user, expect revert | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.setReserveFactor(1500); | |
/// Try to set regenParams as a random user, expect revert | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.setRegenParams(uint32(1 days), uint32(8), uint32(11)); | |
} | |
function testCorrectness_nonGuardianCannotCall() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Try to set spreads as random user, expect revert | |
bytes memory err = abi.encodeWithSelector( | |
Policy_OnlyRole.selector, | |
toRole("operator_admin") | |
); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.setBondContracts(IBondAuctioneer(alice), BondCallback(alice)); | |
/// Try to initialize as a random user, expect revert | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.initialize(); | |
} | |
/* ========== ADMIN TESTS ========== */ | |
/// DONE | |
/// [X] setSpreads | |
/// [X] setThresholdFactor (in Range.t.sol) TODO | |
/// [X] setCushionFactor | |
/// [X] setCushionParams | |
/// [X] setReserveFactor | |
/// [X] setRegenParams | |
/// [X] setBondContracts | |
/// [X] initialize | |
function testCorrectness_setSpreads() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get starting bands | |
OlympusRange.Range memory startRange = range.range(); | |
/// Set spreads larger as admin | |
vm.prank(policy); | |
operator.setSpreads(1500, 3000); | |
/// Get new bands | |
OlympusRange.Range memory newRange = range.range(); | |
/// Check that the spreads have been set and prices are updated | |
assertEq(newRange.cushion.spread, 1500); | |
assertEq(newRange.wall.spread, 3000); | |
assertLt(newRange.cushion.low.price, startRange.cushion.low.price); | |
assertLt(newRange.wall.low.price, startRange.wall.low.price); | |
assertGt(newRange.cushion.high.price, startRange.cushion.high.price); | |
assertGt(newRange.wall.high.price, startRange.wall.high.price); | |
/// Set spreads smaller as admin | |
vm.prank(policy); | |
operator.setSpreads(500, 1000); | |
/// Get new bands | |
newRange = range.range(); | |
/// Check that the spreads have been set and prices are updated | |
assertEq(newRange.cushion.spread, 500); | |
assertEq(newRange.wall.spread, 1000); | |
assertGt(newRange.cushion.low.price, startRange.cushion.low.price); | |
assertGt(newRange.wall.low.price, startRange.wall.low.price); | |
assertLt(newRange.cushion.high.price, startRange.cushion.high.price); | |
assertLt(newRange.wall.high.price, startRange.wall.high.price); | |
} | |
function testCorrectness_setThresholdFactor() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Check that the threshold factor is the same as initialized | |
assertEq(range.thresholdFactor(), 100); | |
/// Set threshold factor larger as admin | |
vm.prank(policy); | |
operator.setThresholdFactor(150); | |
/// Check that the threshold factor has been updated | |
assertEq(range.thresholdFactor(), 150); | |
} | |
function testCorrectness_cannotSetSpreadWithInvalidParams() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Set spreads with invalid params as admin (both too low) | |
bytes memory err = abi.encodeWithSignature("RANGE_InvalidParams()"); | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(99, 99); | |
/// Set spreads with invalid params as admin (both too high) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(10001, 10001); | |
/// Set spreads with invalid params as admin (one high, one low) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(99, 10001); | |
/// Set spreads with invalid params as admin (one high, one low) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(10001, 99); | |
/// Set spreads with invalid params as admin (cushion > wall) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(2000, 1000); | |
/// Set spreads with invalid params as admin (one in, one high) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(1000, 10001); | |
/// Set spreads with invalid params as admin (one in, one low) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setSpreads(99, 2000); | |
} | |
function testCorrectness_setCushionFactor() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get starting cushion factor | |
Operator.Config memory startConfig = operator.config(); | |
/// Set cushion factor as admin | |
vm.prank(policy); | |
operator.setCushionFactor(uint32(1000)); | |
/// Get new cushion factor | |
Operator.Config memory newConfig = operator.config(); | |
/// Check that the cushion factor has been set | |
assertEq(newConfig.cushionFactor, uint32(1000)); | |
assertLt(newConfig.cushionFactor, startConfig.cushionFactor); | |
} | |
function testCorrectness_cannotSetCushionFactorWithInvalidParams() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Set cushion factor with invalid params as admin (too low) | |
bytes memory err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setCushionFactor(uint32(99)); | |
/// Set cushion factor with invalid params as admin (too high) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setCushionFactor(uint32(10001)); | |
} | |
function testCorrectness_setCushionParams() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get starting cushion params | |
Operator.Config memory startConfig = operator.config(); | |
/// Set cushion params as admin | |
vm.prank(policy); | |
operator.setCushionParams(uint32(24 hours), uint32(50_000), uint32(4 hours)); | |
/// Get new cushion params | |
Operator.Config memory newConfig = operator.config(); | |
/// Check that the cushion params has been set | |
assertEq(newConfig.cushionDuration, uint32(24 hours)); | |
assertLt(newConfig.cushionDuration, startConfig.cushionDuration); | |
assertEq(newConfig.cushionDebtBuffer, uint32(50_000)); | |
assertLt(newConfig.cushionDebtBuffer, startConfig.cushionDebtBuffer); | |
assertEq(newConfig.cushionDepositInterval, uint32(4 hours)); | |
assertGt(newConfig.cushionDepositInterval, startConfig.cushionDepositInterval); | |
} | |
function testCorrectness_cannotSetCushionParamsWithInvalidParams() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Set cushion params with invalid duration as admin (too low) | |
bytes memory err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setCushionParams(uint32(1 days) - 1, uint32(100_000), uint32(1 hours)); | |
/// Set cushion params with invalid duration as admin (too high) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setCushionParams(uint32(7 days) + 1, uint32(100_000), uint32(1 hours)); | |
/// Set cushion params with deposit interval greater than duration as admin | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setCushionParams(uint32(1 days), uint32(100_000), uint32(2 days)); | |
/// Set cushion params with invalid debt buffer as admin (too low) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setCushionParams(uint32(2 days), uint32(99), uint32(2 hours)); | |
} | |
function testCorrectness_setReserveFactor() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get starting reserve factor | |
Operator.Config memory startConfig = operator.config(); | |
/// Set reserve factor as admin | |
vm.prank(policy); | |
operator.setReserveFactor(uint32(500)); | |
/// Get new reserve factor | |
Operator.Config memory newConfig = operator.config(); | |
/// Check that the reserve factor has been set | |
assertEq(newConfig.reserveFactor, uint32(500)); | |
assertLt(newConfig.reserveFactor, startConfig.reserveFactor); | |
} | |
function testCorrectness_cannotSetReserveFactorWithInvalidParams() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Set reserve factor with invalid params as admin (too low) | |
bytes memory err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setReserveFactor(uint32(99)); | |
/// Set reserve factor with invalid params as admin (too high) | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setReserveFactor(uint32(10001)); | |
} | |
function testCorrectness_setRegenParams() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Get starting regen params | |
Operator.Config memory startConfig = operator.config(); | |
/// Confirm cannot set with invalid params | |
bytes memory err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
/// Case 1: wait < 1 hours | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setRegenParams(uint32(1 hours) - 1, uint32(11), uint32(15)); | |
/// Case 2: observe == 0 | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setRegenParams(uint32(1 days), uint32(0), uint32(0)); | |
/// Case 3: observe < threshold | |
vm.expectRevert(err); | |
vm.prank(policy); | |
operator.setRegenParams(uint32(1 days), uint32(10), uint32(9)); | |
/// Set regen params as admin with valid params | |
vm.prank(policy); | |
operator.setRegenParams(uint32(1 days), uint32(11), uint32(15)); | |
/// Get new regen params | |
Operator.Config memory newConfig = operator.config(); | |
/// Check that the regen params have been set | |
assertEq(newConfig.regenWait, uint256(1 days)); | |
assertEq(newConfig.regenThreshold, 11); | |
assertEq(newConfig.regenObserve, 15); | |
assertGt(newConfig.regenWait, startConfig.regenWait); | |
assertGt(newConfig.regenThreshold, startConfig.regenThreshold); | |
assertGt(newConfig.regenObserve, startConfig.regenObserve); | |
/// Check that the regen structs have been re-initialized | |
Operator.Status memory status = operator.status(); | |
assertEq(status.high.count, 0); | |
assertEq(status.high.nextObservation, 0); | |
assertEq(status.low.count, 0); | |
assertEq(status.low.nextObservation, 0); | |
for (uint256 i; i < 15; ++i) { | |
assertTrue(!status.high.observations[i]); | |
assertTrue(!status.low.observations[i]); | |
} | |
} | |
function testCorrectness_setBondContracts() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Attempt to set bond contracts to zero address and expect revert | |
bytes memory err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
vm.expectRevert(err); | |
vm.prank(guardian); | |
operator.setBondContracts(IBondAuctioneer(address(0)), BondCallback(address(0))); | |
/// Create new bond contracts | |
BondFixedTermCDA newCDA = new BondFixedTermCDA(teller, aggregator, guardian, auth); | |
BondCallback newCb = new BondCallback(kernel, IBondAggregator(address(aggregator)), ohm); | |
/// Update the bond contracts as guardian | |
vm.prank(guardian); | |
operator.setBondContracts(IBondAuctioneer(address(newCDA)), newCb); | |
/// Check that the bond contracts have been set | |
assertEq(address(operator.auctioneer()), address(newCDA)); | |
assertEq(address(operator.callback()), address(newCb)); | |
} | |
function testCorrectness_initialize() public { | |
/// Confirm that the operator is not initialized yet and walls are down | |
assertTrue(!operator.initialized()); | |
assertTrue(!operator.active()); | |
assertTrue(!range.active(true)); | |
assertTrue(!range.active(false)); | |
assertEq(treasury.withdrawApproval(address(operator), reserve), 0); | |
assertEq(range.price(false, false), 0); | |
assertEq(range.price(true, false), 0); | |
assertEq(range.price(false, true), 0); | |
assertEq(range.price(true, true), 0); | |
assertEq(range.capacity(false), 0); | |
assertEq(range.capacity(true), 0); | |
/// Initialize the operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Confirm that the operator is initialized and walls are up | |
assertTrue(operator.initialized()); | |
assertTrue(operator.active()); | |
assertTrue(range.active(true)); | |
assertTrue(range.active(false)); | |
assertEq(treasury.withdrawApproval(address(operator), reserve), type(uint256).max); | |
assertGt(range.price(false, false), 0); | |
assertGt(range.price(true, false), 0); | |
assertGt(range.price(false, true), 0); | |
assertGt(range.price(true, true), 0); | |
assertGt(range.capacity(false), 0); | |
assertGt(range.capacity(true), 0); | |
} | |
function testCorrectness_cannotInitializeTwice() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Try to initialize the operator again as guardian | |
bytes memory err = abi.encodeWithSignature("Operator_AlreadyInitialized()"); | |
vm.expectRevert(err); | |
vm.prank(guardian); | |
operator.initialize(); | |
} | |
function testCorrectness_regenerate() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
uint48 startTime = uint48(block.timestamp); | |
vm.warp(block.timestamp + 1 hours); | |
uint48 newTime = uint48(block.timestamp); | |
/// Confirm that both sides are currently up | |
assertTrue(range.active(true)); | |
assertTrue(range.active(false)); | |
/// Confirm that the Regen structs are at the initial state | |
Operator.Status memory status = operator.status(); | |
assertEq(status.high.count, uint32(0)); | |
assertEq(status.high.nextObservation, uint32(0)); | |
assertEq(status.high.lastRegen, startTime); | |
assertEq(status.low.count, uint32(0)); | |
assertEq(status.low.nextObservation, uint32(0)); | |
assertEq(status.low.lastRegen, startTime); | |
/// Call operate twice, at different price points, to make the regen counts higher than zero | |
price.setLastPrice(105 * 1e18); | |
vm.prank(heart); | |
operator.operate(); | |
price.setLastPrice(95 * 1e18); | |
vm.prank(heart); | |
operator.operate(); | |
/// Confirm that the Regen structs are updated | |
status = operator.status(); | |
assertEq(status.high.count, uint32(1)); | |
assertEq(status.high.nextObservation, uint32(2)); | |
assertEq(status.high.lastRegen, startTime); | |
assertEq(status.low.count, uint32(1)); | |
assertEq(status.low.nextObservation, uint32(2)); | |
assertEq(status.low.lastRegen, startTime); | |
/// Knock down both walls | |
knockDownWall(true); | |
knockDownWall(false); | |
/// Confirm that both sides are now down | |
assertTrue(!range.active(true)); | |
assertTrue(!range.active(false)); | |
/// Try to call regenerate without being guardian and expect revert | |
bytes memory err = abi.encodeWithSelector( | |
Policy_OnlyRole.selector, | |
toRole("operator_admin") | |
); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.regenerate(true); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.regenerate(false); | |
/// Confirm that the Regen structs are the same as before and walls are still down | |
status = operator.status(); | |
assertEq(status.high.count, uint32(1)); | |
assertEq(status.high.nextObservation, uint32(2)); | |
assertEq(status.low.count, uint32(1)); | |
assertEq(status.low.nextObservation, uint32(2)); | |
assertTrue(!range.active(true)); | |
assertTrue(!range.active(false)); | |
/// Call regenerate as guardian and confirm each side is updated | |
vm.prank(guardian); | |
operator.regenerate(true); | |
vm.prank(guardian); | |
operator.regenerate(false); | |
/// Confirm that the sides have regenerated and the Regen structs are reset | |
status = operator.status(); | |
assertEq(status.high.count, uint32(0)); | |
assertEq(status.high.nextObservation, uint32(0)); | |
assertEq(status.high.lastRegen, newTime); | |
assertEq(status.low.count, uint32(0)); | |
assertEq(status.low.nextObservation, uint32(0)); | |
assertEq(status.low.lastRegen, newTime); | |
assertTrue(range.active(true)); | |
assertTrue(range.active(false)); | |
} | |
function testCorrectness_cannotPerformMarketOpsWhileInactive() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Toggle the operator to inactive | |
vm.prank(guardian); | |
operator.toggleActive(); | |
/// Try to call operator, swap, and bondPurchase, expect reverts | |
bytes memory err = abi.encodeWithSignature("Operator_Inactive()"); | |
vm.expectRevert(err); | |
vm.prank(guardian); | |
operator.operate(); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(ohm, 1e9, 1); | |
vm.expectRevert(err); | |
vm.prank(alice); | |
operator.swap(reserve, 1e18, 1); | |
vm.expectRevert(err); | |
vm.prank(address(callback)); | |
operator.bondPurchase(0, 1e18); | |
} | |
/* ========== VIEW TESTS ========== */ | |
/// DONE | |
/// [X] fullCapacity | |
/// [X] getAmountOut | |
function testCorrectness_viewFullCapacity() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Load config | |
Operator.Config memory config = operator.config(); | |
/// Check that fullCapacity returns the full capacity based on the reserveFactor | |
uint256 resInTreasury = treasury.getReserveBalance(reserve); | |
uint256 lowCapacity = resInTreasury.mulDiv(config.reserveFactor, 1e4); | |
uint256 highCapacity = (lowCapacity.mulDiv( | |
1e9 * 10**price.decimals(), | |
1e18 * range.price(true, true) | |
) * (1e4 + range.spread(true) * 2)) / 1e4; | |
assertEq(operator.fullCapacity(false), lowCapacity); | |
assertEq(operator.fullCapacity(true), highCapacity); | |
} | |
function testCorrectness_viewGetAmountOut() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Check that getAmountOut returns the amount of token to receive for different combinations of inputs | |
/// Case 1: OHM In, less than capacity | |
uint256 amountIn = 100 * 1e9; | |
uint256 expAmountOut = amountIn.mulDiv(1e18 * range.price(true, false), 1e9 * 1e18); | |
assertEq(expAmountOut, operator.getAmountOut(ohm, amountIn)); | |
/// Case 2: OHM In, more than capacity | |
amountIn = range.capacity(false).mulDiv(1e9 * 1e18, 1e18 * range.price(true, false)) + 1e9; | |
bytes memory err = abi.encodeWithSignature("Operator_InsufficientCapacity()"); | |
vm.expectRevert(err); | |
operator.getAmountOut(ohm, amountIn); | |
/// Case 3: Reserve In, less than capacity | |
amountIn = 10000 * 1e18; | |
expAmountOut = amountIn.mulDiv(1e9 * 1e18, 1e18 * range.price(true, true)); | |
assertEq(expAmountOut, operator.getAmountOut(reserve, amountIn)); | |
/// Case 4: Reserve In, more than capacity | |
amountIn = range.capacity(true).mulDiv(1e18 * range.price(true, true), 1e9 * 1e18) + 1e18; | |
vm.expectRevert(err); | |
operator.getAmountOut(reserve, amountIn); | |
/// Case 5: Random, non-accepted token | |
err = abi.encodeWithSignature("Operator_InvalidParams()"); | |
ERC20 token = ERC20(bob); | |
amountIn = 100 * 1e18; | |
vm.expectRevert(err); | |
operator.getAmountOut(token, amountIn); | |
} | |
/* ========== INTERNAL FUNCTION TESTS ========== */ | |
/// DONE | |
/// [X] Range updates from new price data when operate is called (triggers _updateRange) | |
function testCorrectness_updateRange() public { | |
/// Initialize operator | |
vm.prank(guardian); | |
operator.initialize(); | |
/// Store the starting bands | |
OlympusRange.Range memory startRange = range.range(); | |
/// Update moving average upwards and trigger the operator | |
price.setMovingAverage(105 * 1e18); | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the bands have updated | |
assertGt(range.price(false, false), startRange.cushion.low.price); | |
assertGt(range.price(true, false), startRange.wall.low.price); | |
assertGt(range.price(false, true), startRange.cushion.high.price); | |
assertGt(range.price(true, true), startRange.wall.high.price); | |
/// Update moving average downwards and trigger the operator | |
price.setMovingAverage(95 * 1e18); | |
vm.prank(guardian); | |
operator.operate(); | |
/// Check that the bands have updated | |
assertLt(range.price(false, false), startRange.cushion.low.price); | |
assertLt(range.price(true, false), startRange.wall.low.price); | |
assertLt(range.price(false, true), startRange.cushion.high.price); | |
assertLt(range.price(true, true), startRange.wall.high.price); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment