diff --git a/.github/workflows/contracts-tests.yml b/.github/workflows/contracts-tests.yml index 7f8150760..dd981a531 100644 --- a/.github/workflows/contracts-tests.yml +++ b/.github/workflows/contracts-tests.yml @@ -55,52 +55,6 @@ jobs: run: | forge test -vvv --match-contract Mainnet - test-e2e: - name: Foundry "E2E" tests - runs-on: ubuntu-latest - env: - SALT: Liquity2-E2E - FORK_URL: ${{ secrets.MAINNET_RPC_URL }} - FORK_CHAIN_ID: 1 - FORK_BLOCK_NUMBER: 21571000 - ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }} - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install pnpm - uses: pnpm/action-setup@v3.0.0 - with: - version: 8 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version-file: ".node-version" - cache: "pnpm" - cache-dependency-path: "pnpm-lock.yaml" - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: ${{ env.FOUNDRY_VERSION }} - - - name: Fork mainnet - run: ./fork start && sleep 5 - - - name: Deploy BOLD - run: ./fork deploy --mode bold-only - - - name: Deploy everything else - run: ./fork deploy --mode use-existing-bold - - - name: Run E2E tests - run: ./fork e2e -vvv - console-logs: name: Console imports check runs-on: ubuntu-latest @@ -227,7 +181,6 @@ jobs: 'test/*' 'script/*' 'src/Dependencies/Ownable.sol' - 'src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol' 'src/NFTMetadata/*' 'src/MultiTroveGetter.sol' 'src/HintHelpers.sol' diff --git a/.gitmodules b/.gitmodules index 5ad0fdc56..e2289312d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [submodule "contracts/lib/Solady"] path = contracts/lib/Solady url = https://github.com/Vectorized/Solady +[submodule "contracts/lib/openzeppelin-contracts-upgradeable"] + path = contracts/lib/openzeppelin-contracts-upgradeable + url = https://github.com/openzeppelin/openzeppelin-contracts-upgradeable [submodule "contracts/lib/V2-gov"] path = contracts/lib/V2-gov url = https://github.com/liquity/V2-gov diff --git a/contracts/foundry.lock b/contracts/foundry.lock new file mode 100644 index 000000000..42a7865fe --- /dev/null +++ b/contracts/foundry.lock @@ -0,0 +1,17 @@ +{ + "lib/forge-std": { + "rev": "726a6ee5fc8427a0013d6f624e486c9130c0e336" + }, + "lib/Solady": { + "rev": "362b2efd20f38aea7252b391e5e016633ff79641" + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21" + }, + "lib/openzeppelin-contracts": { + "rev": "bd325d56b4c62c9c5c1aff048c37c6bb18ac0290" + }, + "lib/V2-gov": { + "rev": "e7ed5341f2f54fb9bf89497a7be294c61f21ebe3" + } +} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 950446267..d65024722 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -4,12 +4,13 @@ out = "out" libs = ["lib"] evm_version = 'cancun' optimizer = true -optimizer_runs = 200 +optimizer_runs = 0 ignored_error_codes = [3860, 5574] # contract-size fs_permissions = [ { access = "read", path = "./utils/assets/" }, { access = "read-write", path = "./utils/assets/test_output" }, { access = "read-write", path = "./deployment-manifest.json" }, + { access = "read-write", path = "./script/deployment-manifest.json" }, { access = "read", path = "./addresses/" } ] @@ -21,6 +22,10 @@ depth = 50 # failure_persist_dir = "/dev/null" # XXX circumvent this half-baked Foundry feature shrink_run_limit = 0 # XXX shrinking is super broken, results in completely wrong repro sequence +[profile.ci] +# Exclude invariant tests to save CI credits +no_match_test = "invariant_" + [profile.ci.invariant] shrink_run_limit = 0 # takes too damn long to shrink, don't waste Github minutes @@ -37,3 +42,7 @@ broadcast = 'broadcast-e2e' no_storage_caching = true # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +[lint] +# Excludes info/notes from 'forge build' and 'forge lint' output per default as it's quite noisy +severity = ["high", "med", "low"] \ No newline at end of file diff --git a/contracts/lib/openzeppelin-contracts-upgradeable b/contracts/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 000000000..58fa0f81c --- /dev/null +++ b/contracts/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit 58fa0f81c4036f1a3b616fdffad2fd27e5d5ce21 diff --git a/contracts/remappings.txt b/contracts/remappings.txt index c17beb9f8..82ab48fc7 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1 +1,2 @@ openzeppelin/=lib/V2-gov/lib/openzeppelin-contracts/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ diff --git a/contracts/script/Dependencies/GovernanceProxy.sol b/contracts/script/Dependencies/GovernanceProxy.sol deleted file mode 100644 index 558eac14d..000000000 --- a/contracts/script/Dependencies/GovernanceProxy.sol +++ /dev/null @@ -1,280 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.24; - -import {IERC20} from "openzeppelin/contracts/interfaces/IERC20.sol"; -import {IGovernance} from "V2-gov/src/interfaces/IGovernance.sol"; -import {ILQTYStaking} from "V2-gov/src/interfaces/ILQTYStaking.sol"; -import {IMultiDelegateCall} from "V2-gov/src/interfaces/IMultiDelegateCall.sol"; -import {IUserProxyFactory} from "V2-gov/src/interfaces/IUserProxyFactory.sol"; -import {PermitParams} from "V2-gov/src/utils/Types.sol"; -import {Governance} from "V2-gov/src/Governance.sol"; - -contract GovernanceProxy is IGovernance, IUserProxyFactory, IMultiDelegateCall { - Governance public immutable governance; - IERC20 public immutable lqty; - IERC20 public immutable bold; - - constructor(Governance _governance) { - governance = _governance; - lqty = _governance.lqty(); - bold = _governance.bold(); - - address userProxy = _governance.deriveUserProxyAddress(address(this)); - lqty.approve(userProxy, type(uint256).max); - bold.approve(address(_governance), type(uint256).max); - } - - function registerInitialInitiatives(address[] memory) external pure override { - revert("GovernanceProxy: not-implemented"); - } - - function stakingV1() external view override returns (ILQTYStaking) { - return governance.stakingV1(); - } - - function EPOCH_START() external view override returns (uint256) { - return governance.EPOCH_START(); - } - - function EPOCH_DURATION() external view override returns (uint256) { - return governance.EPOCH_DURATION(); - } - - function EPOCH_VOTING_CUTOFF() external view override returns (uint256) { - return governance.EPOCH_VOTING_CUTOFF(); - } - - function MIN_CLAIM() external view override returns (uint256) { - return governance.MIN_CLAIM(); - } - - function MIN_ACCRUAL() external view override returns (uint256) { - return governance.MIN_ACCRUAL(); - } - - function REGISTRATION_FEE() external view override returns (uint256) { - return governance.REGISTRATION_FEE(); - } - - function REGISTRATION_THRESHOLD_FACTOR() external view override returns (uint256) { - return governance.REGISTRATION_THRESHOLD_FACTOR(); - } - - function UNREGISTRATION_THRESHOLD_FACTOR() external view override returns (uint256) { - return governance.UNREGISTRATION_THRESHOLD_FACTOR(); - } - - function UNREGISTRATION_AFTER_EPOCHS() external view override returns (uint256) { - return governance.UNREGISTRATION_AFTER_EPOCHS(); - } - - function VOTING_THRESHOLD_FACTOR() external view override returns (uint256) { - return governance.VOTING_THRESHOLD_FACTOR(); - } - - function boldAccrued() external view override returns (uint256) { - return governance.boldAccrued(); - } - - function votesSnapshot() external view override returns (uint256 votes, uint256 forEpoch) { - return governance.votesSnapshot(); - } - - function votesForInitiativeSnapshot(address _initiative) - external - view - override - returns (uint256 votes, uint256 forEpoch, uint256 lastCountedEpoch, uint256 vetos) - { - return governance.votesForInitiativeSnapshot(_initiative); - } - - function userStates(address _user) - external - view - override - returns (uint256 unallocatedLQTY, uint256 unallocatedOffset, uint256 allocatedLQTY, uint256 allocatedOffset) - { - return governance.userStates(_user); - } - - function initiativeStates(address _initiative) - external - view - override - returns (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset, uint256 lastEpochClaim) - { - return governance.initiativeStates(_initiative); - } - - function globalState() external view override returns (uint256 countedVoteLQTY, uint256 countedVoteOffset) { - return governance.globalState(); - } - - function lqtyAllocatedByUserToInitiative(address _user, address _initiative) - external - view - override - returns (uint256 voteLQTY, uint256 voteOffset, uint256 vetoLQTY, uint256 vetoOffset, uint256 atEpoch) - { - return governance.lqtyAllocatedByUserToInitiative(_user, _initiative); - } - - function registeredInitiatives(address _initiative) external view override returns (uint256 atEpoch) { - return governance.registeredInitiatives(_initiative); - } - - function depositLQTY(uint256 _lqtyAmount) external override { - governance.depositLQTY(_lqtyAmount); - } - - function depositLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) external override { - governance.depositLQTY(_lqtyAmount, _doSendRewards, _recipient); - } - - function depositLQTYViaPermit(uint256, PermitParams calldata) external pure override { - revert("GovernanceProxy: not-implemented"); - } - - function depositLQTYViaPermit(uint256, PermitParams calldata, bool, address) external pure override { - revert("GovernanceProxy: not-implemented"); - } - - function withdrawLQTY(uint256 _lqtyAmount) external override { - governance.withdrawLQTY(_lqtyAmount); - } - - function withdrawLQTY(uint256 _lqtyAmount, bool _doSendRewards, address _recipient) external override { - governance.withdrawLQTY(_lqtyAmount, _doSendRewards, _recipient); - } - - function claimFromStakingV1(address _rewardRecipient) - external - override - returns (uint256 lusdSent, uint256 ethSent) - { - return governance.claimFromStakingV1(_rewardRecipient); - } - - function epoch() external view override returns (uint256) { - return governance.epoch(); - } - - function epochStart() external view override returns (uint256) { - return governance.epochStart(); - } - - function secondsWithinEpoch() external view override returns (uint256) { - return governance.secondsWithinEpoch(); - } - - function lqtyToVotes(uint256 _lqtyAmount, uint256 _timestamp, uint256 _offset) - external - pure - override - returns (uint256) - { - uint256 prod = _lqtyAmount * _timestamp; - return prod > _offset ? prod - _offset : 0; - } - - function calculateVotingThreshold() external override returns (uint256) { - return governance.calculateVotingThreshold(); - } - - function calculateVotingThreshold(uint256 _votes) external view override returns (uint256) { - return governance.calculateVotingThreshold(_votes); - } - - function getTotalVotesAndState() - external - view - override - returns (VoteSnapshot memory snapshot, GlobalState memory state, bool shouldUpdate) - { - return governance.getTotalVotesAndState(); - } - - function getInitiativeSnapshotAndState(address _initiative) - external - view - override - returns ( - InitiativeVoteSnapshot memory initiativeSnapshot, - InitiativeState memory initiativeState, - bool shouldUpdate - ) - { - return governance.getInitiativeSnapshotAndState(_initiative); - } - - function getLatestVotingThreshold() external view override returns (uint256) { - return governance.getLatestVotingThreshold(); - } - - function snapshotVotesForInitiative(address _initiative) - external - override - returns (VoteSnapshot memory voteSnapshot, InitiativeVoteSnapshot memory initiativeVoteSnapshot) - { - return governance.snapshotVotesForInitiative(_initiative); - } - - function getInitiativeState(address _initiative) - external - override - returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) - { - return governance.getInitiativeState(_initiative); - } - - function getInitiativeState( - address _initiative, - VoteSnapshot memory _votesSnapshot, - InitiativeVoteSnapshot memory _votesForInitiativeSnapshot, - InitiativeState memory _initiativeState - ) external view override returns (InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) { - return governance.getInitiativeState(_initiative, _votesSnapshot, _votesForInitiativeSnapshot, _initiativeState); - } - - function registerInitiative(address _initiative) external override { - governance.registerInitiative(_initiative); - } - - function unregisterInitiative(address _initiative) external override { - governance.unregisterInitiative(_initiative); - } - - function allocateLQTY( - address[] calldata _resetInitiatives, - address[] memory _initiatives, - int256[] memory _absoluteLQTYVotes, - int256[] memory absoluteLQTYVetos - ) external override { - governance.allocateLQTY(_resetInitiatives, _initiatives, _absoluteLQTYVotes, absoluteLQTYVetos); - } - - function resetAllocations(address[] calldata _initiativesToReset, bool _checkAll) external { - governance.resetAllocations(_initiativesToReset, _checkAll); - } - - function claimForInitiative(address _initiative) external override returns (uint256 claimed) { - return governance.claimForInitiative(_initiative); - } - - function userProxyImplementation() external view override returns (address) { - return governance.userProxyImplementation(); - } - - function deriveUserProxyAddress(address _user) external view override returns (address) { - return governance.deriveUserProxyAddress(_user); - } - - function deployUserProxy() external override returns (address userProxyAddress) { - return governance.deployUserProxy(); - } - - function multiDelegateCall(bytes[] calldata inputs) external override returns (bytes[] memory returnValues) { - return governance.multiDelegateCall(inputs); - } -} diff --git a/contracts/script/DeployGovernance.s.sol b/contracts/script/DeployGovernance.s.sol deleted file mode 100644 index 88ffa4701..000000000 --- a/contracts/script/DeployGovernance.s.sol +++ /dev/null @@ -1,231 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; - -import {Script} from "forge-std/Script.sol"; - -import {ICurveStableSwapFactoryNG} from "test/Interfaces/Curve/ICurveStableSwapFactoryNG.sol"; -import {ICurveStableSwapNG} from "test/Interfaces/Curve/ICurveStableSwapNG.sol"; -import {ILiquidityGaugeV6} from "test/Interfaces/Curve/ILiquidityGaugeV6.sol"; - -import {IGovernance} from "V2-gov/src/interfaces/IGovernance.sol"; - -import {Governance} from "V2-gov/src/Governance.sol"; -import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; - -import "forge-std/console2.sol"; - -library AddressArray { - using Strings for *; - using AddressArray for *; - - function toJSON(address addr) internal pure returns (string memory) { - return string.concat('"', addr.toHexString(), '"'); - } - - function toJSON(address[] memory addresses) internal pure returns (string memory) { - if (addresses.length == 0) return "[]"; - - string memory commaSeparatedStrings = addresses[0].toJSON(); - for (uint256 i = 1; i < addresses.length; ++i) { - commaSeparatedStrings = string.concat(commaSeparatedStrings, ",", addresses[i].toJSON()); - } - - return string.concat("[", commaSeparatedStrings, "]"); - } -} - -contract DeployGovernance is Script { - using Strings for *; - using AddressArray for *; - - struct DeployGovernanceParams { - uint256 epochStart; - address deployer; - bytes32 salt; - address stakingV1; - address lqty; - address lusd; - address bold; - } - - address constant LUSD = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; - address constant CRV = 0xD533a949740bb3306d119CC777fa900bA034cd52; - address constant FUNDS_SAFE = 0xF06016D822943C42e3Cb7FC3a6A3B1889C1045f8; - address constant DEFI_COLLECTIVE_GRANTS_ADDRESS = 0xDc6f869d2D34E4aee3E89A51f2Af6D54F0F7f690; - - // Governance Constants - uint128 private constant REGISTRATION_FEE = 100e18; - uint128 private constant REGISTRATION_THRESHOLD_FACTOR = 0.0001e18; // 0.01% - uint128 private constant UNREGISTRATION_THRESHOLD_FACTOR = 1e18 + 1; - uint16 private constant UNREGISTRATION_AFTER_EPOCHS = 4; - uint128 private constant VOTING_THRESHOLD_FACTOR = 0.02e18; - uint88 private constant MIN_CLAIM = 0; - uint88 private constant MIN_ACCRUAL = 0; - uint32 internal constant EPOCH_DURATION = 7 days; - uint32 private constant EPOCH_VOTING_CUTOFF = 6 days; - - // CurveV2GaugeRewards Constants - uint256 private constant DURATION = 7 days; - - // Contracts - Governance private governance; - address[] private initialInitiatives; - - ICurveStableSwapNG private curveUsdcBoldPool; - ILiquidityGaugeV6 private curveUsdcBoldGauge; - CurveV2GaugeRewards private curveUsdcBoldInitiative; - - ICurveStableSwapNG private curveLusdBoldPool; - ILiquidityGaugeV6 private curveLusdBoldGauge; - CurveV2GaugeRewards private curveLusdBoldInitiative; - - address private defiCollectiveInitiative; - - function deployGovernance( - DeployGovernanceParams memory p, - address _curveFactoryAddress, - address _curveUsdcBoldPoolAddress, - address _curveLusdBoldPoolAddress - ) internal returns (address, string memory) { - (address governanceAddress, IGovernance.Configuration memory governanceConfiguration) = - computeGovernanceAddressAndConfig(p); - - governance = new Governance{salt: p.salt}( - p.lqty, p.lusd, p.stakingV1, p.bold, governanceConfiguration, p.deployer, initialInitiatives - ); - - assert(governanceAddress == address(governance)); - - curveUsdcBoldPool = ICurveStableSwapNG(_curveUsdcBoldPoolAddress); - curveLusdBoldPool = ICurveStableSwapNG(_curveLusdBoldPoolAddress); - - if (block.chainid == 1) { - // mainnet - (curveUsdcBoldGauge, curveUsdcBoldInitiative) = deployCurveV2GaugeRewards({ - _governance: governance, - _bold: p.bold, - _curveFactoryAddress: _curveFactoryAddress, - _curvePool: curveUsdcBoldPool - }); - - (curveLusdBoldGauge, curveLusdBoldInitiative) = deployCurveV2GaugeRewards({ - _governance: governance, - _bold: p.bold, - _curveFactoryAddress: _curveFactoryAddress, - _curvePool: curveLusdBoldPool - }); - - initialInitiatives.push(address(curveUsdcBoldInitiative)); - initialInitiatives.push(address(curveLusdBoldInitiative)); - initialInitiatives.push(defiCollectiveInitiative = DEFI_COLLECTIVE_GRANTS_ADDRESS); - } else { - initialInitiatives.push(makeAddr("initiative1")); - initialInitiatives.push(makeAddr("initiative2")); - initialInitiatives.push(makeAddr("initiative3")); - } - - governance.registerInitialInitiatives{gas: 600000}(initialInitiatives); - - return (governanceAddress, _getGovernanceManifestJson(p)); - } - - function computeGovernanceAddress(DeployGovernanceParams memory p) internal pure returns (address) { - (address governanceAddress,) = computeGovernanceAddressAndConfig(p); - return governanceAddress; - } - - function computeGovernanceAddressAndConfig(DeployGovernanceParams memory p) - internal - pure - returns (address, IGovernance.Configuration memory) - { - IGovernance.Configuration memory governanceConfiguration = IGovernance.Configuration({ - registrationFee: REGISTRATION_FEE, - registrationThresholdFactor: REGISTRATION_THRESHOLD_FACTOR, - unregistrationThresholdFactor: UNREGISTRATION_THRESHOLD_FACTOR, - unregistrationAfterEpochs: UNREGISTRATION_AFTER_EPOCHS, - votingThresholdFactor: VOTING_THRESHOLD_FACTOR, - minClaim: MIN_CLAIM, - minAccrual: MIN_ACCRUAL, - epochStart: p.epochStart, - epochDuration: EPOCH_DURATION, - epochVotingCutoff: EPOCH_VOTING_CUTOFF - }); - - bytes memory bytecode = abi.encodePacked( - type(Governance).creationCode, - abi.encode(p.lqty, p.lusd, p.stakingV1, p.bold, governanceConfiguration, p.deployer, new address[](0)) - ); - - address governanceAddress = vm.computeCreate2Address(p.salt, keccak256(bytecode)); - return (governanceAddress, governanceConfiguration); - } - - function deployCurveV2GaugeRewards( - IGovernance _governance, - address _bold, - address _curveFactoryAddress, - ICurveStableSwapNG _curvePool - ) private returns (ILiquidityGaugeV6 gauge, CurveV2GaugeRewards curveV2GaugeRewards) { - ICurveStableSwapFactoryNG curveFactory = ICurveStableSwapFactoryNG(_curveFactoryAddress); - gauge = ILiquidityGaugeV6(curveFactory.deploy_gauge(address(_curvePool))); - curveV2GaugeRewards = new CurveV2GaugeRewards(address(_governance), _bold, CRV, address(gauge), DURATION); - - // add BOLD as reward token - gauge.add_reward(_bold, address(curveV2GaugeRewards)); - - // add LUSD as reward token to be distributed by the Funds Safe - gauge.add_reward(LUSD, FUNDS_SAFE); - - // renounce gauge manager role - gauge.set_gauge_manager(address(0)); - } - - function _getGovernanceDeploymentConstants(DeployGovernanceParams memory p) internal pure returns (string memory) { - return string.concat( - "{", - string.concat( - string.concat('"REGISTRATION_FEE":"', REGISTRATION_FEE.toString(), '",'), - string.concat('"REGISTRATION_THRESHOLD_FACTOR":"', REGISTRATION_THRESHOLD_FACTOR.toString(), '",'), - string.concat('"UNREGISTRATION_THRESHOLD_FACTOR":"', UNREGISTRATION_THRESHOLD_FACTOR.toString(), '",'), - string.concat('"UNREGISTRATION_AFTER_EPOCHS":"', UNREGISTRATION_AFTER_EPOCHS.toString(), '",'), - string.concat('"VOTING_THRESHOLD_FACTOR":"', VOTING_THRESHOLD_FACTOR.toString(), '",'), - string.concat('"MIN_CLAIM":"', MIN_CLAIM.toString(), '",'), - string.concat('"MIN_ACCRUAL":"', MIN_ACCRUAL.toString(), '",'), - string.concat('"EPOCH_START":"', p.epochStart.toString(), '",') - ), - string.concat( - string.concat('"EPOCH_DURATION":"', EPOCH_DURATION.toString(), '",'), - string.concat('"EPOCH_VOTING_CUTOFF":"', EPOCH_VOTING_CUTOFF.toString(), '",'), - string.concat('"FUNDS_SAFE":"', FUNDS_SAFE.toHexString(), '"') // no comma - ), - "}" - ); - } - - function _getGovernanceManifestJson(DeployGovernanceParams memory p) internal view returns (string memory) { - return string.concat( - "{", - string.concat( - string.concat('"constants":', _getGovernanceDeploymentConstants(p), ","), - string.concat('"governance":"', address(governance).toHexString(), '",'), - string.concat('"curveUsdcBoldPool":"', address(curveUsdcBoldPool).toHexString(), '",'), - string.concat('"curveUsdcBoldGauge":"', address(curveUsdcBoldGauge).toHexString(), '",'), - string.concat('"curveUsdcBoldInitiative":"', address(curveUsdcBoldInitiative).toHexString(), '",'), - string.concat('"curveLusdBoldPool":"', address(curveLusdBoldPool).toHexString(), '",'), - string.concat('"curveLusdBoldGauge":"', address(curveLusdBoldGauge).toHexString(), '",'), - string.concat('"curveLusdBoldInitiative":"', address(curveLusdBoldInitiative).toHexString(), '",') - ), - string.concat( - string.concat('"defiCollectiveInitiative":"', defiCollectiveInitiative.toHexString(), '",'), - string.concat('"stakingV1":"', p.stakingV1.toHexString(), '",'), - string.concat('"LQTYToken":"', p.lqty.toHexString(), '",'), - string.concat('"LUSDToken":"', p.lusd.toHexString(), '",'), - string.concat('"initialInitiatives":', initialInitiatives.toJSON()) // no comma - ), - "}" - ); - } -} diff --git a/contracts/script/DeployLiquity2.s.sol b/contracts/script/DeployLiquity2.s.sol index ba6cdc4b4..e88e0fba6 100644 --- a/contracts/script/DeployLiquity2.s.sol +++ b/contracts/script/DeployLiquity2.s.sol @@ -4,16 +4,26 @@ pragma solidity 0.8.24; import {StdCheats} from "forge-std/StdCheats.sol"; import {IERC20Metadata} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {IERC20 as IERC20_GOV} from "openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20 as IERC20_GOV} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from + "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IFPMMFactory} from "src/Interfaces/IFPMMFactory.sol"; +import {SystemParams} from "src/SystemParams.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; +import { + INTEREST_RATE_ADJ_COOLDOWN, + MAX_ANNUAL_INTEREST_RATE, + UPFRONT_INTEREST_PERIOD +} from "src/Dependencies/Constants.sol"; +import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; import {StringFormatting} from "test/Utils/StringFormatting.sol"; import {Accounts} from "test/TestContracts/Accounts.sol"; import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {ETH_GAS_COMPENSATION} from "src/Dependencies/Constants.sol"; -import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; +import {WETHTester} from "test/TestContracts/WETHTester.sol"; import "src/AddressesRegistry.sol"; import "src/ActivePool.sol"; -import "src/BoldToken.sol"; import "src/BorrowerOperations.sol"; import "src/TroveManager.sol"; import "src/TroveNFT.sol"; @@ -24,127 +34,22 @@ import "src/HintHelpers.sol"; import "src/MultiTroveGetter.sol"; import "src/SortedTroves.sol"; import "src/StabilityPool.sol"; -import "src/PriceFeeds/WETHPriceFeed.sol"; -import "src/PriceFeeds/WSTETHPriceFeed.sol"; -import "src/PriceFeeds/RETHPriceFeed.sol"; + import "src/CollateralRegistry.sol"; -import "test/TestContracts/PriceFeedTestnet.sol"; +import "test/TestContracts/StableTokenV3.sol"; +import "test/TestContracts/MockFXPriceFeed.sol"; import "test/TestContracts/MetadataDeployment.sol"; import "test/Utils/Logging.sol"; import "test/Utils/StringEquality.sol"; -import "src/Zappers/WETHZapper.sol"; -import "src/Zappers/GasCompZapper.sol"; -import "src/Zappers/LeverageLSTZapper.sol"; -import "src/Zappers/LeverageWETHZapper.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; -import {BalancerFlashLoan} from "src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol"; -import {WETHTester} from "test/TestContracts/WETHTester.sol"; import "forge-std/console2.sol"; -import {IRateProvider, IWeightedPool, IWeightedPoolFactory} from "./Interfaces/Balancer/IWeightedPool.sol"; -import {IVault} from "./Interfaces/Balancer/IVault.sol"; -import {MockStakingV1} from "V2-gov/test/mocks/MockStakingV1.sol"; -import {DeployGovernance} from "./DeployGovernance.s.sol"; - -function _latestUTCMidnightBetweenWednesdayAndThursday() view returns (uint256) { - return block.timestamp / 1 weeks * 1 weeks; -} - -contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, MetadataDeployment, Logging { +contract DeployLiquity2Script is StdCheats, MetadataDeployment, Logging { using Strings for *; using StringFormatting for *; using StringEquality for string; - string constant DEPLOYMENT_MODE_COMPLETE = "complete"; - string constant DEPLOYMENT_MODE_BOLD_ONLY = "bold-only"; - string constant DEPLOYMENT_MODE_USE_EXISTING_BOLD = "use-existing-bold"; - - uint256 constant NUM_BRANCHES = 3; - - address WETH_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address USDC_ADDRESS = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - - // used for gas compensation and as collateral of the first branch - // tapping disallowed - IWETH WETH; - IERC20Metadata USDC; - address WSTETH_ADDRESS = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - address RETH_ADDRESS = 0xae78736Cd615f374D3085123A210448E74Fc6393; - address ETH_ORACLE_ADDRESS = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - address RETH_ORACLE_ADDRESS = 0x536218f9E9Eb48863970252233c8F271f554C2d0; - address STETH_ORACLE_ADDRESS = 0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8; - uint256 ETH_USD_STALENESS_THRESHOLD = 24 hours; - uint256 STETH_USD_STALENESS_THRESHOLD = 24 hours; - uint256 RETH_ETH_STALENESS_THRESHOLD = 48 hours; - - // V1 - address LQTY_ADDRESS = 0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D; - address LQTY_STAKING_ADDRESS = 0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d; - address LUSD_ADDRESS = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0; - - address internal lqty; - address internal stakingV1; - address internal lusd; - - // Curve - ICurveStableswapNGFactory curveStableswapFactory; - // https://docs.curve.fi/deployments/amm/#stableswap-ng - // Sepolia - ICurveStableswapNGFactory constant curveStableswapFactorySepolia = - ICurveStableswapNGFactory(0xfb37b8D939FFa77114005e61CFc2e543d6F49A81); - // Mainnet - ICurveStableswapNGFactory constant curveStableswapFactoryMainnet = - ICurveStableswapNGFactory(0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf); - uint128 constant BOLD_TOKEN_INDEX = 0; - uint128 constant OTHER_TOKEN_INDEX = 1; - - // Uni V3 - uint24 constant UNIV3_FEE = 0.3e4; - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - ISwapRouter uniV3Router; - IQuoterV2 uniV3Quoter; - IUniswapV3Factory uniswapV3Factory; - INonfungiblePositionManager uniV3PositionManager; - // https://docs.uniswap.org/contracts/v3/reference/deployments/ethereum-deployments - // Sepolia - ISwapRouter constant uniV3RouterSepolia = ISwapRouter(0x65669fE35312947050C450Bd5d36e6361F85eC12); - IQuoterV2 constant uniV3QuoterSepolia = IQuoterV2(0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3); - IUniswapV3Factory constant uniswapV3FactorySepolia = IUniswapV3Factory(0x0227628f3F023bb0B980b67D528571c95c6DaC1c); - INonfungiblePositionManager constant uniV3PositionManagerSepolia = - INonfungiblePositionManager(0x1238536071E1c677A632429e3655c799b22cDA52); - // Mainnet - ISwapRouter constant uniV3RouterMainnet = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - IQuoterV2 constant uniV3QuoterMainnet = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); - IUniswapV3Factory constant uniswapV3FactoryMainnet = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - INonfungiblePositionManager constant uniV3PositionManagerMainnet = - INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - - // Balancer - IVault constant balancerVault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - IWeightedPoolFactory balancerFactory; - // Sepolia - // https://docs.balancer.fi/reference/contracts/deployment-addresses/sepolia.html - IWeightedPoolFactory constant balancerFactorySepolia = - IWeightedPoolFactory(0x7920BFa1b2041911b354747CA7A6cDD2dfC50Cfd); - // Mainnet - // https://docs.balancer.fi/reference/contracts/deployment-addresses/mainnet.html - IWeightedPoolFactory constant balancerFactoryMainnet = - IWeightedPoolFactory(0x897888115Ada5773E02aA29F775430BFB5F34c51); - bytes32 SALT; address deployer; - bool useTestnetPriceFeeds; - - uint256 lastTroveIndex; struct LiquityContracts { IAddressesRegistry addressesRegistry; @@ -161,9 +66,7 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, GasPool gasPool; IInterestRouter interestRouter; IERC20Metadata collToken; - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - ILeverageZapper leverageZapper; + ISystemParams systemParams; } struct LiquityContractAddresses { @@ -181,20 +84,6 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, address interestRouter; } - struct Zappers { - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - } - - struct TroveManagerParams { - uint256 CCR; - uint256 MCR; - uint256 SCR; - uint256 BCR; - uint256 LIQUIDATION_PENALTY_SP; - uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; - } - struct DeploymentVars { uint256 numCollaterals; IERC20Metadata[] collaterals; @@ -216,333 +105,63 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, } struct DeploymentResult { - LiquityContracts[] contractsArray; + LiquityContracts contracts; ICollateralRegistry collateralRegistry; - IBoldToken boldToken; - ICurveStableswapNGPool usdcCurvePool; HintHelpers hintHelpers; MultiTroveGetter multiTroveGetter; - IExchangeHelpers exchangeHelpers; + ProxyAdmin proxyAdmin; + IStableTokenV3 stableToken; + ISystemParams systemParams; + address stabilityPoolImpl; + address stableTokenV3Impl; + address systemParamsImpl; + address fpmm; + } + + struct DeploymentConfig { + address USDm_ALFAJORES_ADDRESS; + address proxyAdmin; + address fpmmFactory; + address fpmmImplementation; + address liquidityStrategy; + address oracleAdapter; + address referenceRateFeedID; + string stableTokenName; + string stableTokenSymbol; + address watchdog; } + DeploymentConfig internal CONFIG = DeploymentConfig({ + USDm_ALFAJORES_ADDRESS: 0x9E2d4412d0f434cC85500b79447d9323a7416f09, + proxyAdmin: 0xe4DdacCAdb64114215FCe8251B57B2AEB5C2C0E2, + fpmmFactory: 0xd8098494a749a3fDAD2D2e7Fa5272D8f274D8FF6, + fpmmImplementation: 0x0292efcB331C6603eaa29D570d12eB336D6c01d6, + liquidityStrategy: address(123), // TODO: set liquidity strategy + oracleAdapter: address(234), // TODO: set oracle adapter address + referenceRateFeedID: 0x206B25Ea01E188Ee243131aFdE526bA6E131a016, + stableTokenName: "EUR.v2 Test", + stableTokenSymbol: "EUR.v2", + watchdog: address(345) // TODO: set watchdog address + }); + function run() external { string memory saltStr = vm.envOr("SALT", block.timestamp.toString()); SALT = keccak256(bytes(saltStr)); - if (vm.envBytes("DEPLOYER").length == 20) { - // address - deployer = vm.envAddress("DEPLOYER"); - vm.startBroadcast(deployer); - } else { - // private key - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - } - - string memory deploymentMode = vm.envOr("DEPLOYMENT_MODE", DEPLOYMENT_MODE_COMPLETE); - require( - deploymentMode.eq(DEPLOYMENT_MODE_COMPLETE) || deploymentMode.eq(DEPLOYMENT_MODE_BOLD_ONLY) - || deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD), - string.concat("Bad deployment mode: ", deploymentMode) - ); - - uint256 epochStart = vm.envOr( - "EPOCH_START", - (block.chainid == 1 ? _latestUTCMidnightBetweenWednesdayAndThursday() : block.timestamp) - EPOCH_DURATION - ); - - useTestnetPriceFeeds = vm.envOr("USE_TESTNET_PRICEFEEDS", false); + uint256 privateKey = vm.envUint("DEPLOYER"); + deployer = vm.addr(privateKey); + vm.startBroadcast(privateKey); _log("Deployer: ", deployer.toHexString()); _log("Deployer balance: ", deployer.balance.decimal()); - _log("Deployment mode: ", deploymentMode); _log("CREATE2 salt: ", 'keccak256(bytes("', saltStr, '")) = ', uint256(SALT).toHexString()); - _log("Governance epoch start: ", epochStart.toString()); - _log("Use testnet PriceFeeds: ", useTestnetPriceFeeds ? "yes" : "no"); - - // Deploy Bold or pick up existing deployment - bytes memory boldBytecode = bytes.concat(type(BoldToken).creationCode, abi.encode(deployer)); - address boldAddress = vm.computeCreate2Address(SALT, keccak256(boldBytecode)); - BoldToken boldToken; - - if (deploymentMode.eq(DEPLOYMENT_MODE_USE_EXISTING_BOLD)) { - require(boldAddress.code.length > 0, string.concat("BOLD not found at ", boldAddress.toHexString())); - boldToken = BoldToken(boldAddress); - - // Check BOLD is untouched - require(boldToken.totalSupply() == 0, "Some BOLD has been minted!"); - require(boldToken.collateralRegistryAddress() == address(0), "Collateral registry already set"); - require(boldToken.owner() == deployer, "Not BOLD owner"); - } else { - boldToken = new BoldToken{salt: SALT}(deployer); - assert(address(boldToken) == boldAddress); - } - - if (deploymentMode.eq(DEPLOYMENT_MODE_BOLD_ONLY)) { - vm.writeFile("deployment-manifest.json", string.concat('{"boldToken":"', boldAddress.toHexString(), '"}')); - return; - } - - if (block.chainid == 1) { - // mainnet - WETH = IWETH(WETH_ADDRESS); - USDC = IERC20Metadata(USDC_ADDRESS); - curveStableswapFactory = curveStableswapFactoryMainnet; - uniV3Router = uniV3RouterMainnet; - uniV3Quoter = uniV3QuoterMainnet; - uniswapV3Factory = uniswapV3FactoryMainnet; - uniV3PositionManager = uniV3PositionManagerMainnet; - balancerFactory = balancerFactoryMainnet; - lqty = LQTY_ADDRESS; - stakingV1 = LQTY_STAKING_ADDRESS; - lusd = LUSD_ADDRESS; - } else { - // sepolia, local - if (block.chainid == 31337) { - // local - WETH = new WETHTester({_tapAmount: 100 ether, _tapPeriod: 1 days}); - } else { - // sepolia - WETH = new WETHTester({_tapAmount: 0, _tapPeriod: type(uint256).max}); - } - USDC = new ERC20Faucet("USDC", "USDC", 0, type(uint256).max); - curveStableswapFactory = curveStableswapFactorySepolia; - uniV3Router = uniV3RouterSepolia; - uniV3Quoter = uniV3QuoterSepolia; - uniswapV3Factory = uniswapV3FactorySepolia; - uniV3PositionManager = uniV3PositionManagerSepolia; - balancerFactory = balancerFactorySepolia; - // Needed for Governance (they will be constants for mainnet) - lqty = address(new ERC20Faucet("Liquity", "LQTY", 100 ether, 1 days)); - lusd = address(new ERC20Faucet("Liquity USD", "LUSD", 100 ether, 1 days)); - stakingV1 = address(new MockStakingV1(IERC20_GOV(lqty), IERC20_GOV(lusd))); - - // Let stakingV1 spend anyone's LQTY without approval, like in the real LQTYStaking - ERC20Faucet(lqty).mock_setWildcardSpender(address(stakingV1), true); - } - - TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](NUM_BRANCHES); - - // WETH - troveManagerParamsArray[0] = TroveManagerParams({ - CCR: CCR_WETH, - MCR: MCR_WETH, - SCR: SCR_WETH, - BCR: BCR_ALL, - LIQUIDATION_PENALTY_SP: LIQUIDATION_PENALTY_SP_WETH, - LIQUIDATION_PENALTY_REDISTRIBUTION: LIQUIDATION_PENALTY_REDISTRIBUTION_WETH - }); - - // wstETH - troveManagerParamsArray[1] = TroveManagerParams({ - CCR: CCR_SETH, - MCR: MCR_SETH, - SCR: SCR_SETH, - BCR: BCR_ALL, - LIQUIDATION_PENALTY_SP: LIQUIDATION_PENALTY_SP_SETH, - LIQUIDATION_PENALTY_REDISTRIBUTION: LIQUIDATION_PENALTY_REDISTRIBUTION_SETH - }); - - // rETH (same as wstETH) - troveManagerParamsArray[2] = troveManagerParamsArray[1]; - - string[] memory collNames = new string[](2); - string[] memory collSymbols = new string[](2); - collNames[0] = "Wrapped liquid staked Ether 2.0"; - collSymbols[0] = "wstETH"; - collNames[1] = "Rocket Pool ETH"; - collSymbols[1] = "rETH"; - - DeployGovernanceParams memory deployGovernanceParams = DeployGovernanceParams({ - epochStart: epochStart, - deployer: deployer, - salt: SALT, - stakingV1: stakingV1, - lqty: lqty, - lusd: lusd, - bold: boldAddress - }); + _log("Chain ID: ", block.chainid.toString()); - DeploymentResult memory deployed = - _deployAndConnectContracts(troveManagerParamsArray, collNames, collSymbols, deployGovernanceParams); - - if (block.chainid == 11155111) { - // Provide liquidity for zaps if we're on Sepolia - ERC20Faucet monkeyBalls = new ERC20Faucet("MonkeyBalls", "MB", 0, type(uint256).max); - for (uint256 i = 0; i < deployed.contractsArray.length; ++i) { - PriceFeedTestnet(address(deployed.contractsArray[i].priceFeed)).setPrice(2_000 ether); - _provideFlashloanLiquidity(ERC20Faucet(address(deployed.contractsArray[i].collToken)), monkeyBalls); - if (i == 0) { - // WETH, we do USDC-WETH - (uint256 price,) = deployed.contractsArray[0].priceFeed.fetchPrice(); - uint256 token1Amount = 1_000_000 ether; - _provideUniV3Liquidity( - ERC20Faucet(address(USDC)), ERC20Faucet(address(WETH)), token1Amount, price, UNIV3_FEE_USDC_WETH - ); - } else { - // LSTs, we do WETH-LST - uint256 token1Amount = 1_000 ether; - _provideUniV3Liquidity( - ERC20Faucet(address(WETH)), - ERC20Faucet(address(deployed.contractsArray[i].collToken)), - token1Amount, - 1 ether, - UNIV3_FEE_WETH_COLL - ); - } - } - - _provideCurveLiquidity(deployed.boldToken, deployed.contractsArray[0]); - - // deployed.contractsArray[1].collToken.mint(deployer, 1 ether); - // deployed.contractsArray[1].collToken.approve(address(deployed.contractsArray[1].leverageZapper), 1 ether); - // deployed.contractsArray[1].leverageZapper.openLeveragedTroveWithRawETH{value: ETH_GAS_COMPENSATION}( - // ILeverageZapper.OpenLeveragedTroveParams({ - // owner: deployer, - // ownerIndex: 1, - // collAmount: 1 ether, - // flashLoanAmount: 1 ether, - // boldAmount: 2_000 ether, - // upperHint: 0, - // lowerHint: 0, - // annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - // batchManager: address(0), - // maxUpfrontFee: type(uint256).max, - // addManager: address(0), - // removeManager: address(0), - // receiver: address(0) - // }) - // ); - } - - ICurveStableswapNGPool lusdCurvePool; - if (block.chainid == 1) { - lusdCurvePool = _deployCurvePool(deployed.boldToken, IERC20Metadata(LUSD_ADDRESS)); - } - - // Governance - (address governanceAddress, string memory governanceManifest) = deployGovernance( - deployGovernanceParams, - address(curveStableswapFactory), - address(deployed.usdcCurvePool), - address(lusdCurvePool) - ); - address computedGovernanceAddress = computeGovernanceAddress(deployGovernanceParams); - assert(governanceAddress == computedGovernanceAddress); + DeploymentResult memory deployed = _deployAndConnectContracts(); vm.stopBroadcast(); - vm.writeFile("deployment-manifest.json", _getManifestJson(deployed, governanceManifest)); - - if (vm.envOr("OPEN_DEMO_TROVES", false)) { - // Anvil default accounts - // TODO: get accounts from env - uint256[] memory demoAccounts = new uint256[](8); - demoAccounts[0] = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; - demoAccounts[1] = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; - demoAccounts[2] = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a; - demoAccounts[3] = 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6; - demoAccounts[4] = 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a; - demoAccounts[5] = 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba; - demoAccounts[6] = 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e; - demoAccounts[7] = 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356; - - DemoTroveParams[] memory demoTroves = new DemoTroveParams[](24); - - demoTroves[0] = DemoTroveParams(0, demoAccounts[0], 0, 35 ether, 2_800 ether, 5.0e16); - demoTroves[1] = DemoTroveParams(0, demoAccounts[1], 0, 47 ether, 2_400 ether, 4.7e16); - demoTroves[2] = DemoTroveParams(0, demoAccounts[2], 0, 40 ether, 4_000 ether, 3.3e16); - demoTroves[3] = DemoTroveParams(0, demoAccounts[3], 0, 75 ether, 6_000 ether, 4.3e16); - demoTroves[4] = DemoTroveParams(0, demoAccounts[4], 0, 29 ether, 2_280 ether, 5.0e16); - demoTroves[5] = DemoTroveParams(0, demoAccounts[5], 0, 58.37 ether, 4_400 ether, 4.7e16); - demoTroves[6] = DemoTroveParams(0, demoAccounts[6], 0, 43.92 ether, 5_500 ether, 3.8e16); - demoTroves[7] = DemoTroveParams(0, demoAccounts[7], 0, 57.2 ether, 6_000 ether, 4.3e16); - - demoTroves[8] = DemoTroveParams(1, demoAccounts[0], 0, 31 ether, 2_000 ether, 3.3e16); - demoTroves[9] = DemoTroveParams(1, demoAccounts[1], 0, 26 ether, 2_000 ether, 4.1e16); - demoTroves[10] = DemoTroveParams(1, demoAccounts[2], 0, 28 ether, 2_300 ether, 3.8e16); - demoTroves[11] = DemoTroveParams(1, demoAccounts[3], 0, 32 ether, 2_200 ether, 4.3e16); - demoTroves[12] = DemoTroveParams(1, demoAccounts[4], 0, 95 ether, 12_000 ether, 7.0e16); - demoTroves[13] = DemoTroveParams(1, demoAccounts[5], 0, 97 ether, 4_000 ether, 4.4e16); - demoTroves[14] = DemoTroveParams(1, demoAccounts[6], 0, 81 ether, 11_000 ether, 3.3e16); - demoTroves[15] = DemoTroveParams(1, demoAccounts[7], 0, 94 ether, 12_800 ether, 4.4e16); - - demoTroves[16] = DemoTroveParams(2, demoAccounts[0], 0, 45 ether, 3_000 ether, 2.4e16); - demoTroves[17] = DemoTroveParams(2, demoAccounts[1], 0, 35 ether, 2_100 ether, 5.0e16); - demoTroves[18] = DemoTroveParams(2, demoAccounts[2], 0, 67 ether, 2_200 ether, 4.5e16); - demoTroves[19] = DemoTroveParams(2, demoAccounts[3], 0, 32 ether, 4_900 ether, 3.2e16); - demoTroves[20] = DemoTroveParams(2, demoAccounts[4], 0, 82 ether, 4_500 ether, 6.9e16); - demoTroves[21] = DemoTroveParams(2, demoAccounts[5], 0, 74 ether, 7_300 ether, 4.1e16); - demoTroves[22] = DemoTroveParams(2, demoAccounts[6], 0, 54 ether, 6_900 ether, 2.9e16); - demoTroves[23] = DemoTroveParams(2, demoAccounts[7], 0, 65 ether, 8_100 ether, 1.5e16); - - for (uint256 i = 0; i < deployed.contractsArray.length; i++) { - tapFaucet(demoAccounts, deployed.contractsArray[i]); - } - - openDemoTroves(demoTroves, deployed.contractsArray); - } - } - - function tapFaucet(uint256[] memory accounts, LiquityContracts memory contracts) internal { - for (uint256 i = 0; i < accounts.length; i++) { - ERC20Faucet token = ERC20Faucet(address(contracts.collToken)); - - vm.startBroadcast(accounts[i]); - token.tap(); - vm.stopBroadcast(); - - console2.log( - "%s.tap() => %s (balance: %s)", - token.symbol(), - vm.addr(accounts[i]), - string.concat(formatAmount(token.balanceOf(vm.addr(accounts[i])), 18, 2), " ", token.symbol()) - ); - } - } - - function openDemoTroves(DemoTroveParams[] memory demoTroves, LiquityContracts[] memory contractsArray) internal { - for (uint256 i = 0; i < demoTroves.length; i++) { - console2.log( - "openTrove({ coll: %18e, borrow: %18e, rate: %18e%% })", - demoTroves[i].coll, - demoTroves[i].debt, - demoTroves[i].annualInterestRate * 100 - ); - - DemoTroveParams memory trove = demoTroves[i]; - LiquityContracts memory contracts = contractsArray[trove.collIndex]; - - vm.startBroadcast(trove.owner); - - IERC20 collToken = IERC20(contracts.collToken); - IERC20 wethToken = IERC20(contracts.addressesRegistry.WETH()); - - // Approve collToken to BorrowerOperations - if (collToken == wethToken) { - wethToken.approve(address(contracts.borrowerOperations), trove.coll + ETH_GAS_COMPENSATION); - } else { - wethToken.approve(address(contracts.borrowerOperations), ETH_GAS_COMPENSATION); - collToken.approve(address(contracts.borrowerOperations), trove.coll); - } - - IBorrowerOperations(contracts.borrowerOperations).openTrove( - vm.addr(trove.owner), // _owner - trove.ownerIndex, // _ownerIndex - trove.coll, // _collAmount - trove.debt, // _boldAmount - 0, // _upperHint - 0, // _lowerHint - trove.annualInterestRate, // _annualInterestRate - type(uint256).max, // _maxUpfrontFee - address(0), // _addManager - address(0), // _removeManager - address(0) // _receiver - ); - - vm.stopBroadcast(); - } + vm.writeFile("script/deployment-manifest.json", _getManifestJson(deployed)); } // See: https://solidity-by-example.org/app/create2/ @@ -550,177 +169,219 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } - function _deployAndConnectContracts( - TroveManagerParams[] memory troveManagerParamsArray, - string[] memory _collNames, - string[] memory _collSymbols, - DeployGovernanceParams memory _deployGovernanceParams - ) internal returns (DeploymentResult memory r) { - assert(_collNames.length == troveManagerParamsArray.length - 1); - assert(_collSymbols.length == troveManagerParamsArray.length - 1); - - DeploymentVars memory vars; - vars.numCollaterals = troveManagerParamsArray.length; - r.boldToken = BoldToken(_deployGovernanceParams.bold); - - // USDC and USDC-BOLD pool - r.usdcCurvePool = _deployCurvePool(r.boldToken, USDC); - - r.contractsArray = new LiquityContracts[](vars.numCollaterals); - vars.collaterals = new IERC20Metadata[](vars.numCollaterals); - vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); - vars.troveManagers = new ITroveManager[](vars.numCollaterals); - - // Collaterals - if (block.chainid == 1 && !useTestnetPriceFeeds) { - // mainnet - // ETH - vars.collaterals[0] = IERC20Metadata(WETH); - - // wstETH - vars.collaterals[1] = IERC20Metadata(WSTETH_ADDRESS); - - // RETH - vars.collaterals[2] = IERC20Metadata(RETH_ADDRESS); - } else { - // Sepolia - // Use WETH as collateral for the first branch - vars.collaterals[0] = WETH; - - // Deploy plain ERC20Faucets for the rest of the branches - for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { - vars.collaterals[vars.i] = new ERC20Faucet( - _collNames[vars.i - 1], // _name - _collSymbols[vars.i - 1], // _symbol - 100 ether, // _tapAmount - 1 days // _tapPeriod - ); - } - } - - // Deploy AddressesRegistries and get TroveManager addresses - for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { - (IAddressesRegistry addressesRegistry, address troveManagerAddress) = - _deployAddressesRegistry(troveManagerParamsArray[vars.i]); - vars.addressesRegistries[vars.i] = addressesRegistry; - vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); - } - - r.collateralRegistry = new CollateralRegistry(r.boldToken, vars.collaterals, vars.troveManagers); - r.hintHelpers = new HintHelpers(r.collateralRegistry); + function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) + public + pure + returns (bytes memory) + { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); + } + + function _deployAndConnectContracts() internal returns (DeploymentResult memory r) { + _deployProxyInfrastructure(r); + _deployStableToken(r); + // _deployFPMM(r); + _deploySystemParams(r); + + IAddressesRegistry addressesRegistry = new AddressesRegistry(deployer); + + address troveManagerAddress = + _computeCreate2Address(type(TroveManager).creationCode, address(addressesRegistry), address(r.systemParams)); + + IERC20Metadata collToken = IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS); + + IERC20Metadata[] memory collaterals = new IERC20Metadata[](1); + collaterals[0] = collToken; + + ITroveManager[] memory troveManagers = new ITroveManager[](1); + troveManagers[0] = ITroveManager(troveManagerAddress); + + r.collateralRegistry = + new CollateralRegistry(IBoldToken(address(r.stableToken)), collaterals, troveManagers, r.systemParams, makeAddr("liquidityStrategy")); + r.hintHelpers = new HintHelpers(r.collateralRegistry, r.systemParams); r.multiTroveGetter = new MultiTroveGetter(r.collateralRegistry); - // Deploy per-branch contracts for each branch - for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { - vars.contracts = _deployAndConnectCollateralContracts( - vars.collaterals[vars.i], - r.boldToken, - r.collateralRegistry, - r.usdcCurvePool, - vars.addressesRegistries[vars.i], - address(vars.troveManagers[vars.i]), - r.hintHelpers, - r.multiTroveGetter, - computeGovernanceAddress(_deployGovernanceParams) - ); - r.contractsArray[vars.i] = vars.contracts; - } - - r.boldToken.setCollateralRegistry(address(r.collateralRegistry)); - - // exchange helpers - r.exchangeHelpers = new HybridCurveUniV3ExchangeHelpers( - USDC, - WETH, - r.usdcCurvePool, - OTHER_TOKEN_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Quoter + // TODO: replace with real price feed + IPriceFeed priceFeed = new MockFXPriceFeed(); + + r.contracts = + _deployAndConnectCollateralContracts(collToken, priceFeed, addressesRegistry, troveManagerAddress, r); + } + + function _deployProxyInfrastructure(DeploymentResult memory r) internal { + r.proxyAdmin = ProxyAdmin(CONFIG.proxyAdmin); + r.stableTokenV3Impl = address(new StableTokenV3{salt: SALT}(true)); + r.stabilityPoolImpl = address(new StabilityPool{salt: SALT}(true, r.systemParams)); + + _deploySystemParamsImpl(r); + + assert( + address(r.stableTokenV3Impl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StableTokenV3).creationCode, abi.encode(true))) + ) + ); + assert( + address(r.stabilityPoolImpl) + == vm.computeCreate2Address( + SALT, keccak256(bytes.concat(type(StabilityPool).creationCode, abi.encode(true, r.systemParams))) + ) ); } - function _deployAddressesRegistry(TroveManagerParams memory _troveManagerParams) - internal - returns (IAddressesRegistry, address) - { - IAddressesRegistry addressesRegistry = new AddressesRegistry( - deployer, - _troveManagerParams.CCR, - _troveManagerParams.MCR, - _troveManagerParams.BCR, - _troveManagerParams.SCR, - _troveManagerParams.LIQUIDATION_PENALTY_SP, - _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION + function _deployStableToken(DeploymentResult memory r) internal { + r.stableToken = IStableTokenV3( + address(new TransparentUpgradeableProxy(address(r.stableTokenV3Impl), address(r.proxyAdmin), "")) ); - address troveManagerAddress = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(TroveManager).creationCode, address(addressesRegistry))) + } + + function _deployFPMM(DeploymentResult memory r) internal { + r.fpmm = IFPMMFactory(CONFIG.fpmmFactory).deployFPMM( + CONFIG.fpmmImplementation, address(r.stableToken), CONFIG.USDm_ALFAJORES_ADDRESS, CONFIG.referenceRateFeedID ); + } + + function _deploySystemParamsImpl(DeploymentResult memory r) internal { + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 2000e18}); + + ISystemParams.LiquidationParams memory liquidationParams = + ISystemParams.LiquidationParams({liquidationPenaltySP: 5e16, liquidationPenaltyRedistribution: 10e16}); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + ISystemParams.CollateralParams memory collateralParams = + ISystemParams.CollateralParams({ccr: 150 * 1e16, scr: 110 * 1e16, mcr: 110 * 1e16, bcr: 10 * 1e16}); + + ISystemParams.InterestParams memory interestParams = + ISystemParams.InterestParams({minAnnualInterestRate: 1e18 / 200}); + + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: 1e18 / 200, + initialBaseRate: 1e18, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1 + }); - return (addressesRegistry, troveManagerAddress); + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * (1e18 / 100), + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 + }); + + r.systemParamsImpl = address( + new SystemParams{salt: SALT}( + true, // disableInitializers for implementation + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams + ) + ); + } + + function _deploySystemParams(DeploymentResult memory r) internal { + address systemParamsProxy = + address(new TransparentUpgradeableProxy(address(r.systemParamsImpl), address(r.proxyAdmin), "")); + + r.systemParams = ISystemParams(systemParamsProxy); + r.systemParams.initialize(); } function _deployAndConnectCollateralContracts( IERC20Metadata _collToken, - IBoldToken _boldToken, - ICollateralRegistry _collateralRegistry, - ICurveStableswapNGPool _usdcCurvePool, + IPriceFeed _priceFeed, IAddressesRegistry _addressesRegistry, address _troveManagerAddress, - IHintHelpers _hintHelpers, - IMultiTroveGetter _multiTroveGetter, - address _governance + DeploymentResult memory r ) internal returns (LiquityContracts memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _collToken; - - // Deploy all contracts, using testers for TM and PriceFeed contracts.addressesRegistry = _addressesRegistry; + contracts.priceFeed = _priceFeed; + contracts.systemParams = r.systemParams; + // TODO: replace with governance timelock on mainnet + contracts.interestRouter = IInterestRouter(0x56fD3F2bEE130e9867942D0F463a16fBE49B8d81); + + addresses.troveManager = _troveManagerAddress; - // Deploy Metadata contracts.metadataNFT = deployMetadata(SALT); addresses.metadataNFT = vm.computeCreate2Address( SALT, keccak256(getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader))) ); assert(address(contracts.metadataNFT) == addresses.metadataNFT); - contracts.interestRouter = IInterestRouter(_governance); - addresses.borrowerOperations = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(BorrowerOperations).creationCode, address(contracts.addressesRegistry))) - ); - addresses.troveManager = _troveManagerAddress; - addresses.troveNFT = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry))) - ); - addresses.stabilityPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.activePool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry))) + addresses.borrowerOperations = _computeCreate2Address( + type(BorrowerOperations).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) ); - addresses.defaultPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry))) + addresses.troveNFT = _computeCreate2Address(type(TroveNFT).creationCode, address(contracts.addressesRegistry)); + addresses.activePool = _computeCreate2Address( + type(ActivePool).creationCode, address(contracts.addressesRegistry), address(contracts.systemParams) ); - addresses.gasPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(GasPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.collSurplusPool = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry))) - ); - addresses.sortedTroves = vm.computeCreate2Address( - SALT, keccak256(getBytecode(type(SortedTroves).creationCode, address(contracts.addressesRegistry))) + addresses.defaultPool = + _computeCreate2Address(type(DefaultPool).creationCode, address(contracts.addressesRegistry)); + addresses.gasPool = _computeCreate2Address(type(GasPool).creationCode, address(contracts.addressesRegistry)); + addresses.collSurplusPool = + _computeCreate2Address(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)); + addresses.sortedTroves = + _computeCreate2Address(type(SortedTroves).creationCode, address(contracts.addressesRegistry)); + + // Deploy StabilityPool proxy + address stabilityPool = + address(new TransparentUpgradeableProxy(address(r.stabilityPoolImpl), address(r.proxyAdmin), "")); + + contracts.stabilityPool = IStabilityPool(stabilityPool); + // Set up addresses in registry + _setupAddressesRegistry(contracts, addresses, r); + + // Deploy core protocol contracts + _deployProtocolContracts(contracts, addresses); + + IStabilityPool(stabilityPool).initialize(contracts.addressesRegistry); + + address[] memory minters = new address[](2); + minters[0] = address(contracts.borrowerOperations); + minters[1] = address(contracts.activePool); + + address[] memory burners = new address[](4); + burners[0] = address(contracts.troveManager); + burners[1] = address(r.collateralRegistry); + burners[2] = address(contracts.borrowerOperations); + burners[3] = address(contracts.stabilityPool); + + address[] memory operators = new address[](1); + operators[0] = address(contracts.stabilityPool); + + r.stableToken.initialize( + CONFIG.stableTokenName, + CONFIG.stableTokenSymbol, + deployer, + new address[](0), + new uint256[](0), + minters, + burners, + operators ); + } - contracts.priceFeed = _deployPriceFeed(address(_collToken), addresses.borrowerOperations); - + function _setupAddressesRegistry( + LiquityContracts memory contracts, + LiquityContractAddresses memory addresses, + DeploymentResult memory r + ) internal { IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: _collToken, + collToken: contracts.collToken, borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), troveManager: ITroveManager(addresses.troveManager), troveNFT: ITroveNFT(addresses.troveNFT), metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: IStabilityPool(addresses.stabilityPool), + stabilityPool: contracts.stabilityPool, priceFeed: contracts.priceFeed, activePool: IActivePool(addresses.activePool), defaultPool: IDefaultPool(addresses.defaultPool), @@ -728,19 +389,24 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), sortedTroves: ISortedTroves(addresses.sortedTroves), interestRouter: contracts.interestRouter, - hintHelpers: _hintHelpers, - multiTroveGetter: _multiTroveGetter, - collateralRegistry: _collateralRegistry, - boldToken: _boldToken, - WETH: WETH + hintHelpers: r.hintHelpers, + multiTroveGetter: r.multiTroveGetter, + collateralRegistry: r.collateralRegistry, + boldToken: IBoldToken(address(r.stableToken)), + gasToken: IERC20Metadata(CONFIG.USDm_ALFAJORES_ADDRESS), + liquidityStrategy: CONFIG.liquidityStrategy }); contracts.addressesRegistry.setAddresses(addressVars); + } - contracts.borrowerOperations = new BorrowerOperations{salt: SALT}(contracts.addressesRegistry); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); + function _deployProtocolContracts(LiquityContracts memory contracts, LiquityContractAddresses memory addresses) + internal + { + contracts.borrowerOperations = + new BorrowerOperations{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); + contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, contracts.systemParams); contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); @@ -749,344 +415,27 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); assert(address(contracts.troveManager) == addresses.troveManager); assert(address(contracts.troveNFT) == addresses.troveNFT); - assert(address(contracts.stabilityPool) == addresses.stabilityPool); assert(address(contracts.activePool) == addresses.activePool); assert(address(contracts.defaultPool) == addresses.defaultPool); assert(address(contracts.gasPool) == addresses.gasPool); assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - - // Connect contracts - _boldToken.setBranchAddresses( - address(contracts.troveManager), - address(contracts.stabilityPool), - address(contracts.borrowerOperations), - address(contracts.activePool) - ); - - // deploy zappers - (contracts.gasCompZapper, contracts.wethZapper, contracts.leverageZapper) = - _deployZappers(contracts.addressesRegistry, contracts.collToken, _boldToken, _usdcCurvePool); } - function _deployPriceFeed(address _collTokenAddress, address _borroweOperationsAddress) + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry) internal - returns (IPriceFeed) + view + returns (address) { - if (block.chainid == 1 && !useTestnetPriceFeeds) { - // mainnet - // ETH - if (_collTokenAddress == address(WETH)) { - return new WETHPriceFeed(ETH_ORACLE_ADDRESS, ETH_USD_STALENESS_THRESHOLD, _borroweOperationsAddress); - } else if (_collTokenAddress == WSTETH_ADDRESS) { - // wstETH - return new WSTETHPriceFeed( - ETH_ORACLE_ADDRESS, - STETH_ORACLE_ADDRESS, - WSTETH_ADDRESS, - ETH_USD_STALENESS_THRESHOLD, - STETH_USD_STALENESS_THRESHOLD, - _borroweOperationsAddress - ); - } - // RETH - assert(_collTokenAddress == RETH_ADDRESS); - return new RETHPriceFeed( - ETH_ORACLE_ADDRESS, - RETH_ORACLE_ADDRESS, - RETH_ADDRESS, - ETH_USD_STALENESS_THRESHOLD, - RETH_ETH_STALENESS_THRESHOLD, - _borroweOperationsAddress - ); - } - - // Sepolia - return new PriceFeedTestnet(); + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry))); } - function _deployZappers( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - ICurveStableswapNGPool _usdcCurvePool - ) internal returns (GasCompZapper gasCompZapper, WETHZapper wethZapper, ILeverageZapper leverageZapper) { - IFlashLoanProvider flashLoanProvider = new BalancerFlashLoan(); - - IExchange hybridExchange = new HybridCurveUniV3Exchange( - _collToken, - _boldToken, - USDC, - WETH, - _usdcCurvePool, - OTHER_TOKEN_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Router - ); - - bool lst = _collToken != WETH; - if (lst) { - gasCompZapper = new GasCompZapper(_addressesRegistry, flashLoanProvider, hybridExchange); - } else { - wethZapper = new WETHZapper(_addressesRegistry, flashLoanProvider, hybridExchange); - } - leverageZapper = _deployHybridLeverageZapper(_addressesRegistry, flashLoanProvider, hybridExchange, lst); - } - - function _deployHybridLeverageZapper( - IAddressesRegistry _addressesRegistry, - IFlashLoanProvider _flashLoanProvider, - IExchange _hybridExchange, - bool _lst - ) internal returns (ILeverageZapper) { - ILeverageZapper leverageZapperHybrid; - if (_lst) { - leverageZapperHybrid = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, _hybridExchange); - } else { - leverageZapperHybrid = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, _hybridExchange); - } - - return leverageZapperHybrid; - } - - function _deployCurvePool(IBoldToken _boldToken, IERC20Metadata _otherToken) + function _computeCreate2Address(bytes memory creationCode, address _addressesRegistry, address _systemParams) internal - returns (ICurveStableswapNGPool) + view + returns (address) { - if (block.chainid == 31337) { - // local - return ICurveStableswapNGPool(address(0)); - } - - // deploy Curve StableswapNG pool - address[] memory coins = new address[](2); - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[OTHER_TOKEN_INDEX] = address(_otherToken); - uint8[] memory assetTypes = new uint8[](2); // 0: standard - bytes4[] memory methodIds = new bytes4[](2); - address[] memory oracles = new address[](2); - - ICurveStableswapNGPool curvePool = curveStableswapFactory.deploy_plain_pool({ - name: string.concat("BOLD/", _otherToken.symbol(), " Pool"), - symbol: string.concat("BOLD", _otherToken.symbol()), - coins: coins, - A: 100, - fee: 4000000, - offpeg_fee_multiplier: 20000000000, - ma_exp_time: 866, - implementation_id: 0, - asset_types: assetTypes, - method_ids: methodIds, - oracles: oracles - }); - - return curvePool; - } - - function _provideFlashloanLiquidity(ERC20Faucet _collToken, ERC20Faucet _monkeyBalls) internal { - uint256[] memory amountsIn = new uint256[](2); - amountsIn[0] = 1_000_000 ether; - amountsIn[1] = 1_000_000 ether; - - _collToken.mint(deployer, amountsIn[0]); - _monkeyBalls.mint(deployer, amountsIn[1]); - - IERC20[] memory tokens = new IERC20[](2); - (tokens[0], tokens[1]) = - address(_collToken) < address(_monkeyBalls) ? (_collToken, _monkeyBalls) : (_monkeyBalls, _collToken); - - uint256[] memory normalizedWeights = new uint256[](2); - normalizedWeights[0] = 0.5 ether; - normalizedWeights[1] = 0.5 ether; - - IWeightedPool pool = balancerFactorySepolia.create({ - name: string.concat(_collToken.name(), "-", _monkeyBalls.name()), - symbol: string.concat("bpt", _collToken.symbol(), _monkeyBalls.symbol()), - tokens: tokens, - normalizedWeights: normalizedWeights, - rateProviders: new IRateProvider[](2), // all zeroes - swapFeePercentage: 0.000001 ether, // 0.0001%, which is the minimum allowed - owner: deployer, - salt: bytes32("NaCl") - }); - - _collToken.approve(address(balancerVault), amountsIn[0]); - _monkeyBalls.approve(address(balancerVault), amountsIn[1]); - - balancerVault.joinPool( - pool.getPoolId(), - deployer, - deployer, - IVault.JoinPoolRequest({ - assets: tokens, - maxAmountsIn: amountsIn, - userData: abi.encode(IWeightedPool.JoinKind.INIT, amountsIn), - fromInternalBalance: false - }) - ); - } - - function _mintBold(uint256 _boldAmount, uint256 _price, LiquityContracts memory _contracts) internal { - uint256 collAmount = _boldAmount * 2 ether / _price; // CR of ~200% - - ERC20Faucet(address(_contracts.collToken)).mint(deployer, collAmount); - WETHTester(payable(address(WETH))).mint(deployer, ETH_GAS_COMPENSATION); - - if (_contracts.collToken == WETH) { - WETH.approve(address(_contracts.borrowerOperations), collAmount + ETH_GAS_COMPENSATION); - } else { - _contracts.collToken.approve(address(_contracts.borrowerOperations), collAmount); - WETH.approve(address(_contracts.borrowerOperations), ETH_GAS_COMPENSATION); - } - - _contracts.borrowerOperations.openTrove({ - _owner: deployer, - _ownerIndex: lastTroveIndex++, - _ETHAmount: collAmount, - _boldAmount: _boldAmount, - _upperHint: 0, - _lowerHint: 0, - _annualInterestRate: 0.05 ether, - _maxUpfrontFee: type(uint256).max, - _addManager: address(0), - _removeManager: address(0), - _receiver: address(0) - }); - } - - struct ProvideUniV3LiquidityVars { - uint256 token2Amount; - address[2] tokens; - uint256[2] amounts; - uint256 price; - int24 tickLower; - int24 tickUpper; - } - - // _price should be _token1 / _token2 - function _provideUniV3Liquidity( - ERC20Faucet _token1, - ERC20Faucet _token2, - uint256 _token1Amount, - uint256 _price, - uint24 _fee - ) internal { - ProvideUniV3LiquidityVars memory vars; - // tokens and amounts - vars.token2Amount = _token1Amount * DECIMAL_PRECISION / _price; - - if (address(_token1) < address(_token2)) { - vars.tokens[0] = address(_token1); - vars.tokens[1] = address(_token2); - vars.amounts[0] = _token1Amount; - vars.amounts[1] = vars.token2Amount; - // inverse price if token1 goes first - vars.price = DECIMAL_PRECISION * DECIMAL_PRECISION / _price; - } else { - vars.tokens[0] = address(_token2); - vars.tokens[1] = address(_token1); - vars.amounts[0] = vars.token2Amount; - vars.amounts[1] = _token1Amount; - vars.price = _price; - } - - //console2.log(priceToSqrtPriceX96(vars.price), "_priceToSqrtPrice(price)"); - uniV3PositionManagerSepolia.createAndInitializePoolIfNecessary( - vars.tokens[0], vars.tokens[1], _fee, priceToSqrtPriceX96(vars.price) - ); - - // mint and approve - _token1.mint(deployer, _token1Amount); - _token2.mint(deployer, vars.token2Amount); - _token1.approve(address(uniV3PositionManagerSepolia), _token1Amount); - _token2.approve(address(uniV3PositionManagerSepolia), vars.token2Amount); - - // mint new position - address uniV3PoolAddress = uniswapV3FactorySepolia.getPool(vars.tokens[0], vars.tokens[1], _fee); - int24 TICK_SPACING = IUniswapV3Pool(uniV3PoolAddress).tickSpacing(); - ( /* uint256 finalSqrtPriceX96 */ , int24 tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - //console2.log(finalSqrtPriceX96, "finalSqrtPriceX96"); - vars.tickLower = (tick - 60) / TICK_SPACING * TICK_SPACING; - vars.tickUpper = (tick + 60) / TICK_SPACING * TICK_SPACING; - - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - token0: vars.tokens[0], - token1: vars.tokens[1], - fee: _fee, - tickLower: vars.tickLower, - tickUpper: vars.tickUpper, - amount0Desired: vars.amounts[0], - amount1Desired: vars.amounts[1], - amount0Min: 0, - amount1Min: 0, - recipient: deployer, - deadline: block.timestamp + 600 minutes - }); - - uniV3PositionManagerSepolia.mint(params); - //(finalSqrtPriceX96, tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - //console2.log(finalSqrtPriceX96, "finalSqrtPriceX96"); - - /* - console2.log("--"); - console2.log(_token1.name()); - console2.log(address(_token1), "address(_token1)"); - console2.log(_token1Amount, "_token1Amount"); - console2.log(_token1.balanceOf(uniV3PoolAddress), "token1.balanceOf(pool)"); - console2.log(_token2.name()); - console2.log(address(_token2), "address(_token2)"); - console2.log(vars.token2Amount, "token2Amount"); - console2.log(_token2.balanceOf(uniV3PoolAddress), "token2.balanceOf(pool)"); - */ - } - - function _priceToSqrtPrice(uint256 _price) public pure returns (uint160) { - return uint160(Math.sqrt((_price << 192) / DECIMAL_PRECISION)); - } - - function _provideCurveLiquidity(IBoldToken _boldToken, LiquityContracts memory _contracts) internal { - ICurveStableswapNGPool usdcCurvePool = - HybridCurveUniV3Exchange(address(_contracts.leverageZapper.exchange())).curvePool(); - // Add liquidity to USDC-BOLD - //uint256 usdcAmount = 1e15; // 1B with 6 decimals - //boldAmount = usdcAmount * 1e12; // from 6 to 18 decimals - uint256 usdcAmount = 1e27; - uint256 boldAmount = usdcAmount; - - // mint - ERC20Faucet(address(USDC)).mint(deployer, usdcAmount); - (uint256 price,) = _contracts.priceFeed.fetchPrice(); - _mintBold(boldAmount, price, _contracts); - // approve - USDC.approve(address(usdcCurvePool), usdcAmount); - _boldToken.approve(address(usdcCurvePool), boldAmount); - - uint256[] memory amountsDynamic = new uint256[](2); - amountsDynamic[0] = boldAmount; - amountsDynamic[1] = usdcAmount; - // add liquidity - usdcCurvePool.add_liquidity(amountsDynamic, 0); - } - - function formatAmount(uint256 amount, uint256 decimals, uint256 digits) internal pure returns (string memory) { - if (digits > decimals) { - digits = decimals; - } - - uint256 scaled = amount / (10 ** (decimals - digits)); - string memory whole = Strings.toString(scaled / (10 ** digits)); - - if (digits == 0) { - return whole; - } - - string memory fractional = Strings.toString(scaled % (10 ** digits)); - for (uint256 i = bytes(fractional).length; i < digits; i++) { - fractional = string.concat("0", fractional); - } - return string.concat(whole, ".", fractional); + return vm.computeCreate2Address(SALT, keccak256(getBytecode(creationCode, _addressesRegistry, _systemParams))); } function _getBranchContractsJson(LiquityContracts memory c) internal view returns (string memory) { @@ -1102,7 +451,8 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, string.concat('"borrowerOperations":"', address(c.borrowerOperations).toHexString(), '",'), string.concat('"collSurplusPool":"', address(c.collSurplusPool).toHexString(), '",'), string.concat('"defaultPool":"', address(c.defaultPool).toHexString(), '",'), - string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",') + string.concat('"sortedTroves":"', address(c.sortedTroves).toHexString(), '",'), + string.concat('"systemParams":"', address(c.systemParams).toHexString(), '",') ), string.concat( string.concat('"stabilityPool":"', address(c.stabilityPool).toHexString(), '",'), @@ -1111,59 +461,56 @@ contract DeployLiquity2Script is DeployGovernance, UniPriceConverter, StdCheats, string.concat('"metadataNFT":"', address(c.metadataNFT).toHexString(), '",'), string.concat('"priceFeed":"', address(c.priceFeed).toHexString(), '",'), string.concat('"gasPool":"', address(c.gasPool).toHexString(), '",'), - string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",'), - string.concat('"wethZapper":"', address(c.wethZapper).toHexString(), '",') - ), - string.concat( - string.concat('"gasCompZapper":"', address(c.gasCompZapper).toHexString(), '",'), - string.concat('"leverageZapper":"', address(c.leverageZapper).toHexString(), '"') // no comma + string.concat('"interestRouter":"', address(c.interestRouter).toHexString(), '",') ) ), "}" ); } - function _getDeploymentConstants() internal pure returns (string memory) { + function _getDeploymentConstants(ISystemParams params) internal view returns (string memory) { return string.concat( "{", string.concat( - string.concat('"ETH_GAS_COMPENSATION":"', ETH_GAS_COMPENSATION.toString(), '",'), + string.concat('"ETH_GAS_COMPENSATION":"', params.ETH_GAS_COMPENSATION().toString(), '",'), string.concat('"INTEREST_RATE_ADJ_COOLDOWN":"', INTEREST_RATE_ADJ_COOLDOWN.toString(), '",'), string.concat('"MAX_ANNUAL_INTEREST_RATE":"', MAX_ANNUAL_INTEREST_RATE.toString(), '",'), - string.concat('"MIN_ANNUAL_INTEREST_RATE":"', MIN_ANNUAL_INTEREST_RATE.toString(), '",'), - string.concat('"MIN_DEBT":"', MIN_DEBT.toString(), '",'), - string.concat('"SP_YIELD_SPLIT":"', SP_YIELD_SPLIT.toString(), '",'), + string.concat('"MIN_ANNUAL_INTEREST_RATE":"', params.MIN_ANNUAL_INTEREST_RATE().toString(), '",'), + string.concat('"MIN_DEBT":"', params.MIN_DEBT().toString(), '",'), + string.concat('"SP_YIELD_SPLIT":"', params.SP_YIELD_SPLIT().toString(), '",'), string.concat('"UPFRONT_INTEREST_PERIOD":"', UPFRONT_INTEREST_PERIOD.toString(), '"') // no comma ), "}" ); } - function _getManifestJson(DeploymentResult memory deployed, string memory _governanceManifest) - internal - view - returns (string memory) - { - string[] memory branches = new string[](deployed.contractsArray.length); + function _getManifestJson(DeploymentResult memory deployed) internal view returns (string memory) { + string[] memory branches = new string[](1); - // Poor man's .map() - for (uint256 i = 0; i < branches.length; ++i) { - branches[i] = _getBranchContractsJson(deployed.contractsArray[i]); - } + branches[0] = _getBranchContractsJson(deployed.contracts); - return string.concat( + string memory part1 = string.concat( "{", - string.concat( - string.concat('"constants":', _getDeploymentConstants(), ","), - string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), - string.concat('"boldToken":"', address(deployed.boldToken).toHexString(), '",'), - string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",'), - string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",'), - string.concat('"exchangeHelpers":"', address(deployed.exchangeHelpers).toHexString(), '",'), - string.concat('"branches":[', branches.join(","), "],"), - string.concat('"governance":', _governanceManifest, "") // no comma - ), + string.concat('"constants":', _getDeploymentConstants(deployed.contracts.systemParams), ","), + string.concat('"collateralRegistry":"', address(deployed.collateralRegistry).toHexString(), '",'), + string.concat('"boldToken":"', address(deployed.stableToken).toHexString(), '",'), + string.concat('"hintHelpers":"', address(deployed.hintHelpers).toHexString(), '",') + ); + + string memory part2 = string.concat( + string.concat('"stableTokenV3Impl":"', address(deployed.stableTokenV3Impl).toHexString(), '",'), + string.concat('"stabilityPoolImpl":"', address(deployed.stabilityPoolImpl).toHexString(), '",'), + string.concat('"systemParamsImpl":"', address(deployed.systemParamsImpl).toHexString(), '",'), + string.concat('"systemParams":"', address(deployed.systemParams).toHexString(), '",'), + string.concat('"multiTroveGetter":"', address(deployed.multiTroveGetter).toHexString(), '",') + ); + + string memory part3 = string.concat( + string.concat('"fpmm":"', address(deployed.fpmm).toHexString(), '",'), + string.concat('"branches":[', branches.join(","), "]"), "}" ); + + return string.concat(part1, part2, part3); } } diff --git a/contracts/script/DeployOnlyExchangeHelpers.s.sol b/contracts/script/DeployOnlyExchangeHelpers.s.sol deleted file mode 100644 index 06385edd1..000000000 --- a/contracts/script/DeployOnlyExchangeHelpers.s.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -import {Script} from "forge-std/Script.sol"; - -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; - -import "forge-std/console2.sol"; - -contract DeployOnlyExchangeHelpers is Script { - IERC20 constant USDC = IERC20(0xc4f4dE29be4d05EA0644dfebb44a87a48E3BfcCE); - IWETH constant WETH = IWETH(0xbCDdC15adbe087A75526C0b7273Fcdd27bE9dD18); - ICurveStableswapNGPool constant usdcCurvePool = ICurveStableswapNGPool(0xdCD2D012C1A4fc509763657ED24b83c8Fe6cf756); - - uint128 constant BOLD_TOKEN_INDEX = 0; - uint128 constant USDC_INDEX = 1; - - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - IQuoterV2 constant uniV3QuoterSepolia = IQuoterV2(0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3); - - address deployer; - - function run() external { - if (vm.envBytes("DEPLOYER").length == 20) { - // address - deployer = vm.envAddress("DEPLOYER"); - vm.startBroadcast(deployer); - } else { - // private key - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - } - - console2.log(deployer, "deployer"); - console2.log(deployer.balance, "deployer balance"); - - IExchangeHelpers exchangeHelpers = new HybridCurveUniV3ExchangeHelpers( - USDC, - WETH, - usdcCurvePool, - USDC_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3QuoterSepolia - ); - console2.log(address(exchangeHelpers), "exchangeHelpers"); - } -} diff --git a/contracts/script/DeploySomeCurvePools.s.sol b/contracts/script/DeploySomeCurvePools.s.sol deleted file mode 100644 index 81ad6f857..000000000 --- a/contracts/script/DeploySomeCurvePools.s.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {ICurveStableSwapFactoryNG} from "test/Interfaces/Curve/ICurveStableSwapFactoryNG.sol"; -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; - -contract DeploySomeCurvePools is Script { - using Strings for *; - - ICurveStableSwapFactoryNG constant factory = ICurveStableSwapFactoryNG(0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf); - - function run() external { - vm.startBroadcast(); - - for (uint256 i = 1; i <= 3; ++i) { - address[] memory coins = new address[](2); - uint8[] memory assetTypes = new uint8[](2); - bytes4[] memory methodIds = new bytes4[](2); - address[] memory oracles = new address[](2); - - coins[0] = address( - new ERC20Faucet({ - _name: string.concat("Coin #", i.toString(), ".1"), - _symbol: string.concat("COIN", i.toString(), "1"), - _tapAmount: 0, - _tapPeriod: 0 - }) - ); - - coins[1] = address( - new ERC20Faucet({ - _name: string.concat("Coin #", i.toString(), ".2"), - _symbol: string.concat("COIN", i.toString(), "2"), - _tapAmount: 0, - _tapPeriod: 0 - }) - ); - - factory.deploy_plain_pool({ - _name: string.concat("Fancy Pool #", i.toString()), - _symbol: string.concat(string.concat("POOL", i.toString())), - _coins: coins, - _A: 100, - _fee: 4000000, - _offpeg_fee_multiplier: 20000000000, - _ma_exp_time: 866, - _implementation_idx: 0, - _asset_types: assetTypes, - _method_ids: methodIds, - _oracles: oracles - }); - } - } -} diff --git a/contracts/script/GenerateStakingRewards.s.sol b/contracts/script/GenerateStakingRewards.s.sol deleted file mode 100644 index 0c9ac184a..000000000 --- a/contracts/script/GenerateStakingRewards.s.sol +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; -import {IBorrowerOperationsV1} from "test/Interfaces/LiquityV1/IBorrowerOperationsV1.sol"; -import {IPriceFeedV1} from "test/Interfaces/LiquityV1/IPriceFeedV1.sol"; -import {ISortedTrovesV1} from "test/Interfaces/LiquityV1/ISortedTrovesV1.sol"; -import {ITroveManagerV1} from "test/Interfaces/LiquityV1/ITroveManagerV1.sol"; - -IBorrowerOperationsV1 constant borrowerOperations = IBorrowerOperationsV1(0x24179CD81c9e782A4096035f7eC97fB8B783e007); -IPriceFeedV1 constant priceFeed = IPriceFeedV1(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); -ISortedTrovesV1 constant sortedTroves = ISortedTrovesV1(0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6); -ITroveManagerV1 constant troveManager = ITroveManagerV1(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); - -contract Runner { - function _revert(bytes memory revertData) internal pure { - assembly { - revert(add(32, revertData), mload(revertData)) - } - } - - function run() external payable { - uint256 borrowedLusd = 1_000_000 ether; - uint256 redeemedLusd = 1_000 ether; - - uint256 price = priceFeed.fetchPrice(); - address lastTrove = sortedTroves.getLast(); - - if (troveManager.getCurrentICR(lastTrove, price) < 1.1 ether) { - troveManager.liquidateTroves(50); - lastTrove = sortedTroves.getLast(); - require(troveManager.getCurrentICR(lastTrove, price) >= 1.1 ether, "too much to liquidate, try again"); - } - - uint256 borrowingRate = troveManager.getBorrowingRateWithDecay(); - uint256 borrowingFee = borrowedLusd * borrowingRate / 1 ether; - uint256 debt = borrowedLusd + borrowingFee + 200 ether; - - uint256 coll = Math.ceilDiv(debt * 1.1 ether, price); - require(address(this).balance >= coll, "balance < coll"); - - borrowerOperations.openTrove{value: coll}({ - _LUSDAmount: borrowedLusd, - _maxFeePercentage: borrowingRate, - _upperHint: lastTrove, - _lowerHint: address(0) - }); - - require(sortedTroves.getLast() == address(this), "last Trove != new Trove"); - - uint256 redeemedColl = redeemedLusd * 1 ether / price; - uint256 balanceBefore = address(this).balance; - - troveManager.redeemCollateral({ - _LUSDamount: redeemedLusd, - _maxFeePercentage: 1 ether, - _maxIterations: 1, - _firstRedemptionHint: address(this), - _upperPartialRedemptionHint: lastTrove, - _lowerPartialRedemptionHint: lastTrove, - _partialRedemptionHintNICR: (coll - redeemedColl) * 100 ether / (debt - redeemedLusd) - }); - - uint256 redemptionFee = redeemedColl * troveManager.getBorrowingRateWithDecay() / 1 ether; - require(address(this).balance - balanceBefore == redeemedColl - redemptionFee, "coll received != expected"); - - (bool success, bytes memory returnData) = msg.sender.call{value: address(this).balance}(""); - if (!success) _revert(returnData); - } - - receive() external payable {} -} - -contract GenerateStakingRewards is Script { - function run() external { - vm.startBroadcast(); - - Runner runner = new Runner(); - runner.run{value: msg.sender.balance * 9 / 10}(); - } -} diff --git a/contracts/script/Interfaces/Balancer/IVault.sol b/contracts/script/Interfaces/Balancer/IVault.sol deleted file mode 100644 index d30f9e62c..000000000 --- a/contracts/script/Interfaces/Balancer/IVault.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IVault { - struct JoinPoolRequest { - IERC20[] assets; - uint256[] maxAmountsIn; - bytes userData; - bool fromInternalBalance; - } - - function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request) external; -} diff --git a/contracts/script/Interfaces/Balancer/IWeightedPool.sol b/contracts/script/Interfaces/Balancer/IWeightedPool.sol deleted file mode 100644 index 8f4ca38ca..000000000 --- a/contracts/script/Interfaces/Balancer/IWeightedPool.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IRateProvider {} - -interface IWeightedPoolFactory { - function create( - string memory name, - string memory symbol, - IERC20[] memory tokens, - uint256[] memory normalizedWeights, - IRateProvider[] memory rateProviders, - uint256 swapFeePercentage, - address owner, - bytes32 salt - ) external returns (IWeightedPool); -} - -interface IWeightedPool { - enum JoinKind { - INIT - } - - function getPoolId() external view returns (bytes32); -} diff --git a/contracts/script/LiquidateTrove.s.sol b/contracts/script/LiquidateTrove.s.sol deleted file mode 100644 index 6d0411ae4..000000000 --- a/contracts/script/LiquidateTrove.s.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {IAddressesRegistry} from "src/Interfaces/IAddressesRegistry.sol"; -import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; -import {LatestTroveData} from "src/Types/LatestTroveData.sol"; -import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; -import {IPriceFeedTestnet} from "test/TestContracts/Interfaces/IPriceFeedTestnet.sol"; - -contract LiquidateTrove is Script { - using Strings for uint256; - - function run() external { - vm.startBroadcast(); - - IAddressesRegistry addressesRegistry; - try vm.envAddress("ADDRESSES_REGISTRY") returns (address value) { - addressesRegistry = IAddressesRegistry(value); - } catch { - uint256 i = vm.envUint("BRANCH"); - string memory manifestJson = vm.readFile("deployment-manifest.json"); - addressesRegistry = IAddressesRegistry( - vm.parseJsonAddress(manifestJson, string.concat(".branches[", i.toString(), "].addressesRegistry")) - ); - } - vm.label(address(addressesRegistry), "AddressesRegistry"); - - ITroveManager troveManager = addressesRegistry.troveManager(); - vm.label(address(troveManager), "TroveManager"); - IPriceFeedTestnet priceFeed = IPriceFeedTestnet(address(addressesRegistry.priceFeed())); - vm.label(address(priceFeed), "PriceFeedTestnet"); - - uint256 troveId = vm.envUint("TROVE_ID"); - LatestTroveData memory trove = troveManager.getLatestTroveData(troveId); - - uint256 originalPrice = priceFeed.getPrice(); - uint256 liquidationPrice = (addressesRegistry.MCR() - 0.01 ether) * trove.entireDebt / trove.entireColl; - priceFeed.setPrice(liquidationPrice); - - uint256[] memory troveIds = new uint256[](1); - troveIds[0] = troveId; - troveManager.batchLiquidateTroves(troveIds); - - priceFeed.setPrice(originalPrice); - } -} diff --git a/contracts/script/OpenTroves.s.sol b/contracts/script/OpenTroves.s.sol deleted file mode 100644 index 6d92754fb..000000000 --- a/contracts/script/OpenTroves.s.sol +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {Clones} from "openzeppelin-contracts/contracts/proxy/Clones.sol"; -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {IBorrowerOperations} from "src/Interfaces/IBorrowerOperations.sol"; -import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; -import {IHintHelpers} from "src/Interfaces/IHintHelpers.sol"; -import {ISortedTroves} from "src/Interfaces/ISortedTroves.sol"; -import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; -import {ITroveNFT} from "src/Interfaces/ITroveNFT.sol"; - -import { - ETH_GAS_COMPENSATION, - MAX_ANNUAL_INTEREST_RATE, - MIN_ANNUAL_INTEREST_RATE, - MIN_INTEREST_RATE_CHANGE_PERIOD -} from "src/Dependencies/Constants.sol"; - -function sqrt(uint256 y) pure returns (uint256 z) { - if (y > 3) { - z = y; - uint256 x = y / 2 + 1; - while (x < z) { - z = x; - x = (y / x + x) / 2; - } - } else if (y != 0) { - z = 1; - } -} - -contract Proxy { - function tap(ERC20Faucet faucet) external { - faucet.tap(); - faucet.transfer(msg.sender, faucet.balanceOf(address(this))); - } - - function sweepTrove(ITroveNFT nft, uint256 troveId) external { - nft.transferFrom(address(this), msg.sender, troveId); - } -} - -contract OpenTroves is Script { - struct BranchContracts { - ERC20Faucet collateral; - ITroveManager troveManager; - ISortedTroves sortedTroves; - IBorrowerOperations borrowerOperations; - ITroveNFT nft; - } - - function _findHints(IHintHelpers hintHelpers, BranchContracts memory c, uint256 branch, uint256 interestRate) - internal - view - returns (uint256 upperHint, uint256 lowerHint) - { - // Find approx hint (off-chain) - (uint256 approxHint,,) = hintHelpers.getApproxHint({ - _collIndex: branch, - _interestRate: interestRate, - _numTrials: sqrt(100 * c.troveManager.getTroveIdsCount()), - _inputRandomSeed: block.timestamp - }); - - // Find concrete insert position (off-chain) - (upperHint, lowerHint) = c.sortedTroves.findInsertPosition(interestRate, approxHint, approxHint); - } - - function run() external { - vm.startBroadcast(); - - string memory manifestJson; - try vm.readFile("deployment-manifest.json") returns (string memory content) { - manifestJson = content; - } catch {} - - ICollateralRegistry collateralRegistry; - try vm.envAddress("COLLATERAL_REGISTRY") returns (address value) { - collateralRegistry = ICollateralRegistry(value); - } catch { - collateralRegistry = ICollateralRegistry(vm.parseJsonAddress(manifestJson, ".collateralRegistry")); - } - vm.label(address(collateralRegistry), "CollateralRegistry"); - - IHintHelpers hintHelpers; - try vm.envAddress("HINT_HELPERS") returns (address value) { - hintHelpers = IHintHelpers(value); - } catch { - hintHelpers = IHintHelpers(vm.parseJsonAddress(manifestJson, ".hintHelpers")); - } - vm.label(address(hintHelpers), "HintHelpers"); - - address proxyImplementation = address(new Proxy()); - vm.label(proxyImplementation, "ProxyImplementation"); - - ERC20Faucet weth = ERC20Faucet(address(collateralRegistry.getToken(0))); // branch #0 is WETH - uint256 numBranches = collateralRegistry.totalCollaterals(); - - for (uint256 branch = 0; branch < numBranches; ++branch) { - BranchContracts memory c; - c.collateral = ERC20Faucet(address(collateralRegistry.getToken(branch))); - vm.label(address(c.collateral), "ERC20Faucet"); - c.troveManager = collateralRegistry.getTroveManager(branch); - vm.label(address(c.troveManager), "TroveManager"); - c.sortedTroves = c.troveManager.sortedTroves(); - vm.label(address(c.sortedTroves), "SortedTroves"); - c.borrowerOperations = c.troveManager.borrowerOperations(); - vm.label(address(c.borrowerOperations), "BorrowerOperations"); - c.nft = c.troveManager.troveNFT(); - vm.label(address(c.nft), "TroveNFT"); - - if (c.borrowerOperations.getInterestBatchManager(msg.sender).maxInterestRate == 0) { - // Register ourselves as batch manager, if we haven't - c.borrowerOperations.registerBatchManager({ - minInterestRate: uint128(MIN_ANNUAL_INTEREST_RATE), - maxInterestRate: uint128(MAX_ANNUAL_INTEREST_RATE), - currentInterestRate: 0.025 ether, - fee: 0.001 ether, - minInterestRateChangePeriod: MIN_INTEREST_RATE_CHANGE_PERIOD - }); - } - - for (uint256 i = 1; i <= 4; ++i) { - Proxy proxy = Proxy(Clones.clone(proxyImplementation)); - vm.label(address(proxy), "Proxy"); - - proxy.tap(c.collateral); - uint256 ethAmount = c.collateral.tapAmount() / 2; - - if (branch == 0) { - // collateral == WETH - c.collateral.approve(address(c.borrowerOperations), ethAmount + ETH_GAS_COMPENSATION); - } else { - proxy.tap(weth); - c.collateral.approve(address(c.borrowerOperations), ethAmount); - weth.approve(address(c.borrowerOperations), ETH_GAS_COMPENSATION); - } - - uint256 interestRate = i * 0.01 ether; - (uint256 upperHint, uint256 lowerHint) = _findHints(hintHelpers, c, branch, interestRate); - - uint256 troveId = c.borrowerOperations.openTrove({ - _owner: address(proxy), - _ownerIndex: 0, - _ETHAmount: ethAmount, - _boldAmount: 2_000 ether, - _upperHint: upperHint, - _lowerHint: lowerHint, - _annualInterestRate: interestRate, - _maxUpfrontFee: type(uint256).max, // we don't care about fee slippage - _addManager: address(0), - _removeManager: address(0), - _receiver: address(0) - }); - - proxy.sweepTrove(c.nft, troveId); - c.collateral.transfer(address(0xdead), c.collateral.balanceOf(msg.sender)); - if (branch != 0) weth.transfer(address(0xdead), weth.balanceOf(msg.sender)); - - if (i % 2 == 0) { - interestRate = c.troveManager.getLatestBatchData(msg.sender).annualInterestRate; - (upperHint, lowerHint) = _findHints(hintHelpers, c, branch, interestRate); - - // Have every 2nd Trove delegate to us - c.borrowerOperations.setInterestBatchManager({ - _troveId: troveId, - _newBatchManager: msg.sender, - _upperHint: upperHint, - _lowerHint: lowerHint, - _maxUpfrontFee: type(uint256).max // we don't care about fee slippage - }); - } - } - } - } -} diff --git a/contracts/script/ProvideCurveLiquidity.s.sol b/contracts/script/ProvideCurveLiquidity.s.sol deleted file mode 100644 index 554a41ba7..000000000 --- a/contracts/script/ProvideCurveLiquidity.s.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {UseDeployment} from "test/Utils/UseDeployment.sol"; - -contract ProvideCurveLiquidity is Script, UseDeployment { - function run() external { - vm.startBroadcast(); - _loadDeploymentFromManifest("deployment-manifest.json"); - - uint256 boldAmount = 200_000 ether; - uint256 usdcAmount = boldAmount * 10 ** usdc.decimals() / 10 ** boldToken.decimals(); - - uint256[] memory amounts = new uint256[](2); - (amounts[0], amounts[1]) = curveUsdcBold.coins(0) == BOLD ? (boldAmount, usdcAmount) : (usdcAmount, boldAmount); - - boldToken.approve(address(curveUsdcBold), boldAmount); - usdc.approve(address(curveUsdcBold), usdcAmount); - curveUsdcBold.add_liquidity(amounts, 0); - } -} diff --git a/contracts/script/ProvideUniV3Liquidity.s.sol b/contracts/script/ProvideUniV3Liquidity.s.sol deleted file mode 100644 index 73f5d79a9..000000000 --- a/contracts/script/ProvideUniV3Liquidity.s.sol +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {ERC20Faucet} from "test/TestContracts/ERC20Faucet.sol"; -import {WETHTester} from "test/TestContracts/WETHTester.sol"; - -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; - -import "forge-std/console2.sol"; - -contract ProvideUniV3Liquidity is Script { - uint256 constant DECIMAL_PRECISION = 1e18; - - uint24 constant UNIV3_FEE = 0.3e4; - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - ISwapRouter constant uniV3RouterSepolia = ISwapRouter(0x65669fE35312947050C450Bd5d36e6361F85eC12); - IQuoterV2 constant uniV3QuoterSepolia = IQuoterV2(0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3); - IUniswapV3Factory constant uniswapV3FactorySepolia = IUniswapV3Factory(0x0227628f3F023bb0B980b67D528571c95c6DaC1c); - INonfungiblePositionManager constant uniV3PositionManagerSepolia = - INonfungiblePositionManager(0x1238536071E1c677A632429e3655c799b22cDA52); - - WETHTester constant WETH = WETHTester(payable(0x3e8Bd35e898505EE0dD29277ee42eD92021C82aF)); - ERC20Faucet constant usdc = ERC20Faucet(0xF00ad39d0aC1A422DAB5A2EceBAa5268ea909aD4); - ERC20Faucet constant wstETH = ERC20Faucet(0xC5958986793086593871f207975053cf66d0B764); - ERC20Faucet constant rETH = ERC20Faucet(0x078c20A159eA4EdF8d029Fb21E6bd120455B4acc); - - address deployer; - - function run() external { - if (vm.envBytes("DEPLOYER").length == 20) { - // address - deployer = vm.envAddress("DEPLOYER"); - vm.startBroadcast(deployer); - } else { - // private key - uint256 privateKey = vm.envUint("DEPLOYER"); - deployer = vm.addr(privateKey); - vm.startBroadcast(privateKey); - } - - console2.log(deployer, "deployer"); - console2.log(deployer.balance, "deployer balance"); - - uint256 price = 2_000 ether; - - // WETH - console2.log("WETH"); - uint256 token1Amount = 1_000_000 ether; - _provideUniV3Liquidity(usdc, WETH, token1Amount, price, UNIV3_FEE_USDC_WETH); - - token1Amount = 1_000 ether; - - // wstETH - console2.log("wstETH"); - _provideUniV3Liquidity(WETH, wstETH, token1Amount, 1 ether, UNIV3_FEE_WETH_COLL); - - // rETH - console2.log("rETH"); - _provideUniV3Liquidity(WETH, rETH, token1Amount, 1 ether, UNIV3_FEE_WETH_COLL); - } - - // _price should be _token1 / _token2 - function _provideUniV3Liquidity( - ERC20Faucet _token1, - ERC20Faucet _token2, - uint256 _token1Amount, - uint256 _price, - uint24 _fee - ) internal { - // tokens and amounts - uint256 token2Amount = _token1Amount * DECIMAL_PRECISION / _price; - address[2] memory tokens; - uint256[2] memory amounts; - - uint256 price; - if (address(_token1) < address(_token2)) { - tokens[0] = address(_token1); - tokens[1] = address(_token2); - amounts[0] = _token1Amount; - amounts[1] = token2Amount; - // inverse price if token1 goes first - price = DECIMAL_PRECISION * DECIMAL_PRECISION / _price; - } else { - tokens[0] = address(_token2); - tokens[1] = address(_token1); - amounts[0] = token2Amount; - amounts[1] = _token1Amount; - price = _price; - } - - uniV3PositionManagerSepolia.createAndInitializePoolIfNecessary( - tokens[0], - tokens[1], - _fee, - _priceToSqrtPrice(price) // sqrtPriceX96 - ); - - // mint and approve - _token1.mint(deployer, _token1Amount); - _token2.mint(deployer, token2Amount); - _token1.approve(address(uniV3PositionManagerSepolia), _token1Amount); - _token2.approve(address(uniV3PositionManagerSepolia), token2Amount); - - // mint new position - address uniV3PoolAddress = uniswapV3FactorySepolia.getPool(tokens[0], tokens[1], _fee); - int24 TICK_SPACING = IUniswapV3Pool(uniV3PoolAddress).tickSpacing(); - (, int24 tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - int24 tickLower = (tick - 6000) / TICK_SPACING * TICK_SPACING; - int24 tickUpper = (tick + 6000) / TICK_SPACING * TICK_SPACING; - - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - token0: tokens[0], - token1: tokens[1], - fee: _fee, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: amounts[0], - amount1Desired: amounts[1], - amount0Min: 0, - amount1Min: 0, - recipient: deployer, - deadline: block.timestamp + 600 minutes - }); - - uniV3PositionManagerSepolia.mint(params); - - console2.log("--"); - console2.log(_token1.name()); - console2.log(address(_token1), "address(_token1)"); - console2.log(_token1Amount, "_token1Amount"); - console2.log(_token1.balanceOf(uniV3PoolAddress), "token1.balanceOf(pool)"); - console2.log(_token2.name()); - console2.log(address(_token2), "address(_token2)"); - console2.log(token2Amount, "token2Amount"); - console2.log(_token2.balanceOf(uniV3PoolAddress), "token2.balanceOf(pool)"); - } - - function _priceToSqrtPrice(uint256 _price) public pure returns (uint160) { - return uint160(Math.sqrt((_price << 192) / DECIMAL_PRECISION)); - } -} diff --git a/contracts/script/RedeemCollateral.s.sol b/contracts/script/RedeemCollateral.s.sol deleted file mode 100644 index baecd7267..000000000 --- a/contracts/script/RedeemCollateral.s.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {console} from "forge-std/console.sol"; -import {Script} from "forge-std/Script.sol"; -import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; -import {StringFormatting} from "test/Utils/StringFormatting.sol"; -import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; -import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; -import {DECIMAL_PRECISION} from "src/Dependencies/Constants.sol"; - -contract RedeemCollateral is Script { - using Strings for *; - using StringFormatting for *; - - function run() external { - vm.startBroadcast(); - - string memory manifestJson; - try vm.readFile("deployment-manifest.json") returns (string memory content) { - manifestJson = content; - } catch {} - - ICollateralRegistry collateralRegistry; - try vm.envAddress("COLLATERAL_REGISTRY") returns (address value) { - collateralRegistry = ICollateralRegistry(value); - } catch { - collateralRegistry = ICollateralRegistry(vm.parseJsonAddress(manifestJson, ".collateralRegistry")); - } - vm.label(address(collateralRegistry), "CollateralRegistry"); - - IBoldToken boldToken = IBoldToken(collateralRegistry.boldToken()); - vm.label(address(boldToken), "BoldToken"); - - uint256 boldBefore = boldToken.balanceOf(msg.sender); - uint256[] memory collBefore = new uint256[](collateralRegistry.totalCollaterals()); - for (uint256 i = 0; i < collBefore.length; ++i) { - collBefore[i] = collateralRegistry.getToken(i).balanceOf(msg.sender); - } - - uint256 attemptedBoldAmount = vm.envUint("AMOUNT") * DECIMAL_PRECISION; - console.log("Attempting to redeem (BOLD):", attemptedBoldAmount.decimal()); - - uint256 maxFeePct = collateralRegistry.getRedemptionRateForRedeemedAmount(attemptedBoldAmount); - collateralRegistry.redeemCollateral(attemptedBoldAmount, 10, maxFeePct); - - uint256 actualBoldAmount = boldBefore - boldToken.balanceOf(msg.sender); - console.log("Actually redeemed (BOLD):", actualBoldAmount.decimal()); - - uint256[] memory collAmount = new uint256[](collBefore.length); - for (uint256 i = 0; i < collBefore.length; ++i) { - collAmount[i] = collateralRegistry.getToken(i).balanceOf(msg.sender) - collBefore[i]; - console.log("Received coll", string.concat("#", i.toString(), ":"), collAmount[i].decimal()); - } - } -} diff --git a/contracts/script/RedeployWETHZappers.s.sol b/contracts/script/RedeployWETHZappers.s.sol deleted file mode 100644 index 01ebaf966..000000000 --- a/contracts/script/RedeployWETHZappers.s.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Script} from "forge-std/Script.sol"; -import {console} from "forge-std/console.sol"; -import {BaseZapper} from "src/Zappers/BaseZapper.sol"; -import {WETHZapper} from "src/Zappers/WETHZapper.sol"; -import {LeverageWETHZapper} from "src/Zappers/LeverageWETHZapper.sol"; -import {UseDeployment} from "test/Utils/UseDeployment.sol"; -import {StringEquality} from "test/Utils/StringEquality.sol"; - -contract RedeployWETHZappers is Script, UseDeployment { - using StringEquality for string; - - function run() external { - vm.startBroadcast(); - _loadDeploymentFromManifest("addresses/1.json"); - - BranchContracts storage wethBranch = branches[0]; - require(wethBranch.collToken.symbol().eq("WETH"), "Wrong branch"); - - BaseZapper oldWETHZapper = BaseZapper(address(wethBranch.zapper)); - BaseZapper oldLeverageWETHZapper = BaseZapper(address(wethBranch.leverageZapper)); - - WETHZapper newWETHZapper = new WETHZapper({ - _addressesRegistry: wethBranch.addressesRegistry, - _flashLoanProvider: oldWETHZapper.flashLoanProvider(), - _exchange: oldWETHZapper.exchange() - }); - - LeverageWETHZapper newLeverageWETHZapper = new LeverageWETHZapper({ - _addressesRegistry: wethBranch.addressesRegistry, - _flashLoanProvider: oldLeverageWETHZapper.flashLoanProvider(), - _exchange: oldLeverageWETHZapper.exchange() - }); - - console.log("newWETHZapper: ", address(newWETHZapper)); - console.log("newLeverageWETHZapper:", address(newLeverageWETHZapper)); - } -} diff --git a/contracts/script/bold-vanity.ts b/contracts/script/bold-vanity.ts deleted file mode 100644 index 5983b3feb..000000000 --- a/contracts/script/bold-vanity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import assert from "assert"; - -import { - type ByteArray, - bytesToHex, - concatBytes, - getAddress, - hexToBytes, - keccak256, - padBytes, - stringToBytes, -} from "viem"; - -import BoldToken from "../out/BoldToken.sol/BoldToken.json"; - -const DEPLOYER = "0xbEC25C5590e89596BDE2DfCdc71579E66858772c"; -const SALT_PREFIX = "beBOLD"; -const CREATE2_DEPLOYER = "0x4e59b44847b379578588920cA78FbF26c0B4956C"; -const CREATE2_PREFIX = concatBytes([hexToBytes("0xFF"), hexToBytes(CREATE2_DEPLOYER)]); - -const computeCreate2Address = (salt: ByteArray, initCodeHash: ByteArray): ByteArray => - keccak256(concatBytes([CREATE2_PREFIX, salt, initCodeHash]), "bytes").slice(12); - -const startsWith = (str: string, prefix: T): str is `${T}${string}` => str.startsWith(prefix); -assert(startsWith(BoldToken.bytecode.object, "0x")); - -const boldInitCodeHash = keccak256( - concatBytes([ - hexToBytes(BoldToken.bytecode.object), - padBytes(hexToBytes(DEPLOYER)), - ]), - "bytes", -); - -for (let i = 0;; ++i) { - const saltStr = `${SALT_PREFIX}${i}`; - const salt = keccak256(stringToBytes(saltStr), "bytes"); - const boldAddress = computeCreate2Address(salt, boldInitCodeHash); - - if (boldAddress[0] === 0xb0 && boldAddress[1] === 0x1d /*&& boldAddress[18] === 0xb0 && boldAddress[19] === 0x1d*/) { - console.log("Salt found:", saltStr); - console.log("BOLD address:", getAddress(bytesToHex(boldAddress))); - break; - } -} diff --git a/contracts/script/coverage.sh b/contracts/script/coverage.sh deleted file mode 100755 index a465e49d1..000000000 --- a/contracts/script/coverage.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Foundry coverage -forge coverage --report lcov --report-file lcov_foundry.info - -# # Hardhat coverage -# NODE_OPTIONS="--max-old-space-size=16384" npx hardhat coverage - -# # Remove path from contract names in Hardhat -# sed -i "s/SF:.*src/SF:src/g" coverage/lcov.info - -# # Merge coverage reports -# lcov \ -# --rc lcov_branch_coverage=1 \ -# --add-tracefile lcov_foundry.info \ -# --add-tracefile coverage/lcov.info \ -# --output-file lcov_merged.info - -# Instead of merge -cp lcov_foundry.info lcov_merged.info - -lcov --remove lcov_merged.info -o lcov_merged.info \ - 'test/*' \ - 'script/*' \ - 'src/Dependencies/Ownable.sol' \ - 'src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol' \ - 'src/NFTMetadata/*' \ - 'src/MultiTroveGetter.sol' \ - 'src/HintHelpers.sol' - -genhtml lcov_merged.info --output-directory coverage diff --git a/contracts/script/governance.ts b/contracts/script/governance.ts deleted file mode 100644 index f08e6cf59..000000000 --- a/contracts/script/governance.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { foundry } from "viem/chains"; -import { z } from "zod"; -import { fs } from "zx"; - -import { createTestClient, getContract, maxUint256, publicActions, walletActions, webSocket, zeroAddress } from "viem"; - -import { - abiBoldToken, - abiBorrowerOperations, - abiBribeInitiative, - abiERC20Faucet, - abiGovernanceProxy, - abiPriceFeedTestnet, - abiWETHTester, - bytecodeBribeInitiative, - bytecodeGovernanceProxy, -} from "../abi"; - -const ANVIL_ACCOUNT_0 = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; - -const startsWith = (prefix: T) => (x: string): x is `${T}${string}` => x.startsWith(prefix); - -const Address = z.string().refine(startsWith("0x")); - -const DeploymentManifest = z.object({ - boldToken: Address, - - constants: z.object({ - ETH_GAS_COMPENSATION: z.string(), - MIN_ANNUAL_INTEREST_RATE: z.string(), - MIN_DEBT: z.string(), - }), - - branches: z.object({ - collToken: Address, - borrowerOperations: Address, - priceFeed: Address, - }).array(), - - governance: z.object({ - governance: Address, - LQTYToken: Address, - }), -}); - -const deploymentManifest = DeploymentManifest.parse(fs.readJSONSync("deployment-manifest.json")); - -const ETH_GAS_COMPENSATION = BigInt(deploymentManifest.constants.ETH_GAS_COMPENSATION); -const MIN_ANNUAL_INTEREST_RATE = BigInt(deploymentManifest.constants.MIN_ANNUAL_INTEREST_RATE); -const MIN_DEBT = BigInt(deploymentManifest.constants.MIN_DEBT); - -const client = createTestClient({ - mode: "anvil", - chain: foundry, - account: ANVIL_ACCOUNT_0, - transport: webSocket(), -}) - .extend(publicActions) - .extend(walletActions); - -const lqtyToken = getContract({ - address: deploymentManifest.governance.LQTYToken, - abi: abiERC20Faucet, - client, -}); - -const boldToken = getContract({ - address: deploymentManifest.boldToken, - abi: abiBoldToken, - client, -}); - -const collToken = getContract({ - address: deploymentManifest.branches[0].collToken, - abi: abiWETHTester, - client, -}); - -const borrowerOperations = getContract({ - address: deploymentManifest.branches[0].borrowerOperations, - abi: abiBorrowerOperations, - client, -}); - -const priceFeed = getContract({ - address: deploymentManifest.branches[0].priceFeed, - abi: abiPriceFeedTestnet, - client, -}); - -const mineOnce = () => { - const futureBlockTimestamp = new Promise((resolve) => { - const stopWatching = client.watchBlocks({ - onBlock: (block) => { - stopWatching(); - resolve(block.timestamp); - }, - }); - }); - - client.mine({ blocks: 1 }); - return futureBlockTimestamp; -}; - -const waitForSuccess = async (hash: `0x${string}`) => { - const receipt = await client.waitForTransactionReceipt({ hash }); - if (receipt.status === "reverted") throw Object.assign(new Error("Transaction reverted"), { receipt }); - return receipt; -}; - -const waitForContractAddress = async (hash: `0x${string}`) => { - const receipt = await waitForSuccess(hash); - if (receipt.contractAddress == null) throw Object.assign(new Error("No contract address"), { receipt }); - return receipt.contractAddress; -}; - -const mintBold = async (to: `0x${string}`, amount: bigint) => { - const price = await priceFeed.read.getPrice(); - const collAmount = amount * BigInt(2e18) / price; - - await collToken.write.mint([client.account.address, collAmount + ETH_GAS_COMPENSATION]).then(waitForSuccess); - await collToken.write.approve([borrowerOperations.address, collAmount + ETH_GAS_COMPENSATION]).then(waitForSuccess); - - await borrowerOperations.write.openTrove([ - to, // _owner - 0n, // _ownerIndex - collAmount, // _ETHAmount - amount, // _boldAmount - 0n, // _upperHint - 0n, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // _annualInterestRate - maxUint256, // _maxUpfrontFee - zeroAddress, // _addManager - zeroAddress, // _removeManager - zeroAddress, // _receiver - ]).then(waitForSuccess); - - await boldToken.write.transfer([to, amount]).then(waitForSuccess); -}; - -const main = async () => { - const governanceProxy = getContract({ - client, - abi: abiGovernanceProxy, - address: await client.deployContract({ - abi: abiGovernanceProxy, - bytecode: bytecodeGovernanceProxy, - args: [deploymentManifest.governance.governance], - }).then(waitForContractAddress), - }); - - const EPOCH_DURATION = await governanceProxy.read.EPOCH_DURATION(); - let epochStart = await governanceProxy.read.epochStart(); - - console.log("current epoch:", await governanceProxy.read.epoch()); - - const lqtyAmount = BigInt(1_000e18); - await lqtyToken.write.mint([governanceProxy.address, lqtyAmount]).then(waitForSuccess); - await governanceProxy.write.depositLQTY([lqtyAmount]).then(waitForSuccess); - await mintBold(governanceProxy.address, MIN_DEBT); - - // Warp to the beginning of the next epoch - epochStart += EPOCH_DURATION; - client.setNextBlockTimestamp({ timestamp: epochStart }); - await mineOnce(); - - const bribeInitiative = getContract({ - client, - abi: abiBribeInitiative, - address: await client.deployContract({ - abi: abiBribeInitiative, - bytecode: bytecodeBribeInitiative, - args: [deploymentManifest.governance.governance, boldToken.address, lqtyToken.address], - }).then(waitForContractAddress), - }); - - await governanceProxy.write.registerInitiative([bribeInitiative.address]).then(waitForSuccess); - - // Warp to the beginning of the next epoch - epochStart += EPOCH_DURATION; - client.setNextBlockTimestamp({ timestamp: epochStart }); - await mineOnce(); - - await governanceProxy.write.allocateLQTY([[], [bribeInitiative.address], [lqtyAmount], [0n]]).then(waitForSuccess); -}; - -main().then(() => { - // No way to gracefully close the WebSocket in viem? - process.exit(0); -}).catch((error) => { - console.error(error); - process.exit(1); -}); diff --git a/contracts/src/ActivePool.sol b/contracts/src/ActivePool.sol index eff5042b2..d6b0baa25 100644 --- a/contracts/src/ActivePool.sol +++ b/contracts/src/ActivePool.sol @@ -11,6 +11,7 @@ import "./Interfaces/IAddressesRegistry.sol"; import "./Interfaces/IBoldToken.sol"; import "./Interfaces/IInterestRouter.sol"; import "./Interfaces/IDefaultPool.sol"; +import "./Interfaces/ISystemParams.sol"; /* * The Active Pool holds the collateral and Bold debt (but not Bold tokens) for all active troves. @@ -29,6 +30,7 @@ contract ActivePool is IActivePool { address public immutable troveManagerAddress; address public immutable defaultPoolAddress; + ISystemParams public immutable systemParams; IBoldToken public immutable boldToken; IInterestRouter public immutable interestRouter; @@ -71,7 +73,8 @@ contract ActivePool is IActivePool { event ActivePoolBoldDebtUpdated(uint256 _recordedDebtSum); event ActivePoolCollBalanceUpdated(uint256 _collBalance); - constructor(IAddressesRegistry _addressesRegistry) { + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) { + systemParams = _systemParams; collToken = _addressesRegistry.collToken(); borrowerOperationsAddress = address(_addressesRegistry.borrowerOperations()); troveManagerAddress = address(_addressesRegistry.troveManager()); @@ -113,7 +116,7 @@ contract ActivePool is IActivePool { } function calcPendingSPYield() external view returns (uint256) { - return calcPendingAggInterest() * SP_YIELD_SPLIT / DECIMAL_PRECISION; + return calcPendingAggInterest() * systemParams.SP_YIELD_SPLIT() / DECIMAL_PRECISION; } function calcPendingAggBatchManagementFee() public view returns (uint256) { @@ -250,7 +253,7 @@ contract ActivePool is IActivePool { // Mint part of the BOLD interest to the SP and part to the router for LPs. if (mintedAmount > 0) { - uint256 spYield = SP_YIELD_SPLIT * mintedAmount / DECIMAL_PRECISION; + uint256 spYield = systemParams.SP_YIELD_SPLIT() * mintedAmount / DECIMAL_PRECISION; uint256 remainderToLPs = mintedAmount - spYield; boldToken.mint(address(interestRouter), remainderToLPs); diff --git a/contracts/src/AddressesRegistry.sol b/contracts/src/AddressesRegistry.sol index 3a6a65063..dfee30760 100644 --- a/contracts/src/AddressesRegistry.sol +++ b/contracts/src/AddressesRegistry.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.24; import "./Dependencies/Ownable.sol"; -import {MIN_LIQUIDATION_PENALTY_SP, MAX_LIQUIDATION_PENALTY_REDISTRIBUTION} from "./Dependencies/Constants.sol"; import "./Interfaces/IAddressesRegistry.sol"; contract AddressesRegistry is Ownable, IAddressesRegistry { @@ -24,30 +23,8 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { IMultiTroveGetter public multiTroveGetter; ICollateralRegistry public collateralRegistry; IBoldToken public boldToken; - IWETH public WETH; - - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied - uint256 public immutable CCR; - // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, - // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves. - uint256 public immutable SCR; - - // Minimum collateral ratio for individual troves - uint256 public immutable MCR; - // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) - uint256 public immutable BCR; - // Liquidation penalty for troves offset to the SP - uint256 public immutable LIQUIDATION_PENALTY_SP; - // Liquidation penalty for troves redistributed - uint256 public immutable LIQUIDATION_PENALTY_REDISTRIBUTION; - - error InvalidCCR(); - error InvalidMCR(); - error InvalidBCR(); - error InvalidSCR(); - error SPPenaltyTooLow(); - error SPPenaltyGtRedist(); - error RedistPenaltyTooHigh(); + IERC20Metadata public gasToken; + address public liquidityStrategy; event CollTokenAddressChanged(address _collTokenAddress); event BorrowerOperationsAddressChanged(address _borrowerOperationsAddress); @@ -66,32 +43,10 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { event MultiTroveGetterAddressChanged(address _multiTroveGetterAddress); event CollateralRegistryAddressChanged(address _collateralRegistryAddress); event BoldTokenAddressChanged(address _boldTokenAddress); - event WETHAddressChanged(address _wethAddress); + event GasTokenAddressChanged(address _gasTokenAddress); + event LiquidityStrategyAddressChanged(address _liquidityStrategyAddress); - constructor( - address _owner, - uint256 _ccr, - uint256 _mcr, - uint256 _bcr, - uint256 _scr, - uint256 _liquidationPenaltySP, - uint256 _liquidationPenaltyRedistribution - ) Ownable(_owner) { - if (_ccr <= 1e18 || _ccr >= 2e18) revert InvalidCCR(); - if (_mcr <= 1e18 || _mcr >= 2e18) revert InvalidMCR(); - if (_bcr < 5e16 || _bcr >= 50e16) revert InvalidBCR(); - if (_scr <= 1e18 || _scr >= 2e18) revert InvalidSCR(); - if (_liquidationPenaltySP < MIN_LIQUIDATION_PENALTY_SP) revert SPPenaltyTooLow(); - if (_liquidationPenaltySP > _liquidationPenaltyRedistribution) revert SPPenaltyGtRedist(); - if (_liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION) revert RedistPenaltyTooHigh(); - - CCR = _ccr; - SCR = _scr; - MCR = _mcr; - BCR = _bcr; - LIQUIDATION_PENALTY_SP = _liquidationPenaltySP; - LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationPenaltyRedistribution; - } + constructor(address _owner) Ownable(_owner) {} function setAddresses(AddressVars memory _vars) external onlyOwner { collToken = _vars.collToken; @@ -111,7 +66,8 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { multiTroveGetter = _vars.multiTroveGetter; collateralRegistry = _vars.collateralRegistry; boldToken = _vars.boldToken; - WETH = _vars.WETH; + gasToken = _vars.gasToken; + liquidityStrategy = _vars.liquidityStrategy; emit CollTokenAddressChanged(address(_vars.collToken)); emit BorrowerOperationsAddressChanged(address(_vars.borrowerOperations)); @@ -130,7 +86,8 @@ contract AddressesRegistry is Ownable, IAddressesRegistry { emit MultiTroveGetterAddressChanged(address(_vars.multiTroveGetter)); emit CollateralRegistryAddressChanged(address(_vars.collateralRegistry)); emit BoldTokenAddressChanged(address(_vars.boldToken)); - emit WETHAddressChanged(address(_vars.WETH)); + emit GasTokenAddressChanged(address(_vars.gasToken)); + emit LiquidityStrategyAddressChanged(address(_vars.liquidityStrategy)); _renounceOwnership(); } diff --git a/contracts/src/BatchManagerOperations.sol b/contracts/src/BatchManagerOperations.sol new file mode 100644 index 000000000..e0a689908 --- /dev/null +++ b/contracts/src/BatchManagerOperations.sol @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import "./Interfaces/IBorrowerOperations.sol"; +import "./Interfaces/IAddressesRegistry.sol"; +import "./Interfaces/ITroveManager.sol"; +import "./Interfaces/IActivePool.sol"; +import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/ITroveNFT.sol"; +import "./Interfaces/ISystemParams.sol"; +import "./Interfaces/IPriceFeed.sol"; +import "./Interfaces/IBatchManagerOperations.sol"; +import "./Dependencies/LiquityBase.sol"; +import "./Dependencies/LiquityMath.sol"; +import "./Dependencies/Constants.sol"; +import "./Types/LatestTroveData.sol"; +import "./Types/LatestBatchData.sol"; + +/** + * @title BatchManagerOperations + * @notice Handles batch manager operations for BorrowerOperations + * @dev This contract is extracted to reduce the size of the main BorrowerOperations contract. + * It contains the batch management functions. BorrowerOperations delegates calls here. + */ +contract BatchManagerOperations is IBatchManagerOperations { + IActivePool private immutable activePool; + IDefaultPool private immutable defaultPool; + IPriceFeed private immutable priceFeed; + ITroveManager private immutable troveManager; + ISortedTroves private immutable sortedTroves; + ITroveNFT private immutable troveNFT; + ISystemParams private immutable systemParams; + + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) { + activePool = _addressesRegistry.activePool(); + defaultPool = _addressesRegistry.defaultPool(); + priceFeed = _addressesRegistry.priceFeed(); + troveManager = _addressesRegistry.troveManager(); + sortedTroves = _addressesRegistry.sortedTroves(); + troveNFT = _addressesRegistry.troveNFT(); + systemParams = _systemParams; + } + + // --- Batch Manager Operations --- + + function registerBatchManager( + uint128 _minInterestRate, + uint128 _maxInterestRate, + uint128 _currentInterestRate, + uint128 _annualManagementFee, + uint128 _minInterestRateChangePeriod + ) external { + _requireValidAnnualInterestRate(_minInterestRate); + _requireValidAnnualInterestRate(_maxInterestRate); + // With the check below, it could only be == + _requireOrderedRange(_minInterestRate, _maxInterestRate); + _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate); + if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) { + revert AnnualManagementFeeTooHigh(); + } + if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) { + revert MinInterestRateChangePeriodTooLow(); + } + } + + function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { + LatestBatchData memory batch = troveManager.getLatestBatchData(msg.sender); + if (_newAnnualManagementFee >= batch.annualManagementFee) { + revert NewFeeNotLower(); + } + + // Lower batch fee on TM + troveManager.onLowerBatchManagerAnnualFee( + msg.sender, + batch.entireCollWithoutRedistribution, + batch.entireDebtWithoutRedistribution, + _newAnnualManagementFee + ); + + // active pool mint + TroveChange memory batchChange; + batchChange.batchAccruedManagementFee = batch.accruedManagementFee; + batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; + batchChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate; + batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedBatchManagementFee = + batch.entireDebtWithoutRedistribution * _newAnnualManagementFee; + + activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); + } + + function setBatchManagerAnnualInterestRate( + uint128 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee, + uint256 _minInterestRateChangePeriod + ) external { + LatestBatchData memory batch = troveManager.getLatestBatchData(msg.sender); + _requireBatchInterestRateChangePeriodPassed( + uint256(batch.lastInterestRateAdjTime), _minInterestRateChangePeriod + ); + + uint256 newDebt = batch.entireDebtWithoutRedistribution; + + TroveChange memory batchChange; + batchChange.batchAccruedManagementFee = batch.accruedManagementFee; + batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; + batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; + batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; + batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; + + // Apply upfront fee on premature adjustments + if ( + batch.annualInterestRate != _newAnnualInterestRate + && block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + ) { + uint256 price = priceFeed.fetchPrice(); + + uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(batchChange); + batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate); + _requireUserAcceptsUpfrontFee(batchChange.upfrontFee, _maxUpfrontFee); + + newDebt += batchChange.upfrontFee; + + // Recalculate the batch's weighted terms, now taking into account the upfront fee + batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; + batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; + + // Disallow a premature adjustment if it would result in TCR < CCR + // (which includes the case when TCR is already below CCR before the adjustment). + uint256 newTCR = _getNewTCRFromTroveChange(batchChange, price); + _requireNewTCRisAboveCCR(newTCR); + } + + activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); + + // Check batch is not empty, and then reinsert in sorted list + if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) { + sortedTroves.reInsertBatch(BatchId.wrap(msg.sender), _newAnnualInterestRate, _upperHint, _lowerHint); + } + + troveManager.onSetBatchManagerAnnualInterestRate( + msg.sender, batch.entireCollWithoutRedistribution, newDebt, _newAnnualInterestRate, batchChange.upfrontFee + ); + } + + function setInterestBatchManager( + uint256 _troveId, + address _newBatchManager, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external { + LocalVariables_setInterestBatchManager memory vars; + vars.troveManager = troveManager; + vars.activePool = activePool; + vars.sortedTroves = sortedTroves; + + vars.trove = vars.troveManager.getLatestTroveData(_troveId); + vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager); + + TroveChange memory newBatchTroveChange; + newBatchTroveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; + newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain; + newBatchTroveChange.batchAccruedManagementFee = vars.newBatch.accruedManagementFee; + newBatchTroveChange.oldWeightedRecordedDebt = + vars.newBatch.weightedRecordedDebt + vars.trove.weightedRecordedDebt; + newBatchTroveChange.newWeightedRecordedDebt = + (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; + + // An upfront fee is always charged upon joining a batch to ensure that borrowers can not game the fee logic + // and gain free interest rate updates (e.g. if they also manage the batch they joined) + // It checks the resulting ICR + vars.trove.entireDebt = + _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, newBatchTroveChange, _maxUpfrontFee, true); + + // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee + newBatchTroveChange.newWeightedRecordedDebt = + (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; + + // Add batch fees + newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars.newBatch.weightedRecordedBatchManagementFee; + newBatchTroveChange.newWeightedRecordedBatchManagementFee = + (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualManagementFee; + vars.activePool.mintAggInterestAndAccountForTroveChange(newBatchTroveChange, _newBatchManager); + + vars.troveManager.onSetInterestBatchManager( + ITroveManager.OnSetInterestBatchManagerParams({ + troveId: _troveId, + troveColl: vars.trove.entireColl, + troveDebt: vars.trove.entireDebt, + troveChange: newBatchTroveChange, + newBatchAddress: _newBatchManager, + newBatchColl: vars.newBatch.entireCollWithoutRedistribution, + newBatchDebt: vars.newBatch.entireDebtWithoutRedistribution + }) + ); + + vars.sortedTroves.remove(_troveId); + vars.sortedTroves.insertIntoBatch( + _troveId, BatchId.wrap(_newBatchManager), vars.newBatch.annualInterestRate, _upperHint, _lowerHint + ); + } + + function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external { + _removeFromBatchInternal( + _troveId, + 0, // ignored when kicking + _upperHint, + _lowerHint, + 0, // will use the batch's existing interest rate, so no fee + true + ); + } + + function removeFromBatch( + uint256 _troveId, + uint256 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external { + _removeFromBatchInternal(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint, _maxUpfrontFee, false); + } + + function _removeFromBatchInternal( + uint256 _troveId, + uint256 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee, + bool _kick + ) internal { + LocalVariables_removeFromBatch memory vars; + vars.troveManager = troveManager; + vars.sortedTroves = sortedTroves; + + if (_kick) { + _requireTroveIsOpen(vars.troveManager, _troveId); + } else { + _requireTroveIsActive(vars.troveManager, _troveId); + _requireCallerIsBorrower(_troveId); + _requireValidAnnualInterestRate(_newAnnualInterestRate); + } + + vars.batchManager = _requireIsInBatch(_troveId); + vars.trove = vars.troveManager.getLatestTroveData(_troveId); + vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); + + if (_kick) { + if (vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= vars.batch.entireDebtWithoutRedistribution) { + revert BatchSharesRatioTooLow(); + } + _newAnnualInterestRate = vars.batch.annualInterestRate; + } + + if (!_checkTroveIsZombie(vars.troveManager, _troveId)) { + // Remove trove from Batch in SortedTroves + vars.sortedTroves.removeFromBatch(_troveId); + // Reinsert as single trove + vars.sortedTroves.insert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); + } + + vars.batchFutureDebt = + vars.batch.entireDebtWithoutRedistribution - (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); + + vars.batchChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; + vars.batchChange.appliedRedistCollGain = vars.trove.redistCollGain; + vars.batchChange.batchAccruedManagementFee = vars.batch.accruedManagementFee; + vars.batchChange.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt; + vars.batchChange.newWeightedRecordedDebt = + vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; + + // Apply upfront fee on premature adjustments. It checks the resulting ICR + if ( + vars.batch.annualInterestRate != _newAnnualInterestRate + && block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN + ) { + vars.trove.entireDebt = + _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, vars.batchChange, _maxUpfrontFee, false); + } + + // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee + vars.batchChange.newWeightedRecordedDebt = + vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; + // Add batch fees + vars.batchChange.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee; + vars.batchChange.newWeightedRecordedBatchManagementFee = vars.batchFutureDebt * vars.batch.annualManagementFee; + + activePool.mintAggInterestAndAccountForTroveChange(vars.batchChange, vars.batchManager); + + vars.troveManager.onRemoveFromBatch( + _troveId, + vars.trove.entireColl, + vars.trove.entireDebt, + vars.batchChange, + vars.batchManager, + vars.batch.entireCollWithoutRedistribution, + vars.batch.entireDebtWithoutRedistribution, + _newAnnualInterestRate + ); + } + + // --- Helper Functions --- + + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { + return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); + } + + // Duplicated internal function from BorrowerOperations but calls to getEntireBranchColl and getEntireBranchDebt + // are replaced with their implementations + function _getNewTCRFromTroveChange(TroveChange memory _troveChange, uint256 _price) + internal + view + returns (uint256 newTCR) + { + uint256 activeColl = activePool.getCollBalance(); + uint256 liquidatedColl = defaultPool.getCollBalance(); + uint256 totalColl = activeColl + liquidatedColl + _troveChange.collIncrease - _troveChange.collDecrease; + + uint256 activeDebt = activePool.getBoldDebt(); + uint256 closedDebt = defaultPool.getBoldDebt(); + uint256 totalDebt = + activeDebt + closedDebt + _troveChange.debtIncrease + _troveChange.upfrontFee - _troveChange.debtDecrease; + + newTCR = LiquityMath._computeCR(totalColl, totalDebt, _price); + } + + function _applyUpfrontFee( + uint256 _troveEntireColl, + uint256 _troveEntireDebt, + TroveChange memory _troveChange, + uint256 _maxUpfrontFee, + bool _isTroveInBatch + ) internal returns (uint256) { + uint256 price = priceFeed.fetchPrice(); + + uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); + _troveChange.upfrontFee = _calcUpfrontFee(_troveEntireDebt, avgInterestRate); + _requireUserAcceptsUpfrontFee(_troveChange.upfrontFee, _maxUpfrontFee); + + _troveEntireDebt += _troveChange.upfrontFee; + + // ICR is based on the requested Bold amount + upfront fee. + uint256 newICR = LiquityMath._computeCR(_troveEntireColl, _troveEntireDebt, price); + if (_isTroveInBatch) { + _requireICRisAboveMCRPlusBCR(newICR); + } else { + _requireICRisAboveMCR(newICR); + } + + // Disallow a premature adjustment if it would result in TCR < CCR + // (which includes the case when TCR is already below CCR before the adjustment). + uint256 newTCR = _getNewTCRFromTroveChange(_troveChange, price); + _requireNewTCRisAboveCCR(newTCR); + + return _troveEntireDebt; + } + + function _checkTroveIsZombie(ITroveManager _troveManager, uint256 _troveId) internal view returns (bool) { + ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); + return status == ITroveManager.Status.zombie; + } + + function _calcInterest(uint256 _weightedDebt, uint256 _period) internal pure returns (uint256) { + return _weightedDebt * _period / ONE_YEAR / DECIMAL_PRECISION; + } + + // --- Validation Functions --- + + function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal view { + if (_annualInterestRate < systemParams.MIN_ANNUAL_INTEREST_RATE()) { + revert InterestRateTooLow(); + } + if (_annualInterestRate > MAX_ANNUAL_INTEREST_RATE) { + revert InterestRateTooHigh(); + } + } + + function _requireOrderedRange(uint256 _minInterestRate, uint256 _maxInterestRate) internal pure { + if (_minInterestRate >= _maxInterestRate) revert MinGeMax(); + } + + function _requireInterestRateInRange( + uint256 _annualInterestRate, + uint256 _minInterestRate, + uint256 _maxInterestRate + ) internal pure { + if (_minInterestRate > _annualInterestRate || _annualInterestRate > _maxInterestRate) { + revert InterestNotInRange(); + } + } + + function _requireBatchInterestRateChangePeriodPassed( + uint256 _lastInterestRateAdjTime, + uint256 _minInterestRateChangePeriod + ) internal view { + if (block.timestamp < _lastInterestRateAdjTime + _minInterestRateChangePeriod) { + revert BatchInterestRateChangePeriodNotPassed(); + } + } + + function _requireUserAcceptsUpfrontFee(uint256 _fee, uint256 _maxFee) internal pure { + if (_fee > _maxFee) { + revert UpfrontFeeTooHigh(); + } + } + + function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view { + if (_newTCR < systemParams.CCR()) { + revert TCRBelowCCR(); + } + } + + function _requireTroveIsActive(ITroveManager _troveManager, uint256 _troveId) internal view { + ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); + if (status != ITroveManager.Status.active) { + revert TroveNotActive(); + } + } + + function _requireTroveIsOpen(ITroveManager _troveManager, uint256 _troveId) internal view { + ITroveManager.Status status = _troveManager.getTroveStatus(_troveId); + if (status != ITroveManager.Status.active && status != ITroveManager.Status.zombie) { + revert TroveNotOpen(); + } + } + + function _requireCallerIsBorrower(uint256 _troveId) internal view { + if (msg.sender != troveNFT.ownerOf(_troveId)) { + revert NotBorrower(); + } + } + + function _requireIsInBatch(uint256 _troveId) internal view returns (address) { + address batchManager = IBorrowerOperations(address(this)).interestBatchManagerOf(_troveId); + if (batchManager == address(0)) { + revert TroveNotInBatch(); + } + return batchManager; + } + + function _requireICRisAboveMCR(uint256 _newICR) internal view { + if (_newICR < systemParams.MCR()) { + revert ICRBelowMCR(); + } + } + + function _requireICRisAboveMCRPlusBCR(uint256 _newICR) internal view { + if (_newICR < systemParams.MCR() + systemParams.BCR()) { + revert ICRBelowMCRPlusBCR(); + } + } +} diff --git a/contracts/src/BoldToken.sol b/contracts/src/BoldToken.sol deleted file mode 100644 index f3496ddb6..000000000 --- a/contracts/src/BoldToken.sol +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "./Dependencies/Ownable.sol"; -import "./Interfaces/IBoldToken.sol"; - -/* - * --- Functionality added specific to the BoldToken --- - * - * 1) Transfer protection: blacklist of addresses that are invalid recipients (i.e. core Liquity contracts) in external - * transfer() and transferFrom() calls. The purpose is to protect users from losing tokens by mistakenly sending BOLD directly to a Liquity - * core contract, when they should rather call the right function. - * - * 2) sendToPool() and returnFromPool(): functions callable only Liquity core contracts, which move BOLD tokens between Liquity <-> user. - */ - -contract BoldToken is Ownable, IBoldToken, ERC20Permit { - string internal constant _NAME = "BOLD Stablecoin"; - string internal constant _SYMBOL = "BOLD"; - - // --- Addresses --- - - address public collateralRegistryAddress; - mapping(address => bool) troveManagerAddresses; - mapping(address => bool) stabilityPoolAddresses; - mapping(address => bool) borrowerOperationsAddresses; - mapping(address => bool) activePoolAddresses; - - // --- Events --- - event CollateralRegistryAddressChanged(address _newCollateralRegistryAddress); - event TroveManagerAddressAdded(address _newTroveManagerAddress); - event StabilityPoolAddressAdded(address _newStabilityPoolAddress); - event BorrowerOperationsAddressAdded(address _newBorrowerOperationsAddress); - event ActivePoolAddressAdded(address _newActivePoolAddress); - - constructor(address _owner) Ownable(_owner) ERC20(_NAME, _SYMBOL) ERC20Permit(_NAME) {} - - function setBranchAddresses( - address _troveManagerAddress, - address _stabilityPoolAddress, - address _borrowerOperationsAddress, - address _activePoolAddress - ) external override onlyOwner { - troveManagerAddresses[_troveManagerAddress] = true; - emit TroveManagerAddressAdded(_troveManagerAddress); - - stabilityPoolAddresses[_stabilityPoolAddress] = true; - emit StabilityPoolAddressAdded(_stabilityPoolAddress); - - borrowerOperationsAddresses[_borrowerOperationsAddress] = true; - emit BorrowerOperationsAddressAdded(_borrowerOperationsAddress); - - activePoolAddresses[_activePoolAddress] = true; - emit ActivePoolAddressAdded(_activePoolAddress); - } - - function setCollateralRegistry(address _collateralRegistryAddress) external override onlyOwner { - collateralRegistryAddress = _collateralRegistryAddress; - emit CollateralRegistryAddressChanged(_collateralRegistryAddress); - - _renounceOwnership(); - } - - // --- Functions for intra-Liquity calls --- - - function mint(address _account, uint256 _amount) external override { - _requireCallerIsBOorAP(); - _mint(_account, _amount); - } - - function burn(address _account, uint256 _amount) external override { - _requireCallerIsCRorBOorTMorSP(); - _burn(_account, _amount); - } - - function sendToPool(address _sender, address _poolAddress, uint256 _amount) external override { - _requireCallerIsStabilityPool(); - _transfer(_sender, _poolAddress, _amount); - } - - function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external override { - _requireCallerIsStabilityPool(); - _transfer(_poolAddress, _receiver, _amount); - } - - // --- External functions --- - - function transfer(address recipient, uint256 amount) public override(ERC20, IERC20) returns (bool) { - _requireValidRecipient(recipient); - return super.transfer(recipient, amount); - } - - function transferFrom(address sender, address recipient, uint256 amount) - public - override(ERC20, IERC20) - returns (bool) - { - _requireValidRecipient(recipient); - return super.transferFrom(sender, recipient, amount); - } - - // --- 'require' functions --- - - function _requireValidRecipient(address _recipient) internal view { - require( - _recipient != address(0) && _recipient != address(this), - "BoldToken: Cannot transfer tokens directly to the Bold token contract or the zero address" - ); - } - - function _requireCallerIsBOorAP() internal view { - require( - borrowerOperationsAddresses[msg.sender] || activePoolAddresses[msg.sender], - "BoldToken: Caller is not BO or AP" - ); - } - - function _requireCallerIsCRorBOorTMorSP() internal view { - require( - msg.sender == collateralRegistryAddress || borrowerOperationsAddresses[msg.sender] - || troveManagerAddresses[msg.sender] || stabilityPoolAddresses[msg.sender], - "BoldToken: Caller is neither CR nor BorrowerOperations nor TroveManager nor StabilityPool" - ); - } - - function _requireCallerIsStabilityPool() internal view { - require(stabilityPoolAddresses[msg.sender], "BoldToken: Caller is not the StabilityPool"); - } -} diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 85e771fd2..959dbbbc8 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -10,10 +10,12 @@ import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; import "./Interfaces/ICollSurplusPool.sol"; import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityBase.sol"; import "./Dependencies/AddRemoveManagers.sol"; import "./Types/LatestTroveData.sol"; import "./Types/LatestBatchData.sol"; +import "./BatchManagerOperations.sol"; contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperations { using SafeERC20 for IERC20; @@ -28,28 +30,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // A doubly linked list of Troves, sorted by their collateral ratios ISortedTroves internal sortedTroves; // Wrapped ETH for liquidation reserve (gas compensation) - IWETH internal immutable WETH; + IERC20Metadata internal immutable gasToken; + ISystemParams public immutable systemParams; + // Helper contract for batch management operations + address public batchManagerOperations; - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied - uint256 public immutable CCR; - - // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, - // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves. - uint256 public immutable SCR; bool public hasBeenShutDown; - // Minimum collateral ratio for individual troves - uint256 public immutable MCR; - - // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) - uint256 public immutable BCR; - /* - * Mapping from TroveId to individual delegate for interest rate setting. - * - * This address then has the ability to update the borrower’s interest rate, but not change its debt or collateral. - * Useful for instance for cold/hot wallet setups. - */ + * Mapping from TroveId to individual delegate for interest rate setting. + * + * This address then has the ability to update the borrower’s interest rate, but not change its debt or collateral. + * Useful for instance for cold/hot wallet setups. + */ mapping(uint256 => InterestIndividualDelegate) private interestIndividualDelegateOf; /* @@ -99,26 +92,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio bool newOracleFailureDetected; } - struct LocalVariables_setInterestBatchManager { - ITroveManager troveManager; - IActivePool activePool; - ISortedTroves sortedTroves; - address oldBatchManager; - LatestTroveData trove; - LatestBatchData oldBatch; - LatestBatchData newBatch; - } - - struct LocalVariables_removeFromBatch { - ITroveManager troveManager; - ISortedTroves sortedTroves; - address batchManager; - LatestTroveData trove; - LatestBatchData batch; - uint256 batchFutureDebt; - TroveChange batchChange; - } - error IsShutDown(); error TCRNotBelowSCR(); error ZeroAdjustment(); @@ -150,6 +123,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio error NewFeeNotLower(); error CallerNotTroveManager(); error CallerNotPriceFeed(); + error CallerNotSelf(); error MinGeMax(); error AnnualManagementFeeTooHigh(); error MinInterestRateChangePeriodTooLow(); @@ -164,27 +138,29 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio event ShutDown(uint256 _tcr); - constructor(IAddressesRegistry _addressesRegistry) + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) AddRemoveManagers(_addressesRegistry) LiquityBase(_addressesRegistry) { // This makes impossible to open a trove with zero withdrawn Bold - assert(MIN_DEBT > 0); + assert(_systemParams.MIN_DEBT() > 0); - collToken = _addressesRegistry.collToken(); + systemParams = _systemParams; - WETH = _addressesRegistry.WETH(); + collToken = _addressesRegistry.collToken(); - CCR = _addressesRegistry.CCR(); - SCR = _addressesRegistry.SCR(); - MCR = _addressesRegistry.MCR(); - BCR = _addressesRegistry.BCR(); + gasToken = _addressesRegistry.gasToken(); troveManager = _addressesRegistry.troveManager(); gasPoolAddress = _addressesRegistry.gasPoolAddress(); collSurplusPool = _addressesRegistry.collSurplusPool(); sortedTroves = _addressesRegistry.sortedTroves(); boldToken = _addressesRegistry.boldToken(); + // We can leave the deployment script as-is by just having BorrowerOperations deploy its + // own batchManagerOperations contract + // /!\ If we have to redeploy a BorrowerOps that could need the same batchManagerOps then we + // would replace this line with some extra param, but that seems unlikely + batchManagerOperations = address(new BatchManagerOperations(_addressesRegistry, _systemParams)); emit TroveManagerAddressChanged(address(troveManager)); emit GasPoolAddressChanged(gasPoolAddress); @@ -196,6 +172,14 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio collToken.approve(address(activePool), type(uint256).max); } + function CCR() external view override returns (uint256) { + return systemParams.CCR(); + } + + function MCR() external view override returns (uint256) { + return systemParams.MCR(); + } + // --- Borrower Trove Operations --- function openTrove( @@ -318,7 +302,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.activePool = activePool; vars.boldToken = boldToken; - vars.price = _requireOraclesLive(); + vars.price = priceFeed.fetchPrice(); // --- Checks --- @@ -373,7 +357,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // Mint the requested _boldAmount to the borrower and mint the gas comp to the GasPool vars.boldToken.mint(msg.sender, _boldAmount); - WETH.transferFrom(msg.sender, gasPoolAddress, ETH_GAS_COMPENSATION); + gasToken.transferFrom(msg.sender, gasPoolAddress, systemParams.ETH_GAS_COMPENSATION()); return vars.troveId; } @@ -552,8 +536,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } /* - * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. - */ + * _adjustTrove(): Alongside a debt change, this function can perform either a collateral top-up or a collateral withdrawal. + */ function _adjustTrove( ITroveManager _troveManager, uint256 _troveId, @@ -566,8 +550,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio vars.activePool = activePool; vars.boldToken = boldToken; - vars.price = _requireOraclesLive(); - vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold(vars.price, CCR); + vars.price = priceFeed.fetchPrice(); + vars.isBelowCriticalThreshold = _checkBelowCriticalThreshold(vars.price, systemParams.CCR()); // --- Checks --- @@ -591,7 +575,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // When the adjustment is a debt repayment, check it's a valid amount and that the caller has enough Bold if (_troveChange.debtDecrease > 0) { - uint256 maxRepayment = vars.trove.entireDebt > MIN_DEBT ? vars.trove.entireDebt - MIN_DEBT : 0; + uint256 maxRepayment = + vars.trove.entireDebt > systemParams.MIN_DEBT() ? vars.trove.entireDebt - systemParams.MIN_DEBT() : 0; if (_troveChange.debtDecrease > maxRepayment) { _troveChange.debtDecrease = maxRepayment; } @@ -717,7 +702,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio // troveChange.newWeightedRecordedDebt = 0; } - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 newTCR = _getNewTCRFromTroveChange(troveChange, price); if (!hasBeenShutDown) _requireNewTCRisAboveCCR(newTCR); @@ -738,7 +723,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio activePoolCached.mintAggInterestAndAccountForTroveChange(troveChange, batchManager); // Return ETH gas compensation - WETH.transferFrom(gasPoolAddress, receiver, ETH_GAS_COMPENSATION); + gasToken.transferFrom(gasPoolAddress, receiver, systemParams.ETH_GAS_COMPENSATION()); // Burn the remainder of the Trove's entire debt from the user boldTokenCached.burn(msg.sender, trove.entireDebt); @@ -790,8 +775,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ); activePool.mintAggInterestAndAccountForTroveChange(change, batchManager); - // If the trove was zombie, and now it’s not anymore, put it back in the list - if (_checkTroveIsZombie(troveManagerCached, _troveId) && trove.entireDebt >= MIN_DEBT) { + // If the trove was zombie, and now it's not anymore, put it back in the list + if (_checkTroveIsZombie(troveManagerCached, _troveId) && trove.entireDebt >= systemParams.MIN_DEBT()) { troveManagerCached.setTroveStatusToActive(_troveId); _reInsertIntoSortedTroves( _troveId, trove.annualInterestRate, _upperHint, _lowerHint, batchManager, batch.annualInterestRate @@ -855,51 +840,29 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio ) external { _requireIsNotShutDown(); _requireNonExistentInterestBatchManager(msg.sender); - _requireValidAnnualInterestRate(_minInterestRate); - _requireValidAnnualInterestRate(_maxInterestRate); - // With the check below, it could only be == - _requireOrderedRange(_minInterestRate, _maxInterestRate); - _requireInterestRateInRange(_currentInterestRate, _minInterestRate, _maxInterestRate); - // Not needed, implicitly checked in the condition above: - //_requireValidAnnualInterestRate(_currentInterestRate); - if (_annualManagementFee > MAX_ANNUAL_BATCH_MANAGEMENT_FEE) revert AnnualManagementFeeTooHigh(); - if (_minInterestRateChangePeriod < MIN_INTEREST_RATE_CHANGE_PERIOD) revert MinInterestRateChangePeriodTooLow(); - + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "registerBatchManager(uint128,uint128,uint128,uint128,uint128)", + _minInterestRate, + _maxInterestRate, + _currentInterestRate, + _annualManagementFee, + _minInterestRateChangePeriod + ) + ); + _requireDelegateCallSucceeded(success, data); interestBatchManagers[msg.sender] = InterestBatchManager(_minInterestRate, _maxInterestRate, _minInterestRateChangePeriod); - troveManager.onRegisterBatchManager(msg.sender, _currentInterestRate, _annualManagementFee); } function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external { _requireIsNotShutDown(); _requireValidInterestBatchManager(msg.sender); - - ITroveManager troveManagerCached = troveManager; - - LatestBatchData memory batch = troveManagerCached.getLatestBatchData(msg.sender); - if (_newAnnualManagementFee >= batch.annualManagementFee) { - revert NewFeeNotLower(); - } - - // Lower batch fee on TM - troveManagerCached.onLowerBatchManagerAnnualFee( - msg.sender, - batch.entireCollWithoutRedistribution, - batch.entireDebtWithoutRedistribution, - _newAnnualManagementFee + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature("lowerBatchManagementFee(uint256)", _newAnnualManagementFee) ); - - // active pool mint - TroveChange memory batchChange; - batchChange.batchAccruedManagementFee = batch.accruedManagementFee; - batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchChange.newWeightedRecordedDebt = batch.entireDebtWithoutRedistribution * batch.annualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; - batchChange.newWeightedRecordedBatchManagementFee = - batch.entireDebtWithoutRedistribution * _newAnnualManagementFee; - - activePool.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); + _requireDelegateCallSucceeded(success, data); } function setBatchManagerAnnualInterestRate( @@ -911,57 +874,18 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio _requireIsNotShutDown(); _requireValidInterestBatchManager(msg.sender); _requireInterestRateInBatchManagerRange(msg.sender, _newAnnualInterestRate); - // Not needed, implicitly checked in the condition above: - //_requireValidAnnualInterestRate(_newAnnualInterestRate); - - ITroveManager troveManagerCached = troveManager; - IActivePool activePoolCached = activePool; - - LatestBatchData memory batch = troveManagerCached.getLatestBatchData(msg.sender); - _requireBatchInterestRateChangePeriodPassed(msg.sender, uint256(batch.lastInterestRateAdjTime)); - - uint256 newDebt = batch.entireDebtWithoutRedistribution; - - TroveChange memory batchChange; - batchChange.batchAccruedManagementFee = batch.accruedManagementFee; - batchChange.oldWeightedRecordedDebt = batch.weightedRecordedDebt; - batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - batchChange.oldWeightedRecordedBatchManagementFee = batch.weightedRecordedBatchManagementFee; - batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; - - // Apply upfront fee on premature adjustments - if ( - batch.annualInterestRate != _newAnnualInterestRate - && block.timestamp < batch.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN - ) { - uint256 price = _requireOraclesLive(); - - uint256 avgInterestRate = activePoolCached.getNewApproxAvgInterestRateFromTroveChange(batchChange); - batchChange.upfrontFee = _calcUpfrontFee(newDebt, avgInterestRate); - _requireUserAcceptsUpfrontFee(batchChange.upfrontFee, _maxUpfrontFee); - - newDebt += batchChange.upfrontFee; - - // Recalculate the batch's weighted terms, now taking into account the upfront fee - batchChange.newWeightedRecordedDebt = newDebt * _newAnnualInterestRate; - batchChange.newWeightedRecordedBatchManagementFee = newDebt * batch.annualManagementFee; - - // Disallow a premature adjustment if it would result in TCR < CCR - // (which includes the case when TCR is already below CCR before the adjustment). - uint256 newTCR = _getNewTCRFromTroveChange(batchChange, price); - _requireNewTCRisAboveCCR(newTCR); - } - - activePoolCached.mintAggInterestAndAccountForTroveChange(batchChange, msg.sender); - - // Check batch is not empty, and then reinsert in sorted list - if (!sortedTroves.isEmptyBatch(BatchId.wrap(msg.sender))) { - sortedTroves.reInsertBatch(BatchId.wrap(msg.sender), _newAnnualInterestRate, _upperHint, _lowerHint); - } - - troveManagerCached.onSetBatchManagerAnnualInterestRate( - msg.sender, batch.entireCollWithoutRedistribution, newDebt, _newAnnualInterestRate, batchChange.upfrontFee + InterestBatchManager memory interestBatchManager = interestBatchManagers[msg.sender]; + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "setBatchManagerAnnualInterestRate(uint128,uint256,uint256,uint256,uint256)", + _newAnnualInterestRate, + _upperHint, + _lowerHint, + _maxUpfrontFee, + interestBatchManager.minInterestRateChangePeriod + ) ); + _requireDelegateCallSucceeded(success, data); } function setInterestBatchManager( @@ -972,75 +896,36 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _maxUpfrontFee ) public override { _requireIsNotShutDown(); - LocalVariables_setInterestBatchManager memory vars; - vars.troveManager = troveManager; - vars.activePool = activePool; - vars.sortedTroves = sortedTroves; - - _requireTroveIsActive(vars.troveManager, _troveId); + _requireTroveIsActive(troveManager, _troveId); _requireCallerIsBorrower(_troveId); _requireValidInterestBatchManager(_newBatchManager); _requireIsNotInBatch(_troveId); - interestBatchManagerOf[_troveId] = _newBatchManager; - // Can’t have both individual delegation and batch manager - if (interestIndividualDelegateOf[_troveId].account != address(0)) delete interestIndividualDelegateOf[_troveId]; - - vars.trove = vars.troveManager.getLatestTroveData(_troveId); - vars.newBatch = vars.troveManager.getLatestBatchData(_newBatchManager); - - TroveChange memory newBatchTroveChange; - newBatchTroveChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; - newBatchTroveChange.appliedRedistCollGain = vars.trove.redistCollGain; - newBatchTroveChange.batchAccruedManagementFee = vars.newBatch.accruedManagementFee; - newBatchTroveChange.oldWeightedRecordedDebt = - vars.newBatch.weightedRecordedDebt + vars.trove.weightedRecordedDebt; - newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; - - // An upfront fee is always charged upon joining a batch to ensure that borrowers can not game the fee logic - // and gain free interest rate updates (e.g. if they also manage the batch they joined) - // It checks the resulting ICR - vars.trove.entireDebt = - _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, newBatchTroveChange, _maxUpfrontFee, true); - - // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - newBatchTroveChange.newWeightedRecordedDebt = - (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualInterestRate; - - // Add batch fees - newBatchTroveChange.oldWeightedRecordedBatchManagementFee = vars.newBatch.weightedRecordedBatchManagementFee; - newBatchTroveChange.newWeightedRecordedBatchManagementFee = - (vars.newBatch.entireDebtWithoutRedistribution + vars.trove.entireDebt) * vars.newBatch.annualManagementFee; - vars.activePool.mintAggInterestAndAccountForTroveChange(newBatchTroveChange, _newBatchManager); - - vars.troveManager.onSetInterestBatchManager( - ITroveManager.OnSetInterestBatchManagerParams({ - troveId: _troveId, - troveColl: vars.trove.entireColl, - troveDebt: vars.trove.entireDebt, - troveChange: newBatchTroveChange, - newBatchAddress: _newBatchManager, - newBatchColl: vars.newBatch.entireCollWithoutRedistribution, - newBatchDebt: vars.newBatch.entireDebtWithoutRedistribution - }) - ); + // Can't have both individual delegation and batch manager + if (interestIndividualDelegateOf[_troveId].account != address(0)) { + delete interestIndividualDelegateOf[_troveId]; + } - vars.sortedTroves.remove(_troveId); - vars.sortedTroves.insertIntoBatch( - _troveId, BatchId.wrap(_newBatchManager), vars.newBatch.annualInterestRate, _upperHint, _lowerHint + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "setInterestBatchManager(uint256,address,uint256,uint256,uint256)", + _troveId, + _newBatchManager, + _upperHint, + _lowerHint, + _maxUpfrontFee + ) ); + _requireDelegateCallSucceeded(success, data); } function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external override { - _removeFromBatch({ - _troveId: _troveId, - _newAnnualInterestRate: 0, // ignored when kicking - _upperHint: _upperHint, - _lowerHint: _lowerHint, - _maxUpfrontFee: 0, // will use the batch's existing interest rate, so no fee - _kick: true - }); + _requireIsNotShutDown(); + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature("kickFromBatch(uint256,uint256,uint256)", _troveId, _upperHint, _lowerHint) + ); + _requireDelegateCallSucceeded(success, data); + delete interestBatchManagerOf[_troveId]; } function removeFromBatch( @@ -1050,96 +935,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _lowerHint, uint256 _maxUpfrontFee ) public override { - _removeFromBatch({ - _troveId: _troveId, - _newAnnualInterestRate: _newAnnualInterestRate, - _upperHint: _upperHint, - _lowerHint: _lowerHint, - _maxUpfrontFee: _maxUpfrontFee, - _kick: false - }); - } - - function _removeFromBatch( - uint256 _troveId, - uint256 _newAnnualInterestRate, - uint256 _upperHint, - uint256 _lowerHint, - uint256 _maxUpfrontFee, - bool _kick - ) internal { _requireIsNotShutDown(); - - LocalVariables_removeFromBatch memory vars; - vars.troveManager = troveManager; - vars.sortedTroves = sortedTroves; - - if (_kick) { - _requireTroveIsOpen(vars.troveManager, _troveId); - } else { - _requireTroveIsActive(vars.troveManager, _troveId); - _requireCallerIsBorrower(_troveId); - _requireValidAnnualInterestRate(_newAnnualInterestRate); - } - - vars.batchManager = _requireIsInBatch(_troveId); - vars.trove = vars.troveManager.getLatestTroveData(_troveId); - vars.batch = vars.troveManager.getLatestBatchData(vars.batchManager); - - if (_kick) { - if (vars.batch.totalDebtShares * MAX_BATCH_SHARES_RATIO >= vars.batch.entireDebtWithoutRedistribution) { - revert BatchSharesRatioTooLow(); - } - _newAnnualInterestRate = vars.batch.annualInterestRate; - } - - delete interestBatchManagerOf[_troveId]; - - if (!_checkTroveIsZombie(vars.troveManager, _troveId)) { - // Remove trove from Batch in SortedTroves - vars.sortedTroves.removeFromBatch(_troveId); - // Reinsert as single trove - vars.sortedTroves.insert(_troveId, _newAnnualInterestRate, _upperHint, _lowerHint); - } - - vars.batchFutureDebt = - vars.batch.entireDebtWithoutRedistribution - (vars.trove.entireDebt - vars.trove.redistBoldDebtGain); - - vars.batchChange.appliedRedistBoldDebtGain = vars.trove.redistBoldDebtGain; - vars.batchChange.appliedRedistCollGain = vars.trove.redistCollGain; - vars.batchChange.batchAccruedManagementFee = vars.batch.accruedManagementFee; - vars.batchChange.oldWeightedRecordedDebt = vars.batch.weightedRecordedDebt; - vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; - - // Apply upfront fee on premature adjustments. It checks the resulting ICR - if ( - vars.batch.annualInterestRate != _newAnnualInterestRate - && block.timestamp < vars.trove.lastInterestRateAdjTime + INTEREST_RATE_ADJ_COOLDOWN - ) { - vars.trove.entireDebt = - _applyUpfrontFee(vars.trove.entireColl, vars.trove.entireDebt, vars.batchChange, _maxUpfrontFee, false); - } - - // Recalculate newWeightedRecordedDebt, now taking into account the upfront fee - vars.batchChange.newWeightedRecordedDebt = - vars.batchFutureDebt * vars.batch.annualInterestRate + vars.trove.entireDebt * _newAnnualInterestRate; - // Add batch fees - vars.batchChange.oldWeightedRecordedBatchManagementFee = vars.batch.weightedRecordedBatchManagementFee; - vars.batchChange.newWeightedRecordedBatchManagementFee = vars.batchFutureDebt * vars.batch.annualManagementFee; - - activePool.mintAggInterestAndAccountForTroveChange(vars.batchChange, vars.batchManager); - - vars.troveManager.onRemoveFromBatch( - _troveId, - vars.trove.entireColl, - vars.trove.entireDebt, - vars.batchChange, - vars.batchManager, - vars.batch.entireCollWithoutRedistribution, - vars.batch.entireDebtWithoutRedistribution, - _newAnnualInterestRate + (bool success, bytes memory data) = batchManagerOperations.delegatecall( + abi.encodeWithSignature( + "removeFromBatch(uint256,uint256,uint256,uint256,uint256)", + _troveId, + _newAnnualInterestRate, + _upperHint, + _lowerHint, + _maxUpfrontFee + ) ); + _requireDelegateCallSucceeded(success, data); + delete interestBatchManagerOf[_troveId]; } function switchBatchManager( @@ -1167,7 +975,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 _maxUpfrontFee, bool _isTroveInBatch ) internal returns (uint256) { - uint256 price = _requireOraclesLive(); + uint256 price = priceFeed.fetchPrice(); uint256 avgInterestRate = activePool.getNewApproxAvgInterestRateFromTroveChange(_troveChange); _troveChange.upfrontFee = _calcUpfrontFee(_troveEntireDebt, avgInterestRate); @@ -1221,13 +1029,11 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 totalColl = getEntireBranchColl(); uint256 totalDebt = getEntireBranchDebt(); - (uint256 price, bool newOracleFailureDetected) = priceFeed.fetchPrice(); - // If the oracle failed, the above call to PriceFeed will have shut this branch down - if (newOracleFailureDetected) return; + uint256 price = priceFeed.fetchPrice(); // Otherwise, proceed with the TCR check: uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price); - if (TCR >= SCR) revert TCRNotBelowSCR(); + if (TCR >= systemParams.SCR()) revert TCRNotBelowSCR(); _applyShutdown(); @@ -1412,18 +1218,18 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio bool _isTroveInBatch ) internal view { /* - * Below Critical Threshold, it is not permitted: - * - * - Borrowing, unless it brings TCR up to CCR again - * - Collateral withdrawal except accompanied by a debt repayment of at least the same value - * - * In Normal Mode, ensure: - * - * - The adjustment won't pull the TCR below CCR - * - * In Both cases: - * - The new ICR is above MCR, or MCR+BCR if a batched trove - */ + * Below Critical Threshold, it is not permitted: + * + * - Borrowing, unless it brings TCR up to CCR again + * - Collateral withdrawal except accompanied by a debt repayment of at least the same value + * + * In Normal Mode, ensure: + * + * - The adjustment won't pull the TCR below CCR + * + * In Both cases: + * - The new ICR is above MCR, or MCR+BCR if a batched trove + */ if (_isTroveInBatch) { _requireICRisAboveMCRPlusBCR(_vars.newICR); @@ -1442,19 +1248,19 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } function _requireICRisAboveMCR(uint256 _newICR) internal view { - if (_newICR < MCR) { + if (_newICR < systemParams.MCR()) { revert ICRBelowMCR(); } } function _requireICRisAboveMCRPlusBCR(uint256 _newICR) internal view { - if (_newICR < MCR + BCR) { + if (_newICR < systemParams.MCR() + systemParams.BCR()) { revert ICRBelowMCRPlusBCR(); } } function _requireNoBorrowingUnlessNewTCRisAboveCCR(uint256 _debtIncrease, uint256 _newTCR) internal view { - if (_debtIncrease > 0 && _newTCR < CCR) { + if (_debtIncrease > 0 && _newTCR < systemParams.CCR()) { revert TCRBelowCCR(); } } @@ -1466,13 +1272,13 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } function _requireNewTCRisAboveCCR(uint256 _newTCR) internal view { - if (_newTCR < CCR) { + if (_newTCR < systemParams.CCR()) { revert TCRBelowCCR(); } } - function _requireAtLeastMinDebt(uint256 _debt) internal pure { - if (_debt < MIN_DEBT) { + function _requireAtLeastMinDebt(uint256 _debt) internal view { + if (_debt < systemParams.MIN_DEBT()) { revert DebtBelowMin(); } } @@ -1492,8 +1298,8 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal pure { - if (_annualInterestRate < MIN_ANNUAL_INTEREST_RATE) { + function _requireValidAnnualInterestRate(uint256 _annualInterestRate) internal view { + if (_annualInterestRate < systemParams.MIN_ANNUAL_INTEREST_RATE()) { revert InterestRateTooLow(); } if (_annualInterestRate > MAX_ANNUAL_INTEREST_RATE) { @@ -1534,16 +1340,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireBatchInterestRateChangePeriodPassed( - address _interestBatchManagerAddress, - uint256 _lastInterestRateAdjTime - ) internal view { - InterestBatchManager memory interestBatchManager = interestBatchManagers[_interestBatchManagerAddress]; - if (block.timestamp < _lastInterestRateAdjTime + uint256(interestBatchManager.minInterestRateChangePeriod)) { - revert BatchInterestRateChangePeriodNotPassed(); - } - } - function _requireDelegateInterestRateChangePeriodPassed( uint256 _lastInterestRateAdjTime, uint256 _minInterestRateChangePeriod @@ -1586,13 +1382,12 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } } - function _requireOraclesLive() internal returns (uint256) { - (uint256 price, bool newOracleFailureDetected) = priceFeed.fetchPrice(); - if (newOracleFailureDetected) { - revert NewOracleFailureDetected(); + function _requireDelegateCallSucceeded(bool success, bytes memory data) internal pure { + if (!success) { + assembly { + revert(add(0x20, data), mload(data)) + } } - - return price; } // --- ICR and TCR getters --- diff --git a/contracts/src/CollateralRegistry.sol b/contracts/src/CollateralRegistry.sol index 84dc90f56..67eee28c4 100644 --- a/contracts/src/CollateralRegistry.sol +++ b/contracts/src/CollateralRegistry.sol @@ -6,6 +6,7 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/Constants.sol"; import "./Dependencies/LiquityMath.sol"; @@ -37,21 +38,32 @@ contract CollateralRegistry is ICollateralRegistry { ITroveManager internal immutable troveManager8; ITroveManager internal immutable troveManager9; + ISystemParams public immutable systemParams; IBoldToken public immutable boldToken; uint256 public baseRate; + address public immutable liquidityStrategy; + // The timestamp of the latest fee operation (redemption or new Bold issuance) uint256 public lastFeeOperationTime = block.timestamp; event BaseRateUpdated(uint256 _baseRate); event LastFeeOpTimeUpdated(uint256 _lastFeeOpTime); - - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers) { + event LiquidityStrategyUpdated(address indexed _liquidityStrategy); + + constructor( + IBoldToken _boldToken, + IERC20Metadata[] memory _tokens, + ITroveManager[] memory _troveManagers, + ISystemParams _systemParams, + address _liquidityStrategy + ) { uint256 numTokens = _tokens.length; require(numTokens > 0, "Collateral list cannot be empty"); require(numTokens <= 10, "Collateral list too long"); totalCollaterals = numTokens; + systemParams = _systemParams; boldToken = _boldToken; @@ -78,8 +90,12 @@ contract CollateralRegistry is ICollateralRegistry { troveManager9 = numTokens > 9 ? _troveManagers[9] : ITroveManager(address(0)); // Initialize the baseRate state variable - baseRate = INITIAL_BASE_RATE; - emit BaseRateUpdated(INITIAL_BASE_RATE); + baseRate = _systemParams.INITIAL_BASE_RATE(); + emit BaseRateUpdated(baseRate); + + // Initialize the liquidityStrategy state variable + liquidityStrategy = _liquidityStrategy; + emit LiquidityStrategyUpdated(liquidityStrategy); } struct RedemptionTotals { @@ -89,6 +105,34 @@ contract CollateralRegistry is ICollateralRegistry { uint256 redeemedAmount; } + /** + * @notice Redeems debt tokens with a fixed fee for the trove owner + * @dev This function is used during the rebalancing of a CDP pool and can only be called by the liquidity strategy + * @param _boldAmount The amount of bold to redeem + * @param _maxIterationsPerCollateral The maximum number of iterations per collateral + * @param _troveOwnerFee The fee to pay to the trove owner + */ + function redeemCollateralRebalancing(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _troveOwnerFee) external { + _requireCallerIsLiquidityStrategy(); + _requireAmountGreaterThanZero(_boldAmount); + _requireValidTroveOwnerFee(_troveOwnerFee); + require(totalCollaterals == 1, "CollateralRegistry: Only one collateral supported for rebalancing"); + + ITroveManager troveManager = getTroveManager(0); + (, uint256 price, bool redeemable) = + troveManager.getUnbackedPortionPriceAndRedeemability(); + require(redeemable, "CollateralRegistry: Collateral is not redeemable"); + uint256 redeemedAmount = troveManager.redeemCollateral( + msg.sender, + _boldAmount, + price, + _troveOwnerFee, + _maxIterationsPerCollateral + ); + require(redeemedAmount == _boldAmount, "CollateralRegistry: Redeemed amount does not match requested amount"); + boldToken.burn(msg.sender, redeemedAmount); + } + function redeemCollateral(uint256 _boldAmount, uint256 _maxIterationsPerCollateral, uint256 _maxFeePercentage) external { @@ -220,7 +264,7 @@ contract CollateralRegistry is ICollateralRegistry { // get the fraction of total supply that was redeemed uint256 redeemedBoldFraction = _redeemAmount * DECIMAL_PRECISION / _totalBoldSupply; - uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / REDEMPTION_BETA; + uint256 newBaseRate = decayedBaseRate + redeemedBoldFraction / systemParams.REDEMPTION_BETA(); newBaseRate = LiquityMath._min(newBaseRate, DECIMAL_PRECISION); // cap baseRate at a maximum of 100% return newBaseRate; @@ -228,14 +272,14 @@ contract CollateralRegistry is ICollateralRegistry { function _calcDecayedBaseRate() internal view returns (uint256) { uint256 minutesPassed = _minutesPassedSinceLastFeeOp(); - uint256 decayFactor = LiquityMath._decPow(REDEMPTION_MINUTE_DECAY_FACTOR, minutesPassed); + uint256 decayFactor = LiquityMath._decPow(systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(), minutesPassed); return baseRate * decayFactor / DECIMAL_PRECISION; } - function _calcRedemptionRate(uint256 _baseRate) internal pure returns (uint256) { + function _calcRedemptionRate(uint256 _baseRate) internal view returns (uint256) { return LiquityMath._min( - REDEMPTION_FEE_FLOOR + _baseRate, + systemParams.REDEMPTION_FEE_FLOOR() + _baseRate, DECIMAL_PRECISION // cap at a maximum of 100% ); } @@ -303,9 +347,9 @@ contract CollateralRegistry is ICollateralRegistry { // require functions - function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal pure { + function _requireValidMaxFeePercentage(uint256 _maxFeePercentage) internal view { require( - _maxFeePercentage >= REDEMPTION_FEE_FLOOR && _maxFeePercentage <= DECIMAL_PRECISION, + _maxFeePercentage >= systemParams.REDEMPTION_FEE_FLOOR() && _maxFeePercentage <= DECIMAL_PRECISION, "Max fee percentage must be between 0.5% and 100%" ); } @@ -313,4 +357,12 @@ contract CollateralRegistry is ICollateralRegistry { function _requireAmountGreaterThanZero(uint256 _amount) internal pure { require(_amount > 0, "CollateralRegistry: Amount must be greater than zero"); } + + function _requireCallerIsLiquidityStrategy() internal view { + require(msg.sender == address(liquidityStrategy), "CollateralRegistry: Caller is not LiquidityStrategy"); + } + + function _requireValidTroveOwnerFee(uint256 _troveOwnerFee) internal pure { + require(_troveOwnerFee <= DECIMAL_PRECISION, "CollateralRegistry: Trove owner fee must be between 0% and 100%"); + } } diff --git a/contracts/src/Dependencies/Constants.sol b/contracts/src/Dependencies/Constants.sol index 9e0231988..59d4d502e 100644 --- a/contracts/src/Dependencies/Constants.sol +++ b/contracts/src/Dependencies/Constants.sol @@ -9,81 +9,21 @@ uint256 constant DECIMAL_PRECISION = 1e18; uint256 constant _100pct = DECIMAL_PRECISION; uint256 constant _1pct = DECIMAL_PRECISION / 100; -// Amount of ETH to be locked in gas pool on opening troves -uint256 constant ETH_GAS_COMPENSATION = 0.0375 ether; - -// Liquidation -uint256 constant MIN_LIQUIDATION_PENALTY_SP = 5e16; // 5% -uint256 constant MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20e16; // 20% - -// Collateral branch parameters (SETH = staked ETH, i.e. wstETH / rETH) -uint256 constant CCR_WETH = 150 * _1pct; -uint256 constant CCR_SETH = 160 * _1pct; - -uint256 constant MCR_WETH = 110 * _1pct; -uint256 constant MCR_SETH = 120 * _1pct; - -uint256 constant SCR_WETH = 110 * _1pct; -uint256 constant SCR_SETH = 120 * _1pct; - -// Batch CR buffer (same for all branches for now) -// On top of MCR to join a batch, or adjust inside a batch -uint256 constant BCR_ALL = 10 * _1pct; - -uint256 constant LIQUIDATION_PENALTY_SP_WETH = 5 * _1pct; -uint256 constant LIQUIDATION_PENALTY_SP_SETH = 5 * _1pct; - -uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_WETH = 10 * _1pct; -uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_SETH = 20 * _1pct; - -// Fraction of collateral awarded to liquidator -uint256 constant COLL_GAS_COMPENSATION_DIVISOR = 200; // dividing by 200 yields 0.5% -uint256 constant COLL_GAS_COMPENSATION_CAP = 2 ether; // Max coll gas compensation capped at 2 ETH - -// Minimum amount of net Bold debt a trove must have -uint256 constant MIN_DEBT = 2000e18; - -uint256 constant MIN_ANNUAL_INTEREST_RATE = _1pct / 2; // 0.5% -uint256 constant MAX_ANNUAL_INTEREST_RATE = 250 * _1pct; - -// Batch management params -uint128 constant MAX_ANNUAL_BATCH_MANAGEMENT_FEE = uint128(_100pct / 10); // 10% -uint128 constant MIN_INTEREST_RATE_CHANGE_PERIOD = 1 hours; // only applies to batch managers / batched Troves - -uint256 constant REDEMPTION_FEE_FLOOR = _1pct / 2; // 0.5% - -// For the debt / shares ratio to increase by a factor 1e9 -// at a average annual debt increase (compounded interest + fees) of 10%, it would take more than 217 years (log(1e9)/log(1.1)) -// at a average annual debt increase (compounded interest + fees) of 50%, it would take more than 51 years (log(1e9)/log(1.5)) -// The increase pace could be forced to be higher through an inflation attack, -// but precisely the fact that we have this max value now prevents the attack -uint256 constant MAX_BATCH_SHARES_RATIO = 1e9; - -// Half-life of 6h. 6h = 360 min -// (1/2) = d^360 => d = (1/2)^(1/360) -uint256 constant REDEMPTION_MINUTE_DECAY_FACTOR = 998076443575628800; - -// BETA: 18 digit decimal. Parameter by which to divide the redeemed fraction, in order to calc the new base rate from a redemption. -// Corresponds to (1 / ALPHA) in the white paper. -uint256 constant REDEMPTION_BETA = 1; - -// To prevent redemptions unless Bold depegs below 0.95 and allow the system to take off -uint256 constant INITIAL_BASE_RATE = _100pct; // 100% initial redemption rate - -// Discount to be used once the shutdown thas been triggered -uint256 constant URGENT_REDEMPTION_BONUS = 2e16; // 2% - uint256 constant ONE_MINUTE = 1 minutes; uint256 constant ONE_YEAR = 365 days; + +// Interest rate parameters +uint256 constant MAX_ANNUAL_INTEREST_RATE = 250 * _1pct; // 250% +uint128 constant MAX_ANNUAL_BATCH_MANAGEMENT_FEE = uint128(_100pct / 10); // 10% +uint128 constant MIN_INTEREST_RATE_CHANGE_PERIOD = 1 hours; uint256 constant UPFRONT_INTEREST_PERIOD = 7 days; uint256 constant INTEREST_RATE_ADJ_COOLDOWN = 7 days; -uint256 constant SP_YIELD_SPLIT = 75 * _1pct; // 75% +// Batch parameters +uint256 constant MAX_BATCH_SHARES_RATIO = 1e9; -uint256 constant MIN_BOLD_IN_SP = 1e18; +// Redemption parameters +uint256 constant URGENT_REDEMPTION_BONUS = 1 * _1pct; // 1% -// Dummy contract that lets legacy Hardhat tests query some of the constants -contract Constants { - uint256 public constant _ETH_GAS_COMPENSATION = ETH_GAS_COMPENSATION; - uint256 public constant _MIN_DEBT = MIN_DEBT; -} +// Liquidation parameters +uint256 constant MAX_LIQUIDATION_PENALTY_REDISTRIBUTION = 20 * _1pct; // 20% diff --git a/contracts/src/Dependencies/LiquityBaseInit.sol b/contracts/src/Dependencies/LiquityBaseInit.sol new file mode 100644 index 000000000..a645d4931 --- /dev/null +++ b/contracts/src/Dependencies/LiquityBaseInit.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import "./Constants.sol"; +import "./LiquityMath.sol"; +import "../Interfaces/IAddressesRegistry.sol"; +import "../Interfaces/IActivePool.sol"; +import "../Interfaces/IDefaultPool.sol"; +import "../Interfaces/IPriceFeed.sol"; +import "../Interfaces/ILiquityBase.sol"; +import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; + +/* + * Base contract for TroveManager, BorrowerOperations and StabilityPool. Contains global system constants and + * common functions. + */ +contract LiquityBaseInit is Initializable, ILiquityBase { + IActivePool public activePool; + IDefaultPool internal defaultPool; + IPriceFeed internal priceFeed; + + event ActivePoolAddressChanged(address _newActivePoolAddress); + event DefaultPoolAddressChanged(address _newDefaultPoolAddress); + event PriceFeedAddressChanged(address _newPriceFeedAddress); + + function __LiquityBase_init(IAddressesRegistry _addressesRegistry) internal onlyInitializing { + activePool = _addressesRegistry.activePool(); + defaultPool = _addressesRegistry.defaultPool(); + priceFeed = _addressesRegistry.priceFeed(); + + emit ActivePoolAddressChanged(address(activePool)); + emit DefaultPoolAddressChanged(address(defaultPool)); + emit PriceFeedAddressChanged(address(priceFeed)); + } + + // --- Gas compensation functions --- + + function getEntireBranchColl() public view returns (uint256 entireSystemColl) { + uint256 activeColl = activePool.getCollBalance(); + uint256 liquidatedColl = defaultPool.getCollBalance(); + + return activeColl + liquidatedColl; + } + + function getEntireBranchDebt() public view returns (uint256 entireSystemDebt) { + uint256 activeDebt = activePool.getBoldDebt(); + uint256 closedDebt = defaultPool.getBoldDebt(); + + return activeDebt + closedDebt; + } + + function _getTCR(uint256 _price) internal view returns (uint256 TCR) { + uint256 entireSystemColl = getEntireBranchColl(); + uint256 entireSystemDebt = getEntireBranchDebt(); + + TCR = LiquityMath._computeCR(entireSystemColl, entireSystemDebt, _price); + + return TCR; + } + + function _checkBelowCriticalThreshold(uint256 _price, uint256 _CCR) internal view returns (bool) { + uint256 TCR = _getTCR(_price); + + return TCR < _CCR; + } + + function _calcInterest(uint256 _weightedDebt, uint256 _period) internal pure returns (uint256) { + return (_weightedDebt * _period) / ONE_YEAR / DECIMAL_PRECISION; + } + + uint256[47] private __gap; +} diff --git a/contracts/src/GasPool.sol b/contracts/src/GasPool.sol index cadd7a6dc..917bb5f95 100644 --- a/contracts/src/GasPool.sol +++ b/contracts/src/GasPool.sol @@ -17,13 +17,13 @@ import "./Interfaces/ITroveManager.sol"; */ contract GasPool { constructor(IAddressesRegistry _addressesRegistry) { - IWETH WETH = _addressesRegistry.WETH(); + IERC20Metadata gasToken = _addressesRegistry.gasToken(); IBorrowerOperations borrowerOperations = _addressesRegistry.borrowerOperations(); ITroveManager troveManager = _addressesRegistry.troveManager(); // Allow BorrowerOperations to refund gas compensation - WETH.approve(address(borrowerOperations), type(uint256).max); + gasToken.approve(address(borrowerOperations), type(uint256).max); // Allow TroveManager to pay gas compensation to liquidator - WETH.approve(address(troveManager), type(uint256).max); + gasToken.approve(address(troveManager), type(uint256).max); } } diff --git a/contracts/src/HintHelpers.sol b/contracts/src/HintHelpers.sol index 6cd6c2b99..a9665290e 100644 --- a/contracts/src/HintHelpers.sol +++ b/contracts/src/HintHelpers.sol @@ -3,16 +3,24 @@ pragma solidity 0.8.24; import "./Interfaces/ICollateralRegistry.sol"; +import "./Interfaces/IActivePool.sol"; +import "./Interfaces/ISortedTroves.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityMath.sol"; import "./Dependencies/Constants.sol"; import "./Interfaces/IHintHelpers.sol"; +import "./Types/LatestTroveData.sol"; +import "./Types/TroveChange.sol"; +import "./Types/LatestBatchData.sol"; contract HintHelpers is IHintHelpers { string public constant NAME = "HintHelpers"; ICollateralRegistry public immutable collateralRegistry; + address public immutable systemParamsAddress; - constructor(ICollateralRegistry _collateralRegistry) { + constructor(ICollateralRegistry _collateralRegistry, ISystemParams _systemParams) { + systemParamsAddress = address(_systemParams); collateralRegistry = _collateralRegistry; } @@ -64,7 +72,7 @@ contract HintHelpers is IHintHelpers { } } - function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal view returns (uint256) { return _debt * _avgInterestRate * UPFRONT_INTEREST_PERIOD / ONE_YEAR / DECIMAL_PRECISION; } diff --git a/contracts/src/Interfaces/IAddressesRegistry.sol b/contracts/src/Interfaces/IAddressesRegistry.sol index 5d2f45faf..cb3644fbb 100644 --- a/contracts/src/Interfaces/IAddressesRegistry.sol +++ b/contracts/src/Interfaces/IAddressesRegistry.sol @@ -37,16 +37,10 @@ interface IAddressesRegistry { IMultiTroveGetter multiTroveGetter; ICollateralRegistry collateralRegistry; IBoldToken boldToken; - IWETH WETH; + IERC20Metadata gasToken; + address liquidityStrategy; } - function CCR() external returns (uint256); - function SCR() external returns (uint256); - function MCR() external returns (uint256); - function BCR() external returns (uint256); - function LIQUIDATION_PENALTY_SP() external returns (uint256); - function LIQUIDATION_PENALTY_REDISTRIBUTION() external returns (uint256); - function collToken() external view returns (IERC20Metadata); function borrowerOperations() external view returns (IBorrowerOperations); function troveManager() external view returns (ITroveManager); @@ -64,7 +58,8 @@ interface IAddressesRegistry { function multiTroveGetter() external view returns (IMultiTroveGetter); function collateralRegistry() external view returns (ICollateralRegistry); function boldToken() external view returns (IBoldToken); - function WETH() external returns (IWETH); + function gasToken() external view returns (IERC20Metadata); + function liquidityStrategy() external view returns (address); function setAddresses(AddressVars memory _vars) external; -} +} \ No newline at end of file diff --git a/contracts/src/Interfaces/IBatchManagerOperations.sol b/contracts/src/Interfaces/IBatchManagerOperations.sol new file mode 100644 index 000000000..8dce41b07 --- /dev/null +++ b/contracts/src/Interfaces/IBatchManagerOperations.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./ISortedTroves.sol"; +import "./ITroveManager.sol"; + +interface IBatchManagerOperations { + error IsShutDown(); + error InterestNotInRange(); + error BatchInterestRateChangePeriodNotPassed(); + error InvalidInterestBatchManager(); + error BatchManagerExists(); + error NewFeeNotLower(); + error AnnualManagementFeeTooHigh(); + error MinInterestRateChangePeriodTooLow(); + error MinGeMax(); + error NotBorrower(); + error TroveNotActive(); + error TroveNotInBatch(); + error TroveNotOpen(); + error ICRBelowMCRPlusBCR(); + error TCRBelowCCR(); + error ICRBelowMCR(); + error UpfrontFeeTooHigh(); + error InterestRateTooLow(); + error InterestRateTooHigh(); + error BatchSharesRatioTooLow(); + + struct LocalVariables_setInterestBatchManager { + ITroveManager troveManager; + IActivePool activePool; + ISortedTroves sortedTroves; + LatestTroveData trove; + LatestBatchData newBatch; + } + + struct LocalVariables_removeFromBatch { + ITroveManager troveManager; + ISortedTroves sortedTroves; + address batchManager; + LatestTroveData trove; + LatestBatchData batch; + uint256 batchFutureDebt; + TroveChange batchChange; + } + + function registerBatchManager( + uint128 _minInterestRate, + uint128 _maxInterestRate, + uint128 _currentInterestRate, + uint128 _annualManagementFee, + uint128 _minInterestRateChangePeriod + ) external; + + function lowerBatchManagementFee(uint256 _newAnnualManagementFee) external; + + function setBatchManagerAnnualInterestRate( + uint128 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee, + uint256 _minInterestRateChangePeriod + ) external; + + function setInterestBatchManager( + uint256 _troveId, + address _newBatchManager, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external; + + function kickFromBatch(uint256 _troveId, uint256 _upperHint, uint256 _lowerHint) external; + + function removeFromBatch( + uint256 _troveId, + uint256 _newAnnualInterestRate, + uint256 _upperHint, + uint256 _lowerHint, + uint256 _maxUpfrontFee + ) external; +} diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index 8e8dc38da..7d93cf7e7 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -14,7 +14,6 @@ import "./IWETH.sol"; interface IBorrowerOperations is ILiquityBase, IAddRemoveManagers { function CCR() external view returns (uint256); function MCR() external view returns (uint256); - function SCR() external view returns (uint256); function openTrove( address _owner, diff --git a/contracts/src/Interfaces/ICollateralRegistry.sol b/contracts/src/Interfaces/ICollateralRegistry.sol index 418db3f78..e75f8d8ef 100644 --- a/contracts/src/Interfaces/ICollateralRegistry.sol +++ b/contracts/src/Interfaces/ICollateralRegistry.sol @@ -9,7 +9,8 @@ import "./ITroveManager.sol"; interface ICollateralRegistry { function baseRate() external view returns (uint256); function lastFeeOperationTime() external view returns (uint256); - + function liquidityStrategy() external view returns (address); + function redeemCollateralRebalancing(uint256 _boldamount, uint256 _maxIterationsPerCollateral, uint256 _troveOwnerFee) external; function redeemCollateral(uint256 _boldamount, uint256 _maxIterations, uint256 _maxFeePercentage) external; // getters function totalCollaterals() external view returns (uint256); diff --git a/contracts/src/Interfaces/IFPMMFactory.sol b/contracts/src/Interfaces/IFPMMFactory.sol new file mode 100644 index 000000000..c3c184cb8 --- /dev/null +++ b/contracts/src/Interfaces/IFPMMFactory.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +interface IFPMMFactory { + /* ========================================== */ + /* ================= Events ================= */ + /* ========================================== */ + + /** + * @notice Emitted when a new FPMM is deployed. + * @param token0 The address of the first token + * @param token1 The address of the second token + * @param fpmmProxy The address of the deployed FPMM proxy + * @param fpmmImplementation The address of the deployed FPMM implementation + */ + event FPMMDeployed(address indexed token0, address indexed token1, address fpmmProxy, address fpmmImplementation); + + /** + * @notice Emitted when a new FPMM implementation is registered. + * @param implementation The address of the registered implementation + */ + event FPMMImplementationRegistered(address indexed implementation); + + /** + * @notice Emitted when a new FPMM implementation is unregistered. + * @param implementation The address of the unregistered implementation + */ + event FPMMImplementationUnregistered(address indexed implementation); + + /** + * @notice Emitted when the proxy admin is set. + * @param proxyAdmin The address of the new proxy admin + */ + event ProxyAdminSet(address indexed proxyAdmin); + + /** + * @notice Emitted when the sorted oracles address is set. + * @param sortedOracles The address of the new sorted oracles contract + */ + event SortedOraclesSet(address indexed sortedOracles); + + /** + * @notice Emitted when the breaker box address is set. + * @param breakerBox The address of the new breaker box contract + */ + event BreakerBoxSet(address indexed breakerBox); + + /** + * @notice Emitted when the governance address is set. + * @param governance The address of the new governance contract + */ + event GovernanceSet(address indexed governance); + + /* ======================================================== */ + /* ==================== View Functions ==================== */ + /* ======================================================== */ + + /** + * @notice Gets the address of the sorted oracles contract. + * @return The address of the sorted oracles contract + */ + function sortedOracles() external view returns (address); + + /** + * @notice Gets the address of the proxy admin contract. + * @return The address of the proxy admin contract + */ + function proxyAdmin() external view returns (address); + + /** + * @notice Gets the address of the breaker box contract. + * @return The address of the breaker box contract + */ + function breakerBox() external view returns (address); + + /** + * @notice Gets the address of the governance contract. + * @return The address of the governance contract + */ + function governance() external view returns (address); + + /** + * @notice Gets the address of the deployed FPMM for a token pair. + * @param token0 The address of the first token + * @param token1 The address of the second token + * @return The address of the deployed FPMM for the token pair + */ + function deployedFPMMs(address token0, address token1) external view returns (address); + + /** + * @notice Gets the list of deployed FPMM addresses. + * @return The list of deployed FPMM addresses + */ + function deployedFPMMAddresses() external view returns (address[] memory); + + /** + * @notice Checks if a FPMM implementation is registered. + * @param fpmmImplementation The address of the FPMM implementation + * @return True if the FPMM implementation is registered, false otherwise + */ + function isRegisteredImplementation(address fpmmImplementation) external view returns (bool); + + /** + * @notice Gets the list of registered FPMM implementations. + * @return The list of registered FPMM implementations + */ + function registeredImplementations() external view returns (address[] memory); + + /** + * @notice Gets the precomputed or current proxy address for a token pair. + * @param token0 The address of the first token + * @param token1 The address of the second token + * @return The address of the FPMM proxy for the token pair + */ + function getOrPrecomputeProxyAddress(address token0, address token1) external view returns (address); + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /** + * @notice Initializes the factory with required addresses. + * @param _sortedOracles The address of the sorted oracles contract + * @param _proxyAdmin The address of the proxy admin contract + * @param _breakerBox The address of the breaker box contract + * @param _governance The address of the governance contract + * @param _fpmmImplementation The address of the FPMM implementation + */ + function initialize( + address _sortedOracles, + address _proxyAdmin, + address _breakerBox, + address _governance, + address _fpmmImplementation + ) external; + + /** + * @notice Sets the address of the sorted oracles contract. + * @param _sortedOracles The new address of the sorted oracles contract + */ + function setSortedOracles(address _sortedOracles) external; + + /** + * @notice Sets the address of the proxy admin contract. + * @param _proxyAdmin The new address of the proxy admin contract + */ + function setProxyAdmin(address _proxyAdmin) external; + + /** + * @notice Sets the address of the breaker box contract. + * @param _breakerBox The new address of the breaker box contract + */ + function setBreakerBox(address _breakerBox) external; + + /** + * @notice Sets the address of the governance contract. + * @param _governance The new address of the governance contract + */ + function setGovernance(address _governance) external; + + /** + * @notice Registers a new FPMM implementation address. + * @param fpmmImplementation The FPMM implementation address to register + */ + function registerFPMMImplementation(address fpmmImplementation) external; + + /** + * @notice Unregisters a FPMM implementation address. + * @param fpmmImplementation The FPMM implementation address to unregister + * @param index The index of the FPMM implementation to unregister + */ + function unregisterFPMMImplementation(address fpmmImplementation, uint256 index) external; + + /** + * @notice Deploys a new FPMM for a token pair using the default parameters. + * @param fpmmImplementation The address of the FPMM implementation + * @param token0 The address of the first token + * @param token1 The address of the second token + * @param referenceRateFeedID The address of the reference rate feed + * @return proxy The address of the deployed FPMM proxy + */ + function deployFPMM(address fpmmImplementation, address token0, address token1, address referenceRateFeedID) + external + returns (address proxy); + + /** + * @notice Deploys a new FPMM for a token pair using custom parameters. + * @param fpmmImplementation The address of the FPMM implementation + * @param customSortedOracles The address of the custom sorted oracles contract + * @param customProxyAdmin The address of the custom proxy admin contract + * @param customBreakerBox The address of the custom breaker box contract + * @param customGovernance The address of the custom governance contract + * @param token0 The address of the first token + * @param token1 The address of the second token + * @param referenceRateFeedID The address of the reference rate feed + * @return proxy The address of the deployed FPMM proxy + */ + function deployFPMM( + address fpmmImplementation, + address customSortedOracles, + address customProxyAdmin, + address customBreakerBox, + address customGovernance, + address token0, + address token1, + address referenceRateFeedID + ) external returns (address proxy); +} diff --git a/contracts/src/Interfaces/IMainnetPriceFeed.sol b/contracts/src/Interfaces/IMainnetPriceFeed.sol deleted file mode 100644 index ee8580236..000000000 --- a/contracts/src/Interfaces/IMainnetPriceFeed.sol +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: MIT -import "../Interfaces/IPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IMainnetPriceFeed is IPriceFeed { - enum PriceSource { - primary, - ETHUSDxCanonical, - lastGoodPrice - } - - function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); - function priceSource() external view returns (PriceSource); -} diff --git a/contracts/src/Interfaces/IOracleAdapter.sol b/contracts/src/Interfaces/IOracleAdapter.sol new file mode 100644 index 000000000..083d33ca4 --- /dev/null +++ b/contracts/src/Interfaces/IOracleAdapter.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +/** + * @title IOracleAdapter + * @notice Interface for the Oracle Adapter contract that provides FX rate data + */ +interface IOracleAdapter { + /** + * @notice Returns the exchange rate for a given rate feed ID + * with 18 decimals of precision if considered valid, based on + * FX market hours, trading mode, and recent rate, otherwise reverts + * @param rateFeedID The address of the rate feed + * @return numerator The numerator of the rate + * @return denominator The denominator of the rate + */ + function getFXRateIfValid(address rateFeedID) external view returns (uint256 numerator, uint256 denominator); + + /** + * @notice Returns true if the L2 sequencer has been up and operational for at least the specified duration. + * @param since The minimum number of seconds the L2 sequencer must have been up (e.g., 1 hours = 3600). + * @return up True if the sequencer has been up for at least `since` seconds, false otherwise + */ + function isL2SequencerUp(uint256 since) external view returns (bool up); +} diff --git a/contracts/src/Interfaces/IPriceFeed.sol b/contracts/src/Interfaces/IPriceFeed.sol index ca49b06cf..63c813fd8 100644 --- a/contracts/src/Interfaces/IPriceFeed.sol +++ b/contracts/src/Interfaces/IPriceFeed.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; interface IPriceFeed { - function fetchPrice() external returns (uint256, bool); - function fetchRedemptionPrice() external returns (uint256, bool); - function lastGoodPrice() external view returns (uint256); + function fetchPrice() external returns (uint256); + function isL2SequencerUp() external view returns (bool); } diff --git a/contracts/src/Interfaces/IRETHPriceFeed.sol b/contracts/src/Interfaces/IRETHPriceFeed.sol deleted file mode 100644 index 862bf6874..000000000 --- a/contracts/src/Interfaces/IRETHPriceFeed.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -import "./IMainnetPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IRETHPriceFeed is IMainnetPriceFeed { - function rEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} diff --git a/contracts/src/Interfaces/IRETHToken.sol b/contracts/src/Interfaces/IRETHToken.sol deleted file mode 100644 index df5e840be..000000000 --- a/contracts/src/Interfaces/IRETHToken.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -interface IRETHToken { - function getExchangeRate() external view returns (uint256); -} diff --git a/contracts/src/Interfaces/IStabilityPool.sol b/contracts/src/Interfaces/IStabilityPool.sol index f3229b931..16ced277c 100644 --- a/contracts/src/Interfaces/IStabilityPool.sol +++ b/contracts/src/Interfaces/IStabilityPool.sol @@ -7,6 +7,8 @@ import "./ILiquityBase.sol"; import "./IBoldToken.sol"; import "./ITroveManager.sol"; import "./IBoldRewardsReceiver.sol"; +import "./IAddressesRegistry.sol"; +import "./ISystemParams.sol"; /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. @@ -29,9 +31,12 @@ import "./IBoldRewardsReceiver.sol"; * */ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { + function initialize(IAddressesRegistry _addressesRegistry) external; + function boldToken() external view returns (IBoldToken); function troveManager() external view returns (ITroveManager); - + function systemParams() external view returns (ISystemParams); + /* provideToSP(): * - Calculates depositor's Coll gain * - Calculates the compounded deposit @@ -51,6 +56,14 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function claimAllCollGains() external; + /* + * Stable token liquidity in the stability pool can be used to rebalance FPMM pools. + * Collateral will be swapped for stable tokens in the SP. + * Removed stable tokens will be factored out from LPs' positions. + * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. + */ + function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut) external; + /* * Initial checks: * - Caller is TroveManager @@ -104,6 +117,7 @@ interface IStabilityPool is ILiquityBase, IBoldRewardsReceiver { function P() external view returns (uint256); function currentScale() external view returns (uint256); + function liquidityStrategy() external view returns (address); function P_PRECISION() external view returns (uint256); } diff --git a/contracts/src/Interfaces/IStableTokenV3.sol b/contracts/src/Interfaces/IStableTokenV3.sol new file mode 100644 index 000000000..7b55a9e93 --- /dev/null +++ b/contracts/src/Interfaces/IStableTokenV3.sol @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.24; + +/** + * @title IStableTokenV3 + * @notice Interface for the StableTokenV3 contract. + */ +interface IStableTokenV3 { + /** + * @notice Checks if an address is a minter. + * @param account The address to check. + * @return bool True if the address is a minter, false otherwise. + */ + function isMinter(address account) external view returns (bool); + /** + * @notice Checks if an address is a burner. + * @param account The address to check. + * @return bool True if the address is a burner, false otherwise. + */ + function isBurner(address account) external view returns (bool); + /** + * @notice Checks if an address is an operator. + * @param account The address to check. + * @return bool True if the address is an operator, false otherwise. + */ + function isOperator(address account) external view returns (bool); + + /** + * @notice Initializes a StableTokenV3. + * @param _name The name of the stable token (English) + * @param _symbol A short symbol identifying the token (e.g. "cUSD") + * @param _initialOwner The address that will be the owner of the contract. + * @param initialBalanceAddresses Array of addresses with an initial balance. + * @param initialBalanceValues Array of balance values corresponding to initialBalanceAddresses. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initialize( + string calldata _name, + string calldata _symbol, + address _initialOwner, + address[] calldata initialBalanceAddresses, + uint256[] calldata initialBalanceValues, + address[] calldata _minters, + address[] calldata _burners, + address[] calldata _operators + ) external; + + /** + * @notice Initializes a StableTokenV3 contract + * when upgrading from StableTokenV2.sol. + * It sets the addresses of the minters, burners, and operators. + * @dev This function is only callable once. + * @param _minters The addresses that are allowed to mint. + * @param _burners The addresses that are allowed to burn. + * @param _operators The addresses that are allowed to call the operator functions. + */ + function initializeV3(address[] calldata _minters, address[] calldata _burners, address[] calldata _operators) + external; + + /** + * @notice Sets the operator role for an address. + * @param _operator The address of the operator. + * @param _isOperator The boolean value indicating if the address is an operator. + */ + function setOperator(address _operator, bool _isOperator) external; + + /** + * @notice Sets the minter role for an address. + * @param _minter The address of the minter. + * @param _isMinter The boolean value indicating if the address is a minter. + */ + function setMinter(address _minter, bool _isMinter) external; + + /** + * @notice Sets the burner role for an address. + * @param _burner The address of the burner. + * @param _isBurner The boolean value indicating if the address is a burner. + */ + function setBurner(address _burner, bool _isBurner) external; + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * From openzeppelin's IERC20.sol + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * From openzeppelin's IERC20.sol + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @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 returns (bool); + + /** + * @notice Burns StableToken from the balance of msg.sender. + * @param value The amount of StableToken to burn. + */ + function burn(uint256 value) external returns (bool); + + /** + * @notice Burns StableToken from the balance of an account. + * @param account The account to burn from. + * @param value The amount of StableToken to burn. + */ + function burn(address account, uint256 value) external returns (bool); + + /** + * From openzeppelin's IERC20PermitUpgradeable.sol + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /** + * @notice Transfer token from a specified address to the stability pool. + * @param _sender The address to transfer from. + * @param _poolAddress The address of the pool to transfer to. + * @param _amount The amount to be transferred. + */ + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external; + + /** + * @notice Transfer token to a specified address from the stability pool. + * @param _poolAddress The address of the pool to transfer from + * @param _receiver The address to transfer to. + * @param _amount The amount to be transferred. + */ + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external; + + /** + * @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`. + */ + function debitGasFees(address from, uint256 value) external; + + /** + * @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`. + */ + function creditGasFees( + address from, + address feeRecipient, + address gatewayFeeRecipient, + address communityFund, + uint256 refund, + uint256 tipTxFee, + uint256 gatewayFee, + uint256 baseTxFee + ) external; + + /** + * @notice Credit gas fees to multiple addresses. + * @param recipients The addresses to credit the fees to. + * @param amounts The amounts of fees to credit to each address. + */ + function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) external; +} diff --git a/contracts/src/Interfaces/ISystemParams.sol b/contracts/src/Interfaces/ISystemParams.sol new file mode 100644 index 000000000..3e9e374af --- /dev/null +++ b/contracts/src/Interfaces/ISystemParams.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +interface ISystemParams { + /* ========== PARAMETER STRUCTS ========== */ + + struct DebtParams { + uint256 minDebt; + } + + struct LiquidationParams { + uint256 liquidationPenaltySP; + uint256 liquidationPenaltyRedistribution; + } + + struct GasCompParams { + uint256 collGasCompensationDivisor; + uint256 collGasCompensationCap; + uint256 ethGasCompensation; + } + + struct CollateralParams { + uint256 ccr; + uint256 scr; + uint256 mcr; + uint256 bcr; + } + + struct InterestParams { + uint256 minAnnualInterestRate; + } + + struct RedemptionParams { + uint256 redemptionFeeFloor; + uint256 initialBaseRate; + uint256 redemptionMinuteDecayFactor; + uint256 redemptionBeta; + } + + struct StabilityPoolParams { + uint256 spYieldSplit; + uint256 minBoldInSP; + uint256 minBoldAfterRebalance; + } + + /* ========== ERRORS ========== */ + + error InvalidMinDebt(); + error InvalidInterestRateBounds(); + error InvalidFeeValue(); + error InvalidTimeValue(); + error InvalidGasCompensation(); + error MinInterestRateGtMax(); + error InvalidCCR(); + error InvalidMCR(); + error InvalidBCR(); + error InvalidSCR(); + error SPPenaltyTooLow(); + error SPPenaltyGtRedist(); + error RedistPenaltyTooHigh(); + error InvalidMinBoldInSP(); + + /* ========== DEBT PARAMETERS ========== */ + + /// @notice Minimum amount of net debt a trove must have. + function MIN_DEBT() external view returns (uint256); + + /* ========== LIQUIDATION PARAMETERS ========== */ + + /// @notice Liquidation penalty for troves offset to the SP + function LIQUIDATION_PENALTY_SP() external view returns (uint256); + + /// @notice Liquidation penalty for troves redistributed. + function LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256); + + /* ========== GAS COMPENSATION PARAMETERS ========== */ + + /// @notice Divisor for calculating collateral gas compensation for liquidators. + function COLL_GAS_COMPENSATION_DIVISOR() external view returns (uint256); + + /// @notice Maximum collateral gas compensation cap for liquidators. + function COLL_GAS_COMPENSATION_CAP() external view returns (uint256); + + // TODO(@bayological): Consider rename to native(or just drop eth) if this makes sense + // and update comment. + /// @notice Amount of ETH to be locked in gas pool on opening troves. + function ETH_GAS_COMPENSATION() external view returns (uint256); + + /* ========== COLLATERAL PARAMETERS ========== */ + + /** + * @notice Critical system collateral ratio. + * @dev If the system's total collateral ratio (TCR) falls below the CCR, some borrowing + * operation restrictions are applied. + */ + function CCR() external view returns (uint256); + + /** + * @notice Shutdown system collateral ratio. + * @dev If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, + * the protocol triggers the shutdown of the borrow market and permanently disables all + * borrowing operations except for closing Troves. + */ + function SCR() external view returns (uint256); + + /// @notice Minimum collateral ratio for individual troves. + function MCR() external view returns (uint256); + + /** + * @notice Extra buffer of collateral ratio to join a batch or adjust a trove inside + * a batch (on top of MCR). + */ + function BCR() external view returns (uint256); + + /* ========== INTEREST PARAMETERS ========== */ + + /// @notice Min annual interest rate for a trove. + function MIN_ANNUAL_INTEREST_RATE() external view returns (uint256); + + /* ========== REDEMPTION PARAMETERS ========== */ + + /// @notice Minimum redemption fee percentage. + function REDEMPTION_FEE_FLOOR() external view returns (uint256); + + /// @notice The initial redemption fee value. + function INITIAL_BASE_RATE() external view returns (uint256); + + /// @notice Factor to reduce the redemption fee per minute. + function REDEMPTION_MINUTE_DECAY_FACTOR() external view returns (uint256); + + /// @notice Divisor controlling base rate sensitivity to redemption volume (higher = less sensitive). + function REDEMPTION_BETA() external view returns (uint256); + + /* ========== STABILITY POOL PARAMETERS ========== */ + + /// @notice Percentage of minted interest yield allocated to Stability Pool depositors. + function SP_YIELD_SPLIT() external view returns (uint256); + + /// @notice Minimum BOLD that must remain in Stability Pool to prevent complete drainage. + function MIN_BOLD_IN_SP() external view returns (uint256); + + /// @notice Minimum BOLD that must remain in Stability Pool after a rebalance operation. + function MIN_BOLD_AFTER_REBALANCE() external view returns (uint256); + + function initialize() external; +} diff --git a/contracts/src/Interfaces/IWSTETH.sol b/contracts/src/Interfaces/IWSTETH.sol deleted file mode 100644 index eab84d38d..000000000 --- a/contracts/src/Interfaces/IWSTETH.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface IWSTETH { - function wrap(uint256 _stETHAmount) external returns (uint256); - function unwrap(uint256 _wstETHAmount) external returns (uint256); - function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256); - function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256); - function stEthPerToken() external view returns (uint256); - function tokensPerStEth() external view returns (uint256); -} diff --git a/contracts/src/Interfaces/IWSTETHPriceFeed.sol b/contracts/src/Interfaces/IWSTETHPriceFeed.sol deleted file mode 100644 index cfb20934e..000000000 --- a/contracts/src/Interfaces/IWSTETHPriceFeed.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -import "./IMainnetPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IWSTETHPriceFeed is IMainnetPriceFeed { - function stEthUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} diff --git a/contracts/src/MultiTroveGetter.sol b/contracts/src/MultiTroveGetter.sol index d9a6c47dd..9bee71786 100644 --- a/contracts/src/MultiTroveGetter.sol +++ b/contracts/src/MultiTroveGetter.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IMultiTroveGetter.sol"; +import "./Interfaces/ISortedTroves.sol"; import "./Types/BatchId.sol"; /* Helper contract for grabbing Trove data for the front end. Not part of the core Liquity system. */ diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol deleted file mode 100644 index 995727d14..000000000 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "../Dependencies/LiquityMath.sol"; -import "./MainnetPriceFeedBase.sol"; - -// import "forge-std/console2.sol"; - -// The CompositePriceFeed is used for feeds that incorporate both a market price oracle (e.g. STETH-USD, or RETH-ETH) -// and an LST canonical rate (e.g. WSTETH:STETH, or RETH:ETH). -abstract contract CompositePriceFeed is MainnetPriceFeedBase { - address public rateProviderAddress; - - constructor( - address _ethUsdOracleAddress, - address _rateProviderAddress, - uint256 _ethUsdStalenessThreshold, - address _borrowerOperationsAddress - ) MainnetPriceFeedBase(_ethUsdOracleAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) { - // Store rate provider - rateProviderAddress = _rateProviderAddress; - } - - // Returns: - // - The price, using the current price calculation - // - A bool that is true if: - // --- a) the system was not shut down prior to this call, and - // --- b) an oracle or exchange rate contract failed during this call. - function fetchPrice() public returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(false); - - return _fetchPriceDuringShutdown(); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(true); - - return _fetchPriceDuringShutdown(); - } - - function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice) - internal - returns (uint256) - { - // Shut down the branch - borrowerOperations.shutdownFromOracleFailure(); - - priceSource = PriceSource.ETHUSDxCanonical; - - emit ShutDownFromOracleFailure(_failedOracleAddr); - return _fetchPriceETHUSDxCanonical(_ethUsdPrice); - } - - function _fetchPriceDuringShutdown() internal returns (uint256, bool) { - // When branch is already shut down and using ETH-USD * canonical_rate, try to use that - if (priceSource == PriceSource.ETHUSDxCanonical) { - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice - if (ethUsdOracleDown) { - // No need to shut down, since branch already is shut down - priceSource = PriceSource.lastGoodPrice; - return (lastGoodPrice, false); - } else { - return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); - } - } - - // Otherwise when branch is shut down and already using the lastGoodPrice, continue with it - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - // Only called if the primary LST oracle has failed, branch has shut down, - // and we've switched to using: ETH-USD * canonical_rate. - function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) { - assert(priceSource == PriceSource.ETHUSDxCanonical); - // Get the underlying_per_LST canonical rate directly from the LST contract - (uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate(); - - // If the exchange rate contract is down, switch to (and return) lastGoodPrice. - if (exchangeRateIsDown) { - priceSource = PriceSource.lastGoodPrice; - return lastGoodPrice; - } - - // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * underlying_per_LST - uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18; - - uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice); - - lastGoodPrice = bestPrice; - - return bestPrice; - } - - function _withinDeviationThreshold(uint256 _priceToCheck, uint256 _referencePrice, uint256 _deviationThreshold) - internal - pure - returns (bool) - { - // Calculate the price deviation of the oracle market price relative to the canonical price - uint256 max = _referencePrice * (DECIMAL_PRECISION + _deviationThreshold) / 1e18; - uint256 min = _referencePrice * (DECIMAL_PRECISION - _deviationThreshold) / 1e18; - - return _priceToCheck >= min && _priceToCheck <= max; - } - - // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: - // - The price - // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call - function _fetchPricePrimary(bool _isRedemption) internal virtual returns (uint256, bool); - - // Returns the LST exchange rate and a bool indicating whether the exchange rate failed to return a valid rate. - // Implementation depends on the specific LST. - function _getCanonicalRate() internal view virtual returns (uint256, bool); -} diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol new file mode 100644 index 000000000..44747c521 --- /dev/null +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "../Interfaces/IOracleAdapter.sol"; +import "../Interfaces/IPriceFeed.sol"; +import "../Interfaces/IBorrowerOperations.sol"; + +import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; +/** + * @title FXPriceFeed + * @author Mento Labs + * @notice A contract that fetches the price of an FX rate from an OracleAdapter. + * Implements emergency shutdown functionality to handle oracle failures. + */ + +contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { + /* ==================== State Variables ==================== */ + + /// @notice The OracleAdapter contract that provides FX rate data + IOracleAdapter public oracleAdapter; + + /// @notice The identifier address for the specific rate feed to query + address public rateFeedID; + + // @notice Whether the rate from the OracleAdapter should be inverted + bool public invertRateFeed; + + /// @notice The grace period for the L2 sequencer to recover from failure + uint256 public l2SequencerGracePeriod; + + /// @notice The watchdog contract address authorized to trigger emergency shutdown + address public watchdogAddress; + + /// @notice The BorrowerOperations contract + IBorrowerOperations public borrowerOperations; + + /// @notice The last valid price returned by the OracleAdapter + uint256 public lastValidPrice; + + /// @notice Whether the contract has been shutdown due to an oracle failure + bool public isShutdown; + + /// @notice Thrown when the attempting to shutdown an already shutdown contract + error AlreadyShutdown(); + + /// @notice Thrown when a non-watchdog address attempts to shutdown the contract + error OnlyWatchdog(); + /// @notice Thrown when a zero address is provided as a parameter + error ZeroAddress(); + /// @notice Thrown when an invalid grace period is provided + error InvalidL2SequencerGracePeriod(); + + /// @notice Emitted when the OracleAdapter contract is updated + /// @param _oldOracleAdapterAddress The previous OracleAdapter contract + /// @param _newOracleAdapterAddress The new OracleAdapter contract + event OracleAdapterUpdated(address indexed _oldOracleAdapterAddress, address indexed _newOracleAdapterAddress); + + /// @notice Emitted when the rate feed ID is updated + /// @param _oldRateFeedID The previous rate feed ID + /// @param _newRateFeedID The new rate feed ID + event RateFeedIDUpdated(address indexed _oldRateFeedID, address indexed _newRateFeedID); + + /// @notice Emitted when the invert rate feed flag is updated + /// @param _oldInvertRateFeed The previous invert rate feed flag + /// @param _newInvertRateFeed The new invert rate feed flag + event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); + + /// @notice Emitted when the L2 sequencer grace period is updated + /// @param _oldL2SequencerGracePeriod The previous L2 sequencer grace period + /// @param _newL2SequencerGracePeriod The new L2 sequencer grace period + event L2SequencerGracePeriodUpdated(uint256 indexed _oldL2SequencerGracePeriod, uint256 indexed _newL2SequencerGracePeriod); + + /// @notice Emitted when the watchdog address is updated + /// @param _oldWatchdogAddress The previous watchdog address + /// @param _newWatchdogAddress The new watchdog address + event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); + + /// @notice Emitted when the contract is shutdown due to oracle failure + event FXPriceFeedShutdown(); + + /** + * @notice Contract constructor + * @param disableInitializers Boolean to disable initializers for implementation contract + */ + constructor(bool disableInitializers) { + if (disableInitializers) { + _disableInitializers(); + } + } + + /** + * @notice Initializes the FXPriceFeed contract + * @param _oracleAdapterAddress The address of the OracleAdapter contract + * @param _rateFeedID The address of the rate feed ID + * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted + * @param _l2SequencerGracePeriod The grace period for the L2 sequencer to recover from failure + * @param _borrowerOperationsAddress The address of the BorrowerOperations contract + * @param _watchdogAddress The address of the watchdog contract + * @param _initialOwner The address of the initial owner + */ + function initialize( + address _oracleAdapterAddress, + address _rateFeedID, + bool _invertRateFeed, + uint256 _l2SequencerGracePeriod, + address _borrowerOperationsAddress, + address _watchdogAddress, + address _initialOwner + ) external initializer { + if (_oracleAdapterAddress == address(0)) revert ZeroAddress(); + if (_rateFeedID == address(0)) revert ZeroAddress(); + if (_borrowerOperationsAddress == address(0)) revert ZeroAddress(); + if (_watchdogAddress == address(0)) revert ZeroAddress(); + if (_initialOwner == address(0)) revert ZeroAddress(); + + oracleAdapter = IOracleAdapter(_oracleAdapterAddress); + rateFeedID = _rateFeedID; + invertRateFeed = _invertRateFeed; + l2SequencerGracePeriod = _l2SequencerGracePeriod; + borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); + watchdogAddress = _watchdogAddress; + + fetchPrice(); + + _transferOwnership(_initialOwner); + } + + + /** + * @notice Sets the OracleAdapter contract + * @param _newOracleAdapterAddress The address of the new OracleAdapter contract + */ + function setOracleAdapter(address _newOracleAdapterAddress) external onlyOwner { + if (_newOracleAdapterAddress == address(0)) revert ZeroAddress(); + + address oldOracleAdapter = address(oracleAdapter); + oracleAdapter = IOracleAdapter(_newOracleAdapterAddress); + + emit OracleAdapterUpdated(oldOracleAdapter, _newOracleAdapterAddress); + } + + /** + * @notice Sets the rate feed ID to be queried + * @param _newRateFeedID The address of the new rate feed ID + */ + function setRateFeedID(address _newRateFeedID) external onlyOwner { + if (_newRateFeedID == address(0)) revert ZeroAddress(); + + address oldRateFeedID = rateFeedID; + rateFeedID = _newRateFeedID; + + emit RateFeedIDUpdated(oldRateFeedID, _newRateFeedID); + } + + /** + * @notice Sets the invert rate feed flag + * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted + */ + function setInvertRateFeed(bool _invertRateFeed) external onlyOwner { + bool oldInvertRateFeed = invertRateFeed; + invertRateFeed = _invertRateFeed; + + emit InvertRateFeedUpdated(oldInvertRateFeed, _invertRateFeed); + } + + /** + * @notice Sets the L2 sequencer grace period + * @param _newL2SequencerGracePeriod The new L2 sequencer grace period (in seconds) + */ + function setL2SequencerGracePeriod(uint256 _newL2SequencerGracePeriod) external onlyOwner { + if (_newL2SequencerGracePeriod == 0) revert InvalidL2SequencerGracePeriod(); + + uint256 oldL2SequencerGracePeriod = l2SequencerGracePeriod; + l2SequencerGracePeriod = _newL2SequencerGracePeriod; + + emit L2SequencerGracePeriodUpdated(oldL2SequencerGracePeriod, _newL2SequencerGracePeriod); + } + + /** + * @notice Sets the watchdog address + * @param _newWatchdogAddress The address of the new watchdog contract + */ + function setWatchdogAddress(address _newWatchdogAddress) external onlyOwner { + if (_newWatchdogAddress == address(0)) revert ZeroAddress(); + + address oldWatchdogAddress = watchdogAddress; + watchdogAddress = _newWatchdogAddress; + + emit WatchdogAddressUpdated(oldWatchdogAddress, _newWatchdogAddress); + } + + /** + * @notice Checks if the L2 sequencer is up and the grace period has passed + * @return True if the L2 sequencer is up and the grace period has passed, false otherwise + */ + function isL2SequencerUp() public view returns (bool) { + return oracleAdapter.isL2SequencerUp(l2SequencerGracePeriod); + } + + /** + * @notice Fetches the price of the FX rate, if valid + * @dev If the contract is shutdown due to oracle failure, the last valid price is returned + * @return price The price of the FX rate + */ + function fetchPrice() public returns (uint256 price) { + if (isShutdown) { + return lastValidPrice; + } + + (uint256 numerator, uint256 denominator) = oracleAdapter.getFXRateIfValid(rateFeedID); + + if (invertRateFeed) { + // Multiply by 1e18 to get the price in 18 decimals + price = (denominator * 1e18) / numerator; + } else { + // Denominator is always 1e18, so we only use the numerator as the price + assert(denominator == 1e18); + price = numerator; + } + + lastValidPrice = price; + + return price; + } + + /** + * @notice Shuts down the price feed contract due to oracle failure + * @dev Can only be called by the authorized watchdog address. + * Once shutdown: + * - The contract will only return the last valid price + * - The BorrowerOperations and TroveManager contracts are notified to shut down the collateral branch + * - The shutdown state is permanent and cannot be reversed + */ + function shutdown() external { + if (isShutdown) revert AlreadyShutdown(); + if (msg.sender != watchdogAddress) revert OnlyWatchdog(); + + isShutdown = true; + borrowerOperations.shutdownFromOracleFailure(); + + emit FXPriceFeedShutdown(); + } +} diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol deleted file mode 100644 index f2f8f2412..000000000 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "../Dependencies/AggregatorV3Interface.sol"; -import "../Interfaces/IMainnetPriceFeed.sol"; -import "../BorrowerOperations.sol"; - -// import "forge-std/console2.sol"; - -abstract contract MainnetPriceFeedBase is IMainnetPriceFeed { - // Determines where the PriceFeed sources data from. Possible states: - // - primary: Uses the primary price calcuation, which depends on the specific feed - // - ETHUSDxCanonical: Uses Chainlink's ETH-USD multiplied by the LST' canonical rate - // - lastGoodPrice: the last good price recorded by this PriceFeed. - PriceSource public priceSource; - - // Last good price tracker for the derived USD price - uint256 public lastGoodPrice; - - struct Oracle { - AggregatorV3Interface aggregator; - uint256 stalenessThreshold; - uint8 decimals; - } - - struct ChainlinkResponse { - uint80 roundId; - int256 answer; - uint256 timestamp; - bool success; - } - - error InsufficientGasForExternalCall(); - - event ShutDownFromOracleFailure(address _failedOracleAddr); - - Oracle public ethUsdOracle; - - IBorrowerOperations borrowerOperations; - - constructor(address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold, address _borrowOperationsAddress) { - // Store ETH-USD oracle - ethUsdOracle.aggregator = AggregatorV3Interface(_ethUsdOracleAddress); - ethUsdOracle.stalenessThreshold = _ethUsdStalenessThreshold; - ethUsdOracle.decimals = ethUsdOracle.aggregator.decimals(); - - borrowerOperations = IBorrowerOperations(_borrowOperationsAddress); - - assert(ethUsdOracle.decimals == 8); - } - - function _getOracleAnswer(Oracle memory _oracle) internal view returns (uint256, bool) { - ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse(_oracle.aggregator); - - uint256 scaledPrice; - bool oracleIsDown; - // Check oracle is serving an up-to-date and sensible price. If not, shut down this collateral branch. - if (!_isValidChainlinkPrice(chainlinkResponse, _oracle.stalenessThreshold)) { - oracleIsDown = true; - } else { - scaledPrice = _scaleChainlinkPriceTo18decimals(chainlinkResponse.answer, _oracle.decimals); - } - - return (scaledPrice, oracleIsDown); - } - - function _shutDownAndSwitchToLastGoodPrice(address _failedOracleAddr) internal returns (uint256) { - // Shut down the branch - borrowerOperations.shutdownFromOracleFailure(); - - priceSource = PriceSource.lastGoodPrice; - - emit ShutDownFromOracleFailure(_failedOracleAddr); - return lastGoodPrice; - } - - function _getCurrentChainlinkResponse(AggregatorV3Interface _aggregator) - internal - view - returns (ChainlinkResponse memory chainlinkResponse) - { - uint256 gasBefore = gasleft(); - - // Try to get latest price data: - try _aggregator.latestRoundData() returns ( - uint80 roundId, int256 answer, uint256, /* startedAt */ uint256 updatedAt, uint80 /* answeredInRound */ - ) { - // If call to Chainlink succeeds, return the response and success = true - chainlinkResponse.roundId = roundId; - chainlinkResponse.answer = answer; - chainlinkResponse.timestamp = updatedAt; - chainlinkResponse.success = true; - - return chainlinkResponse; - } catch { - // Require that enough gas was provided to prevent an OOG revert in the call to Chainlink - // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used - // in the check itself. - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - - // If call to Chainlink aggregator reverts, return a zero response with success = false - return chainlinkResponse; - } - } - - // False if: - // - Call to Chainlink aggregator reverts - // - price is too stale, i.e. older than the oracle's staleness threshold - // - Price answer is 0 or negative - function _isValidChainlinkPrice(ChainlinkResponse memory chainlinkResponse, uint256 _stalenessThreshold) - internal - view - returns (bool) - { - return chainlinkResponse.success && block.timestamp - chainlinkResponse.timestamp < _stalenessThreshold - && chainlinkResponse.answer > 0; - } - - // Trust assumption: Chainlink won't change the decimal precision on any feed used in v2 after deployment - function _scaleChainlinkPriceTo18decimals(int256 _price, uint256 _decimals) internal pure returns (uint256) { - // Scale an int price to a uint with 18 decimals - return uint256(_price) * 10 ** (18 - _decimals); - } -} diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol deleted file mode 100644 index 30b513aa6..000000000 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Interfaces/IRETHToken.sol"; -import "../Interfaces/IRETHPriceFeed.sol"; - -// import "forge-std/console2.sol"; - -contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { - constructor( - address _ethUsdOracleAddress, - address _rEthEthOracleAddress, - address _rEthTokenAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _rEthEthStalenessThreshold, - address _borrowerOperationsAddress - ) - CompositePriceFeed(_ethUsdOracleAddress, _rEthTokenAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) - { - // Store RETH-ETH oracle - rEthEthOracle.aggregator = AggregatorV3Interface(_rEthEthOracleAddress); - rEthEthOracle.stalenessThreshold = _rEthEthStalenessThreshold; - rEthEthOracle.decimals = rEthEthOracle.aggregator.decimals(); - - _fetchPricePrimary(false); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - Oracle public rEthEthOracle; - - uint256 public constant RETH_ETH_DEVIATION_THRESHOLD = 2e16; // 2% - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { - assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); - (uint256 ethPerReth, bool exchangeRateIsDown) = _getCanonicalRate(); - - // If either the ETH-USD feed or exchange rate is down, shut down and switch to the last good price - // seen by the system since we need both for primary and fallback price calcs - if (ethUsdOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); - } - if (exchangeRateIsDown) { - return (_shutDownAndSwitchToLastGoodPrice(rateProviderAddress), true); - } - // If the ETH-USD feed is live but the RETH-ETH oracle is down, shutdown and substitute RETH-ETH with the canonical rate - if (rEthEthOracleDown) { - return (_shutDownAndSwitchToETHUSDxCanonical(address(rEthEthOracle.aggregator), ethUsdPrice), true); - } - - // Otherwise, use the primary price calculation: - - // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 rEthUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; - - // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 rEthUsdCanonicalPrice = ethUsdPrice * ethPerReth / 1e18; - - uint256 rEthUsdPrice; - - // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if ( - _isRedemption - && _withinDeviationThreshold(rEthUsdMarketPrice, rEthUsdCanonicalPrice, RETH_ETH_DEVIATION_THRESHOLD) - ) { - rEthUsdPrice = LiquityMath._max(rEthUsdMarketPrice, rEthUsdCanonicalPrice); - } else { - // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. - // Assumes a deviation between market <> canonical of >2% represents a legitimate market price difference. - rEthUsdPrice = LiquityMath._min(rEthUsdMarketPrice, rEthUsdCanonicalPrice); - } - - lastGoodPrice = rEthUsdPrice; - - return (rEthUsdPrice, false); - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); - - try IRETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerReth) { - // If rate is 0, return true - if (ethPerReth == 0) return (0, true); - - return (ethPerReth, false); - } catch { - // Require that enough gas was provided to prevent an OOG revert in the external call - // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used - // in the check itself. - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - - // If call to exchange rate reverts, return true - return (0, true); - } - } -} diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol deleted file mode 100644 index b2eb60420..000000000 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./MainnetPriceFeedBase.sol"; - -// import "forge-std/console2.sol"; - -contract WETHPriceFeed is MainnetPriceFeedBase { - constructor(address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold, address _borrowerOperationsAddress) - MainnetPriceFeedBase(_ethUsdOracleAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) - { - _fetchPricePrimary(); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - function fetchPrice() public returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); - - // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); - } - - function fetchRedemptionPrice() external returns (uint256, bool) { - // Use same price for redemption as all other ops in WETH branch - return fetchPrice(); - } - - // _fetchPricePrimary returns: - // - The price - // - A bool indicating whether a new oracle failure was detected in the call - function _fetchPricePrimary() internal returns (uint256, bool) { - assert(priceSource == PriceSource.primary); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - - // If the ETH-USD Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated - if (ethUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); - - lastGoodPrice = ethUsdPrice; - return (ethUsdPrice, false); - } -} diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol deleted file mode 100644 index f18b92d97..000000000 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Interfaces/IWSTETH.sol"; -import "../Interfaces/IWSTETHPriceFeed.sol"; - -// import "forge-std/console2.sol"; - -contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { - Oracle public stEthUsdOracle; - - uint256 public constant STETH_USD_DEVIATION_THRESHOLD = 1e16; // 1% - - constructor( - address _ethUsdOracleAddress, - address _stEthUsdOracleAddress, - address _wstEthTokenAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _stEthUsdStalenessThreshold, - address _borrowerOperationsAddress - ) - CompositePriceFeed(_ethUsdOracleAddress, _wstEthTokenAddress, _ethUsdStalenessThreshold, _borrowerOperationsAddress) - { - stEthUsdOracle.aggregator = AggregatorV3Interface(_stEthUsdOracleAddress); - stEthUsdOracle.stalenessThreshold = _stEthUsdStalenessThreshold; - stEthUsdOracle.decimals = stEthUsdOracle.aggregator.decimals(); - - _fetchPricePrimary(false); - - // Check the oracle didn't already fail - assert(priceSource == PriceSource.primary); - } - - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { - assert(priceSource == PriceSource.primary); - (uint256 stEthUsdPrice, bool stEthUsdOracleDown) = _getOracleAnswer(stEthUsdOracle); - (uint256 stEthPerWstEth, bool exchangeRateIsDown) = _getCanonicalRate(); - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - - // - If exchange rate or ETH-USD is down, shut down and switch to last good price. Reasoning: - // - Exchange rate is used in all price calcs - // - ETH-USD is used in the fallback calc, and for redemptions in the primary price calc - if (exchangeRateIsDown) { - return (_shutDownAndSwitchToLastGoodPrice(rateProviderAddress), true); - } - if (ethUsdOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); - } - - // If the STETH-USD feed is down, shut down and try to substitute it with the ETH-USD price - if (stEthUsdOracleDown) { - return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true); - } - - // Otherwise, use the primary price calculation: - uint256 wstEthUsdPrice; - - if (_isRedemption && _withinDeviationThreshold(stEthUsdPrice, ethUsdPrice, STETH_USD_DEVIATION_THRESHOLD)) { - // If it's a redemption and within 1%, take the max of (STETH-USD, ETH-USD) to mitigate unwanted redemption arb and convert to WSTETH-USD - wstEthUsdPrice = LiquityMath._max(stEthUsdPrice, ethUsdPrice) * stEthPerWstEth / 1e18; - } else { - // Otherwise, just calculate WSTETH-USD price: USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH - wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18; - } - - lastGoodPrice = wstEthUsdPrice; - - return (wstEthUsdPrice, false); - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { - uint256 gasBefore = gasleft(); - - try IWSTETH(rateProviderAddress).stEthPerToken() returns (uint256 stEthPerWstEth) { - // If rate is 0, return true - if (stEthPerWstEth == 0) return (0, true); - - return (stEthPerWstEth, false); - } catch { - // Require that enough gas was provided to prevent an OOG revert in the external call - // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used - // in the check itself. - if (gasleft() <= gasBefore / 64) revert InsufficientGasForExternalCall(); - - // If call to exchange rate reverted for another reason, return true - return (0, true); - } - } -} diff --git a/contracts/src/StabilityPool.sol b/contracts/src/StabilityPool.sol index 8dd1668c9..3bc9089c8 100644 --- a/contracts/src/StabilityPool.sol +++ b/contracts/src/StabilityPool.sol @@ -9,7 +9,8 @@ import "./Interfaces/IAddressesRegistry.sol"; import "./Interfaces/IStabilityPoolEvents.sol"; import "./Interfaces/ITroveManager.sol"; import "./Interfaces/IBoldToken.sol"; -import "./Dependencies/LiquityBase.sol"; +import "./Interfaces/ISystemParams.sol"; +import "./Dependencies/LiquityBaseInit.sol"; /* * The Stability Pool holds Bold tokens deposited by Stability Pool depositors. @@ -60,7 +61,7 @@ import "./Dependencies/LiquityBase.sol"; * * A series of liquidations that nearly empty the Pool (and thus each multiply P by a very small number in range ]0,1[ ) may push P * to its 36 digit decimal limit, and round it to 0, when in fact the Pool hasn't been emptied: this would break deposit tracking. - * + * * P is stored at 36-digit precision as a uint. That is, a value of "1" is represented by a value of 1e36 in the code. * * So, to track P accurately, we use a scale factor: if a liquidation would cause P to decrease below 1e27, @@ -115,14 +116,14 @@ import "./Dependencies/LiquityBase.sol"; * * */ -contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { +contract StabilityPool is Initializable, LiquityBaseInit, IStabilityPool, IStabilityPoolEvents { using SafeERC20 for IERC20; string public constant NAME = "StabilityPool"; - IERC20 public immutable collToken; - ITroveManager public immutable troveManager; - IBoldToken public immutable boldToken; + IERC20 public collToken; + ITroveManager public troveManager; + IBoldToken public boldToken; uint256 internal collBalance; // deposited coll tracker @@ -175,6 +176,10 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { // Each time the scale of P shifts by SCALE_FACTOR, the scale is incremented by 1 uint256 public currentScale; + address public liquidityStrategy; + + ISystemParams public immutable systemParams; + /* Coll Gain sum 'S': During its lifetime, each deposit d_t earns an Coll gain of ( d_t * [S - S_t] )/P_t, where S_t * is the depositor's snapshot of S taken at the time t when the deposit was made. * @@ -188,11 +193,29 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { event TroveManagerAddressChanged(address _newTroveManagerAddress); event BoldTokenAddressChanged(address _newBoldTokenAddress); + event RebalanceExecuted(uint256 amountCollIn, uint256 amountStableOut); + + /** + * @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. + */ + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(bool disable, ISystemParams _systemParams) { + if (disable) { + _disableInitializers(); + } + + systemParams = _systemParams; + } + + function initialize(IAddressesRegistry _addressesRegistry) external initializer { + __LiquityBase_init(_addressesRegistry); - constructor(IAddressesRegistry _addressesRegistry) LiquityBase(_addressesRegistry) { collToken = _addressesRegistry.collToken(); troveManager = _addressesRegistry.troveManager(); boldToken = _addressesRegistry.boldToken(); + liquidityStrategy = _addressesRegistry.liquidityStrategy(); + P = P_PRECISION; emit TroveManagerAddressChanged(address(troveManager)); emit BoldTokenAddressChanged(address(boldToken)); @@ -315,7 +338,10 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { _sendBoldtoDepositor(msg.sender, boldToWithdraw + yieldGainToSend); _sendCollGainToDepositor(collToSend); - require(newTotalBoldDeposits >= MIN_BOLD_IN_SP, "Withdrawal must leave totalBoldDeposits >= MIN_BOLD_IN_SP"); + require( + newTotalBoldDeposits >= systemParams.MIN_BOLD_IN_SP(), + "Withdrawal must leave totalBoldDeposits >= MIN_BOLD_IN_SP" + ); } function _getNewStashedCollAndCollToSend(address _depositor, uint256 _currentCollGain, bool _doClaim) @@ -361,7 +387,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { // When total deposits is very small, B is not updated. In this case, the BOLD issued is held // until the total deposits reach 1 BOLD (remains in the balance of the SP). - if (totalBoldDeposits < MIN_BOLD_IN_SP) { + if (totalBoldDeposits < systemParams.MIN_BOLD_IN_SP()) { yieldGainsPending = accumulatedYieldGains; return; } @@ -373,6 +399,32 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { emit B_Updated(scaleToB[currentScale], currentScale); } + // --- Liquidity strategy functions --- + + /* + * Stable token liquidity in the stability pool can be used to rebalance FPMM pools. + * Collateral will be swapped for stable tokens in the SP. + * Removed stable tokens will be factored out from LPs' positions. + * Added collateral will be added to LPs collateral gain which can be later claimed by the depositor. + */ + function swapCollateralForStable(uint256 amountCollIn, uint256 amountStableOut) external { + _requireCallerIsLiquidityStrategy(); + _requireNoShutdown(); + + + activePool.mintAggInterest(); + + _updateTrackingVariables(amountStableOut, amountCollIn); + + _swapCollateralForStable(amountCollIn, amountStableOut); + + require( + totalBoldDeposits >= systemParams.MIN_BOLD_AFTER_REBALANCE(), + "Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE" + ); + emit RebalanceExecuted(amountCollIn, amountStableOut); + } + // --- Liquidation functions --- /* @@ -383,10 +435,16 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { function offset(uint256 _debtToOffset, uint256 _collToAdd) external override { _requireCallerIsTroveManager(); - scaleToS[currentScale] += P * _collToAdd / totalBoldDeposits; + _updateTrackingVariables(_debtToOffset, _collToAdd); + + _moveOffsetCollAndDebt(_collToAdd, _debtToOffset); + } + + function _updateTrackingVariables(uint256 _amountStableOut, uint256 _amountCollIn) internal { + scaleToS[currentScale] += P * _amountCollIn / totalBoldDeposits; emit S_Updated(scaleToS[currentScale], currentScale); - uint256 numerator = P * (totalBoldDeposits - _debtToOffset); + uint256 numerator = P * (totalBoldDeposits - _amountStableOut); uint256 newP = numerator / totalBoldDeposits; // For `P` to turn zero, `totalBoldDeposits` has to be greater than `P * (totalBoldDeposits - _debtToOffset)`. @@ -412,8 +470,16 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { emit P_Updated(newP); P = newP; + } - _moveOffsetCollAndDebt(_collToAdd, _debtToOffset); + function _swapCollateralForStable(uint256 _amountCollIn, uint256 _amountStableOut) internal { + _updateTotalBoldDeposits(0, _amountStableOut); + IERC20(address(boldToken)).safeTransfer(liquidityStrategy, _amountStableOut); + + collBalance += _amountCollIn; + collToken.safeTransferFrom(msg.sender, address(this), _amountCollIn); + + emit StabilityPoolCollBalanceUpdated(collBalance); } function _moveOffsetCollAndDebt(uint256 _collToAdd, uint256 _debtToOffset) internal { @@ -485,7 +551,7 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { } function getDepositorYieldGainWithPending(address _depositor) external view override returns (uint256) { - if (totalBoldDeposits < MIN_BOLD_IN_SP) return 0; + if (totalBoldDeposits < systemParams.MIN_BOLD_IN_SP()) return 0; uint256 initialDeposit = deposits[_depositor].initialValue; if (initialDeposit == 0) return 0; @@ -597,7 +663,15 @@ contract StabilityPool is LiquityBase, IStabilityPool, IStabilityPoolEvents { require(initialDeposit == 0, "StabilityPool: User must have no deposit"); } + function _requireCallerIsLiquidityStrategy() internal view { + require(msg.sender == liquidityStrategy, "StabilityPool: Caller is not LiquidityStrategy"); + } + function _requireNonZeroAmount(uint256 _amount) internal pure { require(_amount > 0, "StabilityPool: Amount must be non-zero"); } + + function _requireNoShutdown() internal view { + require(troveManager.shutdownTime() == 0, "StabilityPool: System is shut down"); + } } diff --git a/contracts/src/SystemParams.sol b/contracts/src/SystemParams.sol new file mode 100644 index 000000000..757e475a6 --- /dev/null +++ b/contracts/src/SystemParams.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity 0.8.24; + +import {ISystemParams} from "./Interfaces/ISystemParams.sol"; +import { + _100pct, + _1pct, + MAX_LIQUIDATION_PENALTY_REDISTRIBUTION, + MAX_ANNUAL_INTEREST_RATE +} from "./Dependencies/Constants.sol"; +import "openzeppelin-contracts/contracts/proxy/utils/Initializable.sol"; + +/** + * @title System Parameters + * @author Mento Labs + * @notice This contract manages the system-wide parameters for the protocol. + */ +contract SystemParams is ISystemParams, Initializable { + /* ========== DEBT PARAMETERS ========== */ + + uint256 public immutable MIN_DEBT; + + /* ========== LIQUIDATION PARAMETERS ========== */ + + uint256 public immutable LIQUIDATION_PENALTY_SP; + uint256 public immutable LIQUIDATION_PENALTY_REDISTRIBUTION; + + /* ========== GAS COMPENSATION PARAMETERS ========== */ + + uint256 public immutable COLL_GAS_COMPENSATION_DIVISOR; + uint256 public immutable COLL_GAS_COMPENSATION_CAP; + uint256 public immutable ETH_GAS_COMPENSATION; + + /* ========== COLLATERAL PARAMETERS ========== */ + + uint256 public immutable CCR; + uint256 public immutable SCR; + uint256 public immutable MCR; + uint256 public immutable BCR; + + /* ========== INTEREST PARAMETERS ========== */ + + uint256 public immutable MIN_ANNUAL_INTEREST_RATE; + + /* ========== REDEMPTION PARAMETERS ========== */ + + uint256 public immutable REDEMPTION_FEE_FLOOR; + uint256 public immutable INITIAL_BASE_RATE; + uint256 public immutable REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 public immutable REDEMPTION_BETA; + + /* ========== STABILITY POOL PARAMETERS ========== */ + + uint256 public immutable SP_YIELD_SPLIT; + uint256 public immutable MIN_BOLD_IN_SP; + uint256 public immutable MIN_BOLD_AFTER_REBALANCE; + + /* ========== CONSTRUCTOR ========== */ + + constructor( + bool disableInitializers, + DebtParams memory _debtParams, + LiquidationParams memory _liquidationParams, + GasCompParams memory _gasCompParams, + CollateralParams memory _collateralParams, + InterestParams memory _interestParams, + RedemptionParams memory _redemptionParams, + StabilityPoolParams memory _poolParams + ) { + if (disableInitializers) { + _disableInitializers(); + } + + // minDebt should be choosen depending on the debt currency + if (_debtParams.minDebt == 0) revert InvalidMinDebt(); + + // Validate liquidation parameters + // Hardcoded validation bounds: MIN_LIQUIDATION_PENALTY_SP = 5% + if (_liquidationParams.liquidationPenaltySP < 5 * _1pct) { + revert SPPenaltyTooLow(); + } + if (_liquidationParams.liquidationPenaltySP > _liquidationParams.liquidationPenaltyRedistribution) { + revert SPPenaltyGtRedist(); + } + + // Validate gas compensation parameters + if (_gasCompParams.collGasCompensationDivisor == 0 || _gasCompParams.collGasCompensationDivisor > 1000) { + revert InvalidGasCompensation(); + } + if (_gasCompParams.collGasCompensationCap == 0 || _gasCompParams.collGasCompensationCap > 10 ether) { + revert InvalidGasCompensation(); + } + if (_gasCompParams.ethGasCompensation == 0 || _gasCompParams.ethGasCompensation > 1 ether) { + revert InvalidGasCompensation(); + } + + // Validate collateral parameters + if (_collateralParams.ccr <= _100pct || _collateralParams.ccr >= 2 * _100pct) revert InvalidCCR(); + if (_collateralParams.mcr <= _100pct || _collateralParams.mcr >= 2 * _100pct) revert InvalidMCR(); + if (_collateralParams.bcr < 5 * _1pct || _collateralParams.bcr >= 50 * _1pct) revert InvalidBCR(); + if (_collateralParams.scr <= _100pct || _collateralParams.scr >= 2 * _100pct) revert InvalidSCR(); + + // The redistribution penalty must not exceed the overcollateralization buffer (MCR - 100%) + if ( + _liquidationParams.liquidationPenaltyRedistribution > MAX_LIQUIDATION_PENALTY_REDISTRIBUTION + || _liquidationParams.liquidationPenaltyRedistribution > _collateralParams.mcr - _100pct + ) { + revert RedistPenaltyTooHigh(); + } + + // Validate interest parameters + if (_interestParams.minAnnualInterestRate > MAX_ANNUAL_INTEREST_RATE) { + revert MinInterestRateGtMax(); + } + + // Validate redemption parameters + if (_redemptionParams.redemptionFeeFloor > _100pct) revert InvalidFeeValue(); + if (_redemptionParams.initialBaseRate > 10 * _100pct) revert InvalidFeeValue(); + + // Validate stability pool parameters + if (_poolParams.spYieldSplit > _100pct) revert InvalidFeeValue(); + if (_poolParams.minBoldAfterRebalance < _poolParams.minBoldInSP) revert InvalidMinBoldInSP(); + if (_poolParams.minBoldInSP < 1e18) revert InvalidMinBoldInSP(); + + // Set debt parameters + MIN_DEBT = _debtParams.minDebt; + + // Set liquidation parameters + LIQUIDATION_PENALTY_SP = _liquidationParams.liquidationPenaltySP; + LIQUIDATION_PENALTY_REDISTRIBUTION = _liquidationParams.liquidationPenaltyRedistribution; + + // Set gas compensation parameters + COLL_GAS_COMPENSATION_DIVISOR = _gasCompParams.collGasCompensationDivisor; + COLL_GAS_COMPENSATION_CAP = _gasCompParams.collGasCompensationCap; + ETH_GAS_COMPENSATION = _gasCompParams.ethGasCompensation; + + // Set collateral parameters + CCR = _collateralParams.ccr; + SCR = _collateralParams.scr; + MCR = _collateralParams.mcr; + BCR = _collateralParams.bcr; + + // Set interest parameters + MIN_ANNUAL_INTEREST_RATE = _interestParams.minAnnualInterestRate; + + // Set redemption parameters + REDEMPTION_FEE_FLOOR = _redemptionParams.redemptionFeeFloor; + INITIAL_BASE_RATE = _redemptionParams.initialBaseRate; + REDEMPTION_MINUTE_DECAY_FACTOR = _redemptionParams.redemptionMinuteDecayFactor; + REDEMPTION_BETA = _redemptionParams.redemptionBeta; + + // Set stability pool parameters + SP_YIELD_SPLIT = _poolParams.spYieldSplit; + MIN_BOLD_IN_SP = _poolParams.minBoldInSP; + MIN_BOLD_AFTER_REBALANCE = _poolParams.minBoldAfterRebalance; + } + + /* + * Initializes proxy storage + * All parameters are immutable from constructor. This function + * only marks initialization complete for proxy pattern. + */ + function initialize() external initializer {} +} diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 389586974..a896cb4f9 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -12,7 +12,9 @@ import "./Interfaces/ITroveEvents.sol"; import "./Interfaces/ITroveNFT.sol"; import "./Interfaces/ICollateralRegistry.sol"; import "./Interfaces/IWETH.sol"; +import "./Interfaces/ISystemParams.sol"; import "./Dependencies/LiquityBase.sol"; +import "./Dependencies/Constants.sol"; contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // --- Connected contract declarations --- @@ -26,22 +28,9 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { // A doubly linked list of Troves, sorted by their interest rate ISortedTroves public sortedTroves; ICollateralRegistry internal collateralRegistry; - // Wrapped ETH for liquidation reserve (gas compensation) - IWETH internal immutable WETH; - - // Critical system collateral ratio. If the system's total collateral ratio (TCR) falls below the CCR, some borrowing operation restrictions are applied - uint256 public immutable CCR; - - // Minimum collateral ratio for individual troves - uint256 internal immutable MCR; - // Shutdown system collateral ratio. If the system's total collateral ratio (TCR) for a given collateral falls below the SCR, - // the protocol triggers the shutdown of the borrow market and permanently disables all borrowing operations except for closing Troves. - uint256 internal immutable SCR; - - // Liquidation penalty for troves offset to the SP - uint256 internal immutable LIQUIDATION_PENALTY_SP; - // Liquidation penalty for troves redistributed - uint256 internal immutable LIQUIDATION_PENALTY_REDISTRIBUTION; + // Gas token for liquidation reserve (gas compensation) + IERC20Metadata internal immutable gasToken; + ISystemParams immutable systemParams; // --- Data structures --- @@ -174,6 +163,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { error NotEnoughBoldBalance(); error MinCollNotReached(uint256 _coll); error BatchSharesRatioTooHigh(); + error L2SequencerDown(); // --- Events --- @@ -186,12 +176,8 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { event SortedTrovesAddressChanged(address _sortedTrovesAddress); event CollateralRegistryAddressChanged(address _collateralRegistryAddress); - constructor(IAddressesRegistry _addressesRegistry) LiquityBase(_addressesRegistry) { - CCR = _addressesRegistry.CCR(); - MCR = _addressesRegistry.MCR(); - SCR = _addressesRegistry.SCR(); - LIQUIDATION_PENALTY_SP = _addressesRegistry.LIQUIDATION_PENALTY_SP(); - LIQUIDATION_PENALTY_REDISTRIBUTION = _addressesRegistry.LIQUIDATION_PENALTY_REDISTRIBUTION(); + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) LiquityBase(_addressesRegistry) { + systemParams = _systemParams; troveNFT = _addressesRegistry.troveNFT(); borrowerOperations = _addressesRegistry.borrowerOperations(); @@ -200,7 +186,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { collSurplusPool = _addressesRegistry.collSurplusPool(); boldToken = _addressesRegistry.boldToken(); sortedTroves = _addressesRegistry.sortedTroves(); - WETH = _addressesRegistry.WETH(); + gasToken = _addressesRegistry.gasToken(); collateralRegistry = _addressesRegistry.collateralRegistry(); emit TroveNFTAddressChanged(address(troveNFT)); @@ -332,9 +318,11 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } // Return the amount of Coll to be drawn from a trove's collateral and sent as gas compensation. - function _getCollGasCompensation(uint256 _coll) internal pure returns (uint256) { + function _getCollGasCompensation(uint256 _coll) internal view returns (uint256) { // _entireDebt should never be zero, but we add the condition defensively to avoid an unexpected revert - return LiquityMath._min(_coll / COLL_GAS_COMPENSATION_DIVISOR, COLL_GAS_COMPENSATION_CAP); + return LiquityMath._min( + _coll / systemParams.COLL_GAS_COMPENSATION_DIVISOR(), systemParams.COLL_GAS_COMPENSATION_CAP() + ); } /* In a full liquidation, returns the values for a trove's coll and debt to be offset, and coll and debt to be @@ -376,7 +364,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 collToOffset = collSPPortion - collGasCompensation; (collToSendToSP, collSurplus) = - _getCollPenaltyAndSurplus(collToOffset, debtToOffset, LIQUIDATION_PENALTY_SP, _price); + _getCollPenaltyAndSurplus(collToOffset, debtToOffset, systemParams.LIQUIDATION_PENALTY_SP(), _price); } // Redistribution @@ -387,7 +375,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { (collToRedistribute, collSurplus) = _getCollPenaltyAndSurplus( collRedistributionPortion + collSurplus, // Coll surplus from offset can be eaten up by red. penalty debtToRedistribute, - LIQUIDATION_PENALTY_REDISTRIBUTION, // _penaltyRatio + systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(), // _penaltyRatio _price ); } @@ -415,6 +403,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { * Attempt to liquidate a custom list of troves provided by the caller. */ function batchLiquidateTroves(uint256[] memory _troveArray) public override { + _requireL2SequencerIsUp(); if (_troveArray.length == 0) { revert EmptyData(); } @@ -426,12 +415,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { TroveChange memory troveChange; LiquidationValues memory totals; - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // - If the SP has total deposits >= 1e18, we leave 1e18 in it untouched. // - If it has 0 < x < 1e18 total deposits, we leave x in it. uint256 totalBoldDeposits = stabilityPoolCached.getTotalBoldDeposits(); - uint256 boldToLeaveInSP = LiquityMath._min(MIN_BOLD_IN_SP, totalBoldDeposits); + uint256 boldToLeaveInSP = LiquityMath._min(systemParams.MIN_BOLD_IN_SP(), totalBoldDeposits); uint256 boldInSPForOffsets = totalBoldDeposits - boldToLeaveInSP; // Perform the appropriate liquidation sequence - tally values and obtain their totals. @@ -497,7 +486,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 ICR = getCurrentICR(troveId, _price); - if (ICR < MCR) { + if (ICR < systemParams.MCR()) { LiquidationValues memory singleLiquidation; LatestTroveData memory trove; @@ -518,10 +507,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { LiquidationValues memory _singleLiquidation, LiquidationValues memory totals, TroveChange memory troveChange - ) internal pure { + ) internal view { // Tally all the values with their respective running totals totals.collGasCompensation += _singleLiquidation.collGasCompensation; - totals.ETHGasCompensation += ETH_GAS_COMPENSATION; + totals.ETHGasCompensation += systemParams.ETH_GAS_COMPENSATION(); troveChange.debtDecrease += _trove.entireDebt; troveChange.collDecrease += _trove.entireColl; troveChange.appliedRedistBoldDebtGain += _trove.redistBoldDebtGain; @@ -536,7 +525,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { function _sendGasCompensation(IActivePool _activePool, address _liquidator, uint256 _eth, uint256 _coll) internal { if (_eth > 0) { - WETH.transferFrom(gasPoolAddress, _liquidator, _eth); + gasToken.transferFrom(gasPoolAddress, _liquidator, _eth); } if (_coll > 0) { @@ -692,7 +681,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 newDebt = _applySingleRedemption(_defaultPool, _singleRedemption, isTroveInBatch); // Make Trove zombie if it's tiny (and it wasn’t already), in order to prevent griefing future (normal, sequential) redemptions - if (newDebt < MIN_DEBT) { + if (newDebt < systemParams.MIN_DEBT()) { if (!_singleRedemption.isZombieTrove) { Troves[_singleRedemption.troveId].status = Status.zombie; if (isTroveInBatch) { @@ -710,7 +699,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } // Note: technically, it could happen that the Trove pointed to by `lastZombieTroveId` ends up with - // newDebt >= MIN_DEBT thanks to BOLD debt redistribution, which means it _could_ be made active again, + // newDebt >= systemParams.MIN_DEBT() thanks to BOLD debt redistribution, which means it _could_ be made active again, // however we don't do that here, as it would require hints for re-insertion into `SortedTroves`. } @@ -771,8 +760,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } vars.lastBatchUpdatedInterest = address(0); - // Get the price to use for the redemption collateral calculations - (uint256 redemptionPrice,) = priceFeed.fetchRedemptionPrice(); + uint256 redemptionPrice = priceFeed.fetchPrice(); // Loop through the Troves starting from the one with lowest interest rate until _amount of Bold is exchanged for collateral if (_maxIterations == 0) _maxIterations = type(uint256).max; @@ -880,7 +868,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { TroveChange memory totalsTroveChange; // Use the standard fetchPrice here, since if branch has shut down we don't worry about small redemption arbs - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 remainingBold = _boldAmount; for (uint256 i = 0; i < _troveIds.length; i++) { @@ -1212,6 +1200,12 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { } } + function _requireL2SequencerIsUp() internal view { + if (!priceFeed.isL2SequencerUp()) { + revert L2SequencerDown(); + } + } + // --- Trove property getters --- function getUnbackedPortionPriceAndRedeemability() external returns (uint256, uint256, bool) { @@ -1219,10 +1213,10 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 spSize = stabilityPool.getTotalBoldDeposits(); uint256 unbackedPortion = totalDebt > spSize ? totalDebt - spSize : 0; - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down. // Use the normal price for the TCR check. - bool redeemable = _getTCR(price) >= SCR && shutdownTime == 0; + bool redeemable = _getTCR(price) >= systemParams.SCR() && shutdownTime == 0; return (unbackedPortion, price, redeemable); } @@ -1904,7 +1898,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 _currentBatchDebtShares, uint256 _batchDebt, bool _checkBatchSharesRatio - ) internal pure { + ) internal view { // debt / shares should be below MAX_BATCH_SHARES_RATIO if (_currentBatchDebtShares * MAX_BATCH_SHARES_RATIO < _batchDebt && _checkBatchSharesRatio) { revert BatchSharesRatioTooHigh(); diff --git a/contracts/src/TroveNFT.sol b/contracts/src/TroveNFT.sol index 4aac97eec..6bc2d9a2c 100644 --- a/contracts/src/TroveNFT.sol +++ b/contracts/src/TroveNFT.sol @@ -7,6 +7,7 @@ import "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.s import "./Interfaces/ITroveNFT.sol"; import "./Interfaces/IAddressesRegistry.sol"; +import "./Types/LatestTroveData.sol"; import {IMetadataNFT} from "./NFTMetadata/MetadataNFT.sol"; import {ITroveManager} from "./Interfaces/ITroveManager.sol"; diff --git a/contracts/src/Zappers/BaseZapper.sol b/contracts/src/Zappers/BaseZapper.sol deleted file mode 100644 index c4f30ac33..000000000 --- a/contracts/src/Zappers/BaseZapper.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "../Interfaces/IWETH.sol"; -import "../Interfaces/IAddressesRegistry.sol"; -import "../Interfaces/IBorrowerOperations.sol"; -import "../Dependencies/AddRemoveManagers.sol"; -import "./LeftoversSweep.sol"; -import "./Interfaces/IFlashLoanProvider.sol"; -import "./Interfaces/IFlashLoanReceiver.sol"; -import "./Interfaces/IExchange.sol"; -import "./Interfaces/IZapper.sol"; - -abstract contract BaseZapper is AddRemoveManagers, LeftoversSweep, IFlashLoanReceiver, IZapper { - IBorrowerOperations public immutable borrowerOperations; // LST branch (i.e., not WETH as collateral) - ITroveManager public immutable troveManager; - IWETH public immutable WETH; - IBoldToken public immutable boldToken; - - IFlashLoanProvider public immutable flashLoanProvider; - IExchange public immutable exchange; - - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - AddRemoveManagers(_addressesRegistry) - { - borrowerOperations = _addressesRegistry.borrowerOperations(); - troveManager = _addressesRegistry.troveManager(); - boldToken = _addressesRegistry.boldToken(); - WETH = _addressesRegistry.WETH(); - - flashLoanProvider = _flashLoanProvider; - exchange = _exchange; - } - - function _getTroveIndex(address _sender, uint256 _ownerIndex) internal pure returns (uint256) { - return uint256(keccak256(abi.encode(_sender, _ownerIndex))); - } - - function _getTroveIndex(uint256 _ownerIndex) internal view returns (uint256) { - return _getTroveIndex(msg.sender, _ownerIndex); - } - - function _requireZapperIsReceiver(uint256 _troveId) internal view { - (, address receiver) = borrowerOperations.removeManagerReceiverOf(_troveId); - require(receiver == address(this), "BZ: Zapper is not receiver for this trove"); - } - - function _checkAdjustTroveManagers( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - bool _isDebtIncrease - ) internal view returns (address) { - address owner = troveNFT.ownerOf(_troveId); - address receiver = owner; - - if ((!_isCollIncrease && _collChange > 0) || _isDebtIncrease) { - receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - } else { - // RemoveManager assumes AddManager, so if the former is set, there's no need to check the latter - _requireSenderIsOwnerOrAddManager(_troveId, owner); - // No need to check the type of trove change for two reasons: - // - If the check above fails, it means sender is not owner, nor AddManager, nor RemoveManager. - // An independent 3rd party should not be allowed here. - // - If it's not collIncrease or debtDecrease, _requireNonZeroAdjustment would revert - } - - return receiver; - } -} diff --git a/contracts/src/Zappers/GasCompZapper.sol b/contracts/src/Zappers/GasCompZapper.sol deleted file mode 100644 index 0792052ec..000000000 --- a/contracts/src/Zappers/GasCompZapper.sol +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "./BaseZapper.sol"; -import "../Dependencies/Constants.sol"; - -contract GasCompZapper is BaseZapper { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - BaseZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - collToken = _addressesRegistry.collToken(); - require(address(WETH) != address(collToken), "GCZ: Wrong coll branch"); - - // Approve WETH to BorrowerOperations - WETH.approve(address(borrowerOperations), type(uint256).max); - // Approve coll to BorrowerOperations - collToken.approve(address(borrowerOperations), type(uint256).max); - // Approve Coll to exchange module (for closeTroveFromCollateral) - collToken.approve(address(_exchange), type(uint256).max); - } - - function openTroveWithRawETH(OpenTroveParams calldata _params) external payable returns (uint256) { - require(msg.value == ETH_GAS_COMPENSATION, "GCZ: Wrong ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "GCZ: Cannot choose interest if joining a batch" - ); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - // Pull coll - collToken.safeTransferFrom(msg.sender, address(this), _params.collAmount); - - uint256 troveId; - // Include sender in index - uint256 index = _getTroveIndex(_params.ownerIndex); - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - index, - _params.collAmount, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: index, - collAmount: _params.collAmount, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - boldToken.transfer(msg.sender, _params.boldAmount); - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - return troveId; - } - - function addColl(uint256 _troveId, uint256 _amount) external { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - - IBorrowerOperations borrowerOperationsCached = borrowerOperations; - - // Pull coll - collToken.safeTransferFrom(msg.sender, address(this), _amount); - - borrowerOperationsCached.addColl(_troveId, _amount); - } - - function withdrawColl(uint256 _troveId, uint256 _amount) external { - address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawColl(_troveId, _amount); - - // Send coll left - collToken.safeTransfer(receiver, _amount); - } - - function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external { - address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawBold(_troveId, _boldAmount, _maxUpfrontFee); - - // Send Bold - boldToken.transfer(receiver, _boldAmount); - } - - function repayBold(uint256 _troveId, uint256 _boldAmount) external { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - // Pull Bold - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - - borrowerOperations.repayBold(_troveId, _boldAmount); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function adjustTrove( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _maxUpfrontFee - ) external { - InitialBalances memory initialBalances; - address receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function adjustZombieTrove( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _upperHint, - uint256 _lowerHint, - uint256 _maxUpfrontFee - ) external { - InitialBalances memory initialBalances; - address receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustZombieTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function _adjustTrovePre( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - InitialBalances memory _initialBalances - ) internal returns (address) { - address receiver = _checkAdjustTroveManagers(_troveId, _collChange, _isCollIncrease, _isDebtIncrease); - - // Set initial balances to make sure there are not lefovers - _setInitialTokensAndBalances(collToken, boldToken, _initialBalances); - - // Pull coll - if (_isCollIncrease) { - collToken.safeTransferFrom(msg.sender, address(this), _collChange); - } - - // Pull Bold - if (!_isDebtIncrease) { - boldToken.transferFrom(msg.sender, address(this), _boldChange); - } - - return receiver; - } - - function _adjustTrovePost( - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - address _receiver, - InitialBalances memory _initialBalances - ) internal { - // Send coll left - if (!_isCollIncrease) { - collToken.safeTransfer(_receiver, _collChange); - } - - // Send Bold - if (_isDebtIncrease) { - boldToken.transfer(_receiver, _boldChange); - } - - // return leftovers to user - _returnLeftovers(_initialBalances); - } - - function closeTroveToRawETH(uint256 _troveId) external { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - // pull Bold for repayment - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); - boldToken.transferFrom(msg.sender, address(this), trove.entireDebt); - - borrowerOperations.closeTrove(_troveId); - - // Send coll left - collToken.safeTransfer(receiver, trove.entireColl); - - // Send gas compensation - WETH.withdraw(ETH_GAS_COMPENSATION); - (bool success,) = receiver.call{value: ETH_GAS_COMPENSATION}(""); - require(success, "GCZ: Sending ETH failed"); - } - - function closeTroveFromCollateral(uint256 _troveId, uint256 _flashLoanAmount, uint256 _minExpectedCollateral) - external - override - { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - CloseTroveParams memory params = CloseTroveParams({ - troveId: _troveId, - flashLoanAmount: _flashLoanAmount, - minExpectedCollateral: _minExpectedCollateral, - receiver: receiver - }); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - initialBalances.tokens[0] = collToken; - initialBalances.tokens[1] = boldToken; - _setInitialBalancesAndReceiver(initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _flashLoanAmount, IFlashLoanProvider.Operation.CloseTrove, abi.encode(params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function receiveFlashLoanOnCloseTroveFromCollateral( - CloseTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external { - require(msg.sender == address(flashLoanProvider), "GCZ: Caller not FlashLoan provider"); - - LatestTroveData memory trove = troveManager.getLatestTroveData(_params.troveId); - uint256 collLeft = trove.entireColl - _params.flashLoanAmount; - require(collLeft >= _params.minExpectedCollateral, "GCZ: Not enough collateral received"); - - // Swap Coll from flash loan to Bold, so we can repay and close trove - // We swap the flash loan minus the flash loan fee - exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - - // We asked for a min of entireDebt in swapToBold call above, so we don’t check again here: - //uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - //require(receivedBoldAmount >= trove.entireDebt, "GCZ: Not enough BOLD obtained to repay"); - - borrowerOperations.closeTrove(_params.troveId); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - - // Send coll left - collToken.safeTransfer(_params.receiver, collLeft); - - // Send gas compensation - WETH.withdraw(ETH_GAS_COMPENSATION); - (bool success,) = _params.receiver.call{value: ETH_GAS_COMPENSATION}(""); - require(success, "GCZ: Sending ETH failed"); - } - - receive() external payable {} - - // Unimplemented flash loan receive functions for leverage - function receiveFlashLoanOnOpenLeveragedTrove( - ILeverageZapper.OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverUpTrove( - ILeverageZapper.LeverUpTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverDownTrove( - ILeverageZapper.LeverDownTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} -} diff --git a/contracts/src/Zappers/Interfaces/IExchange.sol b/contracts/src/Zappers/Interfaces/IExchange.sol deleted file mode 100644 index 2203b78eb..000000000 --- a/contracts/src/Zappers/Interfaces/IExchange.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface IExchange { - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external; - - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256); -} diff --git a/contracts/src/Zappers/Interfaces/IExchangeHelpers.sol b/contracts/src/Zappers/Interfaces/IExchangeHelpers.sol deleted file mode 100644 index fc60e0296..000000000 --- a/contracts/src/Zappers/Interfaces/IExchangeHelpers.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IExchangeHelpers { - function getCollFromBold(uint256 _boldAmount, IERC20 _collToken, uint256 _desiredCollAmount) - external /* view */ - returns (uint256, uint256); -} diff --git a/contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol b/contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol deleted file mode 100644 index ab84a723d..000000000 --- a/contracts/src/Zappers/Interfaces/IFlashLoanProvider.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "./ILeverageZapper.sol"; -import "./IFlashLoanReceiver.sol"; - -interface IFlashLoanProvider { - enum Operation { - OpenTrove, - CloseTrove, - LeverUpTrove, - LeverDownTrove - } - - function receiver() external view returns (IFlashLoanReceiver); - - function makeFlashLoan(IERC20 _token, uint256 _amount, Operation _operation, bytes calldata userData) external; -} diff --git a/contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol b/contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol deleted file mode 100644 index 5d66e490b..000000000 --- a/contracts/src/Zappers/Interfaces/IFlashLoanReceiver.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./IZapper.sol"; -import "./ILeverageZapper.sol"; - -interface IFlashLoanReceiver { - function receiveFlashLoanOnOpenLeveragedTrove( - ILeverageZapper.OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; - function receiveFlashLoanOnLeverUpTrove( - ILeverageZapper.LeverUpTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; - function receiveFlashLoanOnLeverDownTrove( - ILeverageZapper.LeverDownTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; - function receiveFlashLoanOnCloseTroveFromCollateral( - IZapper.CloseTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external; -} diff --git a/contracts/src/Zappers/Interfaces/ILeverageZapper.sol b/contracts/src/Zappers/Interfaces/ILeverageZapper.sol deleted file mode 100644 index b1cc8df17..000000000 --- a/contracts/src/Zappers/Interfaces/ILeverageZapper.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./IZapper.sol"; - -interface ILeverageZapper is IZapper { - struct OpenLeveragedTroveParams { - address owner; - uint256 ownerIndex; - uint256 collAmount; - uint256 flashLoanAmount; - uint256 boldAmount; - uint256 upperHint; - uint256 lowerHint; - uint256 annualInterestRate; - address batchManager; - uint256 maxUpfrontFee; - address addManager; - address removeManager; - address receiver; - } - - struct LeverUpTroveParams { - uint256 troveId; - uint256 flashLoanAmount; - uint256 boldAmount; - uint256 maxUpfrontFee; - } - - struct LeverDownTroveParams { - uint256 troveId; - uint256 flashLoanAmount; - uint256 minBoldAmount; - } - - function openLeveragedTroveWithRawETH(OpenLeveragedTroveParams calldata _params) external payable; - - function leverUpTrove(LeverUpTroveParams calldata _params) external; - - function leverDownTrove(LeverDownTroveParams calldata _params) external; - - function leverageRatioToCollateralRatio(uint256 _inputRatio) external pure returns (uint256); -} diff --git a/contracts/src/Zappers/Interfaces/IZapper.sol b/contracts/src/Zappers/Interfaces/IZapper.sol deleted file mode 100644 index 4d5ec13c4..000000000 --- a/contracts/src/Zappers/Interfaces/IZapper.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./IFlashLoanProvider.sol"; -import "./IExchange.sol"; - -interface IZapper { - struct OpenTroveParams { - address owner; - uint256 ownerIndex; - uint256 collAmount; - uint256 boldAmount; - uint256 upperHint; - uint256 lowerHint; - uint256 annualInterestRate; - address batchManager; - uint256 maxUpfrontFee; - address addManager; - address removeManager; - address receiver; - } - - struct CloseTroveParams { - uint256 troveId; - uint256 flashLoanAmount; - uint256 minExpectedCollateral; - address receiver; - } - - function flashLoanProvider() external view returns (IFlashLoanProvider); - - function exchange() external view returns (IExchange); - - function openTroveWithRawETH(OpenTroveParams calldata _params) external payable returns (uint256); - - function closeTroveFromCollateral(uint256 _troveId, uint256 _flashLoanAmount, uint256 _minExpectedCollateral) - external; -} diff --git a/contracts/src/Zappers/LeftoversSweep.sol b/contracts/src/Zappers/LeftoversSweep.sol deleted file mode 100644 index ea464d82e..000000000 --- a/contracts/src/Zappers/LeftoversSweep.sol +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "../Interfaces/IBoldToken.sol"; - -contract LeftoversSweep { - using SafeERC20 for IERC20; - - struct InitialBalances { - IERC20[4] tokens; // paving the way for completely dynamic routes - uint256[4] balances; - address receiver; - } - - function _setInitialTokensAndBalances( - IERC20 _collToken, - IBoldToken _boldToken, - InitialBalances memory _initialBalances - ) internal view { - _setInitialTokensBalancesAndReceiver(_collToken, _boldToken, _initialBalances, msg.sender); - } - - function _setInitialTokensBalancesAndReceiver( - IERC20 _collToken, - IBoldToken _boldToken, - InitialBalances memory _initialBalances, - address _receiver - ) internal view { - _initialBalances.tokens[0] = _collToken; - _initialBalances.tokens[1] = _boldToken; - _setInitialBalancesAndReceiver(_initialBalances, _receiver); - } - - function _setInitialBalances(InitialBalances memory _initialBalances) internal view { - _setInitialBalancesAndReceiver(_initialBalances, msg.sender); - } - - function _setInitialBalancesAndReceiver(InitialBalances memory _initialBalances, address _receiver) internal view { - for (uint256 i = 0; i < _initialBalances.tokens.length; i++) { - if (address(_initialBalances.tokens[i]) == address(0)) break; - - _initialBalances.balances[i] = _initialBalances.tokens[i].balanceOf(address(this)); - } - _initialBalances.receiver = _receiver; - } - - function _returnLeftovers(InitialBalances memory _initialBalances) internal { - for (uint256 i = 0; i < _initialBalances.tokens.length; i++) { - if (address(_initialBalances.tokens[i]) == address(0)) break; - - uint256 currentBalance = _initialBalances.tokens[i].balanceOf(address(this)); - if (currentBalance > _initialBalances.balances[i]) { - _initialBalances.tokens[i].safeTransfer( - _initialBalances.receiver, currentBalance - _initialBalances.balances[i] - ); - } - } - } -} diff --git a/contracts/src/Zappers/LeverageLSTZapper.sol b/contracts/src/Zappers/LeverageLSTZapper.sol deleted file mode 100644 index 71782fdac..000000000 --- a/contracts/src/Zappers/LeverageLSTZapper.sol +++ /dev/null @@ -1,206 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "./GasCompZapper.sol"; -import "../Dependencies/Constants.sol"; - -contract LeverageLSTZapper is GasCompZapper, ILeverageZapper { - using SafeERC20 for IERC20; - - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - GasCompZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - // Approval of WETH and Coll to BorrowerOperations is done in parent GasCompZapper - // Approve Bold to exchange module (Coll is approved in parent GasCompZapper) - boldToken.approve(address(_exchange), type(uint256).max); - } - - function openLeveragedTroveWithRawETH(OpenLeveragedTroveParams memory _params) external payable { - require(msg.value == ETH_GAS_COMPENSATION, "LZ: Wrong ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "LZ: Cannot choose interest if joining a batch" - ); - - // Include the original sender in the index, so it is included in the final troveId - _params.ownerIndex = _getTroveIndex(msg.sender, _params.ownerIndex); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - // Pull own coll - collToken.safeTransferFrom(msg.sender, address(this), _params.collAmount); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _params.flashLoanAmount, IFlashLoanProvider.Operation.OpenTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnOpenLeveragedTrove( - OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external override { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - uint256 totalCollAmount = _params.collAmount + _effectiveFlashLoanAmount; - // We compute boldAmount off-chain for efficiency - - // Open trove - uint256 troveId; - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - _params.ownerIndex, - totalCollAmount, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: _params.ownerIndex, - collAmount: totalCollAmount, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - // Swap Bold to Coll - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - function leverUpTrove(LeverUpTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(collToken, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverUpTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverUpTrove(LeverUpTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Adjust trove - // With the received coll from flash loan, we increase both the trove coll and debt - borrowerOperations.adjustTrove( - _params.troveId, - _effectiveFlashLoanAmount, // flash loan amount minus fee - true, // _isCollIncrease - _params.boldAmount, - true, // _isDebtIncrease - _params.maxUpfrontFee - ); - - // Swap Bold to Coll - // No need to use a min: if the obtained amount is not enough, the flash loan return below won’t be enough - // And the flash loan provider will revert after this function exits - // The frontend should calculate in advance the `_params.boldAmount` needed for this to work - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - function leverDownTrove(LeverDownTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(collToken, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - collToken, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverDownTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverDownTrove(LeverDownTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Swap Coll from flash loan to Bold, so we can repay and downsize trove - // We swap the flash loan minus the flash loan fee - // The frontend should calculate in advance the `_params.minBoldAmount` to achieve the desired leverage ratio - // (with some slippage tolerance) - uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, _params.minBoldAmount); - - // Adjust trove - borrowerOperations.adjustTrove( - _params.troveId, - _params.flashLoanAmount, - false, // _isCollIncrease - receivedBoldAmount, - false, // _isDebtIncrease - 0 - ); - - // Send coll back to return flash loan - collToken.safeTransfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - // As formulas are symmetrical, it can be used in both ways - function leverageRatioToCollateralRatio(uint256 _inputRatio) external pure returns (uint256) { - return _inputRatio * DECIMAL_PRECISION / (_inputRatio - DECIMAL_PRECISION); - } -} diff --git a/contracts/src/Zappers/LeverageWETHZapper.sol b/contracts/src/Zappers/LeverageWETHZapper.sol deleted file mode 100644 index 7e6097d82..000000000 --- a/contracts/src/Zappers/LeverageWETHZapper.sol +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./WETHZapper.sol"; -import "../Dependencies/Constants.sol"; -import "./Interfaces/ILeverageZapper.sol"; - -contract LeverageWETHZapper is WETHZapper, ILeverageZapper { - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - WETHZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - // Approval of coll (WETH) to BorrowerOperations is done in parent WETHZapper - // Approve Bold to exchange module (Coll is approved in parent WETHZapper) - boldToken.approve(address(_exchange), type(uint256).max); - } - - function openLeveragedTroveWithRawETH(OpenLeveragedTroveParams memory _params) external payable { - require(msg.value == ETH_GAS_COMPENSATION + _params.collAmount, "LZ: Wrong amount of ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "LZ: Cannot choose interest if joining a batch" - ); - - // Include the original sender in the index, so it is included in the final troveId - _params.ownerIndex = _getTroveIndex(msg.sender, _params.ownerIndex); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(WETH, boldToken, initialBalances); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _params.flashLoanAmount, IFlashLoanProvider.Operation.OpenTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnOpenLeveragedTrove( - OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external override { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - uint256 totalCollAmount = _params.collAmount + _effectiveFlashLoanAmount; - // We compute boldAmount off-chain for efficiency - - uint256 troveId; - // Open trove - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - _params.ownerIndex, - totalCollAmount, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: _params.ownerIndex, - collAmount: totalCollAmount, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - // Swap Bold to Coll - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - // WETH reverts on failure: https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code - } - - function leverUpTrove(LeverUpTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(WETH, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverUpTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverUpTrove(LeverUpTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Adjust trove - // With the received coll from flash loan, we increase both the trove coll and debt - borrowerOperations.adjustTrove( - _params.troveId, - _effectiveFlashLoanAmount, // flash loan amount minus fee - true, // _isCollIncrease - _params.boldAmount, - true, // _isDebtIncrease - _params.maxUpfrontFee - ); - - // Swap Bold to Coll - // No need to use a min: if the obtained amount is not enough, the flash loan return below won’t be enough - // And the flash loan provider will revert after this function exits - // The frontend should calculate in advance the `_params.boldAmount` needed for this to work - exchange.swapFromBold(_params.boldAmount, _params.flashLoanAmount); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - function leverDownTrove(LeverDownTroveParams calldata _params) external { - address owner = troveNFT.ownerOf(_params.troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_params.troveId, owner); - _requireZapperIsReceiver(_params.troveId); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensBalancesAndReceiver(WETH, boldToken, initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _params.flashLoanAmount, IFlashLoanProvider.Operation.LeverDownTrove, abi.encode(_params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Callback from the flash loan provider - function receiveFlashLoanOnLeverDownTrove(LeverDownTroveParams calldata _params, uint256 _effectiveFlashLoanAmount) - external - override - { - require(msg.sender == address(flashLoanProvider), "LZ: Caller not FlashLoan provider"); - - // Swap Coll from flash loan to Bold, so we can repay and downsize trove - // We swap the flash loan minus the flash loan fee - // The frontend should calculate in advance the `_params.minBoldAmount` to achieve the desired leverage ratio - // (with some slippage tolerance) - uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, _params.minBoldAmount); - - // Adjust trove - borrowerOperations.adjustTrove( - _params.troveId, - _params.flashLoanAmount, - false, // _isCollIncrease - receivedBoldAmount, - false, // _isDebtIncrease - 0 - ); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - } - - // As formulas are symmetrical, it can be used in both ways - function leverageRatioToCollateralRatio(uint256 _inputRatio) external pure returns (uint256) { - return _inputRatio * DECIMAL_PRECISION / (_inputRatio - DECIMAL_PRECISION); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol deleted file mode 100644 index 78e17829b..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./ICurvePool.sol"; - -interface ICurveFactory { - function deploy_pool( - string memory name, - string memory symbol, - address[2] memory coins, - uint256 implementation_id, - uint256 A, - uint256 gamma, - uint256 mid_fee, - uint256 out_fee, - uint256 fee_gamma, - uint256 allowed_extra_profit, - uint256 adjustment_step, - uint256 ma_exp_time, - uint256 initial_price - ) external returns (ICurvePool); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol deleted file mode 100644 index 05d00c918..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface ICurvePool { - function add_liquidity(uint256[2] memory amounts, uint256 min_mint_amount) external returns (uint256); - //function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, bool use_eth, address receiver) external returns (uint256 output); - function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) external returns (uint256 output); - function get_dy(uint256 i, uint256 j, uint256 dx) external view returns (uint256 dy); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol deleted file mode 100644 index 0c689da0b..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "./ICurveStableswapNGPool.sol"; - -interface ICurveStableswapNGFactory { - /* - function deploy_plain_pool( - string memory name, - string memory symbol, - address[2] memory coins, - uint256 A, - uint256 fee, - uint256 asset_type, - uint256 implementation_id - ) external returns (ICurvePool); - */ - function deploy_plain_pool( - string memory name, - string memory symbol, - address[] memory coins, - uint256 A, - uint256 fee, - uint256 offpeg_fee_multiplier, - uint256 ma_exp_time, - uint256 implementation_id, - uint8[] memory asset_types, - bytes4[] memory method_ids, - address[] memory oracles - ) external returns (ICurveStableswapNGPool); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol b/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol deleted file mode 100644 index 96ecb529b..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -interface ICurveStableswapNGPool { - function add_liquidity(uint256[] memory amounts, uint256 min_mint_amount) external returns (uint256); - function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) external returns (uint256 output); - function get_dx(int128 i, int128 j, uint256 dy) external view returns (uint256 dx); - function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256 dy); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol b/contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol deleted file mode 100644 index a0e24efd8..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/CurveExchange.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; - -import "../../../Interfaces/IBoldToken.sol"; -import "./Curve/ICurvePool.sol"; -import "../../Interfaces/IExchange.sol"; - -contract CurveExchange is IExchange { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - IBoldToken public immutable boldToken; - ICurvePool public immutable curvePool; - uint256 public immutable COLL_TOKEN_INDEX; - uint256 public immutable BOLD_TOKEN_INDEX; - - constructor( - IERC20 _collToken, - IBoldToken _boldToken, - ICurvePool _curvePool, - uint256 _collIndex, - uint256 _boldIndex - ) { - collToken = _collToken; - boldToken = _boldToken; - curvePool = _curvePool; - COLL_TOKEN_INDEX = _collIndex; - BOLD_TOKEN_INDEX = _boldIndex; - } - - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external { - ICurvePool curvePoolCached = curvePool; - uint256 initialBoldBalance = boldToken.balanceOf(address(this)); - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - boldToken.approve(address(curvePoolCached), _boldAmount); - - uint256 output = curvePoolCached.exchange(BOLD_TOKEN_INDEX, COLL_TOKEN_INDEX, _boldAmount, _minCollAmount); - collToken.safeTransfer(msg.sender, output); - - uint256 currentBoldBalance = boldToken.balanceOf(address(this)); - if (currentBoldBalance > initialBoldBalance) { - boldToken.transfer(msg.sender, currentBoldBalance - initialBoldBalance); - } - } - - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256) { - ICurvePool curvePoolCached = curvePool; - uint256 initialCollBalance = collToken.balanceOf(address(this)); - collToken.safeTransferFrom(msg.sender, address(this), _collAmount); - collToken.approve(address(curvePoolCached), _collAmount); - - uint256 output = curvePoolCached.exchange(COLL_TOKEN_INDEX, BOLD_TOKEN_INDEX, _collAmount, _minBoldAmount); - boldToken.transfer(msg.sender, output); - - uint256 currentCollBalance = collToken.balanceOf(address(this)); - if (currentCollBalance > initialCollBalance) { - collToken.safeTransfer(msg.sender, currentCollBalance - initialCollBalance); - } - - return output; - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol b/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol deleted file mode 100644 index cc66ec6c7..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import "../../../Interfaces/IWETH.sol"; -import "../../LeftoversSweep.sol"; -// Curve -import "./Curve/ICurveStableswapNGPool.sol"; -// UniV3 -import "./UniswapV3/ISwapRouter.sol"; - -import "../../Interfaces/IExchange.sol"; - -// import "forge-std/console2.sol"; - -contract HybridCurveUniV3Exchange is LeftoversSweep, IExchange { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - IBoldToken public immutable boldToken; - IERC20 public immutable USDC; - IWETH public immutable WETH; - - // Curve - ICurveStableswapNGPool public immutable curvePool; - uint128 public immutable USDC_INDEX; - uint128 public immutable BOLD_TOKEN_INDEX; - - // Uniswap - uint24 public immutable feeUsdcWeth; - uint24 public immutable feeWethColl; - ISwapRouter public immutable uniV3Router; - - constructor( - IERC20 _collToken, - IBoldToken _boldToken, - IERC20 _usdc, - IWETH _weth, - // Curve - ICurveStableswapNGPool _curvePool, - uint128 _usdcIndex, - uint128 _boldIndex, - // UniV3 - uint24 _feeUsdcWeth, - uint24 _feeWethColl, - ISwapRouter _uniV3Router - ) { - collToken = _collToken; - boldToken = _boldToken; - USDC = _usdc; - WETH = _weth; - - // Curve - curvePool = _curvePool; - USDC_INDEX = _usdcIndex; - BOLD_TOKEN_INDEX = _boldIndex; - - // Uniswap - feeUsdcWeth = _feeUsdcWeth; - feeWethColl = _feeWethColl; - uniV3Router = _uniV3Router; - } - - // Bold -> USDC on Curve; then USDC -> WETH, and optionally WETH -> Coll, on UniV3 - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external { - InitialBalances memory initialBalances; - _setHybridExchangeInitialBalances(initialBalances); - - // Curve - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - boldToken.approve(address(curvePool), _boldAmount); - - uint256 curveUsdcAmount = curvePool.exchange(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), _boldAmount, 0); - - // Uniswap - USDC.approve(address(uniV3Router), curveUsdcAmount); - - // See: https://docs.uniswap.org/contracts/v3/guides/swaps/multihop-swaps - bytes memory path; - if (address(WETH) == address(collToken)) { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH); - } else { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, collToken); - } - - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: path, - recipient: msg.sender, - deadline: block.timestamp, - amountIn: curveUsdcAmount, - amountOutMinimum: _minCollAmount - }); - - // Executes the swap. - uniV3Router.exactInput(params); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - // Optionally Coll -> WETH, and WETH -> USDC on UniV3; then USDC -> Bold on Curve - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256) { - InitialBalances memory initialBalances; - _setHybridExchangeInitialBalances(initialBalances); - - // Uniswap - collToken.safeTransferFrom(msg.sender, address(this), _collAmount); - collToken.approve(address(uniV3Router), _collAmount); - - // See: https://docs.uniswap.org/contracts/v3/guides/swaps/multihop-swaps - bytes memory path; - if (address(WETH) == address(collToken)) { - path = abi.encodePacked(WETH, feeUsdcWeth, USDC); - } else { - path = abi.encodePacked(collToken, feeWethColl, WETH, feeUsdcWeth, USDC); - } - - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: path, - recipient: address(this), - deadline: block.timestamp, - amountIn: _collAmount, - amountOutMinimum: 0 - }); - - // Executes the swap. - uint256 uniV3UsdcAmount = uniV3Router.exactInput(params); - - // Curve - USDC.approve(address(curvePool), uniV3UsdcAmount); - - uint256 boldAmount = - curvePool.exchange(int128(USDC_INDEX), int128(BOLD_TOKEN_INDEX), uniV3UsdcAmount, _minBoldAmount); - boldToken.transfer(msg.sender, boldAmount); - - // return leftovers to user - _returnLeftovers(initialBalances); - - return boldAmount; - } - - function _setHybridExchangeInitialBalances(InitialBalances memory initialBalances) internal view { - initialBalances.tokens[0] = boldToken; - initialBalances.tokens[1] = USDC; - initialBalances.tokens[2] = WETH; - if (address(WETH) != address(collToken)) { - initialBalances.tokens[3] = collToken; - } - _setInitialBalances(initialBalances); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol b/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol deleted file mode 100644 index ac55d1efe..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "../../../Interfaces/IWETH.sol"; -// Curve -import "./Curve/ICurveStableswapNGPool.sol"; -// UniV3 -import "./UniswapV3/IQuoterV2.sol"; - -import "../../Interfaces/IExchangeHelpers.sol"; - -contract HybridCurveUniV3ExchangeHelpers is IExchangeHelpers { - uint256 private constant DECIMAL_PRECISION = 1e18; - - //HybridCurveUniV3Exchange public immutable exchange; - - IERC20 public immutable USDC; - IWETH public immutable WETH; - - // Curve - ICurveStableswapNGPool public immutable curvePool; - uint128 public immutable USDC_INDEX; - uint128 public immutable BOLD_TOKEN_INDEX; - - // Uniswap - uint24 public immutable feeUsdcWeth; - uint24 public immutable feeWethColl; - IQuoterV2 public immutable uniV3Quoter; - - /* - constructor(HybridCurveUniV3Exchange _exchange) { - exchange = _exchange; - - USDC = _exchange.USDC(); - WETH = _exchange.WETH(); - - curvePool = _exchange.curvePool(); - USDC_INDEX = _exchange.USDC_INDEX(); - BOLD_TOKEN_INDEX = _exchange.BOLD_INDEX(); - - // Uniswap - feeUsdcWeth = _exchange.feeUsdcWeth(); - feeWethColl = _exchange.feeWethColl(); - uniV3Quoter = _exchange.uniV3Quoter(); - } - */ - - constructor( - IERC20 _usdc, - IWETH _weth, - // Curve - ICurveStableswapNGPool _curvePool, - uint128 _usdcIndex, - uint128 _boldIndex, - // UniV3 - uint24 _feeUsdcWeth, - uint24 _feeWethColl, - IQuoterV2 _uniV3Quoter - ) { - USDC = _usdc; - WETH = _weth; - - // Curve - curvePool = _curvePool; - USDC_INDEX = _usdcIndex; - BOLD_TOKEN_INDEX = _boldIndex; - - // Uniswap - feeUsdcWeth = _feeUsdcWeth; - feeWethColl = _feeWethColl; - uniV3Quoter = _uniV3Quoter; - } - - function getCollFromBold(uint256 _boldAmount, IERC20 _collToken, uint256 _desiredCollAmount) - external /* view */ - returns (uint256 collAmount, uint256 deviation) - { - // BOLD -> USDC - uint256 curveUsdcAmount = curvePool.get_dy(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), _boldAmount); - - // USDC -> Coll - bytes memory path; - if (address(WETH) == address(_collToken)) { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH); - } else { - path = abi.encodePacked(USDC, feeUsdcWeth, WETH, feeWethColl, _collToken); - } - - (collAmount,,,) = uniV3Quoter.quoteExactInput(path, curveUsdcAmount); - - if (_desiredCollAmount > 0 && collAmount <= _desiredCollAmount) { - deviation = DECIMAL_PRECISION - collAmount * DECIMAL_PRECISION / _desiredCollAmount; - } - - return (collAmount, deviation); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol b/contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol deleted file mode 100644 index 0f64a9c42..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniV3Exchange.sol +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import "../../LeftoversSweep.sol"; -import "../../../Interfaces/IBoldToken.sol"; -import "./UniswapV3/ISwapRouter.sol"; -import "./UniswapV3/UniPriceConverter.sol"; -import "../../Interfaces/IExchange.sol"; -import {DECIMAL_PRECISION} from "../../../Dependencies/Constants.sol"; - -contract UniV3Exchange is LeftoversSweep, UniPriceConverter, IExchange { - using SafeERC20 for IERC20; - - IERC20 public immutable collToken; - IBoldToken public immutable boldToken; - uint24 public immutable fee; - ISwapRouter public immutable uniV3Router; - - constructor(IERC20 _collToken, IBoldToken _boldToken, uint24 _fee, ISwapRouter _uniV3Router) { - collToken = _collToken; - boldToken = _boldToken; - fee = _fee; - uniV3Router = _uniV3Router; - } - - function swapFromBold(uint256 _boldAmount, uint256 _minCollAmount) external { - ISwapRouter uniV3RouterCached = uniV3Router; - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - boldToken.approve(address(uniV3RouterCached), _boldAmount); - - ISwapRouter.ExactOutputSingleParams memory params = ISwapRouter.ExactOutputSingleParams({ - tokenIn: address(boldToken), - tokenOut: address(collToken), - fee: fee, - recipient: msg.sender, - deadline: block.timestamp, - amountOut: _minCollAmount, - amountInMaximum: _boldAmount, - sqrtPriceLimitX96: 0 // See: https://ethereum.stackexchange.com/a/156018/9205 - }); - - uniV3RouterCached.exactOutputSingle(params); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function swapToBold(uint256 _collAmount, uint256 _minBoldAmount) external returns (uint256) { - ISwapRouter uniV3RouterCached = uniV3Router; - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(collToken, boldToken, initialBalances); - - collToken.safeTransferFrom(msg.sender, address(this), _collAmount); - collToken.approve(address(uniV3RouterCached), _collAmount); - - ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ - tokenIn: address(collToken), - tokenOut: address(boldToken), - fee: fee, - recipient: msg.sender, - deadline: block.timestamp, - amountIn: _collAmount, - amountOutMinimum: _minBoldAmount, - sqrtPriceLimitX96: 0 // See: https://ethereum.stackexchange.com/a/156018/9205 - }); - - uint256 amountOut = uniV3RouterCached.exactInputSingle(params); - - // return leftovers to user - _returnLeftovers(initialBalances); - - return amountOut; - } - - function priceToSqrtPrice(IBoldToken _boldToken, IERC20 _collToken, uint256 _price) public pure returns (uint160) { - // inverse price if Bold goes first - uint256 price = _zeroForOne(_boldToken, _collToken) ? DECIMAL_PRECISION * DECIMAL_PRECISION / _price : _price; - return priceToSqrtPriceX96(price); - } - - // See: https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol#L207C9-L207C60 - function _zeroForOne(IBoldToken _boldToken, IERC20 _collToken) internal pure returns (bool) { - return address(_boldToken) < address(_collToken); - } -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol deleted file mode 100644 index 251c88020..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -import "./IPoolInitializer.sol"; - -/// @title Non-fungible token for positions -/// @notice Wraps Uniswap V3 positions in a non-fungible token interface which allows for them to be transferred -/// and authorized. -interface INonfungiblePositionManager is IPoolInitializer { - /// @notice Emitted when liquidity is increased for a position NFT - /// @dev Also emitted when a token is minted - /// @param tokenId The ID of the token for which liquidity was increased - /// @param liquidity The amount by which liquidity for the NFT position was increased - /// @param amount0 The amount of token0 that was paid for the increase in liquidity - /// @param amount1 The amount of token1 that was paid for the increase in liquidity - event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); - /// @notice Emitted when liquidity is decreased for a position NFT - /// @param tokenId The ID of the token for which liquidity was decreased - /// @param liquidity The amount by which liquidity for the NFT position was decreased - /// @param amount0 The amount of token0 that was accounted for the decrease in liquidity - /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity - event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); - /// @notice Emitted when tokens are collected for a position NFT - /// @dev The amounts reported may not be exactly equivalent to the amounts transferred, due to rounding behavior - /// @param tokenId The ID of the token for which underlying tokens were collected - /// @param recipient The address of the account that received the collected tokens - /// @param amount0 The amount of token0 owed to the position that was collected - /// @param amount1 The amount of token1 owed to the position that was collected - event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1); - - /// @notice Returns the position information associated with a given token ID. - /// @dev Throws if the token ID is not valid. - /// @param tokenId The ID of the token that represents the position - /// @return nonce The nonce for permits - /// @return operator The address that is approved for spending - /// @return token0 The address of the token0 for a specific pool - /// @return token1 The address of the token1 for a specific pool - /// @return fee The fee associated with the pool - /// @return tickLower The lower end of the tick range for the position - /// @return tickUpper The higher end of the tick range for the position - /// @return liquidity The liquidity of the position - /// @return feeGrowthInside0LastX128 The fee growth of token0 as of the last action on the individual position - /// @return feeGrowthInside1LastX128 The fee growth of token1 as of the last action on the individual position - /// @return tokensOwed0 The uncollected amount of token0 owed to the position as of the last computation - /// @return tokensOwed1 The uncollected amount of token1 owed to the position as of the last computation - function positions(uint256 tokenId) - external - view - returns ( - uint96 nonce, - address operator, - address token0, - address token1, - uint24 fee, - int24 tickLower, - int24 tickUpper, - uint128 liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); - - struct MintParams { - address token0; - address token1; - uint24 fee; - int24 tickLower; - int24 tickUpper; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - address recipient; - uint256 deadline; - } - - /// @notice Creates a new position wrapped in a NFT - /// @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized - /// a method does not exist, i.e. the pool is assumed to be initialized. - /// @param params The params necessary to mint a position, encoded as `MintParams` in calldata - /// @return tokenId The ID of the token that represents the minted position - /// @return liquidity The amount of liquidity for this position - /// @return amount0 The amount of token0 - /// @return amount1 The amount of token1 - function mint(MintParams calldata params) - external - payable - returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1); - - struct IncreaseLiquidityParams { - uint256 tokenId; - uint256 amount0Desired; - uint256 amount1Desired; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - /// @notice Increases the amount of liquidity in a position, with tokens paid by the `msg.sender` - /// @param params tokenId The ID of the token for which liquidity is being increased, - /// amount0Desired The desired amount of token0 to be spent, - /// amount1Desired The desired amount of token1 to be spent, - /// amount0Min The minimum amount of token0 to spend, which serves as a slippage check, - /// amount1Min The minimum amount of token1 to spend, which serves as a slippage check, - /// deadline The time by which the transaction must be included to effect the change - /// @return liquidity The new liquidity amount as a result of the increase - /// @return amount0 The amount of token0 to acheive resulting liquidity - /// @return amount1 The amount of token1 to acheive resulting liquidity - function increaseLiquidity(IncreaseLiquidityParams calldata params) - external - payable - returns (uint128 liquidity, uint256 amount0, uint256 amount1); - - struct DecreaseLiquidityParams { - uint256 tokenId; - uint128 liquidity; - uint256 amount0Min; - uint256 amount1Min; - uint256 deadline; - } - - /// @notice Decreases the amount of liquidity in a position and accounts it to the position - /// @param params tokenId The ID of the token for which liquidity is being decreased, - /// amount The amount by which liquidity will be decreased, - /// amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, - /// amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, - /// deadline The time by which the transaction must be included to effect the change - /// @return amount0 The amount of token0 accounted to the position's tokens owed - /// @return amount1 The amount of token1 accounted to the position's tokens owed - function decreaseLiquidity(DecreaseLiquidityParams calldata params) - external - payable - returns (uint256 amount0, uint256 amount1); - - struct CollectParams { - uint256 tokenId; - address recipient; - uint128 amount0Max; - uint128 amount1Max; - } - - /// @notice Collects up to a maximum amount of fees owed to a specific position to the recipient - /// @param params tokenId The ID of the NFT for which tokens are being collected, - /// recipient The account that should receive the tokens, - /// amount0Max The maximum amount of token0 to collect, - /// amount1Max The maximum amount of token1 to collect - /// @return amount0 The amount of fees collected in token0 - /// @return amount1 The amount of fees collected in token1 - function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1); - - /// @notice Burns a token ID, which deletes it from the NFT contract. The token must have 0 liquidity and all tokens - /// must be collected first. - /// @param tokenId The ID of the token that is being burned - function burn(uint256 tokenId) external payable; -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol deleted file mode 100644 index c2146992d..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IPoolInitializer.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title Creates and initializes V3 Pools -/// @notice Provides a method for creating and initializing a pool, if necessary, for bundling with other methods that -/// require the pool to exist. -interface IPoolInitializer { - /// @notice Creates a new pool if it does not exist, then initializes if not initialized - /// @dev This method can be bundled with others via IMulticall for the first action (e.g. mint) performed against a pool - /// @param token0 The contract address of token0 of the pool - /// @param token1 The contract address of token1 of the pool - /// @param fee The fee amount of the v3 pool for the specified token pair - /// @param sqrtPriceX96 The initial square root price of the pool as a Q64.96 value - /// @return pool Returns the pool address based on the pair of tokens and fee, will return the newly created pool address if necessary - function createAndInitializePoolIfNecessary(address token0, address token1, uint24 fee, uint160 sqrtPriceX96) - external - payable - returns (address pool); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol deleted file mode 100644 index 7f41cb084..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title QuoterV2 Interface -/// @notice Supports quoting the calculated amounts from exact input or exact output swaps. -/// @notice For each pool also tells you the number of initialized ticks crossed and the sqrt price of the pool after the swap. -/// @dev These functions are not marked view because they rely on calling non-view functions and reverting -/// to compute the result. They are also not gas efficient and should not be called on-chain. -interface IQuoterV2 { - /// @notice Returns the amount out received for a given exact input swap without executing the swap - /// @param path The path of the swap, i.e. each token pair and the pool fee - /// @param amountIn The amount of the first token to swap - /// @return amountOut The amount of the last token that would be received - /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path - /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactInput(bytes memory path, uint256 amountIn) - external - returns ( - uint256 amountOut, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksCrossedList, - uint256 gasEstimate - ); - - struct QuoteExactInputSingleParams { - address tokenIn; - address tokenOut; - uint256 amountIn; - uint24 fee; - uint160 sqrtPriceLimitX96; - } - - /// @notice Returns the amount out received for a given exact input but for a swap of a single pool - /// @param params The params for the quote, encoded as `QuoteExactInputSingleParams` - /// tokenIn The token being swapped in - /// tokenOut The token being swapped out - /// fee The fee of the token pool to consider for the pair - /// amountIn The desired input amount - /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap - /// @return amountOut The amount of `tokenOut` that would be received - /// @return sqrtPriceX96After The sqrt price of the pool after the swap - /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactInputSingle(QuoteExactInputSingleParams memory params) - external - returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); - - /// @notice Returns the amount in required for a given exact output swap without executing the swap - /// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order - /// @param amountOut The amount of the last token to receive - /// @return amountIn The amount of first token required to be paid - /// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path - /// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactOutput(bytes memory path, uint256 amountOut) - external - returns ( - uint256 amountIn, - uint160[] memory sqrtPriceX96AfterList, - uint32[] memory initializedTicksCrossedList, - uint256 gasEstimate - ); - - struct QuoteExactOutputSingleParams { - address tokenIn; - address tokenOut; - uint256 amount; - uint24 fee; - uint160 sqrtPriceLimitX96; - } - - /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool - /// @param params The params for the quote, encoded as `QuoteExactOutputSingleParams` - /// tokenIn The token being swapped in - /// tokenOut The token being swapped out - /// fee The fee of the token pool to consider for the pair - /// amountOut The desired output amount - /// sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap - /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` - /// @return sqrtPriceX96After The sqrt price of the pool after the swap - /// @return initializedTicksCrossed The number of initialized ticks that the swap crossed - /// @return gasEstimate The estimate of the gas that the swap consumes - function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) - external - returns (uint256 amountIn, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol deleted file mode 100644 index beb300450..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; -pragma abicoder v2; - -/// @title Router token swapping functionality -/// @notice Functions for swapping tokens via Uniswap V3 -interface ISwapRouter { - struct ExactInputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - address recipient; - uint256 deadline; - uint256 amountIn; - uint256 amountOutMinimum; - uint160 sqrtPriceLimitX96; - } - - /// @notice Swaps `amountIn` of one token for as much as possible of another token - /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata - /// @return amountOut The amount of the received token - function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut); - - struct ExactInputParams { - bytes path; - address recipient; - uint256 deadline; - uint256 amountIn; - uint256 amountOutMinimum; - } - - /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path - /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata - /// @return amountOut The amount of the received token - function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); - - struct ExactOutputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - address recipient; - uint256 deadline; - uint256 amountOut; - uint256 amountInMaximum; - uint160 sqrtPriceLimitX96; - } - - /// @notice Swaps as little as possible of one token for `amountOut` of another token - /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata - /// @return amountIn The amount of the input token - function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn); - - struct ExactOutputParams { - bytes path; - address recipient; - uint256 deadline; - uint256 amountOut; - uint256 amountInMaximum; - } - - /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed) - /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata - /// @return amountIn The amount of the input token - function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol deleted file mode 100644 index 1ce33c793..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -interface IUniswapV3Factory { - /// @notice Emitted when the owner of the factory is changed - /// @param oldOwner The owner before the owner was changed - /// @param newOwner The owner after the owner was changed - event OwnerChanged(address indexed oldOwner, address indexed newOwner); - - /// @notice Emitted when a pool is created - /// @param token0 The first token of the pool by address sort order - /// @param token1 The second token of the pool by address sort order - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks - /// @param pool The address of the created pool - event PoolCreated( - address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool - ); - - /// @notice Emitted when a new fee amount is enabled for pool creation via the factory - /// @param fee The enabled fee, denominated in hundredths of a bip - /// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee - event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing); - - /// @notice Returns the current owner of the factory - /// @dev Can be changed by the current owner via setOwner - /// @return The address of the factory owner - function owner() external view returns (address); - - /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled - /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context - /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee - /// @return The tick spacing - function feeAmountTickSpacing(uint24 fee) external view returns (int24); - - /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist - /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order - /// @param tokenA The contract address of either token0 or token1 - /// @param tokenB The contract address of the other token - /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip - /// @return pool The pool address - function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool); - - /// @notice Creates a pool for the given two tokens and fee - /// @param tokenA One of the two tokens in the desired pool - /// @param tokenB The other of the two tokens in the desired pool - /// @param fee The desired fee for the pool - /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved - /// from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments - /// are invalid. - /// @return pool The address of the newly created pool - function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool); - - /// @notice Updates the owner of the factory - /// @dev Must be called by the current owner - /// @param _owner The new owner of the factory - function setOwner(address _owner) external; - - /// @notice Enables a fee amount with the given tickSpacing - /// @dev Fee amounts may never be removed once enabled - /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6) - /// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount - function enableFeeAmount(uint24 fee, int24 tickSpacing) external; -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol deleted file mode 100644 index 85dbdea6d..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol +++ /dev/null @@ -1,271 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.5.0; - -interface IUniswapV3Pool { - /// @notice The contract that deployed the pool, which must adhere to the IUniswapV3Factory interface - /// @return The contract address - function factory() external view returns (address); - - /// @notice The first of the two tokens of the pool, sorted by address - /// @return The token contract address - function token0() external view returns (address); - - /// @notice The second of the two tokens of the pool, sorted by address - /// @return The token contract address - function token1() external view returns (address); - - /// @notice The pool's fee in hundredths of a bip, i.e. 1e-6 - /// @return The fee - function fee() external view returns (uint24); - - /// @notice The pool tick spacing - /// @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive - /// e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... - /// This value is an int24 to avoid casting even though it is always positive. - /// @return The tick spacing - function tickSpacing() external view returns (int24); - - /// @notice The maximum amount of position liquidity that can use any tick in the range - /// @dev This parameter is enforced per tick to prevent liquidity from overflowing a uint128 at any point, and - /// also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool - /// @return The max amount of liquidity per tick - function maxLiquidityPerTick() external view returns (uint128); - - /// @notice The 0th storage slot in the pool stores many values, and is exposed as a single method to save gas - /// when accessed externally. - /// @return sqrtPriceX96 The current price of the pool as a sqrt(token1/token0) Q64.96 value - /// tick The current tick of the pool, i.e. according to the last tick transition that was run. - /// This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick - /// boundary. - /// observationIndex The index of the last oracle observation that was written, - /// observationCardinality The current maximum number of observations stored in the pool, - /// observationCardinalityNext The next maximum number of observations, to be updated when the observation. - /// feeProtocol The protocol fee for both tokens of the pool. - /// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0 - /// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee. - /// unlocked Whether the pool is currently locked to reentrancy - function slot0() - external - view - returns ( - uint160 sqrtPriceX96, - int24 tick, - uint16 observationIndex, - uint16 observationCardinality, - uint16 observationCardinalityNext, - uint8 feeProtocol, - bool unlocked - ); - - /// @notice The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool - /// @dev This value can overflow the uint256 - function feeGrowthGlobal0X128() external view returns (uint256); - - /// @notice The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool - /// @dev This value can overflow the uint256 - function feeGrowthGlobal1X128() external view returns (uint256); - - /// @notice The amounts of token0 and token1 that are owed to the protocol - /// @dev Protocol fees will never exceed uint128 max in either token - function protocolFees() external view returns (uint128 token0, uint128 token1); - - /// @notice The currently in range liquidity available to the pool - /// @dev This value has no relationship to the total liquidity across all ticks - function liquidity() external view returns (uint128); - - /// @notice Look up information about a specific tick in the pool - /// @param tick The tick to look up - /// @return liquidityGross the total amount of position liquidity that uses the pool either as tick lower or - /// tick upper, - /// liquidityNet how much liquidity changes when the pool price crosses the tick, - /// feeGrowthOutside0X128 the fee growth on the other side of the tick from the current tick in token0, - /// feeGrowthOutside1X128 the fee growth on the other side of the tick from the current tick in token1, - /// tickCumulativeOutside the cumulative tick value on the other side of the tick from the current tick - /// secondsPerLiquidityOutsideX128 the seconds spent per liquidity on the other side of the tick from the current tick, - /// secondsOutside the seconds spent on the other side of the tick from the current tick, - /// initialized Set to true if the tick is initialized, i.e. liquidityGross is greater than 0, otherwise equal to false. - /// Outside values can only be used if the tick is initialized, i.e. if liquidityGross is greater than 0. - /// In addition, these values are only relative and must be used only in comparison to previous snapshots for - /// a specific position. - function ticks(int24 tick) - external - view - returns ( - uint128 liquidityGross, - int128 liquidityNet, - uint256 feeGrowthOutside0X128, - uint256 feeGrowthOutside1X128, - int56 tickCumulativeOutside, - uint160 secondsPerLiquidityOutsideX128, - uint32 secondsOutside, - bool initialized - ); - - /// @notice Returns 256 packed tick initialized boolean values. See TickBitmap for more information - function tickBitmap(int16 wordPosition) external view returns (uint256); - - /// @notice Returns the information about a position by the position's key - /// @param key The position's key is a hash of a preimage composed by the owner, tickLower and tickUpper - /// @return _liquidity The amount of liquidity in the position, - /// Returns feeGrowthInside0LastX128 fee growth of token0 inside the tick range as of the last mint/burn/poke, - /// Returns feeGrowthInside1LastX128 fee growth of token1 inside the tick range as of the last mint/burn/poke, - /// Returns tokensOwed0 the computed amount of token0 owed to the position as of the last mint/burn/poke, - /// Returns tokensOwed1 the computed amount of token1 owed to the position as of the last mint/burn/poke - function positions(bytes32 key) - external - view - returns ( - uint128 _liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ); - - /// @notice Returns data about a specific observation index - /// @param index The element of the observations array to fetch - /// @dev You most likely want to use #observe() instead of this method to get an observation as of some amount of time - /// ago, rather than at a specific index in the array. - /// @return blockTimestamp The timestamp of the observation, - /// Returns tickCumulative the tick multiplied by seconds elapsed for the life of the pool as of the observation timestamp, - /// Returns secondsPerLiquidityCumulativeX128 the seconds per in range liquidity for the life of the pool as of the observation timestamp, - /// Returns initialized whether the observation has been initialized and the values are safe to use - function observations(uint256 index) - external - view - returns ( - uint32 blockTimestamp, - int56 tickCumulative, - uint160 secondsPerLiquidityCumulativeX128, - bool initialized - ); - /// @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp - /// @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing - /// the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick, - /// you must call it with secondsAgos = [3600, 0]. - /// @dev The time weighted average tick represents the geometric time weighted average price of the pool, in - /// log base sqrt(1.0001) of token1 / token0. The TickMath library can be used to go from a tick value to a ratio. - /// @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned - /// @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp - /// @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block - /// timestamp - function observe(uint32[] calldata secondsAgos) - external - view - returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s); - - /// @notice Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range - /// @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed. - /// I.e., snapshots cannot be compared if a position is not held for the entire period between when the first - /// snapshot is taken and the second snapshot is taken. - /// @param tickLower The lower tick of the range - /// @param tickUpper The upper tick of the range - /// @return tickCumulativeInside The snapshot of the tick accumulator for the range - /// @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range - /// @return secondsInside The snapshot of seconds per liquidity for the range - function snapshotCumulativesInside(int24 tickLower, int24 tickUpper) - external - view - returns (int56 tickCumulativeInside, uint160 secondsPerLiquidityInsideX128, uint32 secondsInside); - - /// @notice Sets the initial price for the pool - /// @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value - /// @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 - function initialize(uint160 sqrtPriceX96) external; - - /// @notice Adds liquidity for the given recipient/tickLower/tickUpper position - /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback - /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends - /// on tickLower, tickUpper, the amount of liquidity, and the current price. - /// @param recipient The address for which the liquidity will be created - /// @param tickLower The lower tick of the position in which to add liquidity - /// @param tickUpper The upper tick of the position in which to add liquidity - /// @param amount The amount of liquidity to mint - /// @param data Any data that should be passed through to the callback - /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback - /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback - function mint(address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data) - external - returns (uint256 amount0, uint256 amount1); - - /// @notice Collects tokens owed to a position - /// @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. - /// Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or - /// amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the - /// actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. - /// @param recipient The address which should receive the fees collected - /// @param tickLower The lower tick of the position for which to collect fees - /// @param tickUpper The upper tick of the position for which to collect fees - /// @param amount0Requested How much token0 should be withdrawn from the fees owed - /// @param amount1Requested How much token1 should be withdrawn from the fees owed - /// @return amount0 The amount of fees collected in token0 - /// @return amount1 The amount of fees collected in token1 - function collect( - address recipient, - int24 tickLower, - int24 tickUpper, - uint128 amount0Requested, - uint128 amount1Requested - ) external returns (uint128 amount0, uint128 amount1); - - /// @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position - /// @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 - /// @dev Fees must be collected separately via a call to #collect - /// @param tickLower The lower tick of the position for which to burn liquidity - /// @param tickUpper The upper tick of the position for which to burn liquidity - /// @param amount How much liquidity to burn - /// @return amount0 The amount of token0 sent to the recipient - /// @return amount1 The amount of token1 sent to the recipient - function burn(int24 tickLower, int24 tickUpper, uint128 amount) - external - returns (uint256 amount0, uint256 amount1); - - /// @notice Swap token0 for token1, or token1 for token0 - /// @dev The caller of this method receives a callback in the form of IUniswapV3SwapCallback#uniswapV3SwapCallback - /// @param recipient The address to receive the output of the swap - /// @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 - /// @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) - /// @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this - /// value after the swap. If one for zero, the price cannot be greater than this value after the swap - /// @param data Any data to be passed through to the callback - /// @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive - /// @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive - function swap( - address recipient, - bool zeroForOne, - int256 amountSpecified, - uint160 sqrtPriceLimitX96, - bytes calldata data - ) external returns (int256 amount0, int256 amount1); - - /// @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback - /// @dev The caller of this method receives a callback in the form of IUniswapV3FlashCallback#uniswapV3FlashCallback - /// @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling - /// with 0 amount{0,1} and sending the donation amount(s) from the callback - /// @param recipient The address which will receive the token0 and token1 amounts - /// @param amount0 The amount of token0 to send - /// @param amount1 The amount of token1 to send - /// @param data Any data to be passed through to the callback - function flash(address recipient, uint256 amount0, uint256 amount1, bytes calldata data) external; - - /// @notice Increase the maximum number of price and liquidity observations that this pool will store - /// @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to - /// the input observationCardinalityNext. - /// @param observationCardinalityNext The desired minimum number of observations for the pool to store - function increaseObservationCardinalityNext(uint16 observationCardinalityNext) external; - - /// @notice Set the denominator of the protocol's % share of the fees - /// @param feeProtocol0 new protocol fee for token0 of the pool - /// @param feeProtocol1 new protocol fee for token1 of the pool - function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external; - - /// @notice Collect the protocol fee accrued to the pool - /// @param recipient The address to which collected protocol fees should be sent - /// @param amount0Requested The maximum amount of token0 to send, can be 0 to collect fees in only token1 - /// @param amount1Requested The maximum amount of token1 to send, can be 0 to collect fees in only token0 - /// @return amount0 The protocol fee collected in token0 - /// @return amount1 The protocol fee collected in token1 - function collectProtocol(address recipient, uint128 amount0Requested, uint128 amount1Requested) - external - returns (uint128 amount0, uint128 amount1); -} diff --git a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol b/contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol deleted file mode 100644 index a418baa85..000000000 --- a/contracts/src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {DECIMAL_PRECISION} from "../../../../Dependencies/Constants.sol"; - -contract UniPriceConverter { - function priceToSqrtPriceX96(uint256 _price) public pure returns (uint160 sqrtPriceX96) { - // overflow vs precision - if (_price > (1 << 64)) { - // ~18.4e18 - sqrtPriceX96 = uint160(Math.sqrt(_price / DECIMAL_PRECISION) << 96); - } else { - sqrtPriceX96 = uint160(Math.sqrt((_price << 192) / DECIMAL_PRECISION)); - } - } - - function sqrtPriceX96ToPrice(uint160 _sqrtPriceX96) public pure returns (uint256 price) { - //price = uint256(_sqrtPriceX96) * uint256(_sqrtPriceX96) * DECIMAL_PRECISION / (1 << 192); - uint256 squaredPrice = uint256(_sqrtPriceX96) * uint256(_sqrtPriceX96); - // overflow vs precision - if (squaredPrice > 115e57) { - // max uint256 / 1e18 - price = ((squaredPrice >> 96) * DECIMAL_PRECISION) >> 96; - } else { - price = (squaredPrice * DECIMAL_PRECISION) >> 192; - } - } -} diff --git a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol b/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol deleted file mode 100644 index bcf5b5db8..000000000 --- a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IFlashLoanRecipient.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -pragma solidity >=0.7.0 <0.9.0; - -// Inspired by Aave Protocol's IFlashLoanReceiver. - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - -interface IFlashLoanRecipient { - /** - * @dev When `flashLoan` is called on the Vault, it invokes the `receiveFlashLoan` hook on the recipient. - * - * At the time of the call, the Vault will have transferred `amounts` for `tokens` to the recipient. Before this - * call returns, the recipient must have transferred `amounts` plus `feeAmounts` for each token back to the - * Vault, or else the entire flash loan will revert. - * - * `userData` is the same value passed in the `IVault.flashLoan` call. - */ - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory feeAmounts, - bytes memory userData - ) external; -} diff --git a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol b/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol deleted file mode 100644 index 5a89098ca..000000000 --- a/contracts/src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "./IFlashLoanRecipient.sol"; - -pragma solidity >=0.7.0 <0.9.0; - -/** - * @dev Full external interface for the Vault core contract - no external or public methods exist in the contract that - * don't override one of these declarations. - */ -interface IVault { - // Flash Loans - - /** - * @dev Performs a 'flash loan', sending tokens to `recipient`, executing the `receiveFlashLoan` hook on it, - * and then reverting unless the tokens plus a proportional protocol fee have been returned. - * - * The `tokens` and `amounts` arrays must have the same length, and each entry in these indicates the loan amount - * for each token contract. `tokens` must be sorted in ascending order. - * - * The 'userData' field is ignored by the Vault, and forwarded as-is to `recipient` as part of the - * `receiveFlashLoan` call. - * - * Emits `FlashLoan` events. - */ - function flashLoan( - IFlashLoanRecipient recipient, - IERC20[] memory tokens, - uint256[] memory amounts, - bytes memory userData - ) external; - - /** - * @dev Emitted for each individual flash loan performed by `flashLoan`. - */ - event FlashLoan(IFlashLoanRecipient indexed recipient, IERC20 indexed token, uint256 amount, uint256 feeAmount); - - /** - * @dev Returns the Vault's WETH instance. - */ - //function WETH() external view returns (IWETH); - // solhint-disable-previous-line func-name-mixedcase -} diff --git a/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol b/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol deleted file mode 100644 index 7fdb73c69..000000000 --- a/contracts/src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol +++ /dev/null @@ -1,120 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import "./Balancer/vault/IVault.sol"; -import "./Balancer/vault/IFlashLoanRecipient.sol"; - -import "../../Interfaces/ILeverageZapper.sol"; -import "../../Interfaces/IFlashLoanReceiver.sol"; -import "../../Interfaces/IFlashLoanProvider.sol"; - -contract BalancerFlashLoan is IFlashLoanRecipient, IFlashLoanProvider { - using SafeERC20 for IERC20; - - IVault private constant vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - IFlashLoanReceiver public receiver; - - function makeFlashLoan(IERC20 _token, uint256 _amount, Operation _operation, bytes calldata _params) external { - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = _token; - uint256[] memory amounts = new uint256[](1); - amounts[0] = _amount; - - // Data for the callback receiveFlashLoan - bytes memory userData; - if (_operation == Operation.OpenTrove) { - ILeverageZapper.OpenLeveragedTroveParams memory openTroveParams = - abi.decode(_params, (ILeverageZapper.OpenLeveragedTroveParams)); - userData = abi.encode(_operation, openTroveParams); - } else if (_operation == Operation.LeverUpTrove) { - ILeverageZapper.LeverUpTroveParams memory leverUpTroveParams = - abi.decode(_params, (ILeverageZapper.LeverUpTroveParams)); - userData = abi.encode(_operation, leverUpTroveParams); - } else if (_operation == Operation.LeverDownTrove) { - ILeverageZapper.LeverDownTroveParams memory leverDownTroveParams = - abi.decode(_params, (ILeverageZapper.LeverDownTroveParams)); - userData = abi.encode(_operation, leverDownTroveParams); - } else if (_operation == Operation.CloseTrove) { - IZapper.CloseTroveParams memory closeTroveParams = abi.decode(_params, (IZapper.CloseTroveParams)); - userData = abi.encode(_operation, closeTroveParams); - } else { - revert("LZ: Wrong Operation"); - } - - // This will be used by the callback below no - receiver = IFlashLoanReceiver(msg.sender); - - vault.flashLoan(this, tokens, amounts, userData); - } - - function receiveFlashLoan( - IERC20[] calldata tokens, - uint256[] calldata amounts, - uint256[] calldata feeAmounts, - bytes calldata userData - ) external override { - require(msg.sender == address(vault), "Caller is not Vault"); - require(address(receiver) != address(0), "Flash loan not properly initiated"); - - // Cache and reset receiver, to comply with CEI pattern, as some callbacks in zappers do raw calls - // It’s not necessary, as Balancer flash loans are protected against re-entrancy - // But it’s safer, specially if someone tries to reuse this code, and more gas efficient - IFlashLoanReceiver receiverCached = receiver; - receiver = IFlashLoanReceiver(address(0)); - - // decode and operation - Operation operation = abi.decode(userData[0:32], (Operation)); - - if (operation == Operation.OpenTrove) { - // Open - // decode params - ILeverageZapper.OpenLeveragedTroveParams memory openTroveParams = - abi.decode(userData[32:], (ILeverageZapper.OpenLeveragedTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnOpenLeveragedTrove(openTroveParams, effectiveFlashLoanAmount); - } else if (operation == Operation.LeverUpTrove) { - // Lever up - // decode params - ILeverageZapper.LeverUpTroveParams memory leverUpTroveParams = - abi.decode(userData[32:], (ILeverageZapper.LeverUpTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnLeverUpTrove(leverUpTroveParams, effectiveFlashLoanAmount); - } else if (operation == Operation.LeverDownTrove) { - // Lever down - // decode params - ILeverageZapper.LeverDownTroveParams memory leverDownTroveParams = - abi.decode(userData[32:], (ILeverageZapper.LeverDownTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnLeverDownTrove(leverDownTroveParams, effectiveFlashLoanAmount); - } else if (operation == Operation.CloseTrove) { - // Close trove - // decode params - IZapper.CloseTroveParams memory closeTroveParams = abi.decode(userData[32:], (IZapper.CloseTroveParams)); - // Flash loan minus fees - uint256 effectiveFlashLoanAmount = amounts[0] - feeAmounts[0]; - // We send only effective flash loan, keeping fees here - tokens[0].safeTransfer(address(receiverCached), effectiveFlashLoanAmount); - // Zapper callback - receiverCached.receiveFlashLoanOnCloseTroveFromCollateral(closeTroveParams, effectiveFlashLoanAmount); - } else { - revert("LZ: Wrong Operation"); - } - - // Return flash loan - tokens[0].safeTransfer(address(vault), amounts[0] + feeAmounts[0]); - } -} diff --git a/contracts/src/Zappers/WETHZapper.sol b/contracts/src/Zappers/WETHZapper.sol deleted file mode 100644 index 520575bd8..000000000 --- a/contracts/src/Zappers/WETHZapper.sol +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./BaseZapper.sol"; -import "../Dependencies/Constants.sol"; - -contract WETHZapper is BaseZapper { - constructor(IAddressesRegistry _addressesRegistry, IFlashLoanProvider _flashLoanProvider, IExchange _exchange) - BaseZapper(_addressesRegistry, _flashLoanProvider, _exchange) - { - require(address(WETH) == address(_addressesRegistry.collToken()), "WZ: Wrong coll branch"); - - // Approve coll to BorrowerOperations - WETH.approve(address(borrowerOperations), type(uint256).max); - // Approve Coll to exchange module (for closeTroveFromCollateral) - WETH.approve(address(_exchange), type(uint256).max); - } - - function openTroveWithRawETH(OpenTroveParams calldata _params) external payable returns (uint256) { - require(msg.value > ETH_GAS_COMPENSATION, "WZ: Insufficient ETH"); - require( - _params.batchManager == address(0) || _params.annualInterestRate == 0, - "WZ: Cannot choose interest if joining a batch" - ); - - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - uint256 troveId; - // Include sender in index - uint256 index = _getTroveIndex(_params.ownerIndex); - if (_params.batchManager == address(0)) { - troveId = borrowerOperations.openTrove( - _params.owner, - index, - msg.value - ETH_GAS_COMPENSATION, - _params.boldAmount, - _params.upperHint, - _params.lowerHint, - _params.annualInterestRate, - _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - address(this), // add manager - address(this), // remove manager - address(this) // receiver for remove manager - ); - } else { - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory - openTroveAndJoinInterestBatchManagerParams = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _params.owner, - ownerIndex: index, - collAmount: msg.value - ETH_GAS_COMPENSATION, - boldAmount: _params.boldAmount, - upperHint: _params.upperHint, - lowerHint: _params.lowerHint, - interestBatchManager: _params.batchManager, - maxUpfrontFee: _params.maxUpfrontFee, - // Add this contract as add/receive manager to be able to fully adjust trove, - // while keeping the same management functionality - addManager: address(this), // add manager - removeManager: address(this), // remove manager - receiver: address(this) // receiver for remove manager - }); - troveId = - borrowerOperations.openTroveAndJoinInterestBatchManager(openTroveAndJoinInterestBatchManagerParams); - } - - boldToken.transfer(msg.sender, _params.boldAmount); - - // Set add/remove managers - _setAddManager(troveId, _params.addManager); - _setRemoveManagerAndReceiver(troveId, _params.removeManager, _params.receiver); - - return troveId; - } - - function addCollWithRawETH(uint256 _troveId) external payable { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - // Convert ETH to WETH - WETH.deposit{value: msg.value}(); - - borrowerOperations.addColl(_troveId, msg.value); - } - - function withdrawCollToRawETH(uint256 _troveId, uint256 _amount) external { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawColl(_troveId, _amount); - - // Convert WETH to ETH - WETH.withdraw(_amount); - (bool success,) = receiver.call{value: _amount}(""); - require(success, "WZ: Sending ETH failed"); - } - - function withdrawBold(uint256 _troveId, uint256 _boldAmount, uint256 _maxUpfrontFee) external { - address owner = troveNFT.ownerOf(_troveId); - address receiver = _requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner); - _requireZapperIsReceiver(_troveId); - - borrowerOperations.withdrawBold(_troveId, _boldAmount, _maxUpfrontFee); - - // Send Bold - boldToken.transfer(receiver, _boldAmount); - } - - function repayBold(uint256 _troveId, uint256 _boldAmount) external { - address owner = troveNFT.ownerOf(_troveId); - _requireSenderIsOwnerOrAddManager(_troveId, owner); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - _setInitialTokensAndBalances(WETH, boldToken, initialBalances); - - // Pull Bold - boldToken.transferFrom(msg.sender, address(this), _boldAmount); - - borrowerOperations.repayBold(_troveId, _boldAmount); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function adjustTroveWithRawETH( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _maxUpfrontFee - ) external payable { - InitialBalances memory initialBalances; - address payable receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function adjustZombieTroveWithRawETH( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - uint256 _upperHint, - uint256 _lowerHint, - uint256 _maxUpfrontFee - ) external payable { - InitialBalances memory initialBalances; - address payable receiver = - _adjustTrovePre(_troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, initialBalances); - borrowerOperations.adjustZombieTrove( - _troveId, _collChange, _isCollIncrease, _boldChange, _isDebtIncrease, _upperHint, _lowerHint, _maxUpfrontFee - ); - _adjustTrovePost(_collChange, _isCollIncrease, _boldChange, _isDebtIncrease, receiver, initialBalances); - } - - function _adjustTrovePre( - uint256 _troveId, - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - InitialBalances memory _initialBalances - ) internal returns (address payable) { - if (_isCollIncrease) { - require(_collChange == msg.value, "WZ: Wrong coll amount"); - } else { - require(msg.value == 0, "WZ: Not adding coll, no ETH should be received"); - } - - address payable receiver = - payable(_checkAdjustTroveManagers(_troveId, _collChange, _isCollIncrease, _isDebtIncrease)); - - // Set initial balances to make sure there are not lefovers - _setInitialTokensAndBalances(WETH, boldToken, _initialBalances); - - // ETH -> WETH - if (_isCollIncrease) { - WETH.deposit{value: _collChange}(); - } - - // Pull Bold - if (!_isDebtIncrease) { - boldToken.transferFrom(msg.sender, address(this), _boldChange); - } - - return receiver; - } - - function _adjustTrovePost( - uint256 _collChange, - bool _isCollIncrease, - uint256 _boldChange, - bool _isDebtIncrease, - address payable _receiver, - InitialBalances memory _initialBalances - ) internal { - // Send Bold - if (_isDebtIncrease) { - boldToken.transfer(_receiver, _boldChange); - } - - // return BOLD leftovers to user (trying to repay more than possible) - uint256 currentBoldBalance = boldToken.balanceOf(address(this)); - if (currentBoldBalance > _initialBalances.balances[1]) { - boldToken.transfer(_initialBalances.receiver, currentBoldBalance - _initialBalances.balances[1]); - } - // There shouldn’t be Collateral leftovers, everything sent should end up in the trove - // But ETH and WETH balance can be non-zero if someone accidentally send it to this contract - - // WETH -> ETH - if (!_isCollIncrease && _collChange > 0) { - WETH.withdraw(_collChange); - (bool success,) = _receiver.call{value: _collChange}(""); - require(success, "WZ: Sending ETH failed"); - } - } - - function closeTroveToRawETH(uint256 _troveId) external { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - // pull Bold for repayment - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); - boldToken.transferFrom(msg.sender, address(this), trove.entireDebt); - - borrowerOperations.closeTrove(_troveId); - - WETH.withdraw(trove.entireColl + ETH_GAS_COMPENSATION); - (bool success,) = receiver.call{value: trove.entireColl + ETH_GAS_COMPENSATION}(""); - require(success, "WZ: Sending ETH failed"); - } - - function closeTroveFromCollateral(uint256 _troveId, uint256 _flashLoanAmount, uint256 _minExpectedCollateral) - external - override - { - address owner = troveNFT.ownerOf(_troveId); - address payable receiver = payable(_requireSenderIsOwnerOrRemoveManagerAndGetReceiver(_troveId, owner)); - _requireZapperIsReceiver(_troveId); - - CloseTroveParams memory params = CloseTroveParams({ - troveId: _troveId, - flashLoanAmount: _flashLoanAmount, - minExpectedCollateral: _minExpectedCollateral, - receiver: receiver - }); - - // Set initial balances to make sure there are not lefovers - InitialBalances memory initialBalances; - initialBalances.tokens[0] = WETH; - initialBalances.tokens[1] = boldToken; - _setInitialBalancesAndReceiver(initialBalances, receiver); - - // Flash loan coll - flashLoanProvider.makeFlashLoan( - WETH, _flashLoanAmount, IFlashLoanProvider.Operation.CloseTrove, abi.encode(params) - ); - - // return leftovers to user - _returnLeftovers(initialBalances); - } - - function receiveFlashLoanOnCloseTroveFromCollateral( - CloseTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external { - require(msg.sender == address(flashLoanProvider), "WZ: Caller not FlashLoan provider"); - - LatestTroveData memory trove = troveManager.getLatestTroveData(_params.troveId); - uint256 collLeft = trove.entireColl - _params.flashLoanAmount; - require(collLeft >= _params.minExpectedCollateral, "WZ: Not enough collateral received"); - - // Swap Coll from flash loan to Bold, so we can repay and close trove - // We swap the flash loan minus the flash loan fee - exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - - // We asked for a min of entireDebt in swapToBold call above, so we don’t check again here: - // uint256 receivedBoldAmount = exchange.swapToBold(_effectiveFlashLoanAmount, trove.entireDebt); - //require(receivedBoldAmount >= trove.entireDebt, "WZ: Not enough BOLD obtained to repay"); - - borrowerOperations.closeTrove(_params.troveId); - - // Send coll back to return flash loan - WETH.transfer(address(flashLoanProvider), _params.flashLoanAmount); - - uint256 ethToSendBack = collLeft + ETH_GAS_COMPENSATION; - // Send coll left and gas compensation - WETH.withdraw(ethToSendBack); - (bool success,) = _params.receiver.call{value: ethToSendBack}(""); - require(success, "WZ: Sending ETH failed"); - } - - receive() external payable {} - - // Unimplemented flash loan receive functions for leverage - function receiveFlashLoanOnOpenLeveragedTrove( - ILeverageZapper.OpenLeveragedTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverUpTrove( - ILeverageZapper.LeverUpTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} - function receiveFlashLoanOnLeverDownTrove( - ILeverageZapper.LeverDownTroveParams calldata _params, - uint256 _effectiveFlashLoanAmount - ) external virtual override {} -} diff --git a/contracts/test/AnchoredInvariantsTest.t.sol b/contracts/test/AnchoredInvariantsTest.t.sol index 026b5d716..7ca2a57ea 100644 --- a/contracts/test/AnchoredInvariantsTest.t.sol +++ b/contracts/test/AnchoredInvariantsTest.t.sol @@ -26,8 +26,9 @@ contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollater p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); TestDeployer deployer = new TestDeployer(); Contracts memory contracts; - (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth,) + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); + contracts.systemParams = contracts.branches[0].systemParams; setupContracts(contracts); handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); diff --git a/contracts/test/AnchoredSPInvariantsTest.t.sol b/contracts/test/AnchoredSPInvariantsTest.t.sol index 328ae9d32..cb74e23cb 100644 --- a/contracts/test/AnchoredSPInvariantsTest.t.sol +++ b/contracts/test/AnchoredSPInvariantsTest.t.sol @@ -1,51 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; +import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; + import "./TestContracts/DevTestSetup.sol"; -import {SPInvariantsTestHandler} from "./TestContracts/SPInvariantsTestHandler.t.sol"; +import {BaseInvariantTest} from "./TestContracts/BaseInvariantTest.sol"; +import {BaseMultiCollateralTest} from "./TestContracts/BaseMultiCollateralTest.sol"; +import {AdjustedTroveProperties, InvariantsTestHandler} from "./TestContracts/InvariantsTestHandler.t.sol"; import {Logging} from "./Utils/Logging.sol"; +import {TroveId} from "./Utils/TroveId.sol"; -contract AnchoredSPInvariantsTest is DevTestSetup { +contract AnchoredInvariantsTest is Logging, BaseInvariantTest, BaseMultiCollateralTest, TroveId { + using Strings for uint256; using StringFormatting for uint256; - struct Actor { - string label; - address account; - } - - SPInvariantsTestHandler handler; - - address constant adam = 0x1111111111111111111111111111111111111111; - address constant barb = 0x2222222222222222222222222222222222222222; - address constant carl = 0x3333333333333333333333333333333333333333; - address constant dana = 0x4444444444444444444444444444444444444444; - address constant eric = 0x5555555555555555555555555555555555555555; - address constant fran = 0x6666666666666666666666666666666666666666; - address constant gabe = 0x7777777777777777777777777777777777777777; - address constant hope = 0x8888888888888888888888888888888888888888; - - Actor[] actors; + InvariantsTestHandler handler; function setUp() public override { super.setUp(); + TestDeployer.TroveManagerParams[] memory p = new TestDeployer.TroveManagerParams[](4); + p[0] = TestDeployer.TroveManagerParams(1.5 ether, 1.1 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[1] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[2] = TestDeployer.TroveManagerParams(1.6 ether, 1.2 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); + p[3] = TestDeployer.TroveManagerParams(1.6 ether, 1.25 ether, 0.1 ether, 1.01 ether, 0.05 ether, 0.1 ether); TestDeployer deployer = new TestDeployer(); - (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,,) = - deployer.deployAndConnectContracts(); - stabilityPool = contracts.stabilityPool; - - handler = new SPInvariantsTestHandler( - SPInvariantsTestHandler.Contracts({ - boldToken: boldToken, - borrowerOperations: contracts.borrowerOperations, - collateralToken: contracts.collToken, - priceFeed: contracts.priceFeed, - stabilityPool: contracts.stabilityPool, - troveManager: contracts.troveManager, - collSurplusPool: contracts.pools.collSurplusPool - }), - hintHelpers - ); + Contracts memory contracts; + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) + = deployer.deployAndConnectContractsMultiColl(p); + contracts.systemParams = contracts.branches[0].systemParams; + setupContracts(contracts); + + handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); + vm.label(address(handler), "handler"); actors.push(Actor("adam", adam)); actors.push(Actor("barb", barb)); @@ -58,1196 +45,1072 @@ contract AnchoredSPInvariantsTest is DevTestSetup { for (uint256 i = 0; i < actors.length; ++i) { vm.label(actors[i].account, actors[i].label); } - - vm.label(address(handler), "handler"); - } - - function invariant_allFundsClaimable() internal view { - uint256 stabilityPoolColl = stabilityPool.getCollBalance(); - uint256 stabilityPoolBold = stabilityPool.getTotalBoldDeposits(); - uint256 yieldGainsOwed = stabilityPool.getYieldGainsOwed(); - - uint256 claimableColl = 0; - uint256 claimableBold = 0; - uint256 sumYieldGains = 0; - - for (uint256 i = 0; i < actors.length; ++i) { - claimableColl += stabilityPool.getDepositorCollGain(actors[i].account); - claimableBold += stabilityPool.getCompoundedBoldDeposit(actors[i].account); - sumYieldGains += stabilityPool.getDepositorYieldGain(actors[i].account); - //info("+sumYieldGains: ", sumYieldGains.decimal()); - } - - info("stabilityPoolColl: ", stabilityPoolColl.decimal()); - info("claimableColl: ", claimableColl.decimal()); - info("stabilityPoolBold: ", stabilityPoolBold.decimal()); - info("claimableBold: ", claimableBold.decimal()); - info("yieldGainsOwed: ", yieldGainsOwed.decimal()); - info("sumYieldGains: ", sumYieldGains.decimal()); - for (uint256 i = 0; i < actors.length; ++i) { - info( - actors[i].label, - ": ", - stabilityPool.getDepositorYieldGain(actors[i].account).decimal() - ); - } - info(""); - assertApproxEqAbsDecimal(stabilityPoolColl, claimableColl, 0.00001 ether, 18, "SP Coll !~ claimable Coll"); - assertApproxEqAbsDecimal(stabilityPoolBold, claimableBold, 0.001 ether, 18, "SP BOLD !~ claimable BOLD"); - assertApproxEqAbsDecimal(yieldGainsOwed, sumYieldGains, 0.001 ether, 18, "SP yieldGainsOwed !~= sum(yieldGain)"); - - //assertGe(stabilityPoolBold, claimableBold, "Not enough deposits for all depositors"); - //assertGe(stabilityPoolColl, claimableColl, "Not enough collateral for all depositors"); - //assertGe(yieldGainsOwed, sumYieldGains, "Not enough yield gains for all depositors"); } - function testUnclaimableDeposit() external { - // coll = 581.807407427107718655 ether, debt = 77_574.320990281029153872 ether - vm.prank(hope); - handler.openTrove(77_566.883069986646872666 ether); - - // coll = 735.070487541934665757 ether, debt = 98_009.398338924622100814 ether - vm.prank(barb); - handler.openTrove(98_000.001078547227161224 ether); - - vm.prank(hope); - handler.provideToSp(0.000001023636824878 ether, false); - - // totalBoldDeposits = 0.000001023636824878 ether - - // pulling `deposited` from fixture - vm.prank(gabe); - handler.provideToSp(98_009.398338924622100814 ether, false); - - // totalBoldDeposits = 98_009.398339948258925692 ether - - // coll = 735.070479452054794532 ether, debt = 98_009.397260273972604207 ether - vm.prank(carl); - handler.openTrove(98_000.000000000000001468 ether); - - // coll = 60.195714636403445628 ether, debt = 8_026.095284853792750331 ether - vm.prank(fran); - handler.openTrove(8_025.325733071169487504 ether); - - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808222 ether + function testWrongYield() external { vm.prank(adam); - handler.openTrove(2_000.000000000000000003 ether); + handler.addMeToUrgentRedemptionBatch(); vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 96_009.20655912634111747 ether - // P = 0.979591836959510777 ether - - vm.prank(carl); - handler.provideToSp(0.000000000001265034 ether, false); + handler.registerBatchManager( + 0, + 0.257486338754888547 ether, + 0.580260126400716372 ether, + 0.474304801140122485 ether, + 0.84978254245815657 ether, + 2121012 + ); - // totalBoldDeposits = 96_009.206559126342382504 ether + vm.prank(eric); + handler.registerBatchManager( + 2, + 0.995000000000011223 ether, + 0.999999999997818617 ether, + 0.999999999561578875 ether, + 0.000000000000010359 ether, + 5174410 + ); vm.prank(fran); - handler.liquidateMe(); + handler.warp(3_662_052); - // totalBoldDeposits = 87_983.111274272549632173 ether - // P = 0.89770075895273572 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(hope); - handler.provideToSp(2_000.191780821917810223 ether, false); - - // totalBoldDeposits = 89_983.303055094467442396 ether - - // coll = 568.201183566424575581 ether, debt = 75_760.157808856610077362 ether - vm.prank(dana); - handler.openTrove(75_752.893832735662822023 ether); - - vm.prank(dana); - handler.liquidateMe(); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 14_223.145246237857365034 ether - // P = 0.141894416505527947 ether - - invariant_allFundsClaimable(); - } + vm.prank(barb); + handler.addMeToLiquidationBatch(); - function testUnclaimableDeposit2() external { - // coll = 735.140965662413210774 ether, debt = 98_018.795421655094769748 ether - vm.prank(dana); - handler.openTrove(98_009.397260273972607992 ether); + // upper hint: 0 + // lower hint: 0 + // upfront fee: 1_246.586073354248297808 ether + vm.prank(hope); + handler.openTrove( + 0, 99_999.999999999999999997 ether, 2.251600954885856105 ether, 0.650005595391858041 ether, 8768, 0 + ); - // pulling `deposited` from fixture vm.prank(adam); - handler.provideToSp(9.398161381122161756 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 9.398161381122161756 ether - - // coll = 735.070479452054794705 ether, debt = 98_009.39726027397262726 ether vm.prank(eric); - handler.openTrove(98_000.000000000000024518 ether); - - // pulling `deposited` from fixture - vm.prank(gabe); - handler.provideToSp(98_009.39726027397262726 ether, false); - - // totalBoldDeposits = 98_018.795421655094789016 ether + handler.addMeToLiquidationBatch(); vm.prank(hope); - handler.provideToSp(89_926.427447073294525543 ether, false); + handler.warp(9_396_472); - // totalBoldDeposits = 187_945.222868728389314559 ether - - // coll = 695.649439428737938566 ether, debt = 92_753.258590498391808747 ether vm.prank(gabe); - handler.openTrove(92_744.365295196112729445 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(dana); - handler.provideToSp(12_389.101939905632219407 ether, false); - - // totalBoldDeposits = 200_334.324808634021533966 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(dana); - handler.provideToSp(9_589.959513437908897281 ether, false); - - // totalBoldDeposits = 209_924.284322071930431247 ether - - // coll = 358.727589004694716678 ether, debt = 47_830.345200625962223645 ether - vm.prank(hope); - handler.openTrove(47_825.759168924832445192 ether); - - // coll = 387.627141578823956719 ether, debt = 51_683.618877176527562444 ether - vm.prank(barb); - handler.openTrove(51_678.663388906358459579 ether); - - vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 158_240.665444895402868803 ether - // P = 0.753798761091013085 ether - - vm.prank(barb); - handler.provideToSp(0.000000000000008548 ether, false); - - // totalBoldDeposits = 158_240.665444895402877351 ether + handler.registerBatchManager( + 2, + 0.995000000000011139 ether, + 0.998635073564148166 ether, + 0.996010156573547401 ether, + 0.000000000000011577 ether, + 9078342 + ); vm.prank(carl); - handler.provideToSp(4_591.158415534017479187 ether, false); - - // totalBoldDeposits = 162_831.823860429420356538 ether - - vm.prank(dana); - handler.provideToSp(86_019.232581553804992428 ether, false); - - // totalBoldDeposits = 248_851.056441983225348966 ether + handler.registerBatchManager( + 1, 0.995000004199127012 ether, 1 ether, 0.999139502777974999 ether, 0.059938454189132239 ether, 1706585 + ); + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 897.541972815058774421 ether vm.prank(gabe); - handler.provideToSp(83_736.829497136058174833 ether, false); + handler.provideToSP(0, 58_897.613356828171795189 ether, false); + } - // totalBoldDeposits = 332_587.885939119283523799 ether + function testRedeemUnderflow() external { + vm.prank(fran); + handler.warp(18_162); - // coll = 735.082255482320817013 ether, debt = 98_010.967397642775601628 ether vm.prank(carl); - handler.openTrove(98_001.569986822121425601 ether); - - vm.prank(gabe); - handler.liquidateMe(); - - // totalBoldDeposits = 239_834.627348620891715052 ether - // P = 0.543576758520929938 ether - - // pulling `deposited` from fixture - vm.prank(barb); - handler.provideToSp(47_830.345200625962271476 ether, false); + handler.registerBatchManager( + 0, + 0.995000001857124003 ether, + 0.999999628575220679 ether, + 0.999925530120657388 ether, + 0.249999999999999999 ether, + 12664 + ); - // totalBoldDeposits = 287_664.972549246853986528 ether + vm.prank(hope); + handler.addMeToLiquidationBatch(); - // coll = 735.070479452054794659 ether, debt = 98_009.397260273972621122 ether vm.prank(fran); - handler.openTrove(98_000.000000000000018381 ether); - - // coll = 735.070479452054794701 ether, debt = 98_009.397260273972626757 ether - vm.prank(barb); - handler.openTrove(98_000.000000000000024015 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 189_655.575288972881359771 ether - // P = 0.358376488932287869 ether + vm.prank(fran); + handler.addMeToUrgentRedemptionBatch(); - // coll = 482.348723040733119578 ether, debt = 64_313.163072097749277015 ether vm.prank(gabe); - handler.openTrove(64_306.996647761662542251 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 91_646.178028698908732511 ether - // P = 0.173176219343645801 ether - - vm.prank(hope); - handler.liquidateMe(); + vm.prank(dana); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 43_815.832828072946508866 ether - // P = 0.082795163309295299 ether + vm.prank(eric); + handler.warp(4_641_555); vm.prank(adam); - handler.provideToSp(156_013.932831544173758454 ether, false); - - // totalBoldDeposits = 199_829.76565961712026732 ether - - vm.prank(barb); - handler.provideToSp(149_177.525713798558063626 ether, false); - - // totalBoldDeposits = 349_007.291373415678330946 ether - - // coll = 212.375283704302188719 ether, debt = 28_316.704493906958495745 ether - vm.prank(barb); - handler.openTrove(28_313.989453822345394132 ether); - - vm.prank(fran); - handler.liquidateMe(); - - // totalBoldDeposits = 250_997.894113141705709824 ether - // P = 0.059544348061060947 ether + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(dana); - handler.provideToSp(22_796.354529886855570135 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 273_794.248643028561279959 ether + vm.prank(gabe); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture vm.prank(fran); - handler.provideToSp(3_365.431868318464668052 ether, false); - - // totalBoldDeposits = 277_159.680511347025948011 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(6_369.148148716152063935 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 283_528.828660063178011946 ether - - // coll = 56.130386313173824619 ether, debt = 7_484.051508423176615827 ether vm.prank(hope); - handler.openTrove(7_483.333928457434122145 ether); - - // coll = 409.098077325686901872 ether, debt = 54_546.41031009158691615 ether - vm.prank(fran); - handler.openTrove(54_541.180333895186007903 ether); + handler.registerBatchManager( + 0, + 0.739903753088089514 ether, + 0.780288740735740819 ether, + 0.767858707410717411 ether, + 0.000000000000022941 ether, + 21644 + ); - // coll = 549.477014983678353472 ether, debt = 73_263.601997823780462917 ether + // upper hint: 80084422859880547211683076133703299733277748156566366325829078699459944778998 + // lower hint: 104346312485569601582594868672255666718935311025283394307913733247512361320190 + // upfront fee: 290.81243876303301812 ether vm.prank(adam); - handler.openTrove(73_256.577394511977944484 ether); - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(98_010.967397642775601628 ether, false); - - // totalBoldDeposits = 381_539.796057705953613574 ether - - vm.prank(dana); - handler.provideToSp(13_294.811494145641399612 ether, false); - - // totalBoldDeposits = 394_834.607551851595013186 ether + handler.openTrove( + 3, 39_503.887731534058892956 ether, 1.6863644596244192 ether, 0.38385567397413886 ether, 1, 7433679 + ); vm.prank(adam); - handler.liquidateMe(); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 321_571.005554027814550269 ether - // P = 0.048495586543891854 ether + vm.prank(hope); + handler.warp(23_201); - vm.prank(gabe); - handler.liquidateMe(); + vm.prank(carl); + handler.warp(18_593_995); + + // redemption rate: 0.195871664252157123 ether + // redeemed BOLD: 15_191.361299840412827416 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(carl); + handler.redeemCollateral(15_191.361299840412827416 ether, 0); + + // redemption rate: 0.195871664252157123 ether + // redeemed BOLD: 0.000000000000006302 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(dana); + handler.redeemCollateral(0.000000000000006302 ether, 1); - // totalBoldDeposits = 257_257.842481930065273254 ether - // P = 0.038796625779998194 ether + vm.prank(hope); + handler.registerBatchManager( + 1, + 0.822978751289802582 ether, + 0.835495454680029657 ether, + 0.833312890646159679 ether, + 0.422857251385135959 ether, + 29470036 + ); vm.prank(gabe); - handler.provideToSp(2_881.242711585620903523 ether, false); - - // totalBoldDeposits = 260_139.085193515686176777 ether + handler.addMeToUrgentRedemptionBatch(); vm.prank(barb); - handler.liquidateMe(); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 231_822.380699608727681032 ether - // P = 0.034573528790341043 ether - - invariant_allFundsClaimable(); - } - - function testUnderflow() external { - // coll = 735.070479452054794521 ether, debt = 98_009.39726027397260276 ether vm.prank(gabe); - handler.openTrove(98_000.000000000000000021 ether); - - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(9.397260273972700749 ether, false); - - // totalBoldDeposits = 9.397260273972700749 ether - + handler.warp(31); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0.012686316538387649 ether vm.prank(carl); - handler.provideToSp(0.000000000000019902 ether, false); - - // totalBoldDeposits = 9.397260273972720651 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(12.028493150685049425 ether, false); - - // totalBoldDeposits = 21.425753424657770076 ether - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(24.05698630137009885 ether, false); - - // totalBoldDeposits = 45.482739726027868926 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(48.11397260274029571 ether, false); - - // totalBoldDeposits = 93.596712328768164636 ether - - vm.prank(hope); - handler.provideToSp(22_378.224492402901169486 ether, false); - - // totalBoldDeposits = 22_471.821204731669334122 ether - - // coll = 670.171237313523994506 ether, debt = 89_356.164975136532600748 ether - vm.prank(adam); - handler.openTrove(89_347.597397303914417174 ether); - - vm.prank(adam); - handler.provideToSp(66_119.976516875041256465 ether, false); - - // totalBoldDeposits = 88_591.797721606710590587 ether - - // coll = 735.07047945205479454 ether, debt = 98_009.39726027397260532 ether - vm.prank(hope); - handler.openTrove(98_000.000000000000002581 ether); + handler.provideToSP(3, 0.000000000000021916 ether, false); + // upper hint: 0 + // lower hint: 39695913545351040647077841548061220386885435874215782275463606055905069661493 + // upfront fee: 0 ether vm.prank(carl); - handler.provideToSp(99_018.369068280498073463 ether, false); - - // totalBoldDeposits = 187_610.16678988720866405 ether - - // coll = 727.153278118134034577 ether, debt = 96_953.770415751204610242 ether - vm.prank(eric); - handler.openTrove(96_944.474370263645082632 ether); + handler.setBatchManagerAnnualInterestRate(0, 0.998884384586837808 ether, 15539582, 63731457); - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 90_656.396374136004053808 ether - // P = 0.483216863591758585 ether - - // coll = 170.932345052439560746 ether, debt = 22_790.979340325274766094 ether - //vm.prank(barb); - //handler.openTrove(22_788.794113492474117891 ether); + vm.prank(gabe); + handler.registerBatchManager( + 0, + 0.351143076054309979 ether, + 0.467168361632094569 ether, + 0.433984569464653931 ether, + 0.000000000000000026 ether, + 16482089 + ); vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 1_300.23139899947145306 ether - // P = 0.006930495405697588 ether - - console2.log("-9"); - invariant_allFundsClaimable(); - - // coll = 735.070479452054794697 ether, debt = 98_009.397260273972626152 ether - vm.prank(fran); - handler.openTrove(98_000.000000000000023411 ether); - - console2.log("-8"); - invariant_allFundsClaimable(); + handler.registerBatchManager( + 3, + 0.995000000000006201 ether, + 0.996462074472343849 ether, + 0.995351673013151748 ether, + 0.045759837128294745 ether, + 10150905 + ); - // coll = 735.070479452056938533 ether, debt = 98_009.397260274258470952 ether vm.prank(dana); - handler.openTrove(98_000.000000000285840803 ether); - - console2.log("-7"); - invariant_allFundsClaimable(); - - // coll = 735.073432425759024971 ether, debt = 98_009.790990101203329407 ether - vm.prank(adam); - handler.openTrove(98_000.393692075935773922 ether); - - console2.log("-6"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture - vm.prank(adam); - handler.provideToSp(98_009.39726027397270077 ether, false); - - // totalBoldDeposits = 99_309.62865927344415383 ether - - console2.log("-5"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(98_009.39726027397270077 ether, false); - - // totalBoldDeposits = 197_319.0259195474168546 ether + handler.warp(23_299); - console2.log("-4"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(98_009.790990101203427417 ether, false); - - // totalBoldDeposits = 295_328.816909648620282017 ether - - console2.log("-3"); - invariant_allFundsClaimable(); - - vm.prank(eric); - handler.provideToSp(89_618.132493028108872257 ether, false); - - // totalBoldDeposits = 384_946.949402676729154274 ether - - console2.log("-2"); - invariant_allFundsClaimable(); - - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(22_790.979340325274788885 ether, false); - - // totalBoldDeposits = 407_737.928743002003943159 ether - - console2.log("-1"); - invariant_allFundsClaimable(); - - vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 309_728.531482728031337839 ether - // P = 0.005264587896132409 ether - - console2.log("0"); - invariant_allFundsClaimable(); - - vm.prank(hope); - handler.provideToSp(36_648.420465084212639386 ether, false); - // [FAIL. Reason: panic: arithmetic underflow or overflow (0x11)] test_XXX() (gas: 9712433) + handler.warp(13_319_679); + + // redemption rate: 0.246264103698059017 ether + // redeemed BOLD: 16_223.156659761268542045 ether + // redeemed Troves: [ + // [], + // [], + // [], + // [adam], + // ] + vm.prank(eric); + handler.redeemCollateral(16_223.156659761268542045 ether, 0); } - function testNotEnoughYieldToClaim() external { - // coll = 531.374961037517928877 ether, debt = 70_849.994805002390516918 ether - vm.prank(barb); - handler.openTrove(70_843.201621285280969428 ether); - - // pulling `deposited` from fixture + function testWrongYieldPrecision() external { vm.prank(carl); - handler.provideToSp(6.79318371710954749 ether, false); - - // totalBoldDeposits = 6.79318371710954749 ether - - vm.prank(barb); - handler.provideToSp(54_410.610723992018269811 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 54_417.403907709127817301 ether - - vm.prank(hope); - handler.provideToSp(5_146.80395069777790841 ether, false); - - // totalBoldDeposits = 59_564.207858406905725711 ether - - // coll = 531.425914800905088131 ether, debt = 70_856.788640120678417378 ether - vm.prank(hope); - handler.openTrove(70_849.994805002390516918 ether); - - invariant_allFundsClaimable(); - } - - function testNotEnoughYieldToClaim2() external { - // coll = 735.071211554227610995 ether, debt = 98_009.494873897014799279 ether - vm.prank(barb); - handler.openTrove(98_000.097604263729236202 ether); + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture vm.prank(barb); - handler.provideToSp(9.397269633285661087 ether, false); + handler.warp(19_326); - // totalBoldDeposits = 9.397269633285661087 ether + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); - // coll = 296.145816189419348496 ether, debt = 39_486.108825255913132743 ether - vm.prank(hope); - handler.openTrove(39_482.322849092301542185 ether); + vm.prank(dana); + handler.registerBatchManager( + 3, + 0.30820256993275862 ether, + 0.691797430067250243 ether, + 0.383672204747583321 ether, + 0.000000000000018015 ether, + 11403 + ); - // coll = 690.364688805446856156 ether, debt = 92_048.625174059580820766 ether vm.prank(eric); - handler.openTrove(92_039.79943986671688901 ether); + handler.registerBatchManager( + 3, + 0.018392910495297323 ether, + 0.98160708950470919 ether, + 0.963214179009414206 ether, + 0.000000000000019546 ether, + 13319597 + ); vm.prank(fran); - handler.provideToSp(0.000000000000021173 ether, false); + handler.warp(354); - // totalBoldDeposits = 9.39726963328568226 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // coll = 735.070479452054794549 ether, debt = 98_009.397260273972606523 ether - vm.prank(carl); - handler.openTrove(98_000.000000000000003783 ether); + vm.prank(eric); + handler.warp(15_305_108); - // coll = 515.861846232288564631 ether, debt = 68_781.579497638475284021 ether - vm.prank(adam); - handler.openTrove(68_774.984636098027527957 ether); + // upper hint: 84669063888545001427406517193344625874395507444463583314999084271619652858036 + // lower hint: 69042136817699606427763587628766179145825895354994492055731203083594873444699 + // upfront fee: 1_702.831959251916404109 ether + vm.prank(fran); + handler.openTrove( + 1, 99_999.999999999999999998 ether, 1.883224555937797003 ether, 0.887905235895642125 ether, 4164477, 39 + ); - // coll = 735.070528577820199656 ether, debt = 98_009.403810376026620703 ether vm.prank(dana); - handler.openTrove(98_000.006549474022262404 ether); + handler.warp(996); - // pulling `deposited` from fixture vm.prank(eric); - handler.provideToSp(98_009.397260273972704533 ether, false); - - // totalBoldDeposits = 98_018.794529907258386793 ether + handler.addMeToUrgentRedemptionBatch(); vm.prank(barb); - handler.liquidateMe(); + handler.warp(4_143_017); - // totalBoldDeposits = 9.299656010243587514 ether - // P = 0.000094876253629155 ether - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(98_009.397260273972704533 ether, false); - - // totalBoldDeposits = 98_018.696916284216292047 ether + vm.prank(fran); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0 ether + vm.prank(adam); + handler.provideToSP(0, 0.000000000000011094 ether, true); vm.prank(carl); - handler.liquidateMe(); - - // totalBoldDeposits = 9.299656010243685524 ether - // P = 0.000000009001512467 ether - - // coll = 439.615230826832584704 ether, debt = 58_615.364110244344627095 ether - vm.prank(fran); - handler.openTrove(58_609.743997806198827208 ether); + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture + // upper hint: 0 + // lower hint: 0 + // upfront fee: 1_513.428916567114728229 ether vm.prank(barb); - handler.provideToSp(68_781.579497638475284021 ether, false); - - // totalBoldDeposits = 68_790.879153648718969545 ether - - // pulling `deposited` from fixture - vm.prank(gabe); - handler.provideToSp(39_486.108825255913132743 ether, false); - - // totalBoldDeposits = 108_276.987978904632102288 ether + handler.openTrove( + 2, + 79_311.063107967331806055 ether, + 1.900000000000001559 ether, + 0.995000000000007943 ether, + 3270556590, + 1229144376 + ); - // pulling `deposited` from fixture vm.prank(fran); - handler.provideToSp(98_009.403810376026620703 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 206_286.391789280658722991 ether + // price: 221.052631578948441462 ether + vm.prank(dana); + handler.setPrice(2, 2.100000000000011917 ether); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 1_226.039010661379810958 ether + // pendingInterest: 11_866.268348193546380256 ether + vm.prank(carl); + handler.provideToSP(1, 0.027362680048399155 ether, false); - vm.prank(fran); - handler.provideToSp(67_852.887887440994776149 ether, false); + // upper hint: 0 + // lower hint: 109724453348421969168156614404527408958334892291486496459024204968877369036377 + // upfront fee: 9.807887080131946403 ether + vm.prank(eric); + handler.openTrove( + 3, 30_260.348082017558572105 ether, 1.683511222023706186 ether, 0.016900375815455486 ether, 108, 14159 + ); - // totalBoldDeposits = 274_139.27967672165349914 ether + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); vm.prank(adam); - handler.provideToSp(78_134.913037086847714575 ether, false); - - // totalBoldDeposits = 352_274.192713808501213715 ether - - vm.prank(hope); - handler.liquidateMe(); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 312_788.083888552588080972 ether - // P = 0.000000007992540739 ether - - // pulling `deposited` from fixture vm.prank(adam); - handler.provideToSp(401.55682795488209173 ether, false); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.1474722457669512 ether + // redeemed BOLD: 64_016.697525751186019703 ether + // redeemed Troves: [ + // [], + // [fran], + // [barb], + // [eric], + // ] + vm.prank(dana); + handler.redeemCollateral(64_016.697525751186019705 ether, 0); - // totalBoldDeposits = 313_189.640716507470172702 ether + // upper hint: 102052496222650354016228296600262737092032771006947291868573062530791731100756 + // lower hint: 0 + vm.prank(eric); + handler.applyMyPendingDebt(3, 2542, 468); - // coll = 735.070479452054794648 ether, debt = 98_009.397260273972619611 ether vm.prank(gabe); - handler.openTrove(98_000.00000000000001687 ether); + handler.warp(20_216); - vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 244_408.061218868994888681 ether - // P = 0.000000006237247764 ether - - // coll = 260.952813316840363413 ether, debt = 34_793.708442245381788334 ether vm.prank(carl); - handler.openTrove(34_790.372379140532696158 ether); - - vm.prank(dana); - handler.provideToSp(126_931.523034110680273609 ether, false); - - info(""); - info(" -------------- here it starts! ----------------"); - info(""); - - // totalBoldDeposits = 371_339.584252979675162290 ether - - vm.prank(fran); - handler.liquidateMe(); - - // totalBoldDeposits = 312_724.220142735330535195 ether - // P = 0.000000005252708102 ether - info(""); - info("P ratio: ", (5252708102 * DECIMAL_PRECISION / 6237247764).decimal()); - info( - "deposits ratio: ", - (312_724.220142735330535195 ether * DECIMAL_PRECISION / 371339584252979675162290).decimal() + handler.registerBatchManager( + 1, + 0.995000000000425732 ether, + 0.998288014105982235 ether, + 0.996095220733623871 ether, + 0.000000000000027477 ether, + 3299 ); - info(""); - - info(""); - info(" -------------- here it goes! ----------------"); - info(""); - - uint256 prevError = 99469643824625821462110 * DECIMAL_PRECISION / 371339584252979675162290; - uint256 pWithError = 5252708102 * DECIMAL_PRECISION + prevError; - uint256 newP = pWithError * 705655592866947642 / DECIMAL_PRECISION / DECIMAL_PRECISION; - info("prev error: ", prevError.decimal()); - info("P w prev error: ", pWithError.decimal()); - info("P * F: ", (pWithError * 705655592866947642 / DECIMAL_PRECISION).decimal()); - info("final P: ", newP.decimal()); + vm.prank(carl); + handler.addMeToLiquidationBatch(); + + // redemption rate: 0.108097849716691371 ether + // redeemed BOLD: 0.000151948988774207 ether + // redeemed Troves: [ + // [], + // [fran], + // [barb], + // [eric], + // ] + vm.prank(hope); + handler.redeemCollateral(0.000151948988774209 ether, 0); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 0 ether + // pendingInterest: 0 ether vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 220_675.594968675749714429 ether - // P = 0.000000003706602850 ether - info(""); - info("P ratio: ", (3706602850 * DECIMAL_PRECISION / 6237247764).decimal()); - info( - "deposits ratio: ", - (220_675.594968675749714429 ether * DECIMAL_PRECISION / 371339584252979675162290).decimal() - ); - info("P - 1 ratio: ", (3706602849 * DECIMAL_PRECISION / 6237247764).decimal()); - info(""); + handler.provideToSP(0, 76_740.446487959260685533 ether, true); - // coll = 735.070479452054794556 ether, debt = 98_009.397260273972607451 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // pendingYield: 9_803.032557027063219919 ether + // pendingInterest: 0 ether vm.prank(hope); - handler.openTrove(98_000.000000000000004711 ether); - - vm.prank(carl); - handler.liquidateMe(); + handler.provideToSP(1, 4.127947448768090932 ether, false); + } - // totalBoldDeposits = 185_881.886526430367926095 ether - // P = 0.000000003122186351 ether + function testSortedTroveSize() external { + uint256 i = 1; + TestDeployer.LiquityContractsDev memory c = branches[i]; - //uint256 depositsRatio = 185881886526430367926095 * DECIMAL_PRECISION / 371339584252979675162290; - //uint256 pRatio = 3122186351 * DECIMAL_PRECISION / 6237247764; - info(""); - info("P ratio: ", (3122186351 * DECIMAL_PRECISION / 6237247764).decimal()); - info("deposits ratio: ", (185881886526430367926095 * DECIMAL_PRECISION / 371339584252979675162290).decimal()); - info(""); + vm.prank(adam); + handler.addMeToLiquidationBatch(); - invariant_allFundsClaimable(); + vm.prank(barb); + handler.addMeToLiquidationBatch(); - // coll = 285.918279973646172899 ether, debt = 38_122.437329819489719827 ether vm.prank(adam); - handler.openTrove(38_118.782104138270981514 ether); - - invariant_allFundsClaimable(); - } + handler.addMeToUrgentRedemptionBatch(); - function testNotEnoughYieldToClaim3() external { - // coll = 500.857826775587502992 ether, debt = 66_781.043570078333732226 ether vm.prank(adam); - handler.openTrove(66_774.640522357011826983 ether); + handler.registerBatchManager( + 3, + 0.100944149373120884 ether, + 0.377922952132481818 ether, + 0.343424998629201343 ether, + 0.489955880173256455 ether, + 2070930 + ); - // coll = 455.998017386763941998 ether, debt = 60_799.73565156852559962 ether - vm.prank(fran); - handler.openTrove(60_793.906098928902280224 ether); + vm.prank(carl); + handler.addMeToLiquidationBatch(); + + vm.prank(carl); + handler.warp(9_303_785); - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808219 ether vm.prank(barb); - handler.openTrove(2_000 ether); + handler.registerBatchManager( + 1, + 0.301964103682871801 ether, + 0.756908371280377546 ether, + 0.540898165697757771 ether, + 0.000017102564306416 ether, + 27657915 + ); vm.prank(fran); - handler.provideToSp(0.127035053107027317 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 0.127035053107027317 ether + vm.prank(eric); + handler.addMeToUrgentRedemptionBatch(); - // coll = 390.048079659008251957 ether, debt = 52_006.410621201100260869 ether vm.prank(hope); - handler.openTrove(52_001.424183265718616619 ether); - - // coll = 736.681846984701599582 ether, debt = 98_224.246264626879944156 ether - vm.prank(dana); - handler.openTrove(98_214.828404368926759399 ether); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(52_006.410621201100260869 ether, false); - - // totalBoldDeposits = 52_006.537656254207288186 ether + // upper hint: 30979495632948298397104351002742564073201815129975103483277328125306028611241 + // lower hint: 36051278007718023196469061266077621121244014979449590376694871896669965056265 + // upfront fee: 118.231198854524639989 ether + vm.prank(gabe); + handler.openTrove( + 1, 7_591.289850943621327156 ether, 1.900000000017470971 ether, 0.812103428106344175 ether, 1121, 415425919 + ); - // coll = 735.070479452054794533 ether, debt = 98_009.397260273972604297 ether + // redemption rate: 0.005000000000000004 ether + // redeemed BOLD: 0.000000000000071705 ether + // redeemed Troves: [ + // [], + // [gabe], + // [], + // [], + // ] + vm.prank(hope); + handler.redeemCollateral(0.000000000000071705 ether, 1); + + // redemption rate: 0.387443853477360594 ether + // redeemed BOLD: 5_896.917877499258624384 ether + // redeemed Troves: [ + // [], + // [gabe], + // [], + // [], + // ] vm.prank(gabe); - handler.openTrove(98_000.000000000000001558 ether); + handler.redeemCollateral(5_896.917877499258624384 ether, 1); vm.prank(eric); - handler.provideToSp(98_018.79542165509478359 ether, false); - - // totalBoldDeposits = 150_025.333077909302071776 ether + handler.warp(11_371_761); vm.prank(gabe); - handler.liquidateMe(); - - // totalBoldDeposits = 52_015.935817635329467479 ether - // P = 0.346714349839990399 ether + handler.registerBatchManager( + 0, + 0.23834235868248997 ether, + 0.761711006198436234 ether, + 0.523368647516059893 ether, + 0.761688376122671962 ether, + 31535998 + ); vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 9.52519643422920661 ether - // P = 0.000063490586814979 ether + handler.registerBatchManager( + 2, + 0.036127532604869915 ether, + 0.999999999999999999 ether, + 0.963882428861225203 ether, + 0.848537401570757863 ether, + 29802393 + ); - // coll = 297.885541044368726255 ether, debt = 39_718.072139249163500539 ether - vm.prank(hope); - handler.openTrove(39_714.263922160737128486 ether); + vm.prank(eric); + handler.addMeToUrgentRedemptionBatch(); + // batch manager: hope + // upper hint: 111996671338791781291582287523793567344508255320483065919810498665837663289426 + // lower hint: 37857035535383668733402580992354953018471987882089934484705744026840633200601 + // upfront fee: 1_355.203530437779650125 ether vm.prank(carl); - handler.provideToSp(45_503.134640909581521244 ether, false); - - // totalBoldDeposits = 45_512.659837343810727854 ether - - vm.prank(dana); - handler.provideToSp(13_158.641347715694197298 ether, false); + handler.openTroveAndJoinInterestBatchManager( + 2, 73_312.036791249214758342 ether, 1.900020510596646286 ether, 40, 115, 737 + ); - // totalBoldDeposits = 58_671.301185059504925152 ether + vm.prank(barb); + handler.registerBatchManager( + 0, + 0.955741837871335122 ether, + 0.974535636428930833 ether, + 0.964294359297779033 ether, + 0.000000000000268875 ether, + 3335617 + ); + vm.prank(gabe); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 975.74654191520134809 ether vm.prank(fran); - handler.provideToSp(88_720.671803804390427542 ether, false); - - // totalBoldDeposits = 147_391.972988863895352694 ether - - // pulling `deposited` from fixture - vm.prank(barb); - handler.provideToSp(98_224.246264626880042381 ether, false); + handler.provideToSP(2, 12_633.808570846161076142 ether, true); - // totalBoldDeposits = 245_616.219253490775395075 ether + // batch manager: adam + // upper hint: 7512901306961997563120107574274771509748256751277397278816998908345777536679 + // lower hint: 27989025468780058605431608942843597971189459457295957311648808450848491056535 + // upfront fee: 166.681364294341638522 ether + vm.prank(carl); + handler.openTroveAndJoinInterestBatchManager( + 3, 25_307.541971224954454066 ether, 2.401194840294921108 ether, 142, 6432363, 25223 + ); - // pulling `deposited` from fixture - vm.prank(fran); - handler.provideToSp(2_000.191780821917808219 ether, false); + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 247_616.411034312693203294 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); vm.prank(dana); - handler.liquidateMe(); + handler.warp(8_774_305); - // totalBoldDeposits = 149_392.164769685813259138 ether - // P = 0.000038305200237608 ether + vm.prank(adam); + handler.warp(3_835); - // coll = 16.056327434142920133 ether, debt = 2_140.84365788572268436 ether vm.prank(eric); - handler.openTrove(2_140.638391190677003004 ether); + handler.warp(9_078_180); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether + vm.prank(gabe); + handler.provideToSP(2, 5_179.259567321319728284 ether, true); - vm.prank(fran); - handler.liquidateMe(); + // price: 120.905132749610222778 ether + vm.prank(hope); + handler.setPrice(1, 2.100000000000002648 ether); - // totalBoldDeposits = 88_592.429118117287659518 ether - // P = 0.000022715721016141 ether + vm.prank(barb); + handler.lowerBatchManagementFee(1, 0.000008085711886436 ether); - // pulling `deposited` from fixture vm.prank(hope); - handler.provideToSp(16_730.704105575056759258 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 105_323.133223692344418776 ether + vm.prank(adam); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture - vm.prank(dana); - handler.provideToSp(2_755.636251036681598567 ether, false); + vm.prank(gabe); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 108_078.769474729026017343 ether + // price: 80.314880400478576408 ether + vm.prank(gabe); + handler.setPrice(1, 1.394988326842136963 ether); - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(528.278543964116931444 ether, false); - - // totalBoldDeposits = 108_607.048018693142948787 ether + handler.warp(1_849_907); - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(1_508.113441559160422894 ether, false); - - // totalBoldDeposits = 110_115.161460252303371681 ether - - vm.prank(barb); - handler.provideToSp(0.000000000000017716 ether, false); + // upper hint: 84800337471693920904250232874319843718400766719524250287777680170677855896573 + // lower hint: 0 + // upfront fee: 0 ether + // function: adjustZombieTrove() + vm.prank(gabe); + handler.adjustTrove( + 1, + uint8(AdjustedTroveProperties.onlyColl), + 29.524853479148084596 ether, + true, + 0 ether, + true, + 40, + 14, + 4554760 + ); - // totalBoldDeposits = 110_115.161460252303389397 ether + info("SortedTroves size: ", c.sortedTroves.getSize().toString()); + info("num troves: ", handler.numTroves(i).toString()); + info("num zombies: ", handler.numZombies(i).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); - // coll = 391.562210654678915577 ether, debt = 52_208.294753957188743495 ether + // upper hint: 0 + // lower hint: 74750724351164404027318726202729770837051588626953680774538886892291438048970 + // upfront fee: 773.037543760336600445 ether vm.prank(carl); - handler.openTrove(52_203.28895912549177853 ether); - - // coll = 694.424483959494241787 ether, debt = 92_589.931194599232238236 ether - vm.prank(fran); - handler.openTrove(92_581.05355932642011576 ether); - - vm.prank(fran); - handler.liquidateMe(); - - // totalBoldDeposits = 17_525.230265653071151161 ether - // P = 0.000003615289994393 ether - - info(""); - info("P ratio: ", (3615289994393 * DECIMAL_PRECISION / 22715721016141).decimal()); - info( - "deposits ratio: ", - (17_525.230265653071151161 ether * DECIMAL_PRECISION / 110_115.161460252303389397 ether).decimal() + handler.openTrove( + 0, 40_510.940914935073773948 ether, 2.063402456659389908 ether, 0.995000000000000248 ether, 0, 55487655 ); - info(""); - - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 15_384.386607767348466801 ether - // P = 0.00000317365410496 ether - info(""); - info("P ratio: ", (3173654104960 * DECIMAL_PRECISION / 22715721016141).decimal()); - info( - "deposits ratio: ", - (15_384.386607767348466801 ether * DECIMAL_PRECISION / 110_115.161460252303389397 ether).decimal() + vm.prank(adam); + handler.registerBatchManager( + 0, + 0.541865737266494949 ether, + 0.672692246806001449 ether, + 0.650860934960147488 ether, + 0.070089828074852802 ether, + 29179158 ); - info(""); - // coll = 472.174813390591817645 ether, debt = 62_956.641785412242352578 ether vm.prank(fran); - handler.openTrove(62_950.605425987832560415 ether); + handler.registerBatchManager( + 1, + 0.566980989185701648 ether, + 0.86881504225021711 ether, + 0.702666683322997409 ether, + 0.667232273668645041 ether, + 7007521 + ); - // coll = 735.070479452054794524 ether, debt = 98_009.397260273972603184 ether vm.prank(dana); - handler.openTrove(98_000.000000000000000445 ether); - - invariant_allFundsClaimable(); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 1_129.588991574293634631 ether + vm.prank(barb); + handler.provideToSP(1, 0.000000000000000002 ether, false); + + info("SortedTroves size: ", c.sortedTroves.getSize().toString()); + info("num troves: ", handler.numTroves(i).toString()); + info("num zombies: ", handler.numZombies(i).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + + // redemption rate: 0.184202341360173417 ether + // redeemed BOLD: 66_462.494346928386331338 ether + // redeemed Troves: [ + // [carl], + // [gabe], + // [], + // [carl], + // ] + vm.prank(eric); + handler.redeemCollateral(66_462.49434692838633134 ether, 1); + + info("SortedTroves size: ", c.sortedTroves.getSize().toString()); + info("num troves: ", handler.numTroves(i).toString()); + info("num zombies: ", handler.numZombies(i).toString()); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + assertEq(c.sortedTroves.getSize(), handler.numTroves(i) - handler.numZombies(i), "Wrong SortedTroves size"); } - function testNotEnoughYieldToClaim4() external { - // coll = 735.070479452054794523 ether, debt = 98_009.397260273972602968 ether - vm.prank(dana); - handler.openTrove(98_000.000000000000000229 ether); - - // pulling `deposited` from fixture - vm.prank(hope); - handler.provideToSp(9.397260273972700749 ether, false); + function testAssertLastZombieTroveInABatchHasMoreThanMinDebt() external { + uint256 i = 1; + TestDeployer.LiquityContractsDev memory c = branches[i]; - // totalBoldDeposits = 9.397260273972700749 ether + vm.prank(adam); + handler.addMeToUrgentRedemptionBatch(); - // coll = 735.070479452054794596 ether, debt = 98_009.397260273972612766 ether vm.prank(hope); - handler.openTrove(98_000.000000000000010026 ether); - - vm.prank(gabe); - handler.provideToSp(98_009.397260282669422181 ether, false); - - // totalBoldDeposits = 98_018.79452055664212293 ether - - vm.prank(dana); - handler.liquidateMe(); - - // totalBoldDeposits = 9.397260282669519962 ether - // P = 0.000095872024631956 ether - - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808222 ether - vm.prank(barb); - handler.openTrove(2_000.000000000000000003 ether); + handler.registerBatchManager( + 0, + 0.99500000000072184 ether, + 0.996944021609020651 ether, + 0.99533906344899454 ether, + 0.378970428480541887 ether, + 314055 + ); - // coll = 245.638216964337321336 ether, debt = 32_751.762261911642844722 ether vm.prank(dana); - handler.openTrove(32_748.621983091346414244 ether); + handler.warp(2_225_439); - // coll = 671.510441931192659665 ether, debt = 89_534.72559082568795521 ether - vm.prank(fran); - handler.openTrove(89_526.14089238395250771 ether); - - // coll = 735.070479452054794659 ether, debt = 98_009.397260273972621098 ether vm.prank(adam); - handler.openTrove(98_000.000000000000018357 ether); + handler.addMeToUrgentRedemptionBatch(); - vm.prank(adam); - handler.provideToSp(0.000000000000021956 ether, false); + vm.prank(barb); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 9.397260282669541918 ether + vm.prank(barb); + handler.registerBatchManager( + 2, + 0.995000000000009379 ether, + 0.999999999998128142 ether, + 0.997477804125778004 ether, + 0.000000001035389259 ether, + 10046 + ); - // coll = 376.789379643035011626 ether, debt = 50_238.583952404668216799 ether vm.prank(gabe); - handler.openTrove(50_233.767015841505332726 ether); - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(50_238.583952404668216799 ether, false); + handler.registerBatchManager( + 2, + 0.346476084765605513 ether, + 0.346476084765605514 ether, + 0.346476084765605514 ether, + 0.000000000000000002 ether, + 27010346 + ); - // totalBoldDeposits = 50_247.981212687337758717 ether + vm.prank(fran); + handler.warp(19_697_329); vm.prank(gabe); - handler.liquidateMe(); - - // totalBoldDeposits = 9.397260282669541918 ether - // P = 0.0000000179297625 ether + handler.addMeToUrgentRedemptionBatch(); - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(89_534.72559082568795521 ether, false); - - // totalBoldDeposits = 89_544.122851108357497128 ether + vm.prank(fran); + handler.registerBatchManager( + 1, + 0.995000000000019257 ether, + 0.999999999996150378 ether, + 0.999999999226237651 ether, + 0.696688179568702502 ether, + 7641047 + ); - // coll = 391.027929559007118106 ether, debt = 52_137.057274534282414027 ether vm.prank(gabe); - handler.openTrove(52_132.058310038799241497 ether); + handler.warp(977_685); - vm.prank(dana); - handler.provideToSp(0.000000000000000001 ether, false); - - // totalBoldDeposits = 89_544.122851108357497129 ether - - vm.prank(barb); - handler.liquidateMe(); + vm.prank(gabe); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 87_543.931070286439688907 ether - // P = 0.000000017529256443 ether + vm.prank(fran); + handler.addMeToLiquidationBatch(); - // coll = 362.672606823421941244 ether, debt = 48_356.347576456258832456 ether - vm.prank(eric); - handler.openTrove(48_351.711111007258136471 ether); + // batch manager: fran + // upper hint: 0 + // lower hint: 60678094901167127062962700790111047491633904950610080336398562382189456360809 + // upfront fee: 242.684833433337541236 ether + vm.prank(gabe); + handler.openTroveAndJoinInterestBatchManager( + 1, 12_654.280610244006254376 ether, 2.145058504746006382 ether, 182, 22444926, 124118903 + ); - invariant_allFundsClaimable(); - } + // redemption rate: 0.005 ether + // redeemed BOLD: 0.000000000000017162 ether + // redeemed Troves: [ + // [], + // [gabe], + // [], + // [], + // ] + vm.prank(hope); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(0.000000000000017162 ether, 0); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + + // upper hint: 79178440845664423591903906560915994242429107602729190780850212197412640295587 + // lower hint: 0 + // upfront fee: 624.393448965513162837 ether + vm.prank(hope); + handler.openTrove( + 0, 32_721.264734011072612096 ether, 2.333153121000516764 ether, 0.995000000000109949 ether, 52719, 31482 + ); - function testCollGainsUnderflow3CollSkin() external { - // coll = 289.601984682301661608 ether, debt = 38_613.59795764022154772 ether + // price: 244.435094708283018275 ether vm.prank(dana); - handler.openTrove(38_609.895638880328913441 ether); + handler.setPrice(1, 2.621637893811990143 ether); - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(3.702318759892672893 ether, false); - - // totalBoldDeposits = 3.702318759892672893 ether - - // pulling `deposited` from fixture + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.37622704640950591 ether + // redeemed BOLD: 34_333.025174298345667786 ether + // redeemed Troves: [ + // [hope], + // [gabe], + // [], + // [], + // ] vm.prank(carl); - handler.provideToSp(7.404637519785345786 ether, false); - - // totalBoldDeposits = 11.106956279678018679 ether - - vm.prank(carl); - handler.provideToSp(6_872.312325153568231613 ether, false); - - // totalBoldDeposits = 6_883.419281433246250292 ether - - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(6_884.455930686016187896 ether, false); - - // totalBoldDeposits = 13_767.875212119262438188 ether + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(34_333.025174298345667787 ether, 0); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); - // coll = 485.870086975795226011 ether, debt = 64_782.678263439363468128 ether - vm.prank(adam); - handler.openTrove(64_776.466821415392129157 ether); - - // coll = 735.070479841999818057 ether, debt = 98_009.39731226664240759 ether vm.prank(gabe); - handler.openTrove(98_000.000051987684684402 ether); + handler.addMeToLiquidationBatch(); vm.prank(adam); - handler.provideToSp(56_502.482327086364961955 ether, false); - - // totalBoldDeposits = 70_270.357539205627400143 ether - - // coll = 735.138660165061182493 ether, debt = 98_018.48802200815766572 ether - vm.prank(hope); - handler.openTrove(98_009.089890100887717583 ether); - - // coll = 735.070493012734388559 ether, debt = 98_009.399068364585141076 ether - vm.prank(barb); - handler.openTrove(98_000.001807917250610196 ether); - - vm.prank(dana); - handler.provideToSp(66_572.988267614156561955 ether, false); - - // totalBoldDeposits = 136_843.345806819783962098 ether - - vm.prank(adam); - handler.liquidateMe(); - - // totalBoldDeposits = 72_060.66754338042049397 ether - // P = 0.526592412064432101 ether - - // coll = 735.140965662413210843 ether, debt = 98_018.795421655094779019 ether - vm.prank(eric); - handler.openTrove(98_009.397260273972617262 ether); + handler.addMeToLiquidationBatch(); - // pulling `deposited` from fixture + // upper hint: hope + // lower hint: hope + // upfront fee: 45.851924869044942133 ether vm.prank(carl); - handler.provideToSp(64_782.678263439363532911 ether, false); - - // totalBoldDeposits = 136_843.345806819784026881 ether - - vm.prank(eric); - handler.liquidateMe(); - - // totalBoldDeposits = 38_824.550385164689247862 ether - // P = 0.149402322152386736 ether - - vm.prank(dana); - handler.liquidateMe(); - - // totalBoldDeposits = 210.952427524467700142 ether - // P = 0.000811774565916968 ether - - // coll = 735.237290720905222225 ether, debt = 98_031.638762787362963266 ether - vm.prank(adam); - handler.openTrove(98_022.239369971064368053 ether); - - vm.prank(adam); - handler.provideToSp(370_408.786768579111584211 ether, false); + handler.openTrove( + 0, 3_111.607048463492852195 ether, 1.16895262626418546 ether, 0.142852735597140811 ether, 1885973, 10937 + ); - // totalBoldDeposits = 370_619.739196103579284353 ether + // upper hint: 2646484967802154597987056038088487662712072023062744056283555991417410575365 + // lower hint: 20207836743015961388089283396921182522044498153231052202943306959004515414684 + // upfront fee: 0 ether + // function: addColl() + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + vm.prank(gabe); + handler.adjustTrove( + 1, uint8(AdjustedTroveProperties.onlyColl), 3.631424438531681645 ether, true, 0 ether, false, 86, 703, 9499 + ); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 272_610.340127738994143277 ether - // P = 0.00059710295248084 ether - - // pulling `deposited` from fixture - vm.prank(dana); - handler.provideToSp(508.689744747380433717 ether, false); - - // totalBoldDeposits = 273_119.029872486374576994 ether - - vm.prank(eric); - handler.provideToSp(0.000000000000011519 ether, false); - - // totalBoldDeposits = 273_119.029872486374588513 ether - - vm.prank(dana); - handler.provideToSp(14_325.409601730288741627 ether, false); - - // totalBoldDeposits = 287_444.43947421666333014 ether + handler.lowerBatchManagementFee(2, 0.000000000204221707 ether); vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 189_425.95145220850566442 ether - // P = 0.000393490982450372 ether - - vm.prank(carl); - handler.provideToSp(656.318601037450927984 ether, false); + handler.addMeToLiquidationBatch(); - // totalBoldDeposits = 190_082.270053245956592404 ether + vm.prank(hope); + handler.addMeToLiquidationBatch(); - // coll = 735.070479452062891793 ether, debt = 98_009.39726027505223895 ether + vm.prank(hope); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.37622704640950591 ether + // redeemed BOLD: 0.000000000000005602 ether + // redeemed Troves: [ + // [carl], + // [gabe], + // [], + // [], + // ] vm.prank(carl); - handler.openTrove(98_000.000000001079532694 ether); - - // pulling `deposited` from fixture - vm.prank(eric); - handler.provideToSp(38_613.597957640221586334 ether, false); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(0.000000000000005603 ether, 1); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); - // totalBoldDeposits = 228_695.868010886178178738 ether + vm.prank(fran); + handler.addMeToUrgentRedemptionBatch(); vm.prank(dana); - handler.provideToSp(53_385.94712175149302094 ether, false); - - // totalBoldDeposits = 282_081.815132637671199678 ether - - vm.prank(carl); - handler.provideToSp(6_856.901188404296809837 ether, false); + handler.addMeToUrgentRedemptionBatch(); - // totalBoldDeposits = 288_938.716321041968009515 ether + vm.prank(dana); + handler.registerBatchManager( + 1, 0.995000000000001129 ether, 1 ether, 0.999999999999799729 ether, 0.000000000000000001 ether, 31535999 + ); - // coll = 15.001438356164383562 ether, debt = 2_000.191780821917808219 ether - vm.prank(fran); - handler.openTrove(2_000 ether); + // redemption rate: 0.718476929948594246 ether + // redeemed BOLD: 5_431.066474911544502914 ether + // redeemed Troves: [ + // [carl], + // [gabe], + // [], + // [], + // ] + vm.prank(barb); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + handler.redeemCollateral(10_313.397298437031513085 ether, 1); + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe ent debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); + info("lzti: ", c.troveManager.lastZombieTroveId().toString()); vm.prank(dana); - handler.provideToSp(5_881.506587694815077057 ether, false); + handler.warp(30_167_580); - // totalBoldDeposits = 294_820.222908736783086572 ether + info("gabe ent debt: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); + info("lzti: ", c.troveManager.lastZombieTroveId().toString()); + vm.prank(gabe); + handler.registerBatchManager( + 1, + 0.995000000000002877 ether, + 0.999999999999430967 ether, + 0.996456350847225481 ether, + 0.000000001322368348 ether, + 14343 + ); + info("gabe ent debt 1: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt 1: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); - // coll = 735.072533183874248984 ether, debt = 98_009.671091183233197827 ether vm.prank(hope); - handler.openTrove(98_000.273804654019798669 ether); - - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(98_018.795421655094877038 ether, false); - - // totalBoldDeposits = 392_839.01833039187796361 ether - - vm.prank(eric); - handler.provideToSp(1_186.940091321995741882 ether, false); - - // totalBoldDeposits = 394_025.958421713873705492 ether - - vm.prank(adam); - handler.provideToSp(0.001983727284992749 ether, false); - - invariant_allFundsClaimable(); - } - - function testSPYieldBigDispropRedeem() external { - // coll = 490_098_347_574_376_811.735209341223553774 ether, debt = 65_346_446_343_250_241_564.694578829807169845 ether + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether vm.prank(barb); - handler.openTrove(65_340_180_846_456_745_712.365995789115062922 ether); + handler.provideToSP(3, 1_933.156398582065633891 ether, false); - // coll = 750_071_917_808_219_163.080753424657534401 ether, debt = 100_009_589_041_095_888_410.767123287671253375 ether - vm.prank(gabe); - handler.openTrove(99_999_999_999_999_998_000.000000000000020497 ether); - - // coll = 502_539_092_456_032_564.492320686560399734 ether, debt = 67_005_212_327_471_008_598.976091541386631089 ether vm.prank(hope); - handler.openTrove(66_998_787_786_176_443_734.508398955185448923 ether); + handler.addMeToUrgentRedemptionBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 6_368.077020894268536036 ether + vm.prank(hope); + handler.provideToSP(0, 6_184.412833814428802676 ether, true); - // pulling `deposited` from fixture vm.prank(carl); - handler.provideToSp(65_346_446_343_250_241_630.04102517305741141 ether, false); - - // totalBoldDeposits = 65_346_446_343_250_241_630.04102517305741141 ether - - vm.prank(eric); - handler.provideToSp(0.00000000000000052 ether, false); - - // totalBoldDeposits = 65_346_446_343_250_241_630.04102517305741193 ether + handler.addMeToLiquidationBatch(); + // upper hint: 81940996894813545005963650320412669449148720334632109303327864712326705297348 + // lower hint: carl + // upfront fee: 297.236383200558451701 ether vm.prank(barb); - handler.liquidateMe(); - - // totalBoldDeposits = 65.346446343250242085 ether - // P = 1_000_000_000.000000006962260387 ether + handler.openTrove( + 0, + 69_695.596747080749922615 ether, + 1.900000000000006402 ether, + 0.153255449436557929 ether, + 1498297936, + 1276315316 + ); - // coll = 53_550_456_698_134_716.302091267691508688 ether, debt = 7_140_060_893_084_628_840.27883569220115827 ether - vm.prank(eric); - handler.openTrove(7_139_376_295_357_676_734.290616044087341676 ether); + // upper hint: 0 + // lower hint: 30960623452289762463130736603892188849115197753010878244835568881362241800197 + // upfront fee: 56.245103106642574315 ether + // function: withdrawBold() + vm.prank(hope); + handler.adjustTrove( + 0, + uint8(AdjustedTroveProperties.onlyDebt), + 0 ether, + false, + 7_875.177407392532383015 ether, + true, + 5, + 16648, + 270 + ); - // pulling `deposited` from fixture - vm.prank(carl); - handler.provideToSp(67_005_212_327_471_008_598.976091541386631089 ether, false); + // batch manager: gabe + // upper hint: gabe + // lower hint: 0 + // upfront fee: 1_261.275141740191589507 ether + vm.prank(adam); + handler.openTroveAndJoinInterestBatchManager( + 1, 66_969.454138225567397381 ether, 2.984784797753777921 ether, 4294967294, 1, 52 + ); + info("gabe ent debt 2: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt 2: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); - // totalBoldDeposits = 67_005_212_327_471_008_664.322537884636873174 ether + // batch manager: hope + // upper hint: 0 + // lower hint: barb + // upfront fee: 1_272.067039116734276271 ether + vm.prank(eric); + handler.openTroveAndJoinInterestBatchManager( + 0, 96_538.742068715532219745 ether, 2.762063859567414329 ether, 0, 61578232, 336273331 + ); + // initial deposit: 6_184.412833814428802676 ether + // compounded deposit: 6_184.412833814428802676 ether + // yield gain: 7_538.471959199501948711 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether vm.prank(hope); - handler.liquidateMe(); - - // totalBoldDeposits = 65.346446343250242085 ether - // P = 975_244_224_641_600_008.705577453466833391 ether + handler.provideToSP(0, 0.000000001590447554 ether, true); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether + vm.prank(fran); + handler.provideToSP(3, 180_836.387435487377369461 ether, true); - // coll = 570_376_835_580_790_313.880152031558999999 ether, debt = 76_050_244_744_105_375_184.020270874533333148 ether vm.prank(fran); - handler.openTrove(76_042_952_954_096_078_299.799742132137100824 ether); + handler.addMeToLiquidationBatch(); + + // initial deposit: 0 ether + // compounded deposit: 0 ether + // yield gain: 0 ether + // coll gain: 0 ether + // stashed coll: 0 ether + // blocked SP yield: 0 ether + vm.prank(eric); + handler.provideToSP(2, 0.000000000000000012 ether, true); - // Very extreme edge case. It gets fixed with SCALE_SPAN = 3 - // invariant_allFundsClaimable(); + vm.prank(carl); + handler.addMeToUrgentRedemptionBatch(); + + // redemption rate: 0.00500000000000102 ether + // redeemed BOLD: 0.000000000536305094 ether + // redeemed Troves: [ + // [barb], + // [gabe], + // [], + // [], + // ] + info("gabe trove Id: ", addressToTroveId(gabe).toString()); + info("gabe ent debt e: ", c.troveManager.getTroveEntireDebt(addressToTroveId(gabe)).decimal()); + info("gabe rec debt e: ", c.troveManager.getTroveDebt(addressToTroveId(gabe)).decimal()); + info("lzti: ", c.troveManager.lastZombieTroveId().toString()); + vm.prank(barb); + handler.redeemCollateral(0.000000000536305095 ether, 3); } -} +} \ No newline at end of file diff --git a/contracts/test/E2E.t.sol b/contracts/test/E2E.t.sol deleted file mode 100644 index 542864a5a..000000000 --- a/contracts/test/E2E.t.sol +++ /dev/null @@ -1,468 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {MockStakingV1} from "V2-gov/test/mocks/MockStakingV1.sol"; -import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; -import {Ownable} from "src/Dependencies/Ownable.sol"; -import {ICurveStableSwapNG} from "./Interfaces/Curve/ICurveStableSwapNG.sol"; -import {ILiquidityGaugeV6} from "./Interfaces/Curve/ILiquidityGaugeV6.sol"; -import "./Utils/E2EHelpers.sol"; - -function coalesce(address a, address b) pure returns (address) { - return a != address(0) ? a : b; -} - -contract E2ETest is E2EHelpers { - using SideEffectFreeGetPrice for IPriceFeedV1; - - struct Initiative { - address addr; - ILiquidityGaugeV6 gauge; // optional - } - - address[] ownables; - - function _addCurveLiquidity( - address liquidityProvider, - ICurveStableSwapNG pool, - uint256 coin0Amount, - address coin0, - uint256 coin1Amount, - address coin1 - ) internal { - uint256[] memory amounts = new uint256[](2); - (amounts[0], amounts[1]) = pool.coins(0) == coin0 ? (coin0Amount, coin1Amount) : (coin1Amount, coin0Amount); - - deal(coin0, liquidityProvider, coin0Amount); - deal(coin1, liquidityProvider, coin1Amount); - - vm.startPrank(liquidityProvider); - IERC20(coin0).approve(address(pool), coin0Amount); - IERC20(coin1).approve(address(pool), coin1Amount); - pool.add_liquidity(amounts, 0); - vm.stopPrank(); - } - - function _depositIntoCurveGauge(address liquidityProvider, ILiquidityGaugeV6 gauge, uint256 amount) internal { - vm.startPrank(liquidityProvider); - gauge.lp_token().approve(address(gauge), amount); - gauge.deposit(amount); - vm.stopPrank(); - } - - function _claimRewardsFromCurveGauge(address liquidityProvider, ILiquidityGaugeV6 gauge) internal { - vm.prank(liquidityProvider); - gauge.claim_rewards(); - } - - function _mainnet_V1_openTroveAtTail(address owner, uint256 lusdAmount) internal returns (uint256 borrowingFee) { - uint256 price = mainnet_V1_priceFeed.getPrice(); - address lastTrove = mainnet_V1_sortedTroves.getLast(); - assertGeDecimal(mainnet_V1_troveManager.getCurrentICR(lastTrove, price), 1.1 ether, 18, "last ICR < MCR"); - - uint256 borrowingRate = mainnet_V1_troveManager.getBorrowingRateWithDecay(); - borrowingFee = lusdAmount * borrowingRate / 1 ether; - uint256 debt = lusdAmount + borrowingFee + 200 ether; - uint256 collAmount = Math.ceilDiv(debt * 1.1 ether, price); - deal(owner, collAmount); - - vm.startPrank(owner); - mainnet_V1_borrowerOperations.openTrove{value: collAmount}({ - _LUSDAmount: lusdAmount, - _maxFeePercentage: borrowingRate, - _upperHint: lastTrove, - _lowerHint: address(0) - }); - vm.stopPrank(); - - assertEq(mainnet_V1_sortedTroves.getLast(), owner, "last Trove != new Trove"); - } - - function _mainnet_V1_redeemCollateralFromTroveAtTail(address redeemer, uint256 lusdAmount) - internal - returns (uint256 redemptionFee) - { - address lastTrove = mainnet_V1_sortedTroves.getLast(); - address prevTrove = mainnet_V1_sortedTroves.getPrev(lastTrove); - (uint256 lastTroveDebt, uint256 lastTroveColl,,) = mainnet_V1_troveManager.getEntireDebtAndColl(lastTrove); - assertLeDecimal(lusdAmount, lastTroveDebt - 2_000 ether, 18, "lusdAmount > redeemable from last Trove"); - - uint256 price = mainnet_V1_priceFeed.getPrice(); - uint256 collAmount = lusdAmount * 1 ether / price; - uint256 balanceBefore = redeemer.balance; - - vm.startPrank(redeemer); - mainnet_V1_troveManager.redeemCollateral({ - _LUSDamount: lusdAmount, - _maxFeePercentage: 1 ether, - _maxIterations: 1, - _firstRedemptionHint: lastTrove, - _upperPartialRedemptionHint: prevTrove, - _lowerPartialRedemptionHint: prevTrove, - _partialRedemptionHintNICR: (lastTroveColl - collAmount) * 100 ether / (lastTroveDebt - lusdAmount) - }); - vm.stopPrank(); - - redemptionFee = collAmount * mainnet_V1_troveManager.getBorrowingRateWithDecay() / 1 ether; - assertEqDecimal(redeemer.balance - balanceBefore, collAmount - redemptionFee, 18, "coll received != expected"); - } - - function _generateStakingRewards() internal returns (uint256 lusdAmount, uint256 ethAmount) { - if (block.chainid == 1) { - address stakingRewardGenerator = makeAddr("stakingRewardGenerator"); - lusdAmount = _mainnet_V1_openTroveAtTail(stakingRewardGenerator, 1e6 ether); - ethAmount = _mainnet_V1_redeemCollateralFromTroveAtTail(stakingRewardGenerator, 1_000 ether); - } else { - // Testnet - lusdAmount = 10_000 ether; - ethAmount = 1 ether; - - MockStakingV1 stakingV1 = MockStakingV1(address(governance.stakingV1())); - address owner = stakingV1.owner(); - - deal(LUSD, owner, lusdAmount); - deal(owner, ethAmount); - - vm.startPrank(owner); - lusd.approve(address(stakingV1), lusdAmount); - stakingV1.mock_addLUSDGain(lusdAmount); - stakingV1.mock_addETHGain{value: ethAmount}(); - vm.stopPrank(); - } - } - - function test_OwnershipRenounced() external { - ownables.push(address(boldToken)); - - for (uint256 i = 0; i < branches.length; ++i) { - ownables.push(address(branches[i].addressesRegistry)); - } - - for (uint256 i = 0; i < ownables.length; ++i) { - assertEq( - Ownable(ownables[i]).owner(), - address(0), - string.concat("Ownership of ", vm.getLabel(ownables[i]), " should have been renounced") - ); - } - - ILiquidityGaugeV6[2] memory gauges = [curveUsdcBoldGauge, curveLusdBoldGauge]; - - for (uint256 i = 0; i < gauges.length; ++i) { - if (address(gauges[i]) == address(0)) continue; - address gaugeManager = gauges[i].manager(); - assertEq(gaugeManager, address(0), "Gauge manager role should have been renounced"); - } - } - - function _epoch(uint256 n) internal view returns (uint256) { - return EPOCH_START + (n - 1) * EPOCH_DURATION; - } - - function test_Initially_NewInitiativeCannotBeRegistered() external { - vm.skip(governance.epoch() > 2); - - address registrant = makeAddr("registrant"); - address newInitiative = makeAddr("newInitiative"); - - _openTrove(0, registrant, 0, Math.max(REGISTRATION_FEE, MIN_DEBT)); - - uint256 epoch2 = _epoch(2); - if (block.timestamp < epoch2) vm.warp(epoch2); - - vm.startPrank(registrant); - { - boldToken.approve(address(governance), REGISTRATION_FEE); - vm.expectRevert("Governance: registration-not-yet-enabled"); - governance.registerInitiative(newInitiative); - } - vm.stopPrank(); - } - - function test_AfterOneEpoch_NewInitiativeCanBeRegistered() external { - vm.skip(governance.epoch() > 2); - - address registrant = makeAddr("registrant"); - address newInitiative = makeAddr("newInitiative"); - - _openTrove(0, registrant, 0, Math.max(REGISTRATION_FEE, MIN_DEBT)); - - uint256 epoch3 = _epoch(3); - if (block.timestamp < epoch3) vm.warp(epoch3); - - vm.startPrank(registrant); - { - boldToken.approve(address(governance), REGISTRATION_FEE); - governance.registerInitiative(newInitiative); - } - vm.stopPrank(); - } - - function test_E2E() external { - // Test assumes that all Stability Pools are empty in the beginning - for (uint256 i = 0; i < branches.length; ++i) { - vm.skip(branches[i].stabilityPool.getTotalBoldDeposits() != 0); - } - - uint256 repaid; - uint256 borrowed = boldToken.totalSupply() - boldToken.balanceOf(address(governance)); - - for (uint256 i = 0; i < branches.length; ++i) { - borrowed -= boldToken.balanceOf(address(branches[i].stabilityPool)); - } - - if (block.chainid == 1) { - assertEqDecimal(borrowed, 0, 18, "Mainnet deployment script should not have borrowed anything"); - assertNotEq(address(curveUsdcBoldGauge), address(0), "Mainnet should have USDC-BOLD gauge"); - assertNotEq(address(curveUsdcBoldInitiative), address(0), "Mainnet should have USDC-BOLD initiative"); - assertNotEq(address(curveLusdBold), address(0), "Mainnet should have LUSD-BOLD pool"); - assertNotEq(address(curveLusdBoldGauge), address(0), "Mainnet should have LUSD-BOLD gauge"); - assertNotEq(address(curveLusdBoldInitiative), address(0), "Mainnet should have LUSD-BOLD initiative"); - assertNotEq(address(defiCollectiveInitiative), address(0), "Mainnet should have DeFi Collective initiative"); - } - - address borrower = providerOf[BOLD] = makeAddr("borrower"); - - for (uint256 j = 0; j < 5; ++j) { - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - borrowed += _openTrove(i, borrower, j, 20_000 ether); - } - } - - address liquidityProvider = makeAddr("liquidityProvider"); - { - skip(5 minutes); - - uint256 boldAmount = boldToken.balanceOf(borrower) * 2 / 5; - uint256 usdcAmount = boldAmount * 10 ** usdc.decimals() / 10 ** boldToken.decimals(); - uint256 lusdAmount = boldAmount; - - _addCurveLiquidity(liquidityProvider, curveUsdcBold, boldAmount, BOLD, usdcAmount, USDC); - - if (address(curveLusdBold) != address(0)) { - _addCurveLiquidity(liquidityProvider, curveLusdBold, boldAmount, BOLD, lusdAmount, LUSD); - } - - if (address(curveUsdcBoldGauge) != address(0)) { - _depositIntoCurveGauge( - liquidityProvider, curveUsdcBoldGauge, curveUsdcBold.balanceOf(liquidityProvider) - ); - } - - if (address(curveLusdBoldGauge) != address(0)) { - _depositIntoCurveGauge( - liquidityProvider, curveLusdBoldGauge, curveLusdBold.balanceOf(liquidityProvider) - ); - } - } - - address stabilityDepositor = makeAddr("stabilityDepositor"); - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - _provideToSP(i, stabilityDepositor, boldToken.balanceOf(borrower) / (branches.length - i)); - } - - address leverageSeeker = makeAddr("leverageSeeker"); - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - borrowed += _openLeveragedTrove(i, leverageSeeker, 0, 10_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - borrowed += _leverUpTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - repaid += _leverDownTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - repaid += _closeTroveFromCollateral(i, leverageSeeker, 0, true); - } - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - repaid += _closeTroveFromCollateral(i, borrower, 0, false); - } - - skip(5 minutes); - - Initiative[] memory initiatives = new Initiative[](initialInitiatives.length); - for (uint256 i = 0; i < initiatives.length; ++i) { - initiatives[i].addr = initialInitiatives[i]; - if (initialInitiatives[i] == address(curveUsdcBoldInitiative)) initiatives[i].gauge = curveUsdcBoldGauge; - if (initialInitiatives[i] == address(curveLusdBoldInitiative)) initiatives[i].gauge = curveLusdBoldGauge; - } - - address staker = makeAddr("staker"); - { - uint256 lqtyStake = 30_000 ether; - _depositLQTY(staker, lqtyStake); - - skip(5 minutes); - - (uint256 lusdAmount, uint256 ethAmount) = _generateStakingRewards(); - uint256 totalLQTYStaked = governance.stakingV1().totalLQTYStaked(); - - skip(5 minutes); - - vm.prank(staker); - governance.claimFromStakingV1(staker); - - assertApproxEqAbsDecimal( - lusd.balanceOf(staker), lusdAmount * lqtyStake / totalLQTYStaked, 1e5, 18, "LUSD reward" - ); - assertApproxEqAbsDecimal(staker.balance, ethAmount * lqtyStake / totalLQTYStaked, 1e5, 18, "ETH reward"); - - skip(5 minutes); - - if (initiatives.length > 0) { - // Voting on initial initiatives opens in epoch #2 - uint256 votingStart = _epoch(2); - if (block.timestamp < votingStart) vm.warp(votingStart); - - _allocateLQTY_begin(staker); - - for (uint256 i = 0; i < initiatives.length; ++i) { - _allocateLQTY_vote(initiatives[i].addr, int256(lqtyStake / initiatives.length)); - } - - _allocateLQTY_end(); - } - } - - skip(EPOCH_DURATION); - - for (uint256 i = 0; i < branches.length; ++i) { - skip(5 minutes); - _claimFromSP(i, stabilityDepositor); - } - - uint256 interest = boldToken.totalSupply() + repaid - borrowed; - uint256 spShareOfInterest = boldToken.balanceOf(stabilityDepositor); - uint256 governanceShareOfInterest = boldToken.balanceOf(address(governance)); - - assertApproxEqRelDecimal( - interest, - spShareOfInterest + governanceShareOfInterest, - 1e-16 ether, - 18, - "Stability depositor and Governance should have received the interest" - ); - - if (initiatives.length > 0) { - uint256 initiativeShareOfInterest; - - for (uint256 i = 0; i < initiatives.length; ++i) { - governance.claimForInitiative(initiatives[i].addr); - initiativeShareOfInterest += - boldToken.balanceOf(coalesce(address(initiatives[i].gauge), initiatives[i].addr)); - } - - assertApproxEqRelDecimal( - governanceShareOfInterest, - initiativeShareOfInterest, - 1e-15 ether, - 18, - "Initiatives should have received the interest from Governance" - ); - - uint256 numGauges; - uint256 maxGaugeDuration; - - for (uint256 i = 0; i < initiatives.length; ++i) { - if (address(initiatives[i].gauge) != address(0)) { - maxGaugeDuration = Math.max(maxGaugeDuration, CurveV2GaugeRewards(initiatives[i].addr).duration()); - ++numGauges; - } - } - - skip(maxGaugeDuration); - - if (numGauges > 0) { - uint256 gaugeShareOfInterest; - - for (uint256 i = 0; i < initiatives.length; ++i) { - if (address(initiatives[i].gauge) != address(0)) { - gaugeShareOfInterest += boldToken.balanceOf(address(initiatives[i].gauge)); - _claimRewardsFromCurveGauge(liquidityProvider, initiatives[i].gauge); - } - } - - assertApproxEqRelDecimal( - boldToken.balanceOf(liquidityProvider), - gaugeShareOfInterest, - 1e-13 ether, - 18, - "Liquidity provider should have earned the rewards from the Curve gauges" - ); - } - } - } - - // This can be used to check that everything's still working as expected in a live testnet deployment - function test_Borrowing_InExistingDeployment() external { - for (uint256 i = 0; i < branches.length; ++i) { - vm.skip(branches[i].troveManager.getTroveIdsCount() == 0); - } - - address borrower = makeAddr("borrower"); - - for (uint256 i = 0; i < branches.length; ++i) { - _openTrove(i, borrower, 0, 10_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _closeTroveFromCollateral(i, borrower, 0, false); - } - - address leverageSeeker = makeAddr("leverageSeeker"); - - for (uint256 i = 0; i < branches.length; ++i) { - _openLeveragedTrove(i, leverageSeeker, 0, 10_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _leverUpTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _leverDownTrove(i, leverageSeeker, 0, 1_000 ether); - } - - for (uint256 i = 0; i < branches.length; ++i) { - _closeTroveFromCollateral(i, leverageSeeker, 0, true); - } - } - - function test_ManagerOfCurveGauge_UnlessRenounced_CanReassignRewardDistributor() external { - vm.skip(address(curveUsdcBoldGauge) == address(0)); - - address manager = curveUsdcBoldGauge.manager(); - vm.skip(manager == address(0)); - vm.label(manager, "manager"); - - address newRewardDistributor = makeAddr("newRewardDistributor"); - uint256 rewardAmount = 10_000 ether; - _openTrove(0, newRewardDistributor, 0, rewardAmount); - - vm.startPrank(newRewardDistributor); - boldToken.approve(address(curveUsdcBoldGauge), rewardAmount); - vm.expectRevert(); - curveUsdcBoldGauge.deposit_reward_token(BOLD, rewardAmount, 7 days); - vm.stopPrank(); - - vm.prank(manager); - curveUsdcBoldGauge.set_reward_distributor(BOLD, newRewardDistributor); - - vm.startPrank(newRewardDistributor); - curveUsdcBoldGauge.deposit_reward_token(BOLD, rewardAmount, 7 days); - vm.stopPrank(); - } -} diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol new file mode 100644 index 000000000..0dd3c2802 --- /dev/null +++ b/contracts/test/FXPriceFeed.t.sol @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; +import "../src/PriceFeeds/FXPriceFeed.sol"; + +import { Test } from "forge-std/Test.sol"; + +contract MockBorrowerOperations { + bool public _isShutdown; + + function shutdownFromOracleFailure() external { + _isShutdown = true; + } + + function shutdownCalled() external view returns (bool) { + return _isShutdown; + } +} + +contract MockOracleAdapter { + uint256 numerator; + uint256 denominator; + bool public sequencerUp = true; + + function setFXRate(uint256 _numerator, uint256 _denominator) external { + numerator = _numerator; + denominator = _denominator; + } + + function getFXRateIfValid(address) external view returns (uint256, uint256) { + return (numerator, denominator); + } + + function setIsL2SequencerUp(bool _isUp) external { + sequencerUp = _isUp; + } + + function isL2SequencerUp(uint256) external view returns (bool) { + return sequencerUp; + } +} + + +contract FXPriceFeedTest is Test { + + event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); + event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); + event FXPriceFeedShutdown(); + event OracleAdapterUpdated(address indexed _oldOracleAdapterAddress, address indexed _newOracleAdapterAddress); + event L2SequencerGracePeriodUpdated(uint256 indexed _oldL2SequencerGracePeriod, uint256 indexed _newL2SequencerGracePeriod); + + FXPriceFeed public fxPriceFeed; + MockOracleAdapter public mockOracleAdapter; + MockBorrowerOperations public mockBorrowerOperations; + MockFXPriceFeed public mockFXPriceFeed; + + uint256 public l2SequencerGracePeriod = 6 hours; + address public rateFeedID = makeAddr("rateFeedID"); + address public watchdog = makeAddr("watchdog"); + address public owner = makeAddr("owner"); + + uint256 constant mockRateNumerator = 1200 * 1e18; // 1.2 USD per unit + uint256 constant mockRateDenominator = 1e18; + + modifier initialized() { + vm.startPrank(owner); + fxPriceFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + vm.stopPrank(); + _; + } + + function setUp() public { + mockOracleAdapter = new MockOracleAdapter(); + mockOracleAdapter.setFXRate(mockRateNumerator, mockRateDenominator); + mockOracleAdapter.setIsL2SequencerUp(true); + + mockBorrowerOperations = new MockBorrowerOperations(); + + fxPriceFeed = new FXPriceFeed(false); + } + + function test_constructor_whenDisableInitializersTrue_shouldDisableInitialization() public { + FXPriceFeed newFeed = new FXPriceFeed(true); + + vm.expectRevert(); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_initialize_whenOracleAdapterAddressIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(0), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_initialize_whenRateFeedIDIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + address(0), + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_initialize_whenBorrowerOperationsAddressIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(0), + watchdog, + owner + ); + } + + function test_initialize_whenWatchdogAddressIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + address(0), + owner + ); + } + + function test_initialize_whenInitialOwnerIsZero_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + address(0) + ); + } + + function test_initialize_whenAllParametersValid_shouldSucceed() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + mockOracleAdapter.setFXRate(5e18, 1e18); + + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + + assertEq(address(newFeed.oracleAdapter()), address(mockOracleAdapter)); + assertEq(newFeed.rateFeedID(), rateFeedID); + assertEq(newFeed.l2SequencerGracePeriod(), l2SequencerGracePeriod); + assertEq(address(newFeed.borrowerOperations()), address(mockBorrowerOperations)); + assertEq(newFeed.watchdogAddress(), watchdog); + assertEq(newFeed.owner(), owner); + assertEq(newFeed.lastValidPrice(), 5e18); + } + + function test_initialize_whenCalledTwice_shouldRevert() public { + FXPriceFeed newFeed = new FXPriceFeed(false); + + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + + vm.expectRevert("Initializable: contract is already initialized"); + newFeed.initialize( + address(mockOracleAdapter), + rateFeedID, + false, + l2SequencerGracePeriod, + address(mockBorrowerOperations), + watchdog, + owner + ); + } + + function test_setRateFeedID_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + address newRateFeedID = makeAddr("newRateFeedID"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setRateFeedID(newRateFeedID); + vm.stopPrank(); + } + + function test_setRateFeedID_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setRateFeedID(address(0)); + vm.stopPrank(); + } + + function test_setRateFeedID_whenCalledByOwner_shouldSucceed() initialized public { + address newRateFeedID = makeAddr("newRateFeedID"); + + vm.prank(owner); + fxPriceFeed.setRateFeedID(newRateFeedID); + vm.stopPrank(); + + assertEq(fxPriceFeed.rateFeedID(), newRateFeedID); + } + + function test_setInvertRateFeed_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + bool newInvertRateFeed = true; + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setInvertRateFeed(newInvertRateFeed); + vm.stopPrank(); + } + + function test_setInvertRateFeed_whenCalledByOwner_shouldSucceed() initialized public { + vm.startPrank(owner); + vm.expectEmit(); + emit InvertRateFeedUpdated(false, true); + fxPriceFeed.setInvertRateFeed(true); + vm.stopPrank(); + + assertEq(fxPriceFeed.invertRateFeed(), true); + + vm.startPrank(owner); + vm.expectEmit(); + emit InvertRateFeedUpdated(true, false); + fxPriceFeed.setInvertRateFeed(false); + vm.stopPrank(); + + assertEq(fxPriceFeed.invertRateFeed(), false); + } + + function test_setWatchdogAddress_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + address newWatchdog = makeAddr("newWatchdog"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setWatchdogAddress(newWatchdog); + vm.stopPrank(); + } + + function test_setWatchdogAddress_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setWatchdogAddress(address(0)); + vm.stopPrank(); + } + + function test_setWatchdogAddress_whenCalledByOwner_shouldSucceed() initialized public { + address newWatchdog = makeAddr("newWatchdog"); + + vm.prank(owner); + vm.expectEmit(); + emit WatchdogAddressUpdated(watchdog, newWatchdog); + fxPriceFeed.setWatchdogAddress(newWatchdog); + vm.stopPrank(); + + assertEq(fxPriceFeed.watchdogAddress(), newWatchdog); + } + + function test_fetchPrice_whenNotShutdown_shouldReturnOraclePrice() initialized public { + uint256 price = fxPriceFeed.fetchPrice(); + + assertEq(price, mockRateNumerator); + assertEq(fxPriceFeed.lastValidPrice(), mockRateNumerator); + } + + function test_fetchPrice_whenShutdown_shouldReturnLastValidPrice() initialized public { + uint256 initialPrice = fxPriceFeed.fetchPrice(); + assertEq(initialPrice, mockRateNumerator); + + vm.prank(watchdog); + fxPriceFeed.shutdown(); + vm.stopPrank(); + + mockOracleAdapter.setFXRate(2 * mockRateNumerator, 2 * mockRateDenominator); + + uint256 priceAfterShutdown = fxPriceFeed.fetchPrice(); + + assertEq(priceAfterShutdown, initialPrice); + assertEq(fxPriceFeed.lastValidPrice(), initialPrice); + } + + function test_fetchPrice_whenInvertRateFeedIsTrue_shouldReturnInvertedPrice() initialized public { + vm.startPrank(owner); + fxPriceFeed.setInvertRateFeed(true); + vm.stopPrank(); + + uint256 price = fxPriceFeed.fetchPrice(); + + assertEq(price, (mockRateDenominator * 1e18) / mockRateNumerator); + assertEq(fxPriceFeed.lastValidPrice(), (mockRateDenominator * 1e18) / mockRateNumerator); + + uint256 XOFUSDRateNumerator = 1771165426850867; // 0.001771 USD = ~1 XOF + mockOracleAdapter.setFXRate(XOFUSDRateNumerator, 1e18); + + assertEq(fxPriceFeed.fetchPrice(), 564600000000000277670); // 1 USD = ~564 XOF + assertEq(fxPriceFeed.lastValidPrice(), 564600000000000277670); + } + + function test_shutdown_whenCalledByNonWatchdog_shouldRevert() initialized public { + address notWatchdog = makeAddr("notWatchdog"); + + vm.prank(notWatchdog); + vm.expectRevert(FXPriceFeed.OnlyWatchdog.selector); + fxPriceFeed.shutdown(); + vm.stopPrank(); + } + + function test_shutdown_whenCalledByWatchdog_shouldShutdown() initialized public { + assertEq(fxPriceFeed.isShutdown(), false); + assertEq(mockBorrowerOperations.shutdownCalled(), false); + + vm.prank(watchdog); + vm.expectEmit(); + emit FXPriceFeedShutdown(); + fxPriceFeed.shutdown(); + vm.stopPrank(); + + assertTrue(fxPriceFeed.isShutdown()); + assertTrue(mockBorrowerOperations.shutdownCalled()); + } + + function test_shutdown_whenAlreadyShutdown_shouldRevert() initialized public { + vm.prank(watchdog); + fxPriceFeed.shutdown(); + vm.expectRevert(FXPriceFeed.AlreadyShutdown.selector); + fxPriceFeed.shutdown(); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setOracleAdapter(makeAddr("newOracleAdapter")); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenNewAddressIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.ZeroAddress.selector); + fxPriceFeed.setOracleAdapter(address(0)); + vm.stopPrank(); + } + + function test_setOracleAdapter_whenCalledByOwner_shouldSucceed() initialized public { + address newOracleAdapter = makeAddr("newOracleAdapter"); + + vm.prank(owner); + vm.expectEmit(); + emit OracleAdapterUpdated(address(mockOracleAdapter), newOracleAdapter); + fxPriceFeed.setOracleAdapter(newOracleAdapter); + vm.stopPrank(); + + assertEq(address(fxPriceFeed.oracleAdapter()), newOracleAdapter); + } + + function test_setL2SequencerGracePeriod_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setL2SequencerGracePeriod(12 hours); + vm.stopPrank(); + } + + function test_setL2SequencerGracePeriod_whenNewPeriodIsZero_shouldRevert() initialized public { + vm.prank(owner); + vm.expectRevert(FXPriceFeed.InvalidL2SequencerGracePeriod.selector); + fxPriceFeed.setL2SequencerGracePeriod(0); + vm.stopPrank(); + } + + function test_setL2SequencerGracePeriod_whenCalledByOwner_shouldSucceed() initialized public { + uint256 oldGracePeriod = fxPriceFeed.l2SequencerGracePeriod(); + uint256 newGracePeriod = 12 hours; + + vm.prank(owner); + vm.expectEmit(); + emit L2SequencerGracePeriodUpdated(oldGracePeriod, newGracePeriod); + fxPriceFeed.setL2SequencerGracePeriod(newGracePeriod); + vm.stopPrank(); + + assertEq(fxPriceFeed.l2SequencerGracePeriod(), newGracePeriod); + } + + function test_isL2SequencerUp_whenSequencerIsUp_shouldReturnTrue() initialized public { + mockOracleAdapter.setIsL2SequencerUp(true); + + bool result = fxPriceFeed.isL2SequencerUp(); + assertTrue(result); + } + + function test_isL2SequencerUp_whenSequencerIsDown_shouldReturnFalse() initialized public { + mockOracleAdapter.setIsL2SequencerUp(false); + + bool result = fxPriceFeed.isL2SequencerUp(); + assertFalse(result); + } +} diff --git a/contracts/test/InitiativeSmarDEX.t.sol b/contracts/test/InitiativeSmarDEX.t.sol deleted file mode 100644 index b01b1d60e..000000000 --- a/contracts/test/InitiativeSmarDEX.t.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import "./Utils/E2EHelpers.sol"; -import "forge-std/console2.sol"; - -address constant INITIATIVE_ADDRESS = 0xa58FBe38AAB33b9fadEf1B5Aaff4D7bC27C43aD4; -address constant GOVERNANCE_WHALE = 0xF30da4E4e7e20Dbf5fBE9adCD8699075D62C60A4; - -// Start an anvil node or set up a proper RPC URL -// FOUNDRY_PROFILE=e2e E2E_RPC_URL="http://localhost:8545" forge test --mc InitiativeSmarDEX -vvv - -contract InitiativeSmarDEX is E2EHelpers { - address public NEW_LQTY_WHALE = 0xF977814e90dA44bFA03b6295A0616a897441aceC; - - function setUp() public override { - super.setUp(); - vm.label(NEW_LQTY_WHALE, "LQTY_WHALE"); - providerOf[LQTY] = NEW_LQTY_WHALE; - } - - function testInitiativeRegistrationAndClaim() external { - address borrower = makeAddr("borrower"); - address registrant = GOVERNANCE_WHALE; - - // Open trove and transfer BOLD to registrant - uint256 donationAmount = 10_000 ether; - _openTrove(0, borrower, 0, Math.max(REGISTRATION_FEE, MIN_DEBT) + donationAmount); - vm.startPrank(borrower); - boldToken.transfer(registrant, REGISTRATION_FEE); - vm.stopPrank(); - - // Stake LQTY and accumulate voting power - address staker = makeAddr("staker"); - uint256 lqtyStake = 3_000_000 ether; - _depositLQTY(staker, lqtyStake); - - skip(30 days); - - assertEq(governance.registeredInitiatives(INITIATIVE_ADDRESS), 0, "Initiative should not be registered"); - - // Register initiative - vm.startPrank(registrant); - boldToken.approve(address(governance), REGISTRATION_FEE); - governance.registerInitiative(INITIATIVE_ADDRESS); - vm.stopPrank(); - - assertGt(governance.registeredInitiatives(INITIATIVE_ADDRESS), 0, "Initiative should be registered"); - - skip(7 days); - - // Allocate to initiative - _allocateLQTY_begin(staker); - _allocateLQTY_vote(INITIATIVE_ADDRESS, int256(lqtyStake)); // TODO - _allocateLQTY_end(); - - // Donate - vm.startPrank(borrower); - boldToken.transfer(address(governance), donationAmount); - vm.stopPrank(); - - skip(7 days); - - /* - console2.log(boldToken.balanceOf(address(governance)), "boldToken.balanceOf(address(governance))"); - console2.log(governance.getLatestVotingThreshold(), "voting threshold"); - (Governance.VoteSnapshot memory voteSnapshot, Governance.InitiativeVoteSnapshot memory initiativeVoteSnapshot) = - governance.snapshotVotesForInitiative(INITIATIVE_ADDRESS); - console2.log(initiativeVoteSnapshot.votes, "initiative Votes"); - (Governance.InitiativeStatus status, uint256 lastEpochClaim, uint256 claimableAmount) = - governance.getInitiativeState(INITIATIVE_ADDRESS); - console2.log(uint256(status), "uint(status)"); - console2.log(lastEpochClaim, "lastEpochClaim"); - console2.log(claimableAmount, "claimableAmount"); - */ - // Claim for initiative - governance.claimForInitiative(INITIATIVE_ADDRESS); - //console2.log(boldToken.balanceOf(INITIATIVE_ADDRESS), "boldToken.balanceOf(INITIATIVE_ADDRESS)"); - assertGt(boldToken.balanceOf(INITIATIVE_ADDRESS), 0, "Initiative should have received incentives"); - } -} diff --git a/contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol b/contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol deleted file mode 100644 index e2d790c24..000000000 --- a/contracts/test/Interfaces/LiquityV1/IPriceFeedV1.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -interface IPriceFeedV1 { - function fetchPrice() external returns (uint256); -} diff --git a/contracts/test/Invariants.t.sol b/contracts/test/Invariants.t.sol index 15ff652fb..bf4ed54db 100644 --- a/contracts/test/Invariants.t.sol +++ b/contracts/test/Invariants.t.sol @@ -86,8 +86,9 @@ contract InvariantsTest is Assertions, Logging, BaseInvariantTest, BaseMultiColl TestDeployer deployer = new TestDeployer(); Contracts memory contracts; - (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth,) + (contracts.branches, contracts.collateralRegistry, contracts.boldToken, contracts.hintHelpers,, contracts.weth) = deployer.deployAndConnectContractsMultiColl(p); + contracts.systemParams = contracts.branches[0].systemParams; setupContracts(contracts); handler = new InvariantsTestHandler({contracts: contracts, assumeNoExpectedFailures: true}); diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol deleted file mode 100644 index b21e78a04..000000000 --- a/contracts/test/OracleMainnet.t.sol +++ /dev/null @@ -1,2518 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/PriceFeeds/WSTETHPriceFeed.sol"; -import "src/PriceFeeds/MainnetPriceFeedBase.sol"; -import "src/PriceFeeds/RETHPriceFeed.sol"; -import "src/PriceFeeds/WETHPriceFeed.sol"; - -import "./TestContracts/Accounts.sol"; -import "./TestContracts/ChainlinkOracleMock.sol"; -import "./TestContracts/GasGuzzlerOracle.sol"; -import "./TestContracts/GasGuzzlerToken.sol"; -import "./TestContracts/RETHTokenMock.sol"; -import "./TestContracts/WSTETHTokenMock.sol"; -import "./TestContracts/Deployment.t.sol"; - -import "src/Dependencies/AggregatorV3Interface.sol"; -import "src/Interfaces/IRETHPriceFeed.sol"; -import "src/Interfaces/IWSTETHPriceFeed.sol"; - -import "src/Interfaces/IRETHToken.sol"; -import "src/Interfaces/IWSTETH.sol"; - -import "forge-std/Test.sol"; -import "lib/forge-std/src/console2.sol"; - -contract OraclesMainnet is TestAccounts { - AggregatorV3Interface ethOracle; - AggregatorV3Interface stethOracle; - AggregatorV3Interface rethOracle; - - ChainlinkOracleMock mockOracle; - GasGuzzlerToken gasGuzzlerToken; - GasGuzzlerOracle gasGuzzlerOracle; - - IMainnetPriceFeed wethPriceFeed; - IRETHPriceFeed rethPriceFeed; - IWSTETHPriceFeed wstethPriceFeed; - - IRETHToken rethToken; - IWSTETH wstETH; - - RETHTokenMock mockRethToken; - WSTETHTokenMock mockWstethToken; - - TestDeployer.LiquityContracts[] contractsArray; - CollateralRegistryTester collateralRegistry; - IBoldToken boldToken; - - struct StoredOracle { - AggregatorV3Interface aggregator; - uint256 stalenessThreshold; - uint256 decimals; - } - - struct Vars { - uint256 numCollaterals; - uint256 initialColl; - uint256 price; - uint256 coll; - uint256 debtRequest; - uint256 debt_B; - uint256 debt_C; - uint256 debt_D; - uint256 ICR_A; - uint256 ICR_B; - uint256 ICR_C; - uint256 ICR_D; - uint256 redemptionICR_A; - uint256 redemptionICR_B; - uint256 redemptionICR_C; - uint256 redemptionICR_D; - uint256 troveId_A; - uint256 troveId_B; - uint256 troveId_C; - uint256 troveId_D; - int256 newEthPrice; - uint256 systemPrice; - uint256 newSystemPrice; - uint256 newSystemRedemptionPrice; - int256 ethPerRethMarket; - int256 usdPerEthMarket; - uint256 ethPerRethLST; - LatestTroveData troveDataBefore_A; - LatestTroveData troveDataBefore_B; - LatestTroveData troveDataBefore_C; - LatestTroveData troveDataBefore_D; - LatestTroveData troveDataAfter_A; - LatestTroveData troveDataAfter_B; - LatestTroveData troveDataAfter_C; - LatestTroveData troveDataAfter_D; - } - - function setUp() public { - try vm.envString("MAINNET_RPC_URL") returns (string memory rpcUrl) { - vm.createSelectFork(rpcUrl); - } catch { - vm.skip(true); - } - - Vars memory vars; - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F) = - (accountsList[0], accountsList[1], accountsList[2], accountsList[3], accountsList[4], accountsList[5]); - - vars.numCollaterals = 3; - TestDeployer.TroveManagerParams memory tmParams = - TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = - new TestDeployer.TroveManagerParams[](vars.numCollaterals); - for (uint256 i = 0; i < troveManagerParamsArray.length; i++) { - troveManagerParamsArray[i] = tmParams; - } - - TestDeployer deployer = new TestDeployer(); - TestDeployer.DeploymentResultMainnet memory result = - deployer.deployAndConnectContractsMainnet(troveManagerParamsArray); - collateralRegistry = result.collateralRegistry; - boldToken = result.boldToken; - - ethOracle = AggregatorV3Interface(result.externalAddresses.ETHOracle); - rethOracle = AggregatorV3Interface(result.externalAddresses.RETHOracle); - stethOracle = AggregatorV3Interface(result.externalAddresses.STETHOracle); - - mockOracle = new ChainlinkOracleMock(); - gasGuzzlerToken = new GasGuzzlerToken(); - gasGuzzlerOracle = new GasGuzzlerOracle(); - - rethToken = IRETHToken(result.externalAddresses.RETHToken); - - wstETH = IWSTETH(result.externalAddresses.WSTETHToken); - - mockRethToken = new RETHTokenMock(); - mockWstethToken = new WSTETHTokenMock(); - - // Record contracts - for (uint256 c = 0; c < vars.numCollaterals; c++) { - contractsArray.push(result.contractsArray[c]); - } - - // Give all users all collaterals - vars.initialColl = 1000_000e18; - for (uint256 i = 0; i < 6; i++) { - for (uint256 j = 0; j < vars.numCollaterals; j++) { - deal(address(contractsArray[j].collToken), accountsList[i], vars.initialColl); - vm.startPrank(accountsList[i]); - // Approve all Borrower Ops to use the user's WETH funds - contractsArray[0].collToken.approve(address(contractsArray[j].borrowerOperations), type(uint256).max); - // Approve Borrower Ops in LST branches to use the user's respective LST funds - contractsArray[j].collToken.approve(address(contractsArray[j].borrowerOperations), type(uint256).max); - vm.stopPrank(); - } - - vm.startPrank(accountsList[i]); - } - - wethPriceFeed = IMainnetPriceFeed(address(contractsArray[0].priceFeed)); - rethPriceFeed = IRETHPriceFeed(address(contractsArray[1].priceFeed)); - wstethPriceFeed = IWSTETHPriceFeed(address(contractsArray[2].priceFeed)); - - // log some current blockchain state - // console2.log(block.timestamp, "block.timestamp"); - // console2.log(block.number, "block.number"); - // console2.log(ethOracle.decimals(), "ETHUSD decimals"); - // console2.log(rethOracle.decimals(), "RETHETH decimals"); - // console2.log(stethOracle.decimals(), "STETHETH decimals"); - - // Artificially decay the base rate so we start with a low redemption rate. - // Normally, we would just wait for it to decay "naturally" (with `vm.warp`), but we can't do that here, - // as it would result in all the oracles going stale. - collateralRegistry.setBaseRate(0); - } - - function _getLatestAnswerFromOracle(AggregatorV3Interface _oracle) internal view returns (uint256) { - (, int256 answer,,,) = _oracle.latestRoundData(); - - uint256 decimals = _oracle.decimals(); - assertLe(decimals, 18); - // Convert to uint and scale up to 18 decimals - return uint256(answer) * 10 ** (18 - decimals); - } - - function redeem(address _from, uint256 _boldAmount) public { - vm.startPrank(_from); - collateralRegistry.redeemCollateral(_boldAmount, MAX_UINT256, 1e18); - vm.stopPrank(); - } - - function etchStaleMockToEthOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(ethOracle), _mockOracleCode); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(ethOracle)); - mock.setDecimals(8); - // Fake ETH-USD price of 2000 USD - mock.setPrice(2000e8); - // Make it stale - mock.setUpdatedAt(block.timestamp - 7 days); - } - - function etchStaleMockToRethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the RETH-ETH oracle address - vm.etch(address(rethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - mock.setDecimals(18); - // Set 1 RETH = 1 ETH - mock.setPrice(1e18); - // Make it stale - mock.setUpdatedAt(block.timestamp - 7 days); - } - - function etchStaleMockToStethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the STETH-USD oracle address - vm.etch(address(stethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); - mock.setDecimals(8); - // Set 1 STETH = 2000 USD - mock.setPrice(2000e8); - // Make it stale - mock.setUpdatedAt(block.timestamp - 7 days); - } - - function etchMockToEthOracle() internal returns (ChainlinkOracleMock) { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(ethOracle), address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(ethOracle)); - mock.setDecimals(8); - mock.setPrice(0); - // Make it current - mock.setUpdatedAt(block.timestamp); - - return mock; - } - - function etchMockToRethOracle() internal returns (ChainlinkOracleMock) { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(rethOracle), address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - mock.setDecimals(18); - mock.setPrice(0); - // Make it current - mock.setUpdatedAt(block.timestamp); - - return mock; - } - - function etchMockToStethOracle() internal returns (ChainlinkOracleMock) { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(stethOracle), address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); - mock.setDecimals(8); - mock.setPrice(0); - // Make it current - mock.setUpdatedAt(block.timestamp); - - return mock; - } - - function etchGasGuzzlerToEthOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the ETH-USD oracle address - vm.etch(address(ethOracle), _mockOracleCode); - GasGuzzlerOracle mock = GasGuzzlerOracle(address(ethOracle)); - mock.setDecimals(8); - // Fake ETH-USD price of 2000 USD - mock.setPrice(2000e8); - mock.setUpdatedAt(block.timestamp); - } - - function etchGasGuzzlerToRethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the RETH-ETH oracle address - vm.etch(address(rethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - GasGuzzlerOracle mock = GasGuzzlerOracle(address(rethOracle)); - mock.setDecimals(18); - // Set 1 RETH = 1.1 ETH - mock.setPrice(11e17); - mock.setUpdatedAt(block.timestamp); - } - - function etchGasGuzzlerToStethOracle(bytes memory _mockOracleCode) internal { - // Etch the mock code to the STETH-USD oracle address - vm.etch(address(stethOracle), _mockOracleCode); - // Wrap so we can use the mock's setters - GasGuzzlerOracle mock = GasGuzzlerOracle(address(stethOracle)); - mock.setDecimals(8); - // Set 1 STETH = 2000 USD - mock.setPrice(2000e8); - mock.setUpdatedAt(block.timestamp); - } - - function etchMockToRethToken() internal { - vm.etch(address(rethToken), address(mockRethToken).code); - RETHTokenMock mock = RETHTokenMock(address(rethToken)); - mock.setExchangeRate(0); - } - - function etchGasGuzzlerMockToRethToken(bytes memory _mockTokenCode) internal { - // Etch the mock code to the RETH token address - vm.etch(address(rethToken), _mockTokenCode); - } - - function etchGasGuzzlerMockToWstethToken(bytes memory _mockTokenCode) internal { - // Etch the mock code to the RETH token address - vm.etch(address(wstETH), _mockTokenCode); - } - - // --- lastGoodPrice set on deployment --- - - function testSetLastGoodPriceOnDeploymentWETH() public view { - uint256 lastGoodPriceWeth = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceWeth, 0); - - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - assertEq(lastGoodPriceWeth, latestAnswerEthUsd); - } - - function testSetLastGoodPriceOnDeploymentRETH() public view { - uint256 lastGoodPriceReth = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceReth, 0); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 rate = rethToken.getExchangeRate(); - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(lastGoodPriceReth, expectedPrice); - } - - function testSetLastGoodPriceOnDeploymentWSTETH() public view { - uint256 lastGoodPriceWsteth = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceWsteth, 0); - - uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 stethWstethExchangeRate = wstETH.stEthPerToken(); - - uint256 expectedStoredPrice = latestAnswerStethUsd * stethWstethExchangeRate / 1e18; - - assertEq(lastGoodPriceWsteth, expectedStoredPrice); - } - - // --- fetchPrice --- - - function testFetchPriceReturnsCorrectPriceWETH() public { - (uint256 fetchedEthUsdPrice,) = wethPriceFeed.fetchPrice(); - assertGt(fetchedEthUsdPrice, 0); - - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - assertEq(fetchedEthUsdPrice, latestAnswerEthUsd); - } - - function testFetchPriceReturnsCorrectPriceRETH() public { - (uint256 fetchedRethUsdPrice,) = rethPriceFeed.fetchPrice(); - assertGt(fetchedRethUsdPrice, 0); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 rate = rethToken.getExchangeRate(); - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(fetchedRethUsdPrice, expectedPrice); - } - - function testFetchPriceReturnsCorrectPriceWSTETH() public { - (uint256 fetchedStethUsdPrice,) = wstethPriceFeed.fetchPrice(); - assertGt(fetchedStethUsdPrice, 0); - - uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 stethWstethExchangeRate = wstETH.stEthPerToken(); - - uint256 expectedFetchedPrice = latestAnswerStethUsd * stethWstethExchangeRate / 1e18; - - assertEq(fetchedStethUsdPrice, expectedFetchedPrice); - } - - // --- Thresholds set at deployment --- - - function testEthUsdStalenessThresholdSetWETH() public view { - (, uint256 storedEthUsdStaleness,) = wethPriceFeed.ethUsdOracle(); - assertEq(storedEthUsdStaleness, _24_HOURS); - } - - function testEthUsdStalenessThresholdSetRETH() public view { - (, uint256 storedEthUsdStaleness,) = rethPriceFeed.ethUsdOracle(); - assertEq(storedEthUsdStaleness, _24_HOURS); - } - - function testRethEthStalenessThresholdSetRETH() public view { - (, uint256 storedRethEthStaleness,) = rethPriceFeed.rEthEthOracle(); - assertEq(storedRethEthStaleness, _48_HOURS); - } - - function testStethUsdStalenessThresholdSetWSTETH() public view { - (, uint256 storedStEthUsdStaleness,) = wstethPriceFeed.stEthUsdOracle(); - assertEq(storedStEthUsdStaleness, _24_HOURS); - } - - // --- LST exchange rates and market price oracle sanity checks --- - - function testRETHExchangeRateBetween1And2() public { - uint256 rate = rethToken.getExchangeRate(); - assertGt(rate, 1e18); - assertLt(rate, 2e18); - } - - function testWSTETHExchangeRateBetween1And2() public { - uint256 rate = wstETH.stEthPerToken(); - assertGt(rate, 1e18); - assertLt(rate, 2e18); - } - - function testRETHOracleAnswerBetween1And2() public { - uint256 answer = _getLatestAnswerFromOracle(rethOracle); - assertGt(answer, 1e18); - assertLt(answer, 2e18); - } - - function testSTETHOracleAnswerWithin1PctOfETHOracleAnswer() public { - uint256 stethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 ethUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 relativeDelta; - - if (stethUsd > ethUsd) { - relativeDelta = (stethUsd - ethUsd) * 1e18 / ethUsd; - } else { - relativeDelta = (ethUsd - stethUsd) * 1e18 / stethUsd; - } - - assertLt(relativeDelta, 1e16); - } - - // // --- Basic actions --- - - function testOpenTroveWETH() public { - uint256 price = _getLatestAnswerFromOracle(ethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - uint256 trovesCount = contractsArray[0].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[0].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - function testOpenTroveRETH() public { - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - uint256 trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - function testOpenTroveWSTETH() public { - uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 wstethStethExchangeRate = wstETH.stEthPerToken(); - - uint256 calcdWstethUsdPrice = latestAnswerStethUsd * wstethStethExchangeRate / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdWstethUsdPrice / 2 / 1e18; - - uint256 trovesCount = contractsArray[2].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[2].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - // --- Oracle manipulation tests --- - - function testManipulatedChainlinkReturnsStalePrice() public { - // Replace the ETH Oracle's code with the mock oracle's code that returns a stale price - etchStaleMockToEthOracle(address(mockOracle).code); - - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - - // Confirm it's stale - assertEq(updatedAt, block.timestamp - 7 days); - } - - function testManipulatedChainlinkReturns2kUsdPrice() public { - // Replace the ETH Oracle's code with the mock oracle's code that returns a stale price - etchStaleMockToEthOracle(address(mockOracle).code); - - uint256 price = _getLatestAnswerFromOracle(ethOracle); - assertEq(price, 2000e18); - } - - function testOpenTroveWETHWithStalePriceReverts() public { - Vars memory vars; - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[0].borrowerOperations.hasBeenShutDown()); - - vars.price = _getLatestAnswerFromOracle(ethOracle); - vars.coll = 5 ether; - vars.debtRequest = vars.coll * vars.price / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[0].borrowerOperations.openTrove( - A, 0, vars.coll, vars.debtRequest, 0, 0, 5e16, vars.debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveWETHWithStalePriceReverts() public { - uint256 price = _getLatestAnswerFromOracle(ethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[0].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Replace oracle with a stale oracle - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[0].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - function testOpenTroveWSTETHWithStalePriceReverts() public { - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[2].borrowerOperations.hasBeenShutDown()); - - uint256 price = _getLatestAnswerFromOracle(stethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveWSTETHWithStalePriceReverts() public { - uint256 price = _getLatestAnswerFromOracle(stethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[2].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Replace oracle with a stale oracle - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[2].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - function testOpenTroveRETHWithStaleRETHPriceReverts() public { - // Make only RETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[1].borrowerOperations.hasBeenShutDown()); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveRETHWithStaleRETHPriceReverts() public { - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Make only RETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - function testOpenTroveRETHWithStaleETHPriceReverts() public { - // Make only ETH oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - assertFalse(contractsArray[1].borrowerOperations.hasBeenShutDown()); - - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - } - - function testAdjustTroveRETHWithStaleETHPriceReverts() public { - uint256 latestAnswerREthEth = _getLatestAnswerFromOracle(rethOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - uint256 calcdRethUsdPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; - - vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // confirm Trove was opened - uint256 trovesCount = contractsArray[1].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - - // Make only ETH oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // // Try to adjust Trove - vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - contractsArray[1].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); - } - - // --- WETH shutdown --- - - function testWETHPriceFeedShutsDownWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(ethUsdFailed); - - // Check branch is live, not shut down - assertEq(contractsArray[0].troveManager.shutdownTime(), 0); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, ethUsdFailed) = wethPriceFeed.fetchPrice(); - - // Check oracle call failed this time - assertTrue(ethUsdFailed); - - // Confirm the branch is now shutdown - assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); - } - - function testWETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { - // Fetch price - wethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - // Confirm the lastGoodPrice is not coincidentally equal to the mock oracle's price - assertNotEq(lastGoodPrice1, uint256(mockPrice)); - - // Fetch price again - (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); - - // Check oracle call failed this time - assertTrue(ethUsdFailed); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1, "current price != lastGoodPrice"); - - // Confirm the stored lastGoodPrice has not changed - assertEq(wethPriceFeed.lastGoodPrice(), lastGoodPrice1, "lastGoodPrice not same"); - } - - // --- RETH shutdown --- - - function testRETHPriceFeedShutsDownWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - } - - function testRETHPriceFeedShutsDownWhenExchangeRateFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the exchange rate 0 - etchMockToRethToken(); - uint256 rate = rethToken.getExchangeRate(); - assertEq(rate, 0, "rate not zero"); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check a call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); - } - - function testRETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testRETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the exchange rate 0 - etchMockToRethToken(); - uint256 rate = rethToken.getExchangeRate(); - assertEq(rate, 0); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testRETHPriceSourceIsLastGoodPriceWhenETHUSDFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - assertTrue(oracleFailedWhileBranchLive); - - // Check using lastGoodPrice - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - function testRETHPriceFeedShutsDownWhenRETHETHOracleFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - } - - function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenRETHETHOracleFails() public { - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Calc expected price i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - - uint256 expectedPrice = LiquityMath._min(rethPriceFeed.lastGoodPrice(), ethUsdPrice * exchangeRate / 1e18); - - assertEq(price, expectedPrice, "price not expected price"); - } - - function testRETHPriceSourceIsETHUSDxCanonicalWhenRETHETHFails() public { - // Fetch price - rethPriceFeed.fetchPrice(); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - assertTrue(oracleFailedWhileBranchLive); - - // Check using canonical - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - } - - function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { - // Make the RETH-USD oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); - - // Check using ETHUSDxCanonical - assertEq( - uint8(rethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), - "not using ethusdxcanonical" - ); - - uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - // These should differ since the mock oracle's price should not equal the previous real price - assertNotEq(priceIfDidntFail, lastGoodPrice, "price if didnt fail == lastGoodPrice"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - } - - function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { - // Make the RETH-USD oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); - - // Check using ETHUSDxCanonical - assertEq( - uint8(rethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), - "not using ethusdxcanonical" - ); - - uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - - // Make the exchange rate return 0 - etchMockToRethToken(); - uint256 rate = rethToken.getExchangeRate(); - assertEq(rate, 0, "mock rate non-zero"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - // Check we've switched to lastGoodPrice source - assertEq( - uint8(rethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice), - "not using lastGoodPrice" - ); - } - - function testRETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - // Make lastGoodPrice tiny, and below ETHUSDxCanonical - vm.store( - address(rethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1)) // make lastGoodPrice equal to 1 wei - ); - assertEq(rethPriceFeed.lastGoodPrice(), 1); - - // Fetch the price again - (price,) = rethPriceFeed.fetchPrice(); - - // Check price was lastGoodPrice - assertEq(price, rethPriceFeed.lastGoodPrice()); - - // Now make lastGoodPrice massive, and greater than ETHUSDxCanonical - vm.store( - address(rethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1e27)) // make lastGoodPrice equal to 1e27 i.e. 1 billion (with 18 decimal digits) - ); - assertEq(rethPriceFeed.lastGoodPrice(), 1e27); - - // Fetch the price again - (price,) = rethPriceFeed.fetchPrice(); - - // Check price is expected ETH-USDxCanonical - // Calc expected price if didnt fail, i.e. - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rethToken.getExchangeRate(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - assertEq(price, priceIfDidntFail, "price not equal expected"); - } - - function testRETHPriceFeedShutsDownWhenBothOraclesFail() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - } - - function testRETHPriceFeedReturnsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the RETH-ETH oracle stale too - etchStaleMockToRethOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testRETHPriceSourceIsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the RETH-ETH oracle stale too - etchStaleMockToRethOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - rethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - // --- WSTETH shutdown --- - - function testWSTETHPriceFeedShutsDownWhenExchangeRateFails() public { - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - assertGt(price, 0); - - // Check oracle call didn't fail - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[1].troveManager.shutdownTime(), 0); - - // Make the exchange rate 0 - vm.etch(address(wstETH), address(mockWstethToken).code); - uint256 rate = wstETH.stEthPerToken(); - assertEq(rate, 0); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check a call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the branch is now shutdown - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); - } - - function testWSTETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the exchange rate 0 - vm.etch(address(wstETH), address(mockWstethToken).code); - uint256 rate = wstETH.stEthPerToken(); - assertEq(rate, 0); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check a call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testWSTETHPriceSourceIsLastGoodPricePriceWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price1,) = wstethPriceFeed.fetchPrice(); - assertGt(price1, 0, "price is 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check ncall failed - assertTrue(oracleFailedWhileBranchLive); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - function testWSTETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { - // Fetch price - (uint256 price1,) = wstethPriceFeed.fetchPrice(); - assertGt(price1, 0, "price is 0"); - - uint256 lastGoodPriceBeforeFail = wstethPriceFeed.lastGoodPrice(); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - assertGt(mockPrice, 0, "mockPrice 0"); - - // Fetch price again - (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check oracle failed in this call - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the stored lastGoodPrice - assertEq(price2, lastGoodPriceBeforeFail); - // Confirm the stored last good price didn't change - assertEq(lastGoodPriceBeforeFail, wstethPriceFeed.lastGoodPrice()); - } - - function testWSTETHPriceDoesShutsDownWhenETHUSDOracleFails() public { - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc did fail - assertTrue(oracleFailedWhileBranchLive); - - // Confirm branch is shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testWSTETHPriceShutdownWhenSTETHUSDOracleFails() public { - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that this time the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Confirm branch is now shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenSTETHUSDOracleFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Calc expected price i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - - uint256 expectedPrice = LiquityMath._min(wstethPriceFeed.lastGoodPrice(), ethUsdPrice * exchangeRate / 1e18); - - assertEq(price, expectedPrice, "price not expected price"); - } - - function testSTETHPriceSourceIsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - } - - function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - // These should differ since the mock oracle's price should not equal the previous real price - assertNotEq(priceIfDidntFail, lastGoodPrice, "price if didnt fail == lastGoodPrice"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - } - - function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq( - uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary" - ); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); - - // Check using ETHUSDxCanonical - assertEq( - uint8(wstethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), - "not using ethusdxcanonical" - ); - - uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); - - // Make the exchange rate return 0 - vm.etch(address(wstETH), address(mockWstethToken).code); - uint256 rate = wstETH.stEthPerToken(); - assertEq(rate, 0, "mock rate non-zero"); - - // Now fetch the price - (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // This should be false, since the branch is already shutdown and not live - assertFalse(oracleFailedWhileBranchLive); - - // Confirm the returned price is the last good price - assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); - // Check we've switched to lastGoodPrice source - assertEq( - uint8(wstethPriceFeed.priceSource()), - uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice), - "not using lastGoodPrice" - ); - } - - function testSTETHWhenUsingETHUSDxCanonicalRemainsShutDownWhenETHUSDOracleFails() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - // Check branch is now shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Now fetch the price again - (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - - // Check branch is still down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testSTETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Check using ETHUSDxCanonical - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); - - // Make lastGoodPrice tiny, and below ETHUSDxCanonical - vm.store( - address(wstethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1)) // make lastGoodPrice equal to 1 wei - ); - assertEq(wstethPriceFeed.lastGoodPrice(), 1); - - // Fetch the price again - (price,) = wstethPriceFeed.fetchPrice(); - - // Check price was lastGoodPrice - assertEq(price, wstethPriceFeed.lastGoodPrice()); - - // Now make lastGoodPrice massive, and greater than ETHUSDxCanonical - vm.store( - address(wstethPriceFeed), - bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored - bytes32(uint256(1e27)) // make lastGoodPrice equal to 1e27 i.e. 1 billion (with 18 decimal digits) - ); - assertEq(wstethPriceFeed.lastGoodPrice(), 1e27); - - // Fetch the price again - (price,) = wstethPriceFeed.fetchPrice(); - - // Check price is expected ETH-USDxCanonical - // Calc expected price if didnt fail, i.e. - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - - assertEq(price, priceIfDidntFail); - } - - function testWSTETHPriceShutdownWhenBothOraclesFail() public { - // Fetch price - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); - - // Check branch is live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check that this time the primary calc oracle did fail - assertTrue(oracleFailedWhileBranchLive); - - // Confirm branch is now shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - } - - function testWSTETHPriceFeedReturnsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time - assertTrue(oracleFailedWhileBranchLive); - - // Confirm the PriceFeed's returned price equals the lastGoodPrice - assertEq(price, lastGoodPrice1); - - // Confirm the stored lastGoodPrice has not changed - assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); - } - - function testWSTETHPriceSourceIsLastGoodPriceWhenBothOraclesFail() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Make the ETH-USD oracle stale too - etchStaleMockToEthOracle(address(mockOracle).code); - (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - wstethPriceFeed.fetchPrice(); - - // Check using lastGoodPrice - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - } - - // --- redemptions --- - - function testNormalWETHRedemptionDoesNotHitShutdownBranch() public { - // Fetch price - wethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Make the ETH-USD oracle stale - etchStaleMockToEthOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = wethPriceFeed.fetchPrice(); - assertTrue(oracleFailedWhileBranchLive); - // Confirm branch shutdown - assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); - - uint256 totalBoldRedeemAmount = 100e18; - uint256 branch0DebtBefore = contractsArray[0].activePool.getBoldDebt(); - assertGt(branch0DebtBefore, 0); - - uint256 boldBalBefore_A = boldToken.balanceOf(A); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm A lost no BOLD - assertEq(boldToken.balanceOf(A), boldBalBefore_A); - - // Confirm WETH branch did not get redeemed from - assertEq(contractsArray[0].activePool.getBoldDebt(), branch0DebtBefore); - } - - function testNormalRETHRedemptionDoesNotHitShutdownBranch() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Make the RETH-ETH oracle stale - etchStaleMockToRethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - assertTrue(oracleFailedWhileBranchLive); - // Confirm RETH branch shutdown - assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); - - uint256 totalBoldRedeemAmount = 100e18; - uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); - assertGt(branch1DebtBefore, 0); - - uint256 boldBalBefore_A = boldToken.balanceOf(A); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm A lost no BOLD - assertEq(boldToken.balanceOf(A), boldBalBefore_A); - - // Confirm RETH branch did not get redeemed from - assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore); - } - - function testNormalWSTETHRedemptionDoesNotHitShutdownBranch() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Make the STETH-USD oracle stale - etchStaleMockToStethOracle(address(mockOracle).code); - (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); - assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price again - (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - assertTrue(oracleFailedWhileBranchLive); - // Confirm RETH branch shutdown - assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); - - uint256 totalBoldRedeemAmount = 100e18; - uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); - assertGt(branch2DebtBefore, 0); - - uint256 boldBalBefore_A = boldToken.balanceOf(A); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm A lost no BOLD - assertEq(boldToken.balanceOf(A), boldBalBefore_A); - - // Confirm RETH branch did not get redeemed from - assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore); - } - - function testRedemptionOfWETHUsesETHUSDMarketforPrimaryPrice() public { - // Fetch price - wethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Expected price used for primary calc: ETH-USD market price - uint256 expectedPrice = _getLatestAnswerFromOracle(ethOracle); - assertGt(expectedPrice, 0); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0); - - uint256 branch0DebtBefore = contractsArray[0].activePool.getBoldDebt(); - assertGt(branch0DebtBefore, 0); - uint256 A_collBefore = contractsArray[0].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm WETH branch got redeemed from - assertEq(contractsArray[0].activePool.getBoldDebt(), branch0DebtBefore - totalBoldRedeemAmount); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[0].collToken.balanceOf(A), A_collBefore + expectedCollDelta); - } - - function testRedemptionOfWSTETHUsesMaxETHUSDMarketandWSTETHUSDMarketForPrimaryPriceWhenWithin1pct() public { - // Fetch price - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Expected price used for primary calc: ETH-USD market price - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); - assertNotEq(ethUsdPrice, stethUsdPrice, "raw prices equal"); - // Check STETH-USD is within 1ct of ETH-USD - uint256 max = (1e18 + 1e16) * ethUsdPrice / 1e18; - uint256 min = (1e18 - 1e16) * ethUsdPrice / 1e18; - assertGe(stethUsdPrice, min); - assertLe(stethUsdPrice, max); - - // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH - uint256 expectedPrice = LiquityMath._max(ethUsdPrice, stethUsdPrice) * wstETH.stEthPerToken() / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); - assertGt(branch2DebtBefore, 0); - uint256 A_collBefore = contractsArray[2].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm WSTETH branch got redeemed from - assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore - totalBoldRedeemAmount); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta); - } - - function testRedemptionOfWSTETHUsesMinETHUSDMarketandWSTETHUSDMarketForPrimaryPriceWhenNotWithin1pct() public { - // Fetch price - console.log("test::first wsteth pricefeed call"); - wstethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[2].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Get the raw ETH-USD price (at 8 decimals) for comparison - (, int256 rawEthUsdPrice,,,) = ethOracle.latestRoundData(); - assertGt(rawEthUsdPrice, 0, "eth-usd price not 0"); - - // Replace the STETH-USD Oracle's code with the mock oracle's code - etchStaleMockToStethOracle(address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); - // Reduce STETH-USD price to 90% of ETH-USD price. Use 8 decimal precision on the oracle. - mock.setPrice(int256(rawEthUsdPrice * 90e6 / 1e8)); - // Make it fresh - mock.setUpdatedAt(block.timestamp); - // STETH-USD price has 8 decimals - mock.setDecimals(8); - - assertEq(contractsArray[2].troveManager.shutdownTime(), 0, "is shutdown"); - - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); - console.log(stethUsdPrice, "test stehUsdPrice after replacement"); - console.log(ethUsdPrice, "test ethUsdPrice after replacement"); - console.log(ethUsdPrice * 90e16 / 1e18, "test ethUsdPrice * 90e16 / 1e18"); - - // Confirm that STETH-USD is lower than ETH-USD - assertLt(stethUsdPrice, ethUsdPrice, "steth-usd not < eth-usd"); - - // USD_per_STETH = USD_per_STETH * STETH_per_WSTETH - // Use STETH-USD as expected price since it is out of range of ETH-USD - uint256 expectedPrice = stethUsdPrice * wstETH.stEthPerToken() / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); - assertGt(branch2DebtBefore, 0); - uint256 A_collBefore = contractsArray[2].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - assertEq(contractsArray[2].troveManager.shutdownTime(), 0, "is shutdown"); - - // Confirm WSTETH branch got redeemed from - assertEq( - contractsArray[2].activePool.getBoldDebt(), - branch2DebtBefore - totalBoldRedeemAmount, - "remaining branch debt wrong" - ); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq( - contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "remaining branch coll wrong" - ); - } - - function testRedemptionOfRETHUsesMaxCanonicalAndMarketforPrimaryPriceWhenWithin2pct() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - // Expected price used for primary calc: ETH-USD market price - uint256 canonicalRethRate = rethToken.getExchangeRate(); - uint256 marketRethPrice = _getLatestAnswerFromOracle(rethOracle); - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - assertNotEq(canonicalRethRate, marketRethPrice, "raw price and rate equal"); - - // Check market is within 2pct of max; - uint256 max = (1e18 + 2e16) * canonicalRethRate / 1e18; - uint256 min = (1e18 - 2e16) * canonicalRethRate / 1e18; - assertGe(marketRethPrice, min); - assertLe(marketRethPrice, max); - - // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH - uint256 expectedPrice = LiquityMath._max(canonicalRethRate, marketRethPrice) * ethUsdPrice / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); - assertGt(branch1DebtBefore, 0); - uint256 A_collBefore = contractsArray[1].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm RETH branch got redeemed from - assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore - totalBoldRedeemAmount); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta); - } - - function testRedemptionOfRETHUsesMinCanonicalAndMarketforPrimaryPriceWhenDeviationGreaterThan2pct() public { - // Fetch price - rethPriceFeed.fetchPrice(); - uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); - assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - - // Check using primary - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - - uint256 coll = 100 ether; - uint256 debtRequest = 3000e18; - - vm.startPrank(A); - contractsArray[1].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - vm.stopPrank(); - - // Replace the RETH Oracle's code with the mock oracle's code - etchStaleMockToRethOracle(address(mockOracle).code); - ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - // Set ETH_per_RETH market price to 0.95 - mock.setPrice(95e16); - // Make it fresh - mock.setUpdatedAt(block.timestamp); - // RETH-ETH price has 18 decimals - mock.setDecimals(18); - - (, int256 price,,,) = rethOracle.latestRoundData(); - // Confirm that RETH oracle now returns the artificial low price - assertEq(price, 95e16, "reth-eth price not 0.95"); - - // // Expected price used for primary calc: ETH-USD market price - uint256 canonicalRethRate = rethToken.getExchangeRate(); - uint256 marketRethPrice = _getLatestAnswerFromOracle(rethOracle); - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - assertNotEq(canonicalRethRate, marketRethPrice, "raw price and rate equal"); - - // Check market is not within 2pct of canonical - uint256 min = (1e18 - 2e16) * canonicalRethRate / 1e18; - assertLe(marketRethPrice, min, "market reth-eth price not < min"); - - // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH - uint256 expectedPrice = LiquityMath._min(canonicalRethRate, marketRethPrice) * ethUsdPrice / 1e18; - assertGt(expectedPrice, 0, "expected price not 0"); - - // Calc expected fee based on price, i.e. the minimum - uint256 totalBoldRedeemAmount = 100e18; - uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; - assertGt(totalCorrespondingColl, 0, "coll not 0"); - - uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) - * DECIMAL_PRECISION / totalBoldRedeemAmount; - assertGt(redemptionFeePct, 0, "fee not 0"); - - uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; - - uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; - assertGt(expectedCollDelta, 0, "delta not 0"); - - uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); - assertGt(branch1DebtBefore, 0); - uint256 A_collBefore = contractsArray[1].collToken.balanceOf(A); - assertGt(A_collBefore, 0); - - // Redeem - redeem(A, totalBoldRedeemAmount); - - // Confirm RETH branch got redeemed from - assertEq( - contractsArray[1].activePool.getBoldDebt(), - branch1DebtBefore - totalBoldRedeemAmount, - "active debt != branch - redeemed" - ); - - // Confirm the received amount coll is the expected amount (i.e. used the expected price) - assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "A's coll didn't change"); - } - - // --- Low gas market oracle reverts --- - - function testRevertLowGasSTETHOracle() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the oracle - etchGasGuzzlerToStethOracle(address(gasGuzzlerOracle).code); - - // After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertAsExpected,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertAsExpected); - } - - function testRevertLowGasRETHOracle() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the oracle - etchGasGuzzlerToRethOracle(address(gasGuzzlerOracle).code); - - // After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertAsExpected); - } - - function testRevertLowGasETHOracle() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the oracle - etchGasGuzzlerToEthOracle(address(gasGuzzlerOracle).code); - - // After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertAsExpected,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertAsExpected); - } - - // --- Test with a gas guzzler token, and confirm revert --- - - function testRevertLowGasWSTETHToken() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the LST - etchGasGuzzlerMockToWstethToken(address(gasGuzzlerToken).code); - - // After etching the gas guzzler to the LST, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertsAsExpected,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertsAsExpected); - } - - function testRevertLowGasRETHToken() public { - // Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k - (bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(success); - - // Etch gas guzzler to the LST - etchGasGuzzlerMockToRethToken(address(gasGuzzlerToken).code); - - // After etching the gas guzzler to the LST, confirm the same call with 500k gas now reverts due to OOG - vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - (bool revertsAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); - assertTrue(revertsAsExpected); - } - - /* - function testTMCodeSize() public { - uint256 codeSize = address(contractsArray[0].troveManager).code.length; - uint256 left = 24576 - codeSize; - console.log(codeSize, "TM contract size"); - console.log(left, "space left in TM"); - } - */ - - function testRETHRedemptionOnlyHitsTrovesAtICRGte100() public { - Vars memory vars; - // Set two mock market oracles: RETH-ETH, and ETH-USD - ChainlinkOracleMock mockRETHOracle = etchMockToRethOracle(); - ChainlinkOracleMock mockETHOracle = etchMockToEthOracle(); - - vars.usdPerEthMarket = 2000e8; // 2000 usd, 8 decimal - // Set 1 ETH = 2000 USD on market oracle - mockETHOracle.setPrice(vars.usdPerEthMarket); - - vars.ethPerRethLST = rethToken.getExchangeRate(); - - // Make market RETH price 1% lower than LST exchange rate - vars.ethPerRethMarket = int256(vars.ethPerRethLST) * 99 / 100; - mockRETHOracle.setPrice(vars.ethPerRethMarket); - - console.log(_getLatestAnswerFromOracle(rethOracle), "reth oracle latest answer"); - console.log(_getLatestAnswerFromOracle(ethOracle), "eth oracle latest answer"); - - // Open annchor Trove with high CR and 51m BOLD - vm.startPrank(A); - vars.troveId_A = contractsArray[1].borrowerOperations.openTrove( - A, 0, 1000_000 ether, 51_000_000e18, 0, 0, 5e16, 51_000_000e18, address(0), address(0), address(0) - ); - vm.stopPrank(); - - // Get the calculated RETH-USD price directly from the system - (vars.systemPrice,) = contractsArray[1].priceFeed.fetchPrice(); - - // Open 3 Troves with ICRs clustered together - vars.coll = 10 ether; - vars.debt_B = 10000e18 + 1e18; - vars.debt_C = 10000e18; - vars.debt_D = 10000e18 - 1e18; - - vm.startPrank(B); - vars.troveId_B = contractsArray[1].borrowerOperations.openTrove( - B, 0, vars.coll, vars.debt_B, 0, 0, 5e15, vars.debt_B, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(C); - vars.troveId_C = contractsArray[1].borrowerOperations.openTrove( - C, 0, vars.coll, vars.debt_C, 0, 0, 5e15, vars.debt_C, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(D); - vars.troveId_D = contractsArray[1].borrowerOperations.openTrove( - D, 0, vars.coll, vars.debt_D, 0, 0, 5e15, vars.debt_D, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vars.ICR_C = contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice); - - // Check ICRs are clustered - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.systemPrice), "A ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.systemPrice), "A ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_B, vars.systemPrice), "B ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice), "C ICR"); - // console.log(contractsArray[1].troveManager.getCurrentICR(vars.troveId_D, vars.systemPrice), "D ICR"); - - // Scale the price down such that C has ICR ~100%, ceil division - vars.newEthPrice = vars.usdPerEthMarket * 1e18 / int256(vars.ICR_C) + 1; - mockETHOracle.setPrice(vars.newEthPrice); - - // Get new system price from PriceFeed - (vars.newSystemPrice,) = contractsArray[1].priceFeed.fetchPrice(); - // Calculate the new redemption price, given RETH-ETH market price is 1% greater than exchange rate - vars.newSystemRedemptionPrice = vars.newSystemPrice * 100 / 99; - - // Confirm ICR ordering - vars.ICR_A = contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemPrice); - vars.ICR_B = contractsArray[1].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemPrice); - vars.ICR_C = contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemPrice); - vars.ICR_D = contractsArray[1].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemPrice); - - // console.log(vars.ICR_A, "A ICR after price drop"); - // console.log(vars.ICR_B, "B ICR after price drop"); - // console.log(vars.ICR_C, "C ICR after price drop"); - // console.log(vars.ICR_D, "D ICR after price drop"); - - assertLt(vars.ICR_B, 1e18, "B ICR not < 100%"); - assertGt(vars.ICR_C, 1e18, "C ICR not > 100%"); - assertGt(vars.ICR_D, 1e18, "D ICR not > 100%"); - assertGt(vars.ICR_A, vars.ICR_D, "A ICR not > D ICR"); - - // TODO: Confirm that if we used the *redemption* price to calc ICRs, all ICRs would be > 100% - vars.redemptionICR_A = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemRedemptionPrice); - vars.redemptionICR_B = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemRedemptionPrice); - vars.redemptionICR_C = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemRedemptionPrice); - vars.redemptionICR_D = - contractsArray[1].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemRedemptionPrice); - - // console.log(vars.redemptionICR_A, " vars.redemptionICR_A"); - // console.log(vars.redemptionICR_B, " vars.redemptionICR_B"); - // console.log(vars.redemptionICR_C, " vars.redemptionICR_C"); - // console.log(vars.redemptionICR_D, " vars.redemptionICR_D"); - - assertGe(vars.redemptionICR_A, 1e18, "A ICR not > 100%"); - assertGe(vars.redemptionICR_B, 1e18, "B ICR not > 100%"); - assertGe(vars.redemptionICR_C, 1e18, "C ICR not > 100%"); - assertGe(vars.redemptionICR_D, 1e18, "D ICR not > 100%"); - - // A deposits 25m to WETH and STETH SPs, making them fully backed - - // so A's redemption should now fully hits the RETH branch - vm.startPrank(A); - contractsArray[0].stabilityPool.provideToSP(25_000_000e18, false); - contractsArray[2].stabilityPool.provideToSP(25_000_000e18, false); - - vars.troveDataBefore_A = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataBefore_B = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataBefore_C = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataBefore_D = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_D); - - // A redeems. Expect redemption to hit Troves C, D, A and skip B - collateralRegistry.redeemCollateral(50000e18, 100, 1e18); - - vars.troveDataAfter_A = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataAfter_B = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataAfter_C = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataAfter_D = contractsArray[1].troveManager.getLatestTroveData(vars.troveId_D); - - // Expect B's Trove to be untouched - assertEq(vars.troveDataAfter_B.entireDebt, vars.troveDataBefore_B.entireDebt, "B's debt not same after redeem"); - assertEq(vars.troveDataAfter_B.entireColl, vars.troveDataBefore_B.entireColl, "B's coll not same after redeem"); - - // Expect A, C and D to have been redeemed - assertLt(vars.troveDataAfter_A.entireDebt, vars.troveDataBefore_A.entireDebt, "A's debt not lower after redeem"); - assertLt(vars.troveDataAfter_A.entireColl, vars.troveDataBefore_A.entireColl, "A's coll not lower after redeem"); - // console.log(vars.troveDataAfter_A.entireDebt, "A after"); - // console.log(vars.troveDataBefore_A.entireDebt, "A before"); - assertLt(vars.troveDataAfter_C.entireDebt, vars.troveDataBefore_C.entireDebt, "C's debt not lower after redeem"); - assertLt(vars.troveDataAfter_C.entireColl, vars.troveDataBefore_C.entireColl, "C's coll not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireDebt, vars.troveDataBefore_D.entireDebt, "D's debt not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireColl, vars.troveDataBefore_D.entireColl, "D's coll not lower after redeem"); - - // console.log(vars.troveDataAfter_D.entireDebt, "D after"); - // console.log(vars.troveDataBefore_D.entireDebt, "D before"); - } - - function testSTETHRedemptionOnlyHitsTrovesAtICRGte100() public { - Vars memory vars; - // Set two mock market oracles: STETH-USD, and ETH-USD - ChainlinkOracleMock mockSTETHOracle = etchMockToStethOracle(); - ChainlinkOracleMock mockETHOracle = etchMockToEthOracle(); - - vars.usdPerEthMarket = 2000e8; // 2000 usd, 8 decimal - // Set 1 ETH = 2000 USD on market oracle - mockETHOracle.setPrice(vars.usdPerEthMarket); - - // Make market STETH-USD price 0.5% greater than market ETH-USD price - mockSTETHOracle.setPrice(vars.usdPerEthMarket * 1005 / 1000); - - console.log(_getLatestAnswerFromOracle(stethOracle), "steth oracle latest answer"); - console.log(_getLatestAnswerFromOracle(ethOracle), "eth oracle latest answer"); - - // Open anchor Trove with high CR and 51m BOLD - vm.startPrank(A); - vars.troveId_A = contractsArray[2].borrowerOperations.openTrove( - A, 0, 1000_000 ether, 51_000_000e18, 0, 0, 5e16, 51_000_000e18, address(0), address(0), address(0) - ); - vm.stopPrank(); - - // Get the calculated WSTETH-USD price directly from the system - (vars.systemPrice,) = contractsArray[2].priceFeed.fetchPrice(); - - // Open 3 Troves with ICRs clustered together - vars.coll = 10 ether; - vars.debt_B = 10000e18 + 1e18; - vars.debt_C = 10000e18; - vars.debt_D = 10000e18 - 1e18; - - vm.startPrank(B); - vars.troveId_B = contractsArray[2].borrowerOperations.openTrove( - B, 0, vars.coll, vars.debt_B, 0, 0, 5e15, vars.debt_B, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(C); - vars.troveId_C = contractsArray[2].borrowerOperations.openTrove( - C, 0, vars.coll, vars.debt_C, 0, 0, 5e15, vars.debt_C, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vm.startPrank(D); - vars.troveId_D = contractsArray[2].borrowerOperations.openTrove( - D, 0, vars.coll, vars.debt_D, 0, 0, 5e15, vars.debt_D, address(0), address(0), address(0) - ); - vm.stopPrank(); - - vars.ICR_C = contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice); - - // Check ICRs are clustered - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_A, vars.systemPrice), "A ICR"); - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_B, vars.systemPrice), "B ICR"); - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.systemPrice), "C ICR"); - console.log(contractsArray[2].troveManager.getCurrentICR(vars.troveId_D, vars.systemPrice), "D ICR"); - - // Scale the ETH-USD price down such that C has ICR ~100% - vars.newEthPrice = vars.usdPerEthMarket * 1e18 / int256(vars.ICR_C) + 10; - mockETHOracle.setPrice(vars.newEthPrice); - // Scale STETH-USD price down too, keep it 0.5% above ETH-USD - mockSTETHOracle.setPrice(vars.newEthPrice * 1005 / 1000); - - // Get new system price from PriceFeed - (vars.newSystemPrice,) = contractsArray[2].priceFeed.fetchPrice(); - vars.newSystemRedemptionPrice = vars.newSystemPrice * 1005 / 1000; - - // console.log(_getLatestAnswerFromOracle(stethOracle), "steth oracle latest answer after price drop"); - // console.log(_getLatestAnswerFromOracle(ethOracle), "eth oracle latest answer after price drop"); - - // Confirm ICR ordering - vars.ICR_A = contractsArray[2].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemPrice); - vars.ICR_B = contractsArray[2].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemPrice); - vars.ICR_C = contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemPrice); - vars.ICR_D = contractsArray[2].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemPrice); - - // console.log(vars.ICR_A, "A ICR after price drop"); - // console.log(vars.ICR_B, "B ICR after price drop"); - // console.log(vars.ICR_C, "C ICR after price drop"); - // console.log(vars.ICR_D, "D ICR after price drop"); - - assertLt(vars.ICR_B, 1e18, "B ICR not < 100%"); - assertGt(vars.ICR_C, 1e18, "C ICR not > 100%"); - assertGt(vars.ICR_D, 1e18, "D ICR not > 100%"); - assertGt(vars.ICR_A, vars.ICR_D, "A ICR not > D ICR"); - - // TODO: Confirm that if we used the *redemption* price to calc ICRs, all ICRs would be > 100% - vars.redemptionICR_A = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_A, vars.newSystemRedemptionPrice); - vars.redemptionICR_B = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_B, vars.newSystemRedemptionPrice); - vars.redemptionICR_C = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_C, vars.newSystemRedemptionPrice); - vars.redemptionICR_D = - contractsArray[2].troveManager.getCurrentICR(vars.troveId_D, vars.newSystemRedemptionPrice); - - // console.log(vars.redemptionICR_A, " vars.redemptionICR_A"); - // console.log(vars.redemptionICR_B, " vars.redemptionICR_B"); - // console.log(vars.redemptionICR_C, " vars.redemptionICR_C"); - // console.log(vars.redemptionICR_D, " vars.redemptionICR_D"); - - assertGe(vars.redemptionICR_A, 1e18, "A ICR not > 100%"); - assertGe(vars.redemptionICR_B, 1e18, "B ICR not > 100%"); - assertGe(vars.redemptionICR_C, 1e18, "C ICR not > 100%"); - assertGe(vars.redemptionICR_D, 1e18, "D ICR not > 100%"); - - // A deposits 25m to WETH and RETH SPs, making them fully backed - - // so A's redemption should now fully hit the STETH branch - vm.startPrank(A); - contractsArray[0].stabilityPool.provideToSP(25_000_000e18, false); - contractsArray[1].stabilityPool.provideToSP(25_000_000e18, false); - - vars.troveDataBefore_A = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataBefore_B = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataBefore_C = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataBefore_D = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_D); - - // A redeems. Expect redemption to hit Troves C, D, A and skip B - collateralRegistry.redeemCollateral(50000e18, 100, 1e18); - - vars.troveDataAfter_A = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_A); - vars.troveDataAfter_B = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_B); - vars.troveDataAfter_C = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_C); - vars.troveDataAfter_D = contractsArray[2].troveManager.getLatestTroveData(vars.troveId_D); - - // Expect B's Trove to be untouched - assertEq(vars.troveDataAfter_B.entireDebt, vars.troveDataBefore_B.entireDebt, "B's debt not same after redeem"); - assertEq(vars.troveDataAfter_B.entireColl, vars.troveDataBefore_B.entireColl, "B's coll not same after redeem"); - - // Expect A, C and D to have been redeemed - assertLt(vars.troveDataAfter_A.entireDebt, vars.troveDataBefore_A.entireDebt, "A's debt not lower after redeem"); - assertLt(vars.troveDataAfter_A.entireColl, vars.troveDataBefore_A.entireColl, "A's coll not lower after redeem"); - // console.log(vars.troveDataAfter_A.entireDebt, "A after"); - // console.log(vars.troveDataBefore_A.entireDebt, "A before"); - assertLt(vars.troveDataAfter_C.entireDebt, vars.troveDataBefore_C.entireDebt, "C's debt not lower after redeem"); - assertLt(vars.troveDataAfter_C.entireColl, vars.troveDataBefore_C.entireColl, "C's coll not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireDebt, vars.troveDataBefore_D.entireDebt, "D's debt not lower after redeem"); - assertLt(vars.troveDataAfter_D.entireColl, vars.troveDataBefore_D.entireColl, "D's coll not lower after redeem"); - // console.log(vars.troveDataAfter_D.entireDebt, "D after"); - // console.log(vars.troveDataBefore_D.entireDebt, "D before"); - } - - // - More basic actions tests (adjust, close, etc) - // - liq tests (manipulate aggregator stored price) -} diff --git a/contracts/test/SPInvariants.t.sol b/contracts/test/SPInvariants.t.sol index 310401b97..d84346864 100644 --- a/contracts/test/SPInvariants.t.sol +++ b/contracts/test/SPInvariants.t.sol @@ -17,7 +17,7 @@ abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { super.setUp(); TestDeployer deployer = new TestDeployer(); - (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,,) = + (TestDeployer.LiquityContractsDev memory contracts,, IBoldToken boldToken, HintHelpers hintHelpers,,) = deployer.deployAndConnectContracts(); stabilityPool = contracts.stabilityPool; @@ -29,7 +29,8 @@ abstract contract SPInvariantsBase is Assertions, BaseInvariantTest { priceFeed: contracts.priceFeed, stabilityPool: contracts.stabilityPool, troveManager: contracts.troveManager, - collSurplusPool: contracts.pools.collSurplusPool + collSurplusPool: contracts.pools.collSurplusPool, + systemParams: contracts.systemParams }), hintHelpers ); diff --git a/contracts/test/SortedTroves.t.sol b/contracts/test/SortedTroves.t.sol index d5bd58caa..a961c7c17 100644 --- a/contracts/test/SortedTroves.t.sol +++ b/contracts/test/SortedTroves.t.sol @@ -470,7 +470,7 @@ contract SortedTrovesTest is Test { function setUp() public { bytes32 SALT = keccak256("LiquityV2"); AddressesRegistry addressesRegistry = - new AddressesRegistry(address(this), 150e16, 110e16, 10e16, 110e16, 5e16, 10e16); + new AddressesRegistry(address(this)); bytes32 hash = keccak256( abi.encodePacked( bytes1(0xff), diff --git a/contracts/test/TestContracts/BaseMultiCollateralTest.sol b/contracts/test/TestContracts/BaseMultiCollateralTest.sol index b7c1b1d48..36f4eafb7 100644 --- a/contracts/test/TestContracts/BaseMultiCollateralTest.sol +++ b/contracts/test/TestContracts/BaseMultiCollateralTest.sol @@ -6,7 +6,9 @@ import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; import {ICollateralRegistry} from "src/Interfaces/ICollateralRegistry.sol"; import {IWETH} from "src/Interfaces/IWETH.sol"; import {HintHelpers} from "src/HintHelpers.sol"; +import {MultiTroveGetter} from "src/MultiTroveGetter.sol"; import {TestDeployer} from "./Deployment.t.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; contract BaseMultiCollateralTest { struct Contracts { @@ -14,7 +16,9 @@ contract BaseMultiCollateralTest { ICollateralRegistry collateralRegistry; IBoldToken boldToken; HintHelpers hintHelpers; + MultiTroveGetter multiTroveGetter; TestDeployer.LiquityContractsDev[] branches; + ISystemParams systemParams; } IERC20 weth; @@ -22,12 +26,14 @@ contract BaseMultiCollateralTest { IBoldToken boldToken; HintHelpers hintHelpers; TestDeployer.LiquityContractsDev[] branches; + ISystemParams systemParams; function setupContracts(Contracts memory contracts) internal { weth = contracts.weth; collateralRegistry = contracts.collateralRegistry; boldToken = contracts.boldToken; hintHelpers = contracts.hintHelpers; + systemParams = contracts.systemParams; for (uint256 i = 0; i < contracts.branches.length; ++i) { branches.push(contracts.branches[i]); diff --git a/contracts/test/TestContracts/BaseTest.sol b/contracts/test/TestContracts/BaseTest.sol index 75708d222..84f1e7361 100644 --- a/contracts/test/TestContracts/BaseTest.sol +++ b/contracts/test/TestContracts/BaseTest.sol @@ -12,13 +12,10 @@ import "src/Interfaces/IStabilityPool.sol"; import "./BorrowerOperationsTester.t.sol"; import "./TroveManagerTester.t.sol"; import "src/Interfaces/ICollateralRegistry.sol"; -import "./PriceFeedTestnet.sol"; +import "./MockFXPriceFeed.sol"; import "src/Interfaces/IInterestRouter.sol"; import "src/GasPool.sol"; import "src/HintHelpers.sol"; -import "src/Zappers/WETHZapper.sol"; -import "src/Zappers/GasCompZapper.sol"; -import "src/Zappers/LeverageLSTZapper.sol"; import {mulDivCeil} from "../Utils/Math.sol"; import {Logging} from "../Utils/Logging.sol"; import {StringFormatting} from "../Utils/StringFormatting.sol"; @@ -35,6 +32,15 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 SCR; uint256 LIQUIDATION_PENALTY_SP; uint256 LIQUIDATION_PENALTY_REDISTRIBUTION; + uint256 MIN_DEBT; + uint256 SP_YIELD_SPLIT; + uint256 MIN_ANNUAL_INTEREST_RATE; + uint256 ETH_GAS_COMPENSATION; + uint256 COLL_GAS_COMPENSATION_DIVISOR; + uint256 MIN_BOLD_IN_SP; + uint256 INITIAL_BASE_RATE; + uint256 REDEMPTION_FEE_FLOOR; + uint256 REDEMPTION_MINUTE_DECAY_FACTOR; // Core contracts IAddressesRegistry addressesRegistry; @@ -49,16 +55,13 @@ contract BaseTest is TestAccounts, Logging, TroveId { IMetadataNFT metadataNFT; IBoldToken boldToken; ICollateralRegistry collateralRegistry; - IPriceFeedTestnet priceFeed; + IMockFXPriceFeed priceFeed; GasPool gasPool; IInterestRouter mockInterestRouter; IERC20 collToken; HintHelpers hintHelpers; IWETH WETH; // used for gas compensation - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - ILeverageZapper leverageZapperCurve; - ILeverageZapper leverageZapperUniV3; + ISystemParams systemParams; // Structs for use in test where we need to bi-pass "stack-too-deep" errors struct ABCDEF { @@ -72,72 +75,119 @@ contract BaseTest is TestAccounts, Logging, TroveId { // --- functions --- - function getTroveEntireColl(uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); + function getTroveEntireColl( + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = troveManager.getLatestTroveData( + _troveId + ); return trove.entireColl; } - function getTroveEntireDebt(uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = troveManager.getLatestTroveData(_troveId); + function getTroveEntireDebt( + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = troveManager.getLatestTroveData( + _troveId + ); return trove.entireDebt; } - function getTroveEntireColl(ITroveManager _troveManager, uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = _troveManager.getLatestTroveData(_troveId); + function getTroveEntireColl( + ITroveManager _troveManager, + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = _troveManager.getLatestTroveData( + _troveId + ); return trove.entireColl; } - function getTroveEntireDebt(ITroveManager _troveManager, uint256 _troveId) internal view returns (uint256) { - LatestTroveData memory trove = _troveManager.getLatestTroveData(_troveId); + function getTroveEntireDebt( + ITroveManager _troveManager, + uint256 _troveId + ) internal view returns (uint256) { + LatestTroveData memory trove = _troveManager.getLatestTroveData( + _troveId + ); return trove.entireDebt; } - function calcInterest(uint256 weightedRecordedDebt, uint256 period) internal pure returns (uint256) { - return weightedRecordedDebt * period / 365 days / DECIMAL_PRECISION; + function calcInterest( + uint256 weightedRecordedDebt, + uint256 period + ) internal pure returns (uint256) { + return (weightedRecordedDebt * period) / 365 days / DECIMAL_PRECISION; } - function calcUpfrontFee(uint256 debt, uint256 avgInterestRate) internal pure returns (uint256) { + function calcUpfrontFee( + uint256 debt, + uint256 avgInterestRate + ) internal view returns (uint256) { return calcInterest(debt * avgInterestRate, UPFRONT_INTEREST_PERIOD); } - function predictOpenTroveUpfrontFee(uint256 borrowedAmount, uint256 interestRate) internal view returns (uint256) { - return hintHelpers.predictOpenTroveUpfrontFee(0, borrowedAmount, interestRate); + function predictOpenTroveUpfrontFee( + uint256 borrowedAmount, + uint256 interestRate + ) internal view returns (uint256) { + return + hintHelpers.predictOpenTroveUpfrontFee( + 0, + borrowedAmount, + interestRate + ); } - function predictAdjustInterestRateUpfrontFee(uint256 troveId, uint256 newInterestRate) - internal - view - returns (uint256) - { - return hintHelpers.predictAdjustInterestRateUpfrontFee(0, troveId, newInterestRate); + function predictAdjustInterestRateUpfrontFee( + uint256 troveId, + uint256 newInterestRate + ) internal view returns (uint256) { + return + hintHelpers.predictAdjustInterestRateUpfrontFee( + 0, + troveId, + newInterestRate + ); } - function forcePredictAdjustInterestRateUpfrontFee(uint256 troveId, uint256 newInterestRate) - internal - view - returns (uint256) - { - return hintHelpers.forcePredictAdjustInterestRateUpfrontFee(0, troveId, newInterestRate); + function forcePredictAdjustInterestRateUpfrontFee( + uint256 troveId, + uint256 newInterestRate + ) internal view returns (uint256) { + return + hintHelpers.forcePredictAdjustInterestRateUpfrontFee( + 0, + troveId, + newInterestRate + ); } - function predictAdjustTroveUpfrontFee(uint256 troveId, uint256 debtIncrease) internal view returns (uint256) { - return hintHelpers.predictAdjustTroveUpfrontFee(0, troveId, debtIncrease); + function predictAdjustTroveUpfrontFee( + uint256 troveId, + uint256 debtIncrease + ) internal view returns (uint256) { + return + hintHelpers.predictAdjustTroveUpfrontFee(0, troveId, debtIncrease); } - function predictJoinBatchInterestRateUpfrontFee(uint256 _troveId, address _batchAddress) - internal - view - returns (uint256) - { - return hintHelpers.predictJoinBatchInterestRateUpfrontFee(0, _troveId, _batchAddress); + function predictJoinBatchInterestRateUpfrontFee( + uint256 _troveId, + address _batchAddress + ) internal view returns (uint256) { + return + hintHelpers.predictJoinBatchInterestRateUpfrontFee( + 0, + _troveId, + _batchAddress + ); } // Quick and dirty binary search instead of Newton's, because it's easier - function findAmountToBorrowWithOpenTrove(uint256 targetDebt, uint256 interestRate) - internal - view - returns (uint256 borrow, uint256 upfrontFee) - { + function findAmountToBorrowWithOpenTrove( + uint256 targetDebt, + uint256 interestRate + ) internal view returns (uint256 borrow, uint256 upfrontFee) { uint256 borrowRight = targetDebt; upfrontFee = predictOpenTroveUpfrontFee(borrowRight, interestRate); uint256 borrowLeft = borrowRight - upfrontFee; @@ -157,11 +207,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { } } - function findAmountToBorrowWithAdjustTrove(uint256 troveId, uint256 targetDebt) - internal - view - returns (uint256 borrow, uint256 upfrontFee) - { + function findAmountToBorrowWithAdjustTrove( + uint256 troveId, + uint256 targetDebt + ) internal view returns (uint256 borrow, uint256 upfrontFee) { uint256 entireDebt = troveManager.getTroveEntireDebt(troveId); assert(targetDebt >= entireDebt); @@ -184,15 +233,25 @@ contract BaseTest is TestAccounts, Logging, TroveId { } } - function getRedeemableDebt(uint256 troveId) internal view returns (uint256) { + function getRedeemableDebt( + uint256 troveId + ) internal view returns (uint256) { return troveManager.getTroveEntireDebt(troveId); } - function openTroveNoHints100pct(address _account, uint256 _coll, uint256 _boldAmount, uint256 _annualInterestRate) - public - returns (uint256 troveId) - { - (troveId,) = openTroveHelper(_account, 0, _coll, _boldAmount, _annualInterestRate); + function openTroveNoHints100pct( + address _account, + uint256 _coll, + uint256 _boldAmount, + uint256 _annualInterestRate + ) public returns (uint256 troveId) { + (troveId, ) = openTroveHelper( + _account, + 0, + _coll, + _boldAmount, + _annualInterestRate + ); } function openTroveNoHints100pctWithIndex( @@ -202,7 +261,13 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _boldAmount, uint256 _annualInterestRate ) public returns (uint256 troveId) { - (troveId,) = openTroveHelper(_account, _index, _coll, _boldAmount, _annualInterestRate); + (troveId, ) = openTroveHelper( + _account, + _index, + _coll, + _boldAmount, + _annualInterestRate + ); } function openTroveHelper( @@ -212,7 +277,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _boldAmount, uint256 _annualInterestRate ) public returns (uint256 troveId, uint256 upfrontFee) { - upfrontFee = predictOpenTroveUpfrontFee(_boldAmount, _annualInterestRate); + upfrontFee = predictOpenTroveUpfrontFee( + _boldAmount, + _annualInterestRate + ); vm.startPrank(_account); @@ -240,11 +308,24 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _debt, uint256 _interestRate ) public returns (uint256 troveId) { - (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove(_debt, _interestRate); + (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove( + _debt, + _interestRate + ); vm.prank(_account); troveId = borrowerOperations.openTrove( - _account, _index, _coll, borrow, 0, 0, _interestRate, upfrontFee, address(0), address(0), address(0) + _account, + _index, + _coll, + borrow, + 0, + 0, + _interestRate, + upfrontFee, + address(0), + address(0), + address(0) ); } @@ -255,13 +336,26 @@ contract BaseTest is TestAccounts, Logging, TroveId { uint256 _debt, uint256 _interestRate ) public returns (uint256 troveId, uint256 coll) { - (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove(_debt, _interestRate); + (uint256 borrow, uint256 upfrontFee) = findAmountToBorrowWithOpenTrove( + _debt, + _interestRate + ); uint256 price = priceFeed.getPrice(); coll = mulDivCeil(_debt, _ICR, price); vm.prank(_account); troveId = borrowerOperations.openTrove( - _account, _index, coll, borrow, 0, 0, _interestRate, upfrontFee, address(0), address(0), address(0) + _account, + _index, + coll, + borrow, + 0, + 0, + _interestRate, + upfrontFee, + address(0), + address(0), + address(0) ); } @@ -317,20 +411,32 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function changeInterestRateNoHints(address _account, uint256 _troveId, uint256 _newAnnualInterestRate) - public - returns (uint256 upfrontFee) - { - upfrontFee = predictAdjustInterestRateUpfrontFee(_troveId, _newAnnualInterestRate); + function changeInterestRateNoHints( + address _account, + uint256 _troveId, + uint256 _newAnnualInterestRate + ) public returns (uint256 upfrontFee) { + upfrontFee = predictAdjustInterestRateUpfrontFee( + _troveId, + _newAnnualInterestRate + ); vm.startPrank(_account); - borrowerOperations.adjustTroveInterestRate(_troveId, _newAnnualInterestRate, 0, 0, upfrontFee); + borrowerOperations.adjustTroveInterestRate( + _troveId, + _newAnnualInterestRate, + 0, + 0, + upfrontFee + ); vm.stopPrank(); } function checkBelowCriticalThreshold(bool _true) public view { uint256 price = priceFeed.getPrice(); - bool belowCriticalThreshold = troveManager.checkBelowCriticalThreshold(price); + bool belowCriticalThreshold = troveManager.checkBelowCriticalThreshold( + price + ); assertEq(belowCriticalThreshold, _true); } @@ -346,7 +452,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function makeSPWithdrawalAndClaim(address _account, uint256 _amount) public { + function makeSPWithdrawalAndClaim( + address _account, + uint256 _amount + ) public { vm.startPrank(_account); stabilityPool.withdrawFromSP(_amount, true); vm.stopPrank(); @@ -370,25 +479,45 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function withdrawBold100pct(address _account, uint256 _troveId, uint256 _debtIncrease) public { + function withdrawBold100pct( + address _account, + uint256 _troveId, + uint256 _debtIncrease + ) public { vm.startPrank(_account); - borrowerOperations.withdrawBold(_troveId, _debtIncrease, predictAdjustTroveUpfrontFee(_troveId, _debtIncrease)); + borrowerOperations.withdrawBold( + _troveId, + _debtIncrease, + predictAdjustTroveUpfrontFee(_troveId, _debtIncrease) + ); vm.stopPrank(); } - function repayBold(address _account, uint256 _troveId, uint256 _debtDecrease) public { + function repayBold( + address _account, + uint256 _troveId, + uint256 _debtDecrease + ) public { vm.startPrank(_account); borrowerOperations.repayBold(_troveId, _debtDecrease); vm.stopPrank(); } - function addColl(address _account, uint256 _troveId, uint256 _collIncrease) public { + function addColl( + address _account, + uint256 _troveId, + uint256 _collIncrease + ) public { vm.startPrank(_account); borrowerOperations.addColl(_troveId, _collIncrease); vm.stopPrank(); } - function withdrawColl(address _account, uint256 _troveId, uint256 _collDecrease) public { + function withdrawColl( + address _account, + uint256 _troveId, + uint256 _collDecrease + ) public { vm.startPrank(_account); borrowerOperations.withdrawColl(_troveId, _collDecrease); vm.stopPrank(); @@ -412,7 +541,10 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function batchLiquidateTroves(address _from, uint256[] memory _trovesList) public { + function batchLiquidateTroves( + address _from, + uint256[] memory _trovesList + ) public { vm.startPrank(_from); troveManager.batchLiquidateTroves(_trovesList); vm.stopPrank(); @@ -424,13 +556,23 @@ contract BaseTest is TestAccounts, Logging, TroveId { vm.stopPrank(); } - function getShareofSPReward(address _depositor, uint256 _reward) public view returns (uint256) { - return _reward * stabilityPool.getCompoundedBoldDeposit(_depositor) / stabilityPool.getTotalBoldDeposits(); + function getShareofSPReward( + address _depositor, + uint256 _reward + ) public view returns (uint256) { + return + (_reward * stabilityPool.getCompoundedBoldDeposit(_depositor)) / + stabilityPool.getTotalBoldDeposits(); } function registerBatchManager(address _account) internal { registerBatchManager( - _account, uint128(1e16), uint128(20e16), uint128(5e16), uint128(25e14), MIN_INTEREST_RATE_CHANGE_PERIOD + _account, + uint128(1e16), + uint128(20e16), + uint128(5e16), + uint128(25e14), + MIN_INTEREST_RATE_CHANGE_PERIOD ); } @@ -444,7 +586,11 @@ contract BaseTest is TestAccounts, Logging, TroveId { ) internal { vm.startPrank(_account); borrowerOperations.registerBatchManager( - _minInterestRate, _maxInterestRate, _currentInterestRate, _fee, _minInterestRateChangePeriod + _minInterestRate, + _maxInterestRate, + _currentInterestRate, + _fee, + _minInterestRateChangePeriod ); vm.stopPrank(); } @@ -460,7 +606,15 @@ contract BaseTest is TestAccounts, Logging, TroveId { address _batchAddress, uint256 _annualInterestRate ) internal returns (uint256) { - return openTroveAndJoinBatchManagerWithIndex(_troveOwner, 0, _coll, _debt, _batchAddress, _annualInterestRate); + return + openTroveAndJoinBatchManagerWithIndex( + _troveOwner, + 0, + _coll, + _debt, + _batchAddress, + _annualInterestRate + ); } function openTroveAndJoinBatchManagerWithIndex( @@ -482,30 +636,40 @@ contract BaseTest is TestAccounts, Logging, TroveId { ); } - IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams memory params = IBorrowerOperations - .OpenTroveAndJoinInterestBatchManagerParams({ - owner: _troveOwner, - ownerIndex: _index, - collAmount: _coll, - boldAmount: _debt, - upperHint: 0, - lowerHint: 0, - interestBatchManager: _batchAddress, - maxUpfrontFee: 1e24, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); + IBorrowerOperations.OpenTroveAndJoinInterestBatchManagerParams + memory params = IBorrowerOperations + .OpenTroveAndJoinInterestBatchManagerParams({ + owner: _troveOwner, + ownerIndex: _index, + collAmount: _coll, + boldAmount: _debt, + upperHint: 0, + lowerHint: 0, + interestBatchManager: _batchAddress, + maxUpfrontFee: 1e24, + addManager: address(0), + removeManager: address(0), + receiver: address(0) + }); vm.startPrank(_troveOwner); - uint256 troveId = borrowerOperations.openTroveAndJoinInterestBatchManager(params); + uint256 troveId = borrowerOperations + .openTroveAndJoinInterestBatchManager(params); vm.stopPrank(); return troveId; } - function setBatchInterestRate(address _batchAddress, uint256 _newAnnualInterestRate) internal { + function setBatchInterestRate( + address _batchAddress, + uint256 _newAnnualInterestRate + ) internal { vm.startPrank(_batchAddress); - borrowerOperations.setBatchManagerAnnualInterestRate(uint128(_newAnnualInterestRate), 0, 0, type(uint256).max); + borrowerOperations.setBatchManagerAnnualInterestRate( + uint128(_newAnnualInterestRate), + 0, + 0, + type(uint256).max + ); vm.stopPrank(); } @@ -528,20 +692,53 @@ contract BaseTest is TestAccounts, Logging, TroveId { setInterestBatchManager(_troveOwner, _troveId, _newBatchManager); } - function setInterestBatchManager(address _troveOwner, uint256 _troveId, address _newBatchManager) internal { + function setInterestBatchManager( + address _troveOwner, + uint256 _troveId, + address _newBatchManager + ) internal { vm.startPrank(_troveOwner); - borrowerOperations.setInterestBatchManager(_troveId, _newBatchManager, 0, 0, type(uint256).max); + borrowerOperations.setInterestBatchManager( + _troveId, + _newBatchManager, + 0, + 0, + type(uint256).max + ); vm.stopPrank(); } - function removeFromBatch(address _troveOwner, uint256 _troveId, uint256 _newAnnualInterestRate) internal { + function removeFromBatch( + address _troveOwner, + uint256 _troveId, + uint256 _newAnnualInterestRate + ) internal { vm.startPrank(_troveOwner); - borrowerOperations.removeFromBatch(_troveId, _newAnnualInterestRate, 0, 0, type(uint256).max); + borrowerOperations.removeFromBatch( + _troveId, + _newAnnualInterestRate, + 0, + 0, + type(uint256).max + ); vm.stopPrank(); } - function switchBatchManager(address _troveOwner, uint256 _troveId, address _newBatchManager) internal { - switchBatchManager(_troveOwner, _troveId, 0, 0, _newBatchManager, 0, 0, type(uint256).max); + function switchBatchManager( + address _troveOwner, + uint256 _troveId, + address _newBatchManager + ) internal { + switchBatchManager( + _troveOwner, + _troveId, + 0, + 0, + _newBatchManager, + 0, + 0, + type(uint256).max + ); } function switchBatchManager( @@ -556,7 +753,13 @@ contract BaseTest is TestAccounts, Logging, TroveId { ) internal { vm.startPrank(_troveOwner); borrowerOperations.switchBatchManager( - _troveId, _removeUpperHint, _removeLowerHint, _newBatchManager, _addUpperHint, _addLowerHint, _maxUpfrontFee + _troveId, + _removeUpperHint, + _removeLowerHint, + _newBatchManager, + _addUpperHint, + _addLowerHint, + _maxUpfrontFee ); vm.stopPrank(); } @@ -577,15 +780,26 @@ contract BaseTest is TestAccounts, Logging, TroveId { return x > y ? x - y : y - x; } - function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin) public pure { + function assertApproximatelyEqual( + uint256 _x, + uint256 _y, + uint256 _margin + ) public pure { assertApproxEqAbs(_x, _y, _margin, ""); } - function assertApproximatelyEqual(uint256 _x, uint256 _y, uint256 _margin, string memory _reason) public pure { + function assertApproximatelyEqual( + uint256 _x, + uint256 _y, + uint256 _margin, + string memory _reason + ) public pure { assertApproxEqAbs(_x, _y, _margin, _reason); } - function uintToArray(uint256 _value) public pure returns (uint256[] memory result) { + function uintToArray( + uint256 _value + ) public pure returns (uint256[] memory result) { result = new uint256[](1); result[0] = _value; } diff --git a/contracts/test/TestContracts/BoldTokenTester.sol b/contracts/test/TestContracts/BoldTokenTester.sol deleted file mode 100644 index eaa912470..000000000 --- a/contracts/test/TestContracts/BoldTokenTester.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/BoldToken.sol"; - -contract BoldTokenTester is BoldToken { - constructor(address _owner) BoldToken(_owner) {} - - function unprotectedMint(address _account, uint256 _amount) external { - _mint(_account, _amount); - } -} diff --git a/contracts/test/TestContracts/BorrowerOperationsTester.t.sol b/contracts/test/TestContracts/BorrowerOperationsTester.t.sol index ab24961d2..ba19c73f2 100644 --- a/contracts/test/TestContracts/BorrowerOperationsTester.t.sol +++ b/contracts/test/TestContracts/BorrowerOperationsTester.t.sol @@ -3,16 +3,17 @@ pragma solidity 0.8.24; import "src/Interfaces/IAddressesRegistry.sol"; +import "src/Interfaces/ISystemParams.sol"; import "src/BorrowerOperations.sol"; import "./Interfaces/IBorrowerOperationsTester.sol"; /* Tester contract inherits from BorrowerOperations, and provides external functions for testing the parent's internal functions. */ contract BorrowerOperationsTester is BorrowerOperations, IBorrowerOperationsTester { - constructor(IAddressesRegistry _addressesRegistry) BorrowerOperations(_addressesRegistry) {} + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) BorrowerOperations(_addressesRegistry, _systemParams) {} function get_CCR() external view returns (uint256) { - return CCR; + return systemParams.CCR(); } function getCollToken() external view returns (IERC20) { diff --git a/contracts/test/TestContracts/CollateralRegistryTester.sol b/contracts/test/TestContracts/CollateralRegistryTester.sol index e6254944b..1101abffd 100644 --- a/contracts/test/TestContracts/CollateralRegistryTester.sol +++ b/contracts/test/TestContracts/CollateralRegistryTester.sol @@ -3,13 +3,14 @@ pragma solidity 0.8.24; import "src/CollateralRegistry.sol"; +import "src/Interfaces/ISystemParams.sol"; /* Tester contract inherits from CollateralRegistry, and provides external functions for testing the parent's internal functions. */ contract CollateralRegistryTester is CollateralRegistry { - constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers) - CollateralRegistry(_boldToken, _tokens, _troveManagers) + constructor(IBoldToken _boldToken, IERC20Metadata[] memory _tokens, ITroveManager[] memory _troveManagers, ISystemParams _systemParams, address _liquidityStrategy) + CollateralRegistry(_boldToken, _tokens, _troveManagers, _systemParams, _liquidityStrategy) {} function unprotectedDecayBaseRateFromBorrowing() external returns (uint256) { diff --git a/contracts/test/TestContracts/Deployment.t.sol b/contracts/test/TestContracts/Deployment.t.sol index 27425e7f0..e4ccc4841 100644 --- a/contracts/test/TestContracts/Deployment.t.sol +++ b/contracts/test/TestContracts/Deployment.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "src/AddressesRegistry.sol"; import "src/ActivePool.sol"; -import "src/BoldToken.sol"; +import "./StableTokenV3.sol"; import "src/BorrowerOperations.sol"; import "src/CollSurplusPool.sol"; import "src/DefaultPool.sol"; @@ -20,56 +20,20 @@ import "src/TroveNFT.sol"; import "src/NFTMetadata/MetadataNFT.sol"; import "src/CollateralRegistry.sol"; import "./MockInterestRouter.sol"; -import "./PriceFeedTestnet.sol"; +import "./MockFXPriceFeed.sol"; import "./MetadataDeployment.sol"; -import "src/Zappers/WETHZapper.sol"; -import "src/Zappers/GasCompZapper.sol"; -import "src/Zappers/LeverageLSTZapper.sol"; -import "src/Zappers/LeverageWETHZapper.sol"; -import "src/Zappers/Modules/FlashLoans/BalancerFlashLoan.sol"; -import "src/Zappers/Interfaces/IFlashLoanProvider.sol"; -import "src/Zappers/Interfaces/IExchange.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveFactory.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGPool.sol"; -import "src/Zappers/Modules/Exchanges/CurveExchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/UniV3Exchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol"; +import "src/SystemParams.sol"; + import {WETHTester} from "./WETHTester.sol"; import {ERC20Faucet} from "./ERC20Faucet.sol"; -import "src/PriceFeeds/WETHPriceFeed.sol"; -import "src/PriceFeeds/WSTETHPriceFeed.sol"; -import "src/PriceFeeds/RETHPriceFeed.sol"; - import "forge-std/console2.sol"; uint256 constant _24_HOURS = 86400; uint256 constant _48_HOURS = 172800; -// TODO: Split dev and mainnet contract TestDeployer is MetadataDeployment { IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - IWETH constant WETH_MAINNET = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - - // Curve - ICurveFactory constant curveFactory = ICurveFactory(0x98EE851a00abeE0d95D08cF4CA2BdCE32aeaAF7F); - ICurveStableswapNGFactory constant curveStableswapFactory = - ICurveStableswapNGFactory(0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf); - uint128 constant BOLD_TOKEN_INDEX = 0; - uint256 constant COLL_TOKEN_INDEX = 1; - uint128 constant USDC_INDEX = 1; - - // UniV3 - ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - INonfungiblePositionManager constant uniV3PositionManager = - INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - uint24 constant UNIV3_FEE = 3000; // 0.3% - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% bytes32 constant SALT = keccak256("LiquityV2"); @@ -87,10 +51,11 @@ contract TestDeployer is MetadataDeployment { IStabilityPool stabilityPool; ITroveManagerTester troveManager; // Tester ITroveNFT troveNFT; - IPriceFeedTestnet priceFeed; // Tester + IMockFXPriceFeed priceFeed; // Tester IInterestRouter interestRouter; IERC20Metadata collToken; LiquityContractsDevPools pools; + ISystemParams systemParams; } struct LiquityContracts { @@ -107,14 +72,7 @@ contract TestDeployer is MetadataDeployment { GasPool gasPool; IInterestRouter interestRouter; IERC20Metadata collToken; - } - - struct Zappers { - WETHZapper wethZapper; - GasCompZapper gasCompZapper; - ILeverageZapper leverageZapperCurve; - ILeverageZapper leverageZapperUniV3; - ILeverageZapper leverageZapperHybrid; + ISystemParams systemParams; } struct LiquityContractAddresses { @@ -151,42 +109,6 @@ contract TestDeployer is MetadataDeployment { uint256 i; } - struct DeploymentResultMainnet { - LiquityContracts[] contractsArray; - ExternalAddresses externalAddresses; - CollateralRegistryTester collateralRegistry; - IBoldToken boldToken; - HintHelpers hintHelpers; - MultiTroveGetter multiTroveGetter; - Zappers[] zappersArray; - } - - struct DeploymentVarsMainnet { - OracleParams oracleParams; - uint256 numCollaterals; - IERC20Metadata[] collaterals; - IAddressesRegistry[] addressesRegistries; - ITroveManager[] troveManagers; - IPriceFeed[] priceFeeds; - bytes bytecode; - address boldTokenAddress; - uint256 i; - } - - struct DeploymentParamsMainnet { - uint256 branch; - IERC20Metadata collToken; - IPriceFeed priceFeed; - IBoldToken boldToken; - ICollateralRegistry collateralRegistry; - IWETH weth; - IAddressesRegistry addressesRegistry; - address troveManagerAddress; - IHintHelpers hintHelpers; - IMultiTroveGetter multiTroveGetter; - ICurveStableswapNGPool usdcCurvePool; - } - struct ExternalAddresses { address ETHOracle; address STETHOracle; @@ -206,6 +128,26 @@ contract TestDeployer is MetadataDeployment { return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry)); } + function getBytecode(bytes memory _creationCode, address _addressesRegistry, address _systemParams) + public + pure + returns (bytes memory) + { + return abi.encodePacked(_creationCode, abi.encode(_addressesRegistry, _systemParams)); + } + + function getBytecode(bytes memory _creationCode, bool _disable) public pure returns (bytes memory) { + return abi.encodePacked(_creationCode, abi.encode(_disable)); + } + + function getBytecode(bytes memory _creationCode, bool _disable, address _systemParams) + public + pure + returns (bytes memory) + { + return abi.encodePacked(_creationCode, abi.encode(_disable, _systemParams)); + } + function getAddress(address _deployer, bytes memory _bytecode, bytes32 _salt) public pure returns (address) { bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), _deployer, _salt, keccak256(_bytecode))); @@ -221,8 +163,7 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken, HintHelpers hintHelpers, MultiTroveGetter multiTroveGetter, - IWETH WETH, // for gas compensation - Zappers memory zappers + IWETH WETH // for gas compensation ) { return deployAndConnectContracts(TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16)); @@ -236,20 +177,17 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken, HintHelpers hintHelpers, MultiTroveGetter multiTroveGetter, - IWETH WETH, // for gas compensation - Zappers memory zappers + IWETH WETH // for gas compensation ) { LiquityContractsDev[] memory contractsArray; TroveManagerParams[] memory troveManagerParamsArray = new TroveManagerParams[](1); - Zappers[] memory zappersArray; troveManagerParamsArray[0] = troveManagerParams; - (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter, WETH, zappersArray) = + (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter, WETH) = deployAndConnectContractsMultiColl(troveManagerParamsArray); contracts = contractsArray[0]; - zappers = zappersArray[0]; } function deployAndConnectContractsMultiColl(TroveManagerParams[] memory troveManagerParamsArray) @@ -260,8 +198,7 @@ contract TestDeployer is MetadataDeployment { IBoldToken boldToken, HintHelpers hintHelpers, MultiTroveGetter multiTroveGetter, - IWETH WETH, // for gas compensation - Zappers[] memory zappersArray + IWETH WETH // for gas compensation ) { // used for gas compensation and as collateral of the first branch @@ -269,7 +206,7 @@ contract TestDeployer is MetadataDeployment { 100 ether, // _tapAmount 1 days // _tapPeriod ); - (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter, zappersArray) = + (contractsArray, collateralRegistry, boldToken, hintHelpers, multiTroveGetter) = deployAndConnectContracts(troveManagerParamsArray, WETH); } @@ -292,28 +229,34 @@ contract TestDeployer is MetadataDeployment { ICollateralRegistry collateralRegistry, IBoldToken boldToken, HintHelpers hintHelpers, - MultiTroveGetter multiTroveGetter, - Zappers[] memory zappersArray + MultiTroveGetter multiTroveGetter ) { DeploymentVarsDev memory vars; vars.numCollaterals = troveManagerParamsArray.length; - // Deploy Bold - vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); + + // Deploy Bold (StableTokenV3) + vars.bytecode = abi.encodePacked(type(StableTokenV3).creationCode, abi.encode(false)); vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - boldToken = new BoldToken{salt: SALT}(address(this)); + StableTokenV3 stableTokenV3 = new StableTokenV3{salt: SALT}(false); + boldToken = IBoldToken(address(stableTokenV3)); assert(address(boldToken) == vars.boldTokenAddress); contractsArray = new LiquityContractsDev[](vars.numCollaterals); - zappersArray = new Zappers[](vars.numCollaterals); vars.collaterals = new IERC20Metadata[](vars.numCollaterals); vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); vars.troveManagers = new ITroveManager[](vars.numCollaterals); + ISystemParams[] memory systemParamsArray = new ISystemParams[](vars.numCollaterals); + + for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { + systemParamsArray[vars.i] = deploySystemParamsDev(troveManagerParamsArray[vars.i], vars.i); + } + // Deploy the first branch with WETH collateral vars.collaterals[0] = _WETH; (IAddressesRegistry addressesRegistry, address troveManagerAddress) = - _deployAddressesRegistryDev(troveManagerParamsArray[0]); + _deployAddressesRegistryDev(systemParamsArray[0]); vars.addressesRegistries[0] = addressesRegistry; vars.troveManagers[0] = ITroveManager(troveManagerAddress); for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { @@ -325,16 +268,17 @@ contract TestDeployer is MetadataDeployment { ); vars.collaterals[vars.i] = collToken; // Addresses registry and TM address - (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(troveManagerParamsArray[vars.i]); + (addressesRegistry, troveManagerAddress) = _deployAddressesRegistryDev(systemParamsArray[vars.i]); vars.addressesRegistries[vars.i] = addressesRegistry; vars.troveManagers[vars.i] = ITroveManager(troveManagerAddress); } - collateralRegistry = new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers); - hintHelpers = new HintHelpers(collateralRegistry); + collateralRegistry = + new CollateralRegistry(boldToken, vars.collaterals, vars.troveManagers, systemParamsArray[0], makeAddr("liquidityStrategy")); + hintHelpers = new HintHelpers(collateralRegistry, systemParamsArray[0]); multiTroveGetter = new MultiTroveGetter(collateralRegistry); - (contractsArray[0], zappersArray[0]) = _deployAndConnectCollateralContractsDev( + contractsArray[0] = _deployAndConnectCollateralContractsDev( _WETH, boldToken, collateralRegistry, @@ -342,12 +286,13 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries[0], address(vars.troveManagers[0]), hintHelpers, - multiTroveGetter + multiTroveGetter, + systemParamsArray[0] ); // Deploy the remaining branches with LST collateral for (vars.i = 1; vars.i < vars.numCollaterals; vars.i++) { - (contractsArray[vars.i], zappersArray[vars.i]) = _deployAndConnectCollateralContractsDev( + contractsArray[vars.i] = _deployAndConnectCollateralContractsDev( vars.collaterals[vars.i], boldToken, collateralRegistry, @@ -355,49 +300,116 @@ contract TestDeployer is MetadataDeployment { vars.addressesRegistries[vars.i], address(vars.troveManagers[vars.i]), hintHelpers, - multiTroveGetter + multiTroveGetter, + systemParamsArray[vars.i] ); } - boldToken.setCollateralRegistry(address(collateralRegistry)); + // Initialize StableTokenV3 with all minters, burners, and operators + // This should handle both cases with multiple collaterals and single collateral + address[] memory minters = new address[](vars.numCollaterals * 2); + address[] memory burners = new address[](vars.numCollaterals * 3 + 1); + address[] memory operators = new address[](vars.numCollaterals); + + for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { + minters[vars.i * 2] = address(contractsArray[vars.i].borrowerOperations); + minters[vars.i * 2 + 1] = address(contractsArray[vars.i].activePool); + + burners[vars.i * 3] = address(contractsArray[vars.i].troveManager); + burners[vars.i * 3 + 1] = address(contractsArray[vars.i].borrowerOperations); + burners[vars.i * 3 + 2] = address(contractsArray[vars.i].stabilityPool); + + operators[vars.i] = address(contractsArray[vars.i].stabilityPool); + } + burners[vars.numCollaterals * 3] = address(collateralRegistry); + + stableTokenV3.initialize("Bold Token", "BOLD", address(this), new address[](0), new uint256[](0), minters, burners, operators); } - function _deployAddressesRegistryDev(TroveManagerParams memory _troveManagerParams) - internal - returns (IAddressesRegistry, address) - { - IAddressesRegistry addressesRegistry = new AddressesRegistry( - address(this), - _troveManagerParams.CCR, - _troveManagerParams.MCR, - _troveManagerParams.BCR, - _troveManagerParams.SCR, - _troveManagerParams.LIQUIDATION_PENALTY_SP, - _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION - ); + function _deployAddressesRegistryDev(ISystemParams _systemParams) internal returns (IAddressesRegistry, address) { + IAddressesRegistry addressesRegistry = new AddressesRegistry(address(this)); address troveManagerAddress = getAddress( - address(this), getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry)), SALT + address(this), + getBytecode(type(TroveManagerTester).creationCode, address(addressesRegistry), address(_systemParams)), + SALT ); return (addressesRegistry, troveManagerAddress); } + function deploySystemParamsDev(TroveManagerParams memory params, uint256 index) public returns (ISystemParams) { + bytes32 uniqueSalt = keccak256(abi.encodePacked(SALT, index)); + + // Create parameter structs based on constants + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({ + minDebt: 2000e18 // MIN_DEBT + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: params.LIQUIDATION_PENALTY_SP, + liquidationPenaltyRedistribution: params.LIQUIDATION_PENALTY_REDISTRIBUTION + }); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, // COLL_GAS_COMPENSATION_DIVISOR + collGasCompensationCap: 2 ether, // COLL_GAS_COMPENSATION_CAP + ethGasCompensation: 0.0375 ether // ETH_GAS_COMPENSATION + }); + + ISystemParams.CollateralParams memory collateralParams = + ISystemParams.CollateralParams({ccr: params.CCR, scr: params.SCR, mcr: params.MCR, bcr: params.BCR}); + + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: DECIMAL_PRECISION / 200 // MIN_ANNUAL_INTEREST_RATE (0.5%) + }); + + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: DECIMAL_PRECISION / 200, // REDEMPTION_FEE_FLOOR (0.5%) + initialBaseRate: DECIMAL_PRECISION, // INITIAL_BASE_RATE (100%) + redemptionMinuteDecayFactor: 998076443575628800, // REDEMPTION_MINUTE_DECAY_FACTOR + redemptionBeta: 1 // REDEMPTION_BETA + }); + + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * (DECIMAL_PRECISION / 100), // SP_YIELD_SPLIT (75%) + minBoldInSP: 1e18, // MIN_BOLD_IN_SP + minBoldAfterRebalance: 1_000e18 // MIN_BOLD_AFTER_REBALANCE + }); + + SystemParams systemParams = new SystemParams{salt: uniqueSalt}( + false, + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams + ); + + systemParams.initialize(); + + return ISystemParams(systemParams); + } + function _deployAndConnectCollateralContractsDev( IERC20Metadata _collToken, IBoldToken _boldToken, ICollateralRegistry _collateralRegistry, - IWETH _weth, + IERC20Metadata _gasToken, IAddressesRegistry _addressesRegistry, address _troveManagerAddress, IHintHelpers _hintHelpers, - IMultiTroveGetter _multiTroveGetter - ) internal returns (LiquityContractsDev memory contracts, Zappers memory zappers) { + IMultiTroveGetter _multiTroveGetter, + ISystemParams _systemParams + ) internal returns (LiquityContractsDev memory contracts) { LiquityContractAddresses memory addresses; contracts.collToken = _collToken; + contracts.systemParams = _systemParams; // Deploy all contracts, using testers for TM and PriceFeed contracts.addressesRegistry = _addressesRegistry; - contracts.priceFeed = new PriceFeedTestnet(); + contracts.priceFeed = new MockFXPriceFeed(); contracts.interestRouter = new MockInterestRouter(); // Deploy Metadata @@ -410,18 +422,27 @@ contract TestDeployer is MetadataDeployment { // Pre-calc addresses addresses.borrowerOperations = getAddress( address(this), - getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry)), + getBytecode( + type(BorrowerOperationsTester).creationCode, + address(contracts.addressesRegistry), + address(_systemParams) + ), SALT ); addresses.troveManager = _troveManagerAddress; addresses.troveNFT = getAddress( address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT ); + bytes32 stabilityPoolSalt = keccak256(abi.encodePacked(address(contracts.addressesRegistry))); addresses.stabilityPool = getAddress( - address(this), getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry)), SALT + address(this), + getBytecode(type(StabilityPool).creationCode, bool(false), address(_systemParams)), + stabilityPoolSalt ); addresses.activePool = getAddress( - address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT + address(this), + getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry), address(_systemParams)), + SALT ); addresses.defaultPool = getAddress( address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT @@ -438,7 +459,6 @@ contract TestDeployer is MetadataDeployment { // Deploy contracts IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: _collToken, borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), troveManager: ITroveManager(addresses.troveManager), troveNFT: ITroveNFT(addresses.troveNFT), @@ -455,15 +475,19 @@ contract TestDeployer is MetadataDeployment { multiTroveGetter: _multiTroveGetter, collateralRegistry: _collateralRegistry, boldToken: _boldToken, - WETH: _weth + collToken: _collToken, + gasToken: _gasToken, + // TODO: add liquidity strategy + liquidityStrategy: makeAddr("liquidityStrategy") }); contracts.addressesRegistry.setAddresses(addressVars); - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); - contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry); + contracts.borrowerOperations = + new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry, _systemParams); + contracts.troveManager = new TroveManagerTester{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); + contracts.stabilityPool = new StabilityPool{salt: stabilityPoolSalt}(false, _systemParams); + contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry, _systemParams); contracts.pools.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); contracts.pools.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); @@ -479,497 +503,9 @@ contract TestDeployer is MetadataDeployment { assert(address(contracts.pools.collSurplusPool) == addresses.collSurplusPool); assert(address(contracts.sortedTroves) == addresses.sortedTroves); - // Connect contracts - _boldToken.setBranchAddresses( - address(contracts.troveManager), - address(contracts.stabilityPool), - address(contracts.borrowerOperations), - address(contracts.activePool) - ); - - // deploy zappers - _deployZappers( - contracts.addressesRegistry, - contracts.collToken, - _boldToken, - _weth, - contracts.priceFeed, - ICurveStableswapNGPool(address(0)), - false, - zappers - ); - } - - // Creates individual PriceFeed contracts based on oracle addresses. - // Still uses mock collaterals rather than real mainnet WETH and LST addresses. - - function deployAndConnectContractsMainnet(TroveManagerParams[] memory _troveManagerParamsArray) - public - returns (DeploymentResultMainnet memory result) - { - DeploymentVarsMainnet memory vars; - - result.externalAddresses.ETHOracle = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; - result.externalAddresses.RETHOracle = 0x536218f9E9Eb48863970252233c8F271f554C2d0; - result.externalAddresses.STETHOracle = 0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8; - result.externalAddresses.WSTETHToken = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - - result.externalAddresses.RETHToken = 0xae78736Cd615f374D3085123A210448E74Fc6393; - - vars.oracleParams.ethUsdStalenessThreshold = _24_HOURS; - vars.oracleParams.stEthUsdStalenessThreshold = _24_HOURS; - vars.oracleParams.rEthEthStalenessThreshold = _48_HOURS; - - // Colls: WETH, WSTETH, RETH - vars.numCollaterals = 3; - result.contractsArray = new LiquityContracts[](vars.numCollaterals); - result.zappersArray = new Zappers[](vars.numCollaterals); - vars.collaterals = new IERC20Metadata[](vars.numCollaterals); - vars.addressesRegistries = new IAddressesRegistry[](vars.numCollaterals); - vars.troveManagers = new ITroveManager[](vars.numCollaterals); - address troveManagerAddress; - - // Deploy Bold - vars.bytecode = abi.encodePacked(type(BoldToken).creationCode, abi.encode(address(this))); - vars.boldTokenAddress = getAddress(address(this), vars.bytecode, SALT); - result.boldToken = new BoldToken{salt: SALT}(address(this)); - assert(address(result.boldToken) == vars.boldTokenAddress); - - // WETH - IWETH WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - vars.collaterals[0] = WETH; - (vars.addressesRegistries[0], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[0]); - vars.troveManagers[0] = ITroveManager(troveManagerAddress); - - // RETH - vars.collaterals[1] = IERC20Metadata(0xae78736Cd615f374D3085123A210448E74Fc6393); - (vars.addressesRegistries[1], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[1]); - vars.troveManagers[1] = ITroveManager(troveManagerAddress); - - // WSTETH - vars.collaterals[2] = IERC20Metadata(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0); - (vars.addressesRegistries[2], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[2]); - vars.troveManagers[2] = ITroveManager(troveManagerAddress); - - // Deploy registry and register the TMs - result.collateralRegistry = new CollateralRegistryTester(result.boldToken, vars.collaterals, vars.troveManagers); - - result.hintHelpers = new HintHelpers(result.collateralRegistry); - result.multiTroveGetter = new MultiTroveGetter(result.collateralRegistry); - - ICurveStableswapNGPool usdcCurvePool = _deployCurveBoldUsdcPool(result.boldToken, true); - - // Deploy each set of core contracts - for (vars.i = 0; vars.i < vars.numCollaterals; vars.i++) { - DeploymentParamsMainnet memory params; - params.branch = vars.i; - params.collToken = vars.collaterals[vars.i]; - params.boldToken = result.boldToken; - params.collateralRegistry = result.collateralRegistry; - params.weth = WETH; - params.addressesRegistry = vars.addressesRegistries[vars.i]; - params.troveManagerAddress = address(vars.troveManagers[vars.i]); - params.hintHelpers = result.hintHelpers; - params.multiTroveGetter = result.multiTroveGetter; - params.usdcCurvePool = usdcCurvePool; - (result.contractsArray[vars.i], result.zappersArray[vars.i]) = - _deployAndConnectCollateralContractsMainnet(params, result.externalAddresses, vars.oracleParams); - } - - result.boldToken.setCollateralRegistry(address(result.collateralRegistry)); - } - - function _deployAddressesRegistryMainnet(TroveManagerParams memory _troveManagerParams) - internal - returns (IAddressesRegistry, address) - { - IAddressesRegistry addressesRegistry = new AddressesRegistry( - address(this), - _troveManagerParams.CCR, - _troveManagerParams.MCR, - _troveManagerParams.BCR, - _troveManagerParams.SCR, - _troveManagerParams.LIQUIDATION_PENALTY_SP, - _troveManagerParams.LIQUIDATION_PENALTY_REDISTRIBUTION - ); - address troveManagerAddress = - getAddress(address(this), getBytecode(type(TroveManager).creationCode, address(addressesRegistry)), SALT); - - return (addressesRegistry, troveManagerAddress); - } - - function _deployAndConnectCollateralContractsMainnet( - DeploymentParamsMainnet memory _params, - ExternalAddresses memory _externalAddresses, - OracleParams memory _oracleParams - ) internal returns (LiquityContracts memory contracts, Zappers memory zappers) { - LiquityContractAddresses memory addresses; - contracts.collToken = _params.collToken; - contracts.interestRouter = new MockInterestRouter(); - - contracts.addressesRegistry = _params.addressesRegistry; - - // Deploy Metadata - MetadataNFT metadataNFT = deployMetadata(SALT); - addresses.metadataNFT = getAddress( - address(this), getBytecode(type(MetadataNFT).creationCode, address(initializedFixedAssetReader)), SALT - ); - assert(address(metadataNFT) == addresses.metadataNFT); - - // Pre-calc addresses - addresses.borrowerOperations = getAddress( - address(this), - getBytecode(type(BorrowerOperationsTester).creationCode, address(contracts.addressesRegistry)), - SALT - ); - addresses.troveManager = _params.troveManagerAddress; - addresses.troveNFT = getAddress( - address(this), getBytecode(type(TroveNFT).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.stabilityPool = getAddress( - address(this), getBytecode(type(StabilityPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.activePool = getAddress( - address(this), getBytecode(type(ActivePool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.defaultPool = getAddress( - address(this), getBytecode(type(DefaultPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.gasPool = getAddress( - address(this), getBytecode(type(GasPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.collSurplusPool = getAddress( - address(this), getBytecode(type(CollSurplusPool).creationCode, address(contracts.addressesRegistry)), SALT - ); - addresses.sortedTroves = getAddress( - address(this), getBytecode(type(SortedTroves).creationCode, address(contracts.addressesRegistry)), SALT - ); - - contracts.priceFeed = - _deployPriceFeed(_params.branch, _externalAddresses, _oracleParams, addresses.borrowerOperations); - - // Deploy contracts - IAddressesRegistry.AddressVars memory addressVars = IAddressesRegistry.AddressVars({ - collToken: _params.collToken, - borrowerOperations: IBorrowerOperations(addresses.borrowerOperations), - troveManager: ITroveManager(addresses.troveManager), - troveNFT: ITroveNFT(addresses.troveNFT), - metadataNFT: IMetadataNFT(addresses.metadataNFT), - stabilityPool: IStabilityPool(addresses.stabilityPool), - priceFeed: contracts.priceFeed, - activePool: IActivePool(addresses.activePool), - defaultPool: IDefaultPool(addresses.defaultPool), - gasPoolAddress: addresses.gasPool, - collSurplusPool: ICollSurplusPool(addresses.collSurplusPool), - sortedTroves: ISortedTroves(addresses.sortedTroves), - interestRouter: contracts.interestRouter, - hintHelpers: _params.hintHelpers, - multiTroveGetter: _params.multiTroveGetter, - collateralRegistry: _params.collateralRegistry, - boldToken: _params.boldToken, - WETH: _params.weth - }); - contracts.addressesRegistry.setAddresses(addressVars); - - contracts.borrowerOperations = new BorrowerOperationsTester{salt: SALT}(contracts.addressesRegistry); - contracts.troveManager = new TroveManager{salt: SALT}(contracts.addressesRegistry); - contracts.troveNFT = new TroveNFT{salt: SALT}(contracts.addressesRegistry); - contracts.stabilityPool = new StabilityPool{salt: SALT}(contracts.addressesRegistry); - contracts.activePool = new ActivePool{salt: SALT}(contracts.addressesRegistry); - contracts.defaultPool = new DefaultPool{salt: SALT}(contracts.addressesRegistry); - contracts.gasPool = new GasPool{salt: SALT}(contracts.addressesRegistry); - contracts.collSurplusPool = new CollSurplusPool{salt: SALT}(contracts.addressesRegistry); - contracts.sortedTroves = new SortedTroves{salt: SALT}(contracts.addressesRegistry); - - assert(address(contracts.borrowerOperations) == addresses.borrowerOperations); - assert(address(contracts.troveManager) == addresses.troveManager); - assert(address(contracts.troveNFT) == addresses.troveNFT); - assert(address(contracts.stabilityPool) == addresses.stabilityPool); - assert(address(contracts.activePool) == addresses.activePool); - assert(address(contracts.defaultPool) == addresses.defaultPool); - assert(address(contracts.gasPool) == addresses.gasPool); - assert(address(contracts.collSurplusPool) == addresses.collSurplusPool); - assert(address(contracts.sortedTroves) == addresses.sortedTroves); - - // Connect contracts - _params.boldToken.setBranchAddresses( - address(contracts.troveManager), - address(contracts.stabilityPool), - address(contracts.borrowerOperations), - address(contracts.activePool) - ); - - // deploy zappers - _deployZappers( - contracts.addressesRegistry, - contracts.collToken, - _params.boldToken, - _params.weth, - contracts.priceFeed, - _params.usdcCurvePool, - true, - zappers - ); - } - - function _deployPriceFeed( - uint256 _branch, - ExternalAddresses memory _externalAddresses, - OracleParams memory _oracleParams, - address _borrowerOperationsAddress - ) internal returns (IPriceFeed) { - //assert(_branch < vars.numCollaterals); - // Price feeds - // ETH - if (_branch == 0) { - return new WETHPriceFeed( - _externalAddresses.ETHOracle, _oracleParams.ethUsdStalenessThreshold, _borrowerOperationsAddress - ); - } else if (_branch == 1) { - // RETH - return new RETHPriceFeed( - _externalAddresses.ETHOracle, - _externalAddresses.RETHOracle, - _externalAddresses.RETHToken, - _oracleParams.ethUsdStalenessThreshold, - _oracleParams.rEthEthStalenessThreshold, - _borrowerOperationsAddress - ); - } - - // wstETH - return new WSTETHPriceFeed( - _externalAddresses.ETHOracle, - _externalAddresses.STETHOracle, - _externalAddresses.WSTETHToken, - _oracleParams.ethUsdStalenessThreshold, - _oracleParams.stEthUsdStalenessThreshold, - _borrowerOperationsAddress - ); - } - - function _deployZappers( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IWETH _weth, - IPriceFeed _priceFeed, - ICurveStableswapNGPool _usdcCurvePool, - bool _mainnet, - Zappers memory zappers // result - ) internal { - IFlashLoanProvider flashLoanProvider = new BalancerFlashLoan(); - IExchange curveExchange = _deployCurveExchange(_collToken, _boldToken, _priceFeed, _mainnet); - - // TODO: Deploy base zappers versions with Uni V3 exchange - bool lst = _collToken != _weth; - if (lst) { - zappers.gasCompZapper = new GasCompZapper(_addressesRegistry, flashLoanProvider, curveExchange); - } else { - zappers.wethZapper = new WETHZapper(_addressesRegistry, flashLoanProvider, curveExchange); - } - - if (_mainnet) { - _deployLeverageZappers( - _addressesRegistry, - _collToken, - _boldToken, - _priceFeed, - flashLoanProvider, - curveExchange, - _usdcCurvePool, - lst, - zappers - ); - } - } - - function _deployCurveExchange(IERC20 _collToken, IBoldToken _boldToken, IPriceFeed _priceFeed, bool _mainnet) - internal - returns (IExchange) - { - if (!_mainnet) return new CurveExchange(_collToken, _boldToken, ICurvePool(address(0)), 1, 0); - - (uint256 price,) = _priceFeed.fetchPrice(); - - // deploy Curve Twocrypto NG pool - address[2] memory coins; - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[COLL_TOKEN_INDEX] = address(_collToken); - ICurvePool curvePool = curveFactory.deploy_pool( - "LST-Bold pool", - "LBLD", - coins, - 0, // implementation id - 400000, // A - 145000000000000, // gamma - 26000000, // mid_fee - 45000000, // out_fee - 230000000000000, // fee_gamma - 2000000000000, // allowed_extra_profit - 146000000000000, // adjustment_step - 600, // ma_exp_time - price // initial_price - ); - - IExchange curveExchange = new CurveExchange(_collToken, _boldToken, curvePool, 1, 0); - - return curveExchange; - } - - function _deployLeverageZappers( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IPriceFeed _priceFeed, - IFlashLoanProvider _flashLoanProvider, - IExchange _curveExchange, - ICurveStableswapNGPool _usdcCurvePool, - bool _lst, - Zappers memory zappers // result - ) internal { - zappers.leverageZapperCurve = - _deployCurveLeverageZapper(_addressesRegistry, _flashLoanProvider, _curveExchange, _lst); - zappers.leverageZapperUniV3 = - _deployUniV3LeverageZapper(_addressesRegistry, _collToken, _boldToken, _priceFeed, _flashLoanProvider, _lst); - zappers.leverageZapperHybrid = _deployHybridLeverageZapper( - _addressesRegistry, _collToken, _boldToken, _flashLoanProvider, _usdcCurvePool, _lst - ); - } - - function _deployCurveLeverageZapper( - IAddressesRegistry _addressesRegistry, - IFlashLoanProvider _flashLoanProvider, - IExchange _curveExchange, - bool _lst - ) internal returns (ILeverageZapper) { - ILeverageZapper leverageZapperCurve; - if (_lst) { - leverageZapperCurve = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, _curveExchange); - } else { - leverageZapperCurve = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, _curveExchange); - } - - return leverageZapperCurve; - } - - struct UniV3Vars { - IExchange uniV3Exchange; - uint256 price; - address[2] tokens; - } - - function _deployUniV3LeverageZapper( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IPriceFeed _priceFeed, - IFlashLoanProvider _flashLoanProvider, - bool _lst - ) internal returns (ILeverageZapper) { - UniV3Vars memory vars; - vars.uniV3Exchange = new UniV3Exchange(_collToken, _boldToken, UNIV3_FEE, uniV3Router); - ILeverageZapper leverageZapperUniV3; - if (_lst) { - leverageZapperUniV3 = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, vars.uniV3Exchange); - } else { - leverageZapperUniV3 = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, vars.uniV3Exchange); - } - - // Create Uni V3 pool - (vars.price,) = _priceFeed.fetchPrice(); - if (address(_boldToken) < address(_collToken)) { - //console2.log("b < c"); - vars.tokens[0] = address(_boldToken); - vars.tokens[1] = address(_collToken); - } else { - //console2.log("c < b"); - vars.tokens[0] = address(_collToken); - vars.tokens[1] = address(_boldToken); - } - uniV3PositionManager.createAndInitializePoolIfNecessary( - vars.tokens[0], // token0, - vars.tokens[1], // token1, - UNIV3_FEE, // fee, - UniV3Exchange(address(vars.uniV3Exchange)).priceToSqrtPrice(_boldToken, _collToken, vars.price) // sqrtPriceX96 - ); - - return leverageZapperUniV3; - } - - function _deployHybridLeverageZapper( - IAddressesRegistry _addressesRegistry, - IERC20 _collToken, - IBoldToken _boldToken, - IFlashLoanProvider _flashLoanProvider, - ICurveStableswapNGPool _usdcCurvePool, - bool _lst - ) internal returns (ILeverageZapper) { - IExchange hybridExchange = new HybridCurveUniV3Exchange( - _collToken, - _boldToken, - USDC, - WETH_MAINNET, - _usdcCurvePool, - USDC_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Router - ); - - ILeverageZapper leverageZapperHybrid; - if (_lst) { - leverageZapperHybrid = new LeverageLSTZapper(_addressesRegistry, _flashLoanProvider, hybridExchange); - } else { - leverageZapperHybrid = new LeverageWETHZapper(_addressesRegistry, _flashLoanProvider, hybridExchange); - } - - return leverageZapperHybrid; - } - - function _deployCurveBoldUsdcPool(IBoldToken _boldToken, bool _mainnet) internal returns (ICurveStableswapNGPool) { - if (!_mainnet) return ICurveStableswapNGPool(address(0)); - - // deploy Curve Stableswap pool - /* - address[2] memory coins; - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[USDC_INDEX] = address(USDC); - ICurvePool curvePool = curveStableswapFactory.deploy_plain_pool( - "USDC-Bold pool", - "USDCBOLD", - coins, - 4000, // A - 0, // asset type: USD - 1000000, // fee - 0 // implementation id - ); - */ - // deploy Curve StableswapNG pool - address[] memory coins = new address[](2); - coins[BOLD_TOKEN_INDEX] = address(_boldToken); - coins[USDC_INDEX] = address(USDC); - uint8[] memory assetTypes = new uint8[](2); // 0: standard - bytes4[] memory methodIds = new bytes4[](2); - address[] memory oracles = new address[](2); - ICurveStableswapNGPool curvePool = curveStableswapFactory.deploy_plain_pool( - "USDC-BOLD", - "USDCBOLD", - coins, - 4000, // A - 1000000, // fee - 20000000000, // _offpeg_fee_multiplier - 865, // _ma_exp_time - 0, // implementation id - assetTypes, - methodIds, - oracles - ); + contracts.stabilityPool.initialize(contracts.addressesRegistry); - return curvePool; + // Note: StableTokenV3 initialization is done after all branches are deployed + // in the deployAndConnectContracts function } } diff --git a/contracts/test/TestContracts/DevTestSetup.sol b/contracts/test/TestContracts/DevTestSetup.sol index 59194d4b6..4a77690e1 100644 --- a/contracts/test/TestContracts/DevTestSetup.sol +++ b/contracts/test/TestContracts/DevTestSetup.sol @@ -49,8 +49,7 @@ contract DevTestSetup is BaseTest { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev memory contracts; - TestDeployer.Zappers memory zappers; - (contracts, collateralRegistry, boldToken, hintHelpers,, WETH, zappers) = deployer.deployAndConnectContracts(); + (contracts, collateralRegistry, boldToken, hintHelpers,, WETH) = deployer.deployAndConnectContracts(); addressesRegistry = contracts.addressesRegistry; collToken = contracts.collToken; activePool = contracts.activePool; @@ -65,10 +64,7 @@ contract DevTestSetup is BaseTest { troveNFT = contracts.troveNFT; metadataNFT = addressesRegistry.metadataNFT(); mockInterestRouter = contracts.interestRouter; - wethZapper = zappers.wethZapper; - gasCompZapper = zappers.gasCompZapper; - leverageZapperCurve = zappers.leverageZapperCurve; - leverageZapperUniV3 = zappers.leverageZapperUniV3; + systemParams = contracts.systemParams; // Give some Coll to test accounts, and approve it to BorrowerOperations uint256 initialCollAmount = 10_000_000_000e18; @@ -82,6 +78,16 @@ contract DevTestSetup is BaseTest { BCR = troveManager.get_BCR(); LIQUIDATION_PENALTY_SP = troveManager.get_LIQUIDATION_PENALTY_SP(); LIQUIDATION_PENALTY_REDISTRIBUTION = troveManager.get_LIQUIDATION_PENALTY_REDISTRIBUTION(); + + MIN_DEBT = systemParams.MIN_DEBT(); + SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); + COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); + REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); + INITIAL_BASE_RATE = systemParams.INITIAL_BASE_RATE(); + REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); } function _setupForWithdrawCollGainToTrove() internal returns (uint256, uint256, uint256) { @@ -342,7 +348,7 @@ contract DevTestSetup is BaseTest { assertEq(uint8(troveManager.getTroveStatus(_troveIDs.B)), uint8(ITroveManager.Status.active)); } - function _getSPYield(uint256 _aggInterest) internal pure returns (uint256) { + function _getSPYield(uint256 _aggInterest) internal view returns (uint256) { uint256 spYield = SP_YIELD_SPLIT * _aggInterest / 1e18; assertGt(spYield, 0); assertLe(spYield, _aggInterest); diff --git a/contracts/test/TestContracts/GasGuzzlerOracle.sol b/contracts/test/TestContracts/GasGuzzlerOracle.sol deleted file mode 100644 index 016ce94b1..000000000 --- a/contracts/test/TestContracts/GasGuzzlerOracle.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.24; - -import "src/Dependencies/AggregatorV3Interface.sol"; - -// Mock oracle that consumes all gas in the price getter. -// this contract code is etched over mainnet oracle addresses in mainnet fork tests. -contract GasGuzzlerOracle is AggregatorV3Interface { - uint8 decimal; - - int256 price; - - uint256 lastUpdateTime; - - uint256 pointlessStorageVar = 42; - - // We use 8 decimals unless set to 18 - function decimals() external view returns (uint8) { - return decimal; - } - - function latestRoundData() - external - view - returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) - { - // Expensive SLOAD loop that hits the block gas limit before completing - for (uint256 i = 0; i < 1000000; i++) { - uint256 unusedVar = pointlessStorageVar + i; - } - - return (0, price, 0, lastUpdateTime, 0); - } - - function setDecimals(uint8 _decimals) external { - decimal = _decimals; - } - - function setPrice(int256 _price) external { - price = _price; - } - - function setUpdatedAt(uint256 _updatedAt) external { - lastUpdateTime = _updatedAt; - } -} diff --git a/contracts/test/TestContracts/GasGuzzlerToken.sol b/contracts/test/TestContracts/GasGuzzlerToken.sol deleted file mode 100644 index ef1cbd60f..000000000 --- a/contracts/test/TestContracts/GasGuzzlerToken.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 - -pragma solidity 0.8.24; - -// Mock token that uses all available gas on exchange rate calls. -// This contract code is etched over LST token addresses in mainnet fork tests. -// Has exchange rate functions for WSTETH and RETH. -contract GasGuzzlerToken { - uint256 pointlessStorageVar = 42; - - // RETH exchange rate getter - function getExchangeRate() external view returns (uint256) { - // Expensive SLOAD loop that hits the block gas limit before completing - for (uint256 i = 0; i < 1000000; i++) { - uint256 unusedVar = pointlessStorageVar + i; - } - return 11e17; - } - - // WSTETH exchange rate getter - function stEthPerToken() external view returns (uint256) { - // Expensive SLOAD loop that hits the block gas limit before completing - for (uint256 i = 0; i < 1000000; i++) { - uint256 unusedVar = pointlessStorageVar + i; - } - return 11e17; - } -} diff --git a/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol new file mode 100644 index 000000000..d2a05e163 --- /dev/null +++ b/contracts/test/TestContracts/Interfaces/IMockFXPriceFeed.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "src/Interfaces/IPriceFeed.sol"; + +interface IMockFXPriceFeed is IPriceFeed { + function REVERT_MSG() external view returns (string memory); + function setPrice(uint256 _price) external; + function getPrice() external view returns (uint256); + function setValidPrice(bool valid) external; + function setL2SequencerUp(bool up) external; +} diff --git a/contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol b/contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol deleted file mode 100644 index bf2d65cac..000000000 --- a/contracts/test/TestContracts/Interfaces/IPriceFeedMock.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "src/Interfaces/IPriceFeed.sol"; - -interface IPriceFeedMock is IPriceFeed { - function setPrice(uint256 _price) external; -} diff --git a/contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol b/contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol deleted file mode 100644 index 53732c373..000000000 --- a/contracts/test/TestContracts/Interfaces/IPriceFeedTestnet.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/Interfaces/IPriceFeed.sol"; - -interface IPriceFeedTestnet is IPriceFeed { - function setPrice(uint256 _price) external returns (bool); - function getPrice() external view returns (uint256); -} diff --git a/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol b/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol index 31d5e1bb7..fbdb4a162 100644 --- a/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol +++ b/contracts/test/TestContracts/Interfaces/ITroveManagerTester.sol @@ -31,10 +31,10 @@ interface ITroveManagerTester is ITroveManager { function checkBelowCriticalThreshold(uint256 _price) external view returns (bool); function computeICR(uint256 _coll, uint256 _debt, uint256 _price) external pure returns (uint256); - function getCollGasCompensation(uint256 _coll) external pure returns (uint256); + function getCollGasCompensation(uint256 _coll) external view returns (uint256); function getCollGasCompensation(uint256 _coll, uint256 _debt, uint256 _boldInSPForOffsets) external - pure + view returns (uint256); function getEffectiveRedemptionFeeInColl(uint256 _redeemAmount, uint256 _price) external view returns (uint256); diff --git a/contracts/test/TestContracts/InvariantsTestHandler.t.sol b/contracts/test/TestContracts/InvariantsTestHandler.t.sol index 36e86535b..2a765e291 100644 --- a/contracts/test/TestContracts/InvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/InvariantsTestHandler.t.sol @@ -24,30 +24,18 @@ import {Assertions} from "./Assertions.sol"; import {BaseHandler} from "./BaseHandler.sol"; import {BaseMultiCollateralTest} from "./BaseMultiCollateralTest.sol"; import {TestDeployer} from "./Deployment.t.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; import { _100pct, _1pct, - COLL_GAS_COMPENSATION_CAP, - COLL_GAS_COMPENSATION_DIVISOR, DECIMAL_PRECISION, - ETH_GAS_COMPENSATION, - INITIAL_BASE_RATE, - INTEREST_RATE_ADJ_COOLDOWN, - MAX_ANNUAL_BATCH_MANAGEMENT_FEE, - MAX_ANNUAL_INTEREST_RATE, - MIN_ANNUAL_INTEREST_RATE, - MIN_ANNUAL_INTEREST_RATE, - MIN_BOLD_IN_SP, - MIN_DEBT, - MIN_INTEREST_RATE_CHANGE_PERIOD, ONE_MINUTE, ONE_YEAR, - REDEMPTION_BETA, - REDEMPTION_FEE_FLOOR, - REDEMPTION_MINUTE_DECAY_FACTOR, - SP_YIELD_SPLIT, - UPFRONT_INTEREST_PERIOD, + MAX_ANNUAL_INTEREST_RATE, + MAX_ANNUAL_BATCH_MANAGEMENT_FEE, + MIN_INTEREST_RATE_CHANGE_PERIOD, + INTEREST_RATE_ADJ_COOLDOWN, URGENT_REDEMPTION_BONUS } from "src/Dependencies/Constants.sol"; @@ -57,21 +45,12 @@ uint256 constant TIME_DELTA_MAX = ONE_YEAR; uint256 constant BORROWED_MIN = 0 ether; // Sometimes try borrowing too little uint256 constant BORROWED_MAX = 100_000 ether; -uint256 constant INTEREST_RATE_MIN = MIN_ANNUAL_INTEREST_RATE - 1; // Sometimes try rates lower than the min -uint256 constant INTEREST_RATE_MAX = MAX_ANNUAL_INTEREST_RATE + 1; // Sometimes try rates exceeding the max - uint256 constant ICR_MIN = 1.1 ether - 1; uint256 constant ICR_MAX = 3 ether; uint256 constant TCR_MIN = 0.9 ether; uint256 constant TCR_MAX = 3 ether; -uint256 constant BATCH_MANAGEMENT_FEE_MIN = 0; -uint256 constant BATCH_MANAGEMENT_FEE_MAX = MAX_ANNUAL_BATCH_MANAGEMENT_FEE + 1; // Sometimes try too high - -uint256 constant RATE_CHANGE_PERIOD_MIN = MIN_INTEREST_RATE_CHANGE_PERIOD - 1; // Sometimes try too low -uint256 constant RATE_CHANGE_PERIOD_MAX = TIME_DELTA_MAX; - enum AdjustedTroveProperties { onlyColl, onlyDebt, @@ -361,7 +340,7 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe uint256 _handlerBold; // Used to keep track of base rate - uint256 _baseRate = INITIAL_BASE_RATE; + uint256 _baseRate; uint256 _timeSinceLastRedemption = 0; // Used to keep track of mintable interest @@ -389,6 +368,25 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe // Urgent redemption transient state UrgentRedemptionTransientState _urgentRedemption; + uint256 constant BATCH_MANAGEMENT_FEE_MIN = 0; + uint256 constant BATCH_MANAGEMENT_FEE_MAX = MAX_ANNUAL_BATCH_MANAGEMENT_FEE + 1; // Sometimes try too high + uint256 constant INTEREST_RATE_MAX = MAX_ANNUAL_INTEREST_RATE + 1; // Sometimes try rates exceeding the max + uint256 constant RATE_CHANGE_PERIOD_MIN = MIN_INTEREST_RATE_CHANGE_PERIOD - 1; // Sometimes try too low + + // System params-based variables + uint256 immutable INTEREST_RATE_MIN; + uint256 immutable RATE_CHANGE_PERIOD_MAX = TIME_DELTA_MAX; + uint256 immutable ETH_GAS_COMPENSATION; + uint256 immutable MIN_ANNUAL_INTEREST_RATE; + uint256 immutable MIN_DEBT; + uint256 immutable MIN_BOLD_IN_SP; + uint256 immutable REDEMPTION_MINUTE_DECAY_FACTOR; + uint256 immutable REDEMPTION_BETA; + uint256 immutable REDEMPTION_FEE_FLOOR; + uint256 immutable SP_YIELD_SPLIT; + uint256 immutable COLL_GAS_COMPENSATION_DIVISOR; + uint256 immutable COLL_GAS_COMPENSATION_CAP; + constructor(Contracts memory contracts, bool assumeNoExpectedFailures) { _functionCaller = new FunctionCaller(); _assumeNoExpectedFailures = assumeNoExpectedFailures; @@ -404,6 +402,20 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe LIQ_PENALTY_REDIST[i] = c.troveManager.get_LIQUIDATION_PENALTY_REDISTRIBUTION(); _price[i] = c.priceFeed.getPrice(); } + + // Set system params-based variables + INTEREST_RATE_MIN = systemParams.MIN_ANNUAL_INTEREST_RATE() - 1; // Sometimes try rates lower than the min + _baseRate = systemParams.INITIAL_BASE_RATE(); + ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + MIN_DEBT = systemParams.MIN_DEBT(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); + REDEMPTION_MINUTE_DECAY_FACTOR = systemParams.REDEMPTION_MINUTE_DECAY_FACTOR(); + REDEMPTION_BETA = systemParams.REDEMPTION_BETA(); + REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); + SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); + COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); + COLL_GAS_COMPENSATION_CAP = systemParams.COLL_GAS_COMPENSATION_CAP(); } ////////////////////////////////////////////// @@ -2399,11 +2411,11 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe return _baseRate * decaySinceLastRedemption / DECIMAL_PRECISION; } - function _getBaseRateIncrease(uint256 boldSupply, uint256 redeemed) internal pure returns (uint256) { + function _getBaseRateIncrease(uint256 boldSupply, uint256 redeemed) internal view returns (uint256) { return boldSupply > 0 ? redeemed * DECIMAL_PRECISION / boldSupply / REDEMPTION_BETA : 0; } - function _getRedemptionRate(uint256 baseRate) internal pure returns (uint256) { + function _getRedemptionRate(uint256 baseRate) internal view returns (uint256) { return Math.min(REDEMPTION_FEE_FLOOR + baseRate, _100pct); } @@ -3006,11 +3018,11 @@ contract InvariantsTestHandler is Assertions, BaseHandler, BaseMultiCollateralTe return (selector, "AddRemoveManagers.NotOwnerNorRemoveManager()"); } - if (selector == AddressesRegistry.InvalidMCR.selector) { + if (selector == ISystemParams.InvalidMCR.selector) { return (selector, "BorrowerOperations.InvalidMCR()"); } - if (selector == AddressesRegistry.InvalidSCR.selector) { + if (selector == ISystemParams.InvalidSCR.selector) { return (selector, "BorrowerOperations.InvalidSCR()"); } diff --git a/contracts/test/TestContracts/MockFXPriceFeed.sol b/contracts/test/TestContracts/MockFXPriceFeed.sol new file mode 100644 index 000000000..1c4b7a493 --- /dev/null +++ b/contracts/test/TestContracts/MockFXPriceFeed.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./Interfaces/IMockFXPriceFeed.sol"; + +/* +* Mock FXPriceFeed contract for testing purposes. +* The price is simply set manually and saved in a state variable. +*/ +contract MockFXPriceFeed is IMockFXPriceFeed { + + string private _revertMsg = "MockFXPriceFeed: no valid price"; + uint256 private _price = 200 * 1e18; + bool private _hasValidPrice = true; + bool private _isL2SequencerUp = true; + + function getPrice() external view override returns (uint256) { + return _price; + } + + function setValidPrice(bool valid) external { + _hasValidPrice = valid; + } + + function setPrice(uint256 price) external { + _price = price; + } + + function setL2SequencerUp(bool up) external { + _isL2SequencerUp = up; + } + + function fetchPrice() external view override returns (uint256) { + require(_hasValidPrice, _revertMsg); + + return _price; + } + + function isL2SequencerUp() external view override returns (bool) { + return _isL2SequencerUp; + } + + function REVERT_MSG() external view override returns (string memory) { + return _revertMsg; + } +} diff --git a/contracts/test/TestContracts/PriceFeedMock.sol b/contracts/test/TestContracts/PriceFeedMock.sol deleted file mode 100644 index 1c8e46fa1..000000000 --- a/contracts/test/TestContracts/PriceFeedMock.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./Interfaces/IPriceFeedMock.sol"; - -contract PriceFeedMock is IPriceFeedMock { - uint256 private PRICE; - - function setPrice(uint256 _price) external { - PRICE = _price; - } - - function getPrice() external view returns (uint256 _price) { - return PRICE; - } - - function fetchPrice() external view returns (uint256, bool) { - return (PRICE, false); - } - - function fetchRedemptionPrice() external view returns (uint256, bool) { - return (PRICE, false); - } - - function lastGoodPrice() external view returns (uint256) { - return PRICE; - } - - function getEthUsdStalenessThreshold() external pure returns (uint256) { - return 0; - } -} diff --git a/contracts/test/TestContracts/PriceFeedTestnet.sol b/contracts/test/TestContracts/PriceFeedTestnet.sol deleted file mode 100644 index 1b5bd89e0..000000000 --- a/contracts/test/TestContracts/PriceFeedTestnet.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./Interfaces/IPriceFeedTestnet.sol"; - -/* -* PriceFeed placeholder for testnet and development. The price is simply set manually and saved in a state -* variable. The contract does not connect to a live Chainlink price feed. -*/ -contract PriceFeedTestnet is IPriceFeedTestnet { - event LastGoodPriceUpdated(uint256 _lastGoodPrice); - - uint256 private _price = 200 * 1e18; - - // --- Functions --- - - // View price getter for simplicity in tests - function getPrice() external view override returns (uint256) { - return _price; - } - - function lastGoodPrice() external view returns (uint256) { - return _price; - } - - function fetchPrice() external override returns (uint256, bool) { - // Fire an event just like the mainnet version would. - // This lets the subgraph rely on events to get the latest price even when developing locally. - emit LastGoodPriceUpdated(_price); - return (_price, false); - } - - function fetchRedemptionPrice() external override returns (uint256, bool) { - // Fire an event just like the mainnet version would. - // This lets the subgraph rely on events to get the latest price even when developing locally. - emit LastGoodPriceUpdated(_price); - return (_price, false); - } - - // Manual external price setter. - function setPrice(uint256 price) external returns (bool) { - _price = price; - return true; - } -} diff --git a/contracts/test/TestContracts/RETHTokenMock.sol b/contracts/test/TestContracts/RETHTokenMock.sol deleted file mode 100644 index 074c8ca59..000000000 --- a/contracts/test/TestContracts/RETHTokenMock.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/Interfaces/IRETHToken.sol"; -import "lib/forge-std/src/console2.sol"; - -contract RETHTokenMock is IRETHToken { - uint256 ethPerReth; - - function getExchangeRate() external view returns (uint256) { - return ethPerReth; - } - - function setExchangeRate(uint256 _ethPerReth) external { - ethPerReth = _ethPerReth; - } -} diff --git a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol index f8d821ec0..3304fe2ac 100644 --- a/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol +++ b/contracts/test/TestContracts/SPInvariantsTestHandler.t.sol @@ -8,9 +8,10 @@ import {IStabilityPool} from "src/Interfaces/IStabilityPool.sol"; import {ITroveManager} from "src/Interfaces/ITroveManager.sol"; import {ICollSurplusPool} from "src/Interfaces/ICollSurplusPool.sol"; import {HintHelpers} from "src/HintHelpers.sol"; -import {IPriceFeedTestnet} from "./Interfaces/IPriceFeedTestnet.sol"; +import {IMockFXPriceFeed} from "./Interfaces/IMockFXPriceFeed.sol"; import {ITroveManagerTester} from "./Interfaces/ITroveManagerTester.sol"; import {LiquityMath} from "src/Dependencies/LiquityMath.sol"; +import {ISystemParams} from "src/Interfaces/ISystemParams.sol"; import {mulDivCeil} from "../Utils/Math.sol"; import {StringFormatting} from "../Utils/StringFormatting.sol"; import {TroveId} from "../Utils/TroveId.sol"; @@ -19,11 +20,7 @@ import {BaseHandler} from "./BaseHandler.sol"; import { DECIMAL_PRECISION, _1pct, - _100pct, - ETH_GAS_COMPENSATION, - COLL_GAS_COMPENSATION_DIVISOR, - MIN_ANNUAL_INTEREST_RATE, - MIN_BOLD_IN_SP + _100pct } from "src/Dependencies/Constants.sol"; using {mulDivCeil} for uint256; @@ -44,24 +41,32 @@ contract SPInvariantsTestHandler is BaseHandler, TroveId { IBoldToken boldToken; IBorrowerOperations borrowerOperations; IERC20 collateralToken; - IPriceFeedTestnet priceFeed; + IMockFXPriceFeed priceFeed; IStabilityPool stabilityPool; ITroveManagerTester troveManager; ICollSurplusPool collSurplusPool; + ISystemParams systemParams; } IBoldToken immutable boldToken; IBorrowerOperations immutable borrowerOperations; IERC20 collateralToken; - IPriceFeedTestnet immutable priceFeed; + IMockFXPriceFeed immutable priceFeed; IStabilityPool immutable stabilityPool; ITroveManagerTester immutable troveManager; ICollSurplusPool immutable collSurplusPool; + ISystemParams immutable systemParams; HintHelpers immutable hintHelpers; uint256 immutable initialPrice; mapping(address owner => uint256) troveIndexOf; + // System params + uint256 immutable ETH_GAS_COMPENSATION; + uint256 immutable MIN_ANNUAL_INTEREST_RATE; + uint256 immutable MIN_BOLD_IN_SP; + uint256 immutable COLL_GAS_COMPENSATION_DIVISOR; + // Ghost variables uint256 myBold = 0; uint256 spBold = 0; @@ -78,9 +83,16 @@ contract SPInvariantsTestHandler is BaseHandler, TroveId { stabilityPool = contracts.stabilityPool; troveManager = contracts.troveManager; collSurplusPool = contracts.collSurplusPool; + systemParams = contracts.systemParams; hintHelpers = hintHelpers_; initialPrice = priceFeed.getPrice(); + + // Initialize system params + ETH_GAS_COMPENSATION = systemParams.ETH_GAS_COMPENSATION(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); + COLL_GAS_COMPENSATION_DIVISOR = systemParams.COLL_GAS_COMPENSATION_DIVISOR(); } function openTrove(uint256 borrowed) external returns (uint256 debt) { diff --git a/contracts/test/TestContracts/StableTokenV3.sol b/contracts/test/TestContracts/StableTokenV3.sol new file mode 100644 index 000000000..bfe0af954 --- /dev/null +++ b/contracts/test/TestContracts/StableTokenV3.sol @@ -0,0 +1,292 @@ +// // SPDX-License-Identifier: GPL-3.0-or-later +// // solhint-disable gas-custom-errors +pragma solidity 0.8.24; + +import { ERC20PermitUpgradeable } from "./patched/ERC20PermitUpgradeable.sol"; +import { ERC20Upgradeable } from "./patched/ERC20Upgradeable.sol"; + +import { IStableTokenV3 } from "src/Interfaces/IStableTokenV3.sol"; + + +contract CalledByVm { + modifier onlyVm() { + require(msg.sender == address(0), "Only VM can call"); + _; + } +} + +/** + * @title ERC20 token with minting and burning permissiones to a minter and burner roles. + * Direct transfers between the protocol and the user are done by the operator role. + */ +contract StableTokenV3 is ERC20PermitUpgradeable, IStableTokenV3, CalledByVm { + /* ========================================================= */ + /* ==================== State Variables ==================== */ + /* ========================================================= */ + + // Deprecated storage slots for backwards compatibility with StableTokenV2 + // slither-disable-start constable-states + // solhint-disable-next-line var-name-mixedcase + address public deprecated_validators_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_broker_storage_slot__; + // solhint-disable-next-line var-name-mixedcase + address public deprecated_exchange_storage_slot__; + // slither-disable-end constable-states + + // Mapping of allowed addresses that can mint + mapping(address => bool) public isMinter; + // Mapping of allowed addresses that can burn + mapping(address => bool) public isBurner; + // Mapping of allowed addresses that can call the operator functions + // These functions are used to do direct transfers between the protocol and the user + // This will be the StabilityPools + mapping(address => bool) public isOperator; + + /* ========================================================= */ + /* ======================== Events ========================= */ + /* ========================================================= */ + + event MinterUpdated(address indexed minter, bool isMinter); + event BurnerUpdated(address indexed burner, bool isBurner); + event OperatorUpdated(address indexed operator, bool isOperator); + + /* ========================================================= */ + /* ====================== Modifiers ======================== */ + /* ========================================================= */ + + /// @dev Restricts a function so it can only be executed by an address that's allowed to mint. + modifier onlyMinter() { + address sender = _msgSender(); + require(isMinter[sender], "StableTokenV3: not allowed to mint"); + _; + } + + /// @dev Restricts a function so it can only be executed by an address that's allowed to burn. + modifier onlyBurner() { + address sender = _msgSender(); + require(isBurner[sender], "StableTokenV3: not allowed to burn"); + _; + } + + /// @dev Restricts a function so it can only be executed by the operator role. + modifier onlyOperator() { + address sender = _msgSender(); + require(isOperator[sender], "StableTokenV3: not allowed to call only by operator"); + _; + } + + /* ========================================================= */ + /* ====================== Constructor ====================== */ + /* ========================================================= */ + + /** + * @notice The constructor for the StableTokenV3 contract. + * @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 IStableTokenV3 + function initialize( + // slither-disable-start shadowing-local + string memory _name, + string memory _symbol, + address _initialOwner, + // slither-disable-end shadowing-local + address[] memory initialBalanceAddresses, + uint256[] memory initialBalanceValues, + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) external reinitializer(3) { + __ERC20_init_unchained(_name, _symbol); + __ERC20Permit_init(_symbol); + _transferOwnership(_initialOwner); + + require(initialBalanceAddresses.length == initialBalanceValues.length, "Array length mismatch"); + for (uint256 i = 0; i < initialBalanceAddresses.length; i += 1) { + _mint(initialBalanceAddresses[i], initialBalanceValues[i]); + } + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /// @inheritdoc IStableTokenV3 + function initializeV3( + address[] memory _minters, + address[] memory _burners, + address[] memory _operators + ) public reinitializer(3) onlyOwner { + for (uint256 i = 0; i < _minters.length; i += 1) { + _setMinter(_minters[i], true); + } + for (uint256 i = 0; i < _burners.length; i += 1) { + _setBurner(_burners[i], true); + } + for (uint256 i = 0; i < _operators.length; i += 1) { + _setOperator(_operators[i], true); + } + } + + /* ============================================================ */ + /* ==================== Mutative Functions ==================== */ + /* ============================================================ */ + + /// @inheritdoc IStableTokenV3 + function setOperator(address _operator, bool _isOperator) external onlyOwner { + _setOperator(_operator, _isOperator); + } + + /// @inheritdoc IStableTokenV3 + function setMinter(address _minter, bool _isMinter) external onlyOwner { + _setMinter(_minter, _isMinter); + } + + /// @inheritdoc IStableTokenV3 + function setBurner(address _burner, bool _isBurner) external onlyOwner { + _setBurner(_burner, _isBurner); + } + + /// @inheritdoc IStableTokenV3 + function mint(address to, uint256 value) external onlyMinter returns (bool) { + _mint(to, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function burn(uint256 value) external onlyBurner returns (bool) { + _burn(msg.sender, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function burn(address account, uint256 value) external onlyBurner returns (bool) { + _burn(account, value); + return true; + } + + /// @inheritdoc IStableTokenV3 + function sendToPool(address _sender, address _poolAddress, uint256 _amount) external onlyOperator { + _transfer(_sender, _poolAddress, _amount); + } + + /// @inheritdoc IStableTokenV3 + function returnFromPool(address _poolAddress, address _receiver, uint256 _amount) external onlyOperator { + _transfer(_poolAddress, _receiver, _amount); + } + + /// @inheritdoc IStableTokenV3 + function transferFrom( + address from, + address to, + uint256 amount + ) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transferFrom(from, to, amount); + } + + /// @inheritdoc IStableTokenV3 + function transfer(address to, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.transfer(to, amount); + } + + /// @inheritdoc IStableTokenV3 + function balanceOf(address account) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.balanceOf(account); + } + + /// @inheritdoc IStableTokenV3 + function approve(address spender, uint256 amount) public override(ERC20Upgradeable, IStableTokenV3) returns (bool) { + return ERC20Upgradeable.approve(spender, amount); + } + + /// @inheritdoc IStableTokenV3 + function allowance( + address owner, + address spender + ) public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.allowance(owner, spender); + } + + /// @inheritdoc IStableTokenV3 + function totalSupply() public view override(ERC20Upgradeable, IStableTokenV3) returns (uint256) { + return ERC20Upgradeable.totalSupply(); + } + + /// @inheritdoc IStableTokenV3 + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public override(ERC20PermitUpgradeable, IStableTokenV3) { + ERC20PermitUpgradeable.permit(owner, spender, value, deadline, v, r, s); + } + + /// @inheritdoc IStableTokenV3 + function debitGasFees(address from, uint256 value) external onlyVm { + _burn(from, value); + } + + /// @inheritdoc IStableTokenV3 + function creditGasFees( + address refundRecipient, + address tipRecipient, + address, // _gatewayFeeRecipient, unused + address baseFeeRecipient, + uint256 refundAmount, + uint256 tipAmount, + uint256, // _gatewayFeeAmount, unused + uint256 baseFeeAmount + ) external onlyVm { + _mint(refundRecipient, refundAmount); + _mint(tipRecipient, tipAmount); + _mint(baseFeeRecipient, baseFeeAmount); + } + + /// @inheritdoc IStableTokenV3 + function creditGasFees(address[] calldata recipients, uint256[] calldata amounts) external onlyVm { + require(recipients.length == amounts.length, "StableTokenV3: recipients and amounts must be the same length."); + + for (uint256 i = 0; i < recipients.length; i++) { + _mint(recipients[i], amounts[i]); + } + } + + /* =========================================================== */ + /* ==================== Private Functions ==================== */ + /* =========================================================== */ + + function _setOperator(address _operator, bool _isOperator) internal { + isOperator[_operator] = _isOperator; + emit OperatorUpdated(_operator, _isOperator); + } + + function _setMinter(address _minter, bool _isMinter) internal { + isMinter[_minter] = _isMinter; + emit MinterUpdated(_minter, _isMinter); + } + + function _setBurner(address _burner, bool _isBurner) internal { + isBurner[_burner] = _isBurner; + emit BurnerUpdated(_burner, _isBurner); + } +} \ No newline at end of file diff --git a/contracts/test/TestContracts/TroveManagerTester.t.sol b/contracts/test/TestContracts/TroveManagerTester.t.sol index 87242aff6..6c337ec70 100644 --- a/contracts/test/TestContracts/TroveManagerTester.t.sol +++ b/contracts/test/TestContracts/TroveManagerTester.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.24; import "src/Interfaces/IAddressesRegistry.sol"; import "src/Interfaces/ICollateralRegistry.sol"; +import "src/Interfaces/ISystemParams.sol"; import "src/TroveManager.sol"; import "./Interfaces/ITroveManagerTester.sol"; @@ -13,11 +14,8 @@ for testing the parent's internal functions. */ contract TroveManagerTester is ITroveManagerTester, TroveManager { uint256 constant STALE_TROVE_DURATION = 90 days; - // Extra buffer of collateral ratio to join a batch or adjust a trove inside a batch (on top of MCR) - uint256 public immutable BCR; - constructor(IAddressesRegistry _addressesRegistry) TroveManager(_addressesRegistry) { - BCR = _addressesRegistry.BCR(); + constructor(IAddressesRegistry _addressesRegistry, ISystemParams _systemParams) TroveManager(_addressesRegistry, _systemParams) { } // Single liquidation function. Closes the trove if its ICR is lower than the minimum collateral ratio. @@ -28,27 +26,27 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } function get_CCR() external view returns (uint256) { - return CCR; + return systemParams.CCR(); } function get_MCR() external view returns (uint256) { - return MCR; + return systemParams.MCR(); } function get_BCR() external view returns (uint256) { - return BCR; + return systemParams.BCR(); } function get_SCR() external view returns (uint256) { - return SCR; + return systemParams.SCR(); } function get_LIQUIDATION_PENALTY_SP() external view returns (uint256) { - return LIQUIDATION_PENALTY_SP; + return systemParams.LIQUIDATION_PENALTY_SP(); } function get_LIQUIDATION_PENALTY_REDISTRIBUTION() external view returns (uint256) { - return LIQUIDATION_PENALTY_REDISTRIBUTION; + return systemParams.LIQUIDATION_PENALTY_REDISTRIBUTION(); } function getBoldToken() external view returns (IBoldToken) { @@ -96,7 +94,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { } function checkBelowCriticalThreshold(uint256 _price) external view override returns (bool) { - return _checkBelowCriticalThreshold(_price, CCR); + return _checkBelowCriticalThreshold(_price, systemParams.CCR()); } function computeICR(uint256 _coll, uint256 _debt, uint256 _price) external pure returns (uint256) { @@ -105,7 +103,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { function getCollGasCompensation(uint256 _entireColl, uint256 _entireDebt, uint256 _boldInSPForOffsets) external - pure + view returns (uint256) { uint256 collSubjectToGasCompensation = _entireColl; @@ -115,12 +113,16 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return _getCollGasCompensation(collSubjectToGasCompensation); } - function getCollGasCompensation(uint256 _coll) external pure returns (uint256) { + function getCollGasCompensation(uint256 _coll) external view returns (uint256) { return _getCollGasCompensation(_coll); } - function getETHGasCompensation() external pure returns (uint256) { - return ETH_GAS_COMPENSATION; + function getETHGasCompensation() external view returns (uint256) { + return systemParams.ETH_GAS_COMPENSATION(); + } + + function get_MIN_DEBT() external view returns (uint256) { + return systemParams.MIN_DEBT(); } /* @@ -158,7 +160,7 @@ contract TroveManagerTester is ITroveManagerTester, TroveManager { return _calcUpfrontFee(openTrove.debtIncrease, avgInterestRate); } - function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal pure returns (uint256) { + function _calcUpfrontFee(uint256 _debt, uint256 _avgInterestRate) internal view returns (uint256) { return _calcInterest(_debt * _avgInterestRate, UPFRONT_INTEREST_PERIOD); } diff --git a/contracts/test/TestContracts/WSTETHTokenMock.sol b/contracts/test/TestContracts/WSTETHTokenMock.sol deleted file mode 100644 index 073f37eb7..000000000 --- a/contracts/test/TestContracts/WSTETHTokenMock.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "src/Interfaces/IWSTETH.sol"; - -contract WSTETHTokenMock is IWSTETH { - function stEthPerToken() external pure returns (uint256) { - return 0; - } - - function wrap(uint256 _stETHAmount) external pure returns (uint256) { - return _stETHAmount; - } - - function unwrap(uint256 _wstETHAmount) external pure returns (uint256) { - return _wstETHAmount; - } - - function getWstETHByStETH(uint256 _stETHAmount) external pure returns (uint256) { - return _stETHAmount; - } - - function getStETHByWstETH(uint256 _wstETHAmount) external pure returns (uint256) { - return _wstETHAmount; - } - - function tokensPerStEth() external pure returns (uint256) { - return 0; - } -} diff --git a/contracts/test/TestContracts/patched/ERC20PermitUpgradeable.sol b/contracts/test/TestContracts/patched/ERC20PermitUpgradeable.sol new file mode 100644 index 000000000..d00d309a4 --- /dev/null +++ b/contracts/test/TestContracts/patched/ERC20PermitUpgradeable.sol @@ -0,0 +1,117 @@ +// 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, + * and only changes the import of ERC20Upgradeable to be the local one + * which is modified in order to keep storage variables consistent + * with the pervious implementation of StableToken. + */ + +pragma solidity ^0.8.0; + +import "./ERC20Upgradeable.sol"; + +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/draft-IERC20PermitUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/utils/cryptography/ECDSAUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/utils/cryptography/EIP712Upgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/utils/CountersUpgradeable.sol"; + +/** + * @dev Implementation of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on `{IERC20-approve}`, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ + * + * @custom:storage-size 51 + */ +abstract contract ERC20PermitUpgradeable is ERC20Upgradeable, IERC20PermitUpgradeable, EIP712Upgradeable { + using CountersUpgradeable for CountersUpgradeable.Counter; + + mapping(address => CountersUpgradeable.Counter) private _nonces; + + // solhint-disable-next-line var-name-mixedcase + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + /** + * @dev In previous versions `_PERMIT_TYPEHASH` was declared as `immutable`. + * However, to ensure consistency with the upgradeable transpiler, we will continue + * 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"`. + * + * It's a good idea to use the same `name` that is defined as the ERC20 token name. + */ + // solhint-disable-next-line func-name-mixedcase + function __ERC20Permit_init(string memory name) internal onlyInitializing { + __EIP712_init_unchained(name, "1"); + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC20Permit_init_unchained(string memory) internal onlyInitializing {} + + /** + * @dev See {IERC20Permit-permit}. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + virtual + override + { + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSAUpgradeable.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _approve(owner, spender, value); + } + + /** + * @dev See {IERC20Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + /** + * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consume a nonce": return the current value and increment. + * + * _Available since v4.1._ + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + CountersUpgradeable.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /** + * @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[49] private __gap; +} diff --git a/contracts/test/TestContracts/patched/ERC20Upgradeable.sol b/contracts/test/TestContracts/patched/ERC20Upgradeable.sol new file mode 100644 index 000000000..391a3eb8d --- /dev/null +++ b/contracts/test/TestContracts/patched/ERC20Upgradeable.sol @@ -0,0 +1,393 @@ +// 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 + * the ordering of storage variables to keep it consistent with the existing + * StableToken, so this can act as a new implementation for the proxy. + */ + +pragma solidity ^0.8.0; + +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "openzeppelin-contracts/contracts/access/Ownable.sol"; + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin Contracts guidelines: functions revert + * instead returning `false` on failure. This behavior is nonetheless + * conventional and does not conflict with the expectations of ERC20 + * applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20Upgradeable is Ownable, Initializable, IERC20Upgradeable, IERC20MetadataUpgradeable { + // solhint-disable var-name-mixedcase + address private __deprecated_registry_storage_slot__; + string private _name; + string private _symbol; + uint8 private __deprecated_decimals_storage_slot__; + + mapping(address => uint256) private _balances; + uint256 private _totalSupply; + mapping(address => mapping(address => uint256)) private _allowances; + + uint256[4] private __deprecated_inflationState_storage_slot__; + bytes32 private __deprecated_exchangeRegistryId_storage_slot__; + + // solhint-enable var-name-mixedcase + + /** + * @dev Sets the values for {name} and {symbol}. + * + * The default value of {decimals} is 18. To select a different value for + * {decimals} you should overload it. + * + * All two of these values are immutable: they can only be set once during + * construction. + */ + + // solhint-disable-next-line func-name-mixedcase + function __ERC20_init(string memory name_, string memory symbol_) internal onlyInitializing { + __ERC20_init_unchained(name_, symbol_); + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC20_init_unchained(string memory name_, string memory symbol_) internal onlyInitializing { + _name = name_; + _symbol = symbol_; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5.05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless this function is + * overridden; + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view virtual override returns (uint8) { + return 18; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view virtual override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `to` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address to, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * NOTE: If `amount` is the maximum `uint256`, the allowance is not updated on + * `transferFrom`. This is semantically equivalent to an infinite approval. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}. + * + * NOTE: Does not update the allowance if the current allowance + * is the maximum `uint256`. + * + * Requirements: + * + * - `from` and `to` cannot be the zero address. + * - `from` must have a balance of at least `amount`. + * - 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) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, allowance(owner, spender) + addedValue); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + address owner = _msgSender(); + uint256 currentAllowance = allowance(owner, spender); + require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + unchecked { + _approve(owner, spender, currentAllowance - subtractedValue); + } + + return true; + } + + /** + * @dev Moves `amount` of tokens from `from` to `to`. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `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 { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + /** + * @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements: + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + 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"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Updates `owner` s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance amount in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Might emit an {Approval} event. + */ + 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"); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * 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 {} + + /** + * @dev Hook that is called after any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * has been transferred to `to`. + * - when `from` is zero, `amount` tokens have been minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens have been burned. + * - `from` and `to` are never both zero. + * + * 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 {} + + /** + * @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[40] private __gap; +} diff --git a/contracts/test/Utils/E2EHelpers.sol b/contracts/test/Utils/E2EHelpers.sol deleted file mode 100644 index f9026c135..000000000 --- a/contracts/test/Utils/E2EHelpers.sol +++ /dev/null @@ -1,339 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.24; - -import {Vm} from "forge-std/Vm.sol"; -import {Test} from "forge-std/Test.sol"; -import {IERC20Metadata as IERC20} from "openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import {Math} from "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {Governance} from "V2-gov/src/Governance.sol"; -import {ILeverageZapper} from "src/Zappers/Interfaces/ILeverageZapper.sol"; -import {IZapper} from "src/Zappers/Interfaces/IZapper.sol"; -import {IPriceFeed} from "src/Interfaces/IPriceFeed.sol"; -import {IBorrowerOperationsV1} from "../Interfaces/LiquityV1/IBorrowerOperationsV1.sol"; -import {IPriceFeedV1} from "../Interfaces/LiquityV1/IPriceFeedV1.sol"; -import {ISortedTrovesV1} from "../Interfaces/LiquityV1/ISortedTrovesV1.sol"; -import {ITroveManagerV1} from "../Interfaces/LiquityV1/ITroveManagerV1.sol"; -import {ERC20Faucet} from "../TestContracts/ERC20Faucet.sol"; - -import {StringEquality} from "./StringEquality.sol"; -import {UseDeployment} from "./UseDeployment.sol"; -import {TroveId} from "./TroveId.sol"; - -uint256 constant PRICE_TOLERANCE = 0.05 ether; - -address constant ETH_WHALE = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; // Anvil account #1 -address constant WETH_WHALE = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; // Anvil account #2 -address constant WSTETH_WHALE = 0xd85351181b3F264ee0FDFa94518464d7c3DefaDa; -address constant RETH_WHALE = 0xE76af4a9A3E71681F4C9BE600A0BA8D9d249175b; -address constant USDC_WHALE = 0x37305B1cD40574E4C5Ce33f8e8306Be057fD7341; -address constant LQTY_WHALE = 0xA78f19D7f331247212C6d9C0F27D3d9464D3604D; -address constant LUSD_WHALE = 0xcd6Eb888e76450eF584E8B51bB73c76ffBa21FF2; - -IBorrowerOperationsV1 constant mainnet_V1_borrowerOperations = - IBorrowerOperationsV1(0x24179CD81c9e782A4096035f7eC97fB8B783e007); -IPriceFeedV1 constant mainnet_V1_priceFeed = IPriceFeedV1(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); -ISortedTrovesV1 constant mainnet_V1_sortedTroves = ISortedTrovesV1(0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6); -ITroveManagerV1 constant mainnet_V1_troveManager = ITroveManagerV1(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); - -contract SideEffectFreeGetPriceHelper { - function _revert(bytes memory revertData) internal pure { - assembly { - revert(add(32, revertData), mload(revertData)) - } - } - - function throwPrice(IPriceFeed priceFeed) external { - (uint256 price,) = priceFeed.fetchPrice(); - _revert(abi.encode(price)); - } - - function throwPriceV1(IPriceFeedV1 priceFeed) external { - _revert(abi.encode(priceFeed.fetchPrice())); - } -} - -library SideEffectFreeGetPrice { - Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - - // Random address - address private constant helperDeployer = 0x9C82588e2B9229168aDbb55E730e0d20c0581a3B; - - // Deterministic address of first contract deployed by `helperDeployer` - SideEffectFreeGetPriceHelper private constant helper = - SideEffectFreeGetPriceHelper(0xc583097AE39B039fA74bB5bd6479469290B7cDe5); - - function deploy() internal { - if (address(helper).code.length == 0) { - vm.prank(helperDeployer); - new SideEffectFreeGetPriceHelper(); - } - } - - function getPrice(IPriceFeed priceFeed) internal returns (uint256) { - deploy(); - - try helper.throwPrice(priceFeed) { - revert("SideEffectFreeGetPrice: throwPrice() should have reverted"); - } catch (bytes memory revertData) { - return abi.decode(revertData, (uint256)); - } - } - - function getPrice(IPriceFeedV1 priceFeed) internal returns (uint256) { - deploy(); - - try helper.throwPriceV1(priceFeed) { - revert("SideEffectFreeGetPrice: throwPriceV1() should have reverted"); - } catch (bytes memory revertData) { - return abi.decode(revertData, (uint256)); - } - } -} - -contract E2EHelpers is Test, UseDeployment, TroveId { - using SideEffectFreeGetPrice for IPriceFeed; - using StringEquality for string; - - mapping(address token => address) providerOf; - - address[] _allocateLQTY_initiativesToReset; - address[] _allocateLQTY_initiatives; - int256[] _allocateLQTY_votes; - int256[] _allocateLQTY_vetos; - - function setUp() public virtual { - vm.skip(vm.envOr("FOUNDRY_PROFILE", string("")).notEq("e2e")); - vm.createSelectFork(vm.envString("E2E_RPC_URL")); - _loadDeploymentFromManifest("deployment-manifest.json"); - - vm.label(ETH_WHALE, "ETH_WHALE"); - vm.label(WETH_WHALE, "WETH_WHALE"); - vm.label(WSTETH_WHALE, "WSTETH_WHALE"); - vm.label(RETH_WHALE, "RETH_WHALE"); - vm.label(USDC_WHALE, "USDC_WHALE"); - vm.label(LQTY_WHALE, "LQTY_WHALE"); - vm.label(LUSD_WHALE, "LUSD_WHALE"); - - providerOf[WETH] = WETH_WHALE; - providerOf[WSTETH] = WSTETH_WHALE; - providerOf[RETH] = RETH_WHALE; - providerOf[USDC] = USDC_WHALE; - providerOf[LQTY] = LQTY_WHALE; - providerOf[LUSD] = LUSD_WHALE; - - vm.prank(WETH_WHALE); - weth.deposit{value: WETH_WHALE.balance}(); - - // Testnet - if (block.chainid != 1) { - address[5] memory coins = [WSTETH, RETH, USDC, LQTY, LUSD]; - - for (uint256 i = 0; i < coins.length; ++i) { - ERC20Faucet faucet = ERC20Faucet(coins[i]); - vm.prank(faucet.owner()); - faucet.mint(providerOf[coins[i]], 1e6 ether); - } - } - } - - function deal(address to, uint256 give) internal virtual override { - if (to.balance < give) { - vm.prank(ETH_WHALE); - payable(to).transfer(give - to.balance); - } else { - vm.prank(to); - payable(ETH_WHALE).transfer(to.balance - give); - } - } - - function deal(address token, address to, uint256 give) internal virtual override { - uint256 balance = IERC20(token).balanceOf(to); - address provider = providerOf[token]; - - assertNotEq(provider, address(0), string.concat("No provider for ", IERC20(token).symbol())); - - if (balance < give) { - vm.prank(provider); - IERC20(token).transfer(to, give - balance); - } else { - vm.prank(to); - IERC20(token).transfer(provider, balance - give); - } - } - - function deal(address token, address to, uint256 give, bool) internal virtual override { - deal(token, to, give); - } - - function _openTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) internal returns (uint256) { - IZapper.OpenTroveParams memory p; - p.owner = owner; - p.ownerIndex = ownerIndex; - p.boldAmount = boldAmount; - p.collAmount = boldAmount * 2 ether / branches[i].priceFeed.getPrice(); - p.annualInterestRate = 0.05 ether; - p.maxUpfrontFee = hintHelpers.predictOpenTroveUpfrontFee(i, boldAmount, p.annualInterestRate); - - (uint256 collTokenAmount, uint256 value) = branches[i].collToken == weth - ? (0, p.collAmount + ETH_GAS_COMPENSATION) - : (p.collAmount, ETH_GAS_COMPENSATION); - - deal(owner, value); - deal(address(branches[i].collToken), owner, collTokenAmount); - - vm.startPrank(owner); - branches[i].collToken.approve(address(branches[i].zapper), collTokenAmount); - branches[i].zapper.openTroveWithRawETH{value: value}(p); - vm.stopPrank(); - - return boldAmount; - } - - function _closeTroveFromCollateral(uint256 i, address owner, uint256 ownerIndex, bool _leveraged) - internal - returns (uint256) - { - IZapper zapper; - if (_leveraged) { - zapper = branches[i].leverageZapper; - } else { - zapper = branches[i].zapper; - } - uint256 troveId = addressToTroveIdThroughZapper(address(zapper), owner, ownerIndex); - uint256 debt = branches[i].troveManager.getLatestTroveData(troveId).entireDebt; - uint256 coll = branches[i].troveManager.getLatestTroveData(troveId).entireColl; - uint256 flashLoanAmount = debt * (1 ether + PRICE_TOLERANCE) / branches[i].priceFeed.getPrice(); - - vm.startPrank(owner); - zapper.closeTroveFromCollateral({ - _troveId: troveId, - _flashLoanAmount: flashLoanAmount, - _minExpectedCollateral: coll - flashLoanAmount - }); - vm.stopPrank(); - - return debt; - } - - function _openLeveragedTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) - internal - returns (uint256) - { - uint256 price = branches[i].priceFeed.getPrice(); - - ILeverageZapper.OpenLeveragedTroveParams memory p; - p.owner = owner; - p.ownerIndex = ownerIndex; - p.boldAmount = boldAmount; - p.collAmount = boldAmount * 0.5 ether / price; - p.flashLoanAmount = boldAmount * (1 ether - PRICE_TOLERANCE) / price; - p.annualInterestRate = 0.1 ether; - p.maxUpfrontFee = hintHelpers.predictOpenTroveUpfrontFee(i, boldAmount, p.annualInterestRate); - - (uint256 collTokenAmount, uint256 value) = branches[i].collToken == weth - ? (0, p.collAmount + ETH_GAS_COMPENSATION) - : (p.collAmount, ETH_GAS_COMPENSATION); - - deal(owner, value); - deal(address(branches[i].collToken), owner, collTokenAmount); - - vm.startPrank(owner); - branches[i].collToken.approve(address(branches[i].leverageZapper), collTokenAmount); - branches[i].leverageZapper.openLeveragedTroveWithRawETH{value: value}(p); - vm.stopPrank(); - - return boldAmount; - } - - function _leverUpTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) - internal - returns (uint256) - { - uint256 troveId = addressToTroveIdThroughZapper(address(branches[i].leverageZapper), owner, ownerIndex); - - ILeverageZapper.LeverUpTroveParams memory p = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - boldAmount: boldAmount, - flashLoanAmount: boldAmount * (1 ether - PRICE_TOLERANCE) / branches[i].priceFeed.getPrice(), - maxUpfrontFee: hintHelpers.predictAdjustTroveUpfrontFee(i, troveId, boldAmount) - }); - - vm.prank(owner); - branches[i].leverageZapper.leverUpTrove(p); - - return boldAmount; - } - - function _leverDownTrove(uint256 i, address owner, uint256 ownerIndex, uint256 boldAmount) - internal - returns (uint256) - { - uint256 troveId = addressToTroveIdThroughZapper(address(branches[i].leverageZapper), owner, ownerIndex); - uint256 debtBefore = branches[i].troveManager.getLatestTroveData(troveId).entireDebt; - - ILeverageZapper.LeverDownTroveParams memory p = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - minBoldAmount: boldAmount, - flashLoanAmount: boldAmount * (1 ether + PRICE_TOLERANCE) / branches[i].priceFeed.getPrice() - }); - - vm.prank(owner); - branches[i].leverageZapper.leverDownTrove(p); - - return debtBefore - branches[i].troveManager.getLatestTroveData(troveId).entireDebt; - } - - function _provideToSP(uint256 i, address depositor, uint256 boldAmount) internal { - deal(BOLD, depositor, boldAmount); - vm.prank(depositor); - branches[i].stabilityPool.provideToSP(boldAmount, false); - } - - function _claimFromSP(uint256 i, address depositor) internal { - vm.prank(depositor); - branches[i].stabilityPool.withdrawFromSP(0, true); - } - - function _depositLQTY(address voter, uint256 amount) internal { - deal(LQTY, voter, amount); - - vm.startPrank(voter); - lqty.approve(governance.deriveUserProxyAddress(voter), amount); - governance.depositLQTY(amount); - vm.stopPrank(); - } - - function _allocateLQTY_begin(address voter) internal { - vm.startPrank(voter); - } - - function _allocateLQTY_reset(address initiative) internal { - _allocateLQTY_initiativesToReset.push(initiative); - } - - function _allocateLQTY_vote(address initiative, int256 lqtyAmount) internal { - _allocateLQTY_initiatives.push(initiative); - _allocateLQTY_votes.push(lqtyAmount); - _allocateLQTY_vetos.push(); - } - - function _allocateLQTY_veto(address initiative, int256 lqtyAmount) internal { - _allocateLQTY_initiatives.push(initiative); - _allocateLQTY_votes.push(); - _allocateLQTY_vetos.push(lqtyAmount); - } - - function _allocateLQTY_end() internal { - governance.allocateLQTY( - _allocateLQTY_initiativesToReset, _allocateLQTY_initiatives, _allocateLQTY_votes, _allocateLQTY_vetos - ); - - delete _allocateLQTY_initiativesToReset; - delete _allocateLQTY_initiatives; - delete _allocateLQTY_votes; - delete _allocateLQTY_vetos; - - vm.stopPrank(); - } -} diff --git a/contracts/test/Utils/UniPriceConverterLog.sol b/contracts/test/Utils/UniPriceConverterLog.sol deleted file mode 100644 index f99b8dc57..000000000 --- a/contracts/test/Utils/UniPriceConverterLog.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "openzeppelin-contracts/contracts/utils/math/Math.sol"; - -import {UniPriceConverter} from "src/Zappers/Modules/Exchanges/UniswapV3/UniPriceConverter.sol"; -import {Logging} from "./Logging.sol"; -import {StringFormatting} from "./StringFormatting.sol"; -import {DECIMAL_PRECISION} from "src/Dependencies/Constants.sol"; - -import "forge-std/console2.sol"; - -contract UniPriceConverterLog is UniPriceConverter, Logging { - using StringFormatting for uint256; - - function priceToSqrtPrice(uint256 _price) public pure returns (uint256, uint256, uint256) { - uint256 sqrtPriceX96 = priceToSqrtPriceX96(_price); - - uint256 inversePrice = DECIMAL_PRECISION * DECIMAL_PRECISION / _price; - uint256 sqrtInversePriceX96 = priceToSqrtPriceX96(inversePrice); - - console2.log(""); - info("Price: ", _price.decimal()); - info("Uni sqrt Price: ", sqrtPriceX96.decimal()); - info("Inverse price: ", inversePrice.decimal()); - info("Uni sqrt inv Price: ", sqrtInversePriceX96.decimal()); - - console2.log("Price: ", _price); - console2.log("Uni sqrt Price: ", sqrtPriceX96); - console2.log("Inverse price: ", inversePrice); - console2.log("Uni sqrt inv Price: ", sqrtInversePriceX96); - - return (sqrtPriceX96, inversePrice, sqrtInversePriceX96); - } - - function sqrtPriceToPrice(uint160 _sqrtPriceX96) public pure returns (uint256 price) { - price = sqrtPriceX96ToPrice(_sqrtPriceX96); - - console2.log(""); - info("Uni sqrt Price: ", uint256(_sqrtPriceX96).decimal()); - info("Price: ", price.decimal()); - console2.log("Uni sqrt Price: ", uint256(_sqrtPriceX96)); - console2.log("Price: ", price); - } -} diff --git a/contracts/test/Utils/UseDeployment.sol b/contracts/test/Utils/UseDeployment.sol index 2c924f13d..5af44fd5f 100644 --- a/contracts/test/Utils/UseDeployment.sol +++ b/contracts/test/Utils/UseDeployment.sol @@ -8,8 +8,6 @@ import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import {IUserProxy} from "V2-gov/src/interfaces/IUserProxy.sol"; import {CurveV2GaugeRewards} from "V2-gov/src/CurveV2GaugeRewards.sol"; import {Governance} from "V2-gov/src/Governance.sol"; -import {ILeverageZapper} from "src/Zappers/Interfaces/ILeverageZapper.sol"; -import {IZapper} from "src/Zappers/Interfaces/IZapper.sol"; import {IActivePool} from "src/Interfaces/IActivePool.sol"; import {IAddressesRegistry} from "src/Interfaces/IAddressesRegistry.sol"; import {IBoldToken} from "src/Interfaces/IBoldToken.sol"; @@ -47,8 +45,6 @@ contract UseDeployment is CommonBase { IActivePool activePool; IDefaultPool defaultPool; IStabilityPool stabilityPool; - ILeverageZapper leverageZapper; - IZapper zapper; } address WETH; @@ -128,14 +124,7 @@ contract UseDeployment is CommonBase { sortedTroves: ISortedTroves(json.readAddress(string.concat(branch, ".sortedTroves"))), activePool: IActivePool(json.readAddress(string.concat(branch, ".activePool"))), defaultPool: IDefaultPool(json.readAddress(string.concat(branch, ".defaultPool"))), - stabilityPool: IStabilityPool(json.readAddress(string.concat(branch, ".stabilityPool"))), - leverageZapper: ILeverageZapper(json.readAddress(string.concat(branch, ".leverageZapper"))), - zapper: IZapper( - coalesce( - json.readAddress(string.concat(branch, ".wethZapper")), - json.readAddress(string.concat(branch, ".gasCompZapper")) - ) - ) + stabilityPool: IStabilityPool(json.readAddress(string.concat(branch, ".stabilityPool"))) }); vm.label(address(branches[i].priceFeed), "PriceFeed"); @@ -146,8 +135,6 @@ contract UseDeployment is CommonBase { vm.label(address(branches[i].activePool), "ActivePool"); vm.label(address(branches[i].defaultPool), "DefaultPool"); vm.label(address(branches[i].stabilityPool), "StabilityPool"); - vm.label(address(branches[i].leverageZapper), "LeverageZapper"); - vm.label(address(branches[i].zapper), "Zapper"); string memory collSymbol = branches[i].collToken.symbol(); if (collSymbol.eq("WETH")) { diff --git a/contracts/test/basicOps.t.sol b/contracts/test/basicOps.t.sol index 3c154f4c0..1e68064eb 100644 --- a/contracts/test/basicOps.t.sol +++ b/contracts/test/basicOps.t.sol @@ -42,6 +42,17 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 1); } + function testOpenTrove_whenNoValidPrice_shouldRevert() public { + priceFeed.setValidPrice(false); + + vm.startPrank(A); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + borrowerOperations.openTrove( + A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + } + function testCloseTrove() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -69,6 +80,34 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 1); } + function testCloseTrove_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + vm.startPrank(A); + borrowerOperations.openTrove( + A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + // Transfer some Bold to B so that B can close Trove accounting for interest and upfront fee + boldToken.transfer(B, 100e18); + vm.stopPrank(); + + vm.startPrank(B); + uint256 B_Id = borrowerOperations.openTrove( + B, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + + priceFeed.setValidPrice(false); + + vm.startPrank(B); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + borrowerOperations.closeTrove(B_Id); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + } + function testAdjustTrove() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -92,6 +131,37 @@ contract BasicOps is DevTestSetup { assertGt(coll_2, coll_1); } + function testAdjustTrove_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + vm.startPrank(A); + uint256 A_Id = borrowerOperations.openTrove( + A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + + uint256 initialDebt = troveManager.getTroveDebt(A_Id); + assertGt(initialDebt, 0); + uint256 initialColl = troveManager.getTroveColl(A_Id); + assertGt(initialColl, 0); + + priceFeed.setValidPrice(false); + + vm.startPrank(A); + uint256 upfrontFee = predictAdjustTroveUpfrontFee(A_Id, 500e18); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + borrowerOperations.adjustTrove( + A_Id, + 1e18, + true, + 500e18, + true, + upfrontFee + ); + vm.stopPrank(); + + assertEq(troveManager.getTroveDebt(A_Id), initialDebt); + assertEq(troveManager.getTroveColl(A_Id), initialColl); + } + function testRedeem() public { priceFeed.setPrice(2000e18); @@ -130,6 +200,38 @@ contract BasicOps is DevTestSetup { assertLt(coll_2, coll_1, "Coll mismatch after"); } + function testRedeem_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + + vm.startPrank(A); + borrowerOperations.openTrove( + A, 0, 5e18, 5_000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + uint256 B_Id = borrowerOperations.openTrove( + B, 0, 5e18, 4_000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + uint256 debt_1 = troveManager.getTroveDebt(B_Id); + assertGt(debt_1, 0, "Debt cannot be zero"); + uint256 coll_1 = troveManager.getTroveColl(B_Id); + assertGt(coll_1, 0, "Coll cannot be zero"); + vm.stopPrank(); + + vm.warp(block.timestamp + 7 days); + + priceFeed.setValidPrice(false); + + vm.startPrank(A); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + collateralRegistry.redeemCollateral(1000e18, 10, 1e18); + vm.stopPrank(); + + assertEq(troveManager.getTroveDebt(B_Id), debt_1, "Debt mismatch after"); + assertEq(troveManager.getTroveColl(B_Id), coll_1, "Coll mismatch after"); + } + function testLiquidation() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -145,7 +247,7 @@ contract BasicOps is DevTestSetup { // Price drops priceFeed.setPrice(1200e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Check CR_A < MCR and TCR > CCR assertLt(troveManager.getCurrentICR(A_Id, price), MCR); @@ -161,6 +263,39 @@ contract BasicOps is DevTestSetup { assertEq(trovesCount, 1); } + function testLiquidation_whenNoValidPrice_shouldRevert() public { + priceFeed.setPrice(2000e18); + + vm.startPrank(A); + uint256 A_Id = borrowerOperations.openTrove( + A, 0, 2e18, 2200e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove( + B, 0, 10e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) + ); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + + priceFeed.setPrice(1200e18); + uint256 price = priceFeed.fetchPrice(); + + assertLt(troveManager.getCurrentICR(A_Id, price), MCR); + assertGt(troveManager.getTCR(price), CCR); + + priceFeed.setValidPrice(false); + + vm.startPrank(B); + vm.expectRevert(bytes(priceFeed.REVERT_MSG())); + troveManager.liquidate(A_Id); + vm.stopPrank(); + + assertEq(troveManager.getTroveIdsCount(), 2); + } + function testSPDeposit() public { priceFeed.setPrice(2000e18); vm.startPrank(A); @@ -168,6 +303,9 @@ contract BasicOps is DevTestSetup { A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) ); + // Simulate a revert on the feed which shouldn't matter, as the price is not used for SP deposits + priceFeed.setValidPrice(false); + // A makes an SP deposit makeSPDepositAndClaim(A, 100e18); @@ -190,6 +328,9 @@ contract BasicOps is DevTestSetup { A, 0, 2e18, 2000e18, 0, 0, MIN_ANNUAL_INTEREST_RATE, 1000e18, address(0), address(0), address(0) ); + // Simulate a revert on the feed which shouldn't matter, as the price is not used for SP withdrawals + priceFeed.setValidPrice(false); + // A makes an SP deposit makeSPDepositAndClaim(A, 100e18); diff --git a/contracts/test/interestBatchManagement.t.sol b/contracts/test/interestBatchManagement.t.sol index 710ba81c8..9ab7736dc 100644 --- a/contracts/test/interestBatchManagement.t.sol +++ b/contracts/test/interestBatchManagement.t.sol @@ -1513,4 +1513,109 @@ contract InterestBatchManagementTest is DevTestSetup { borrowerOperations.adjustTrove(troveId, 1 ether, true, 100e18, false, 1000e18); vm.stopPrank(); } + + function testRemoveFromBatchDeletesInterestBatchManagerOf() public { + uint256 troveId = openTroveAndJoinBatchManager(); + + // Verify trove is in batch + address batchManagerAddress = borrowerOperations.interestBatchManagerOf(troveId); + assertEq(batchManagerAddress, B, "Trove should be in batch B"); + + // Remove from batch + vm.startPrank(A); + borrowerOperations.removeFromBatch(troveId, 4e16, 0, 0, 1e24); + vm.stopPrank(); + + // Verify mapping is deleted + assertEq(borrowerOperations.interestBatchManagerOf(troveId), address(0), "interestBatchManagerOf should be deleted"); + (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); + assertEq(tmBatchManagerAddress, address(0), "TM batch manager should be address(0)"); + } + + function testKickFromBatchDeletesInterestBatchManagerOf() public { + registerBatchManager({ + _account: B, + _minInterestRate: uint128(MIN_ANNUAL_INTEREST_RATE), + _maxInterestRate: uint128(MAX_ANNUAL_INTEREST_RATE), + _currentInterestRate: uint128(MAX_ANNUAL_INTEREST_RATE), + _fee: MAX_ANNUAL_BATCH_MANAGEMENT_FEE, + _minInterestRateChangePeriod: MIN_INTEREST_RATE_CHANGE_PERIOD + }); + + // Placeholder Trove so that the batch isn't wiped out fully when we redeem the target Trove later + uint256 placeholderTrove = openTroveAndJoinBatchManager({ + _troveOwner: C, + _coll: 1_000_000 ether, + _debt: MIN_DEBT, + _batchAddress: B, + _annualInterestRate: 0 // ignored + }); + + // Open the target Trove, the one we will make irredeemable + uint256 targetTrove = openTroveAndJoinBatchManager({ + _troveOwner: A, + _coll: 1_000_000 ether, + _debt: MIN_DEBT, + _batchAddress: B, + _annualInterestRate: 0 // ignored + }); + + // Verify trove is in batch + address batchManagerAddress = borrowerOperations.interestBatchManagerOf(targetTrove); + assertEq(batchManagerAddress, B, "Trove should be in batch B"); + + // Another Trove to provide funds and keep the average interest rate high, + // which speeds up our manipulation of the batch:shares ratio + openTroveHelper({ + _account: A, + _index: 1, + _coll: 1_000_000 ether, + _boldAmount: 10_000_000 ether, + _annualInterestRate: MAX_ANNUAL_INTEREST_RATE + }); + + // Increase the batch:shares ratio past the limit + for (uint256 i = 1;; ++i) { + skip(MIN_INTEREST_RATE_CHANGE_PERIOD); + setBatchInterestRate(B, MAX_ANNUAL_INTEREST_RATE - i % 2); + + (uint256 debt,,,,,,, uint256 shares) = troveManager.getBatch(B); + if (shares * MAX_BATCH_SHARES_RATIO < debt) break; + + // Keep debt low to minimize interest and maintain healthy TCR + repayBold(A, targetTrove, troveManager.getTroveEntireDebt(targetTrove) - MIN_DEBT); + repayBold(A, placeholderTrove, troveManager.getTroveEntireDebt(placeholderTrove) - MIN_DEBT); + } + + // Make a zombie out of the target Trove + skip(MIN_INTEREST_RATE_CHANGE_PERIOD); + setBatchInterestRate(B, MIN_ANNUAL_INTEREST_RATE); + redeem(A, troveManager.getTroveEntireDebt(targetTrove)); + assertTrue(troveManager.checkTroveIsZombie(targetTrove), "not a zombie"); + + // Open a Trove to be liquidated + (uint256 liquidatedTrove,) = openTroveWithExactICRAndDebt({ + _account: D, + _index: 0, + _ICR: MCR, + _debt: 100_000 ether, + _interestRate: MIN_ANNUAL_INTEREST_RATE + }); + + // Liquidate by redistribution + priceFeed.setPrice(priceFeed.getPrice() * 99 / 100); + liquidate(A, liquidatedTrove); + + // Verify trove is still in batch before kicking + batchManagerAddress = borrowerOperations.interestBatchManagerOf(targetTrove); + assertEq(batchManagerAddress, B, "Trove should still be in batch B before kick"); + + // Kick the trove from batch + borrowerOperations.kickFromBatch(targetTrove, 0, 0); + + // Verify mapping is deleted after kick + assertEq(borrowerOperations.interestBatchManagerOf(targetTrove), address(0), "interestBatchManagerOf should be deleted after kick"); + (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(targetTrove); + assertEq(tmBatchManagerAddress, address(0), "TM batch manager should be address(0) after kick"); + } } diff --git a/contracts/test/interestRateAggregate.t.sol b/contracts/test/interestRateAggregate.t.sol index 3d6227e33..78d25d875 100644 --- a/contracts/test/interestRateAggregate.t.sol +++ b/contracts/test/interestRateAggregate.t.sol @@ -1833,14 +1833,14 @@ contract InterestRateAggregate is DevTestSetup { // --- TCR tests --- function testGetTCRReturnsMaxUint256ForEmptySystem() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 TCR = troveManager.getTCR(price); assertEq(TCR, MAX_UINT256); } function testGetTCRReturnsICRofTroveForSystemWithOneTrove() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; @@ -1887,13 +1887,13 @@ contract InterestRateAggregate is DevTestSetup { rd.B = r.B * debt.B; debt.C += calcUpfrontFee(debt.C, (rd.A + rd.B + r.C * debt.C) / (debt.A + debt.B + debt.C)); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 sizeWeightedCR = (coll.A + coll.B + coll.C) * price / (debt.A + debt.B + debt.C); assertEq(sizeWeightedCR, troveManager.getTCR(price)); } function testGetTCRIncorporatesTroveInterestForSystemWithSingleTrove() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; @@ -1967,7 +1967,7 @@ contract InterestRateAggregate is DevTestSetup { debt.B += calcInterest(rd.B, interval); debt.C += calcInterest(rd.C, interval); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 expectedTCR = (coll.A + coll.B + coll.C) * price / (debt.A + debt.B + debt.C); assertEq(expectedTCR, troveManager.getTCR(price)); } @@ -1977,14 +1977,14 @@ contract InterestRateAggregate is DevTestSetup { // - 0 for non-existent Trove function testGetCurrentICRReturnsInfinityForNonExistentTrove() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 ICR = troveManager.getCurrentICR(addressToTroveId(A), price); assertEq(ICR, MAX_UINT256); } function testGetCurrentICRReturnsCorrectValueForNoInterest() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; @@ -1999,7 +1999,7 @@ contract InterestRateAggregate is DevTestSetup { } function testGetCurrentICRReturnsCorrectValueWithAccruedInterest() public { - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 troveDebtRequest = 2000e18; uint256 coll = 20 ether; uint256 interestRate = 25e16; diff --git a/contracts/test/liquidationCosts.t.sol b/contracts/test/liquidationCosts.t.sol index 467db6035..a5ba8eec5 100644 --- a/contracts/test/liquidationCosts.t.sol +++ b/contracts/test/liquidationCosts.t.sol @@ -26,7 +26,7 @@ contract LiquidationCostsTest is DevTestSetup { // Price drops priceFeed.setPrice(1000e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Check not RM assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); @@ -60,7 +60,7 @@ contract LiquidationCostsTest is DevTestSetup { // Price drops priceFeed.setPrice(1000e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); // Check not RM assertEq(troveManager.checkBelowCriticalThreshold(price), false, "System should not be below CT"); diff --git a/contracts/test/liquidations.t.sol b/contracts/test/liquidations.t.sol index 96c085342..4e8e39236 100644 --- a/contracts/test/liquidations.t.sol +++ b/contracts/test/liquidations.t.sol @@ -61,7 +61,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1100e18 - 1); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); LiquidationsTestVars memory initialValues; initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); @@ -166,7 +166,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1030e18); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 initialSPBoldBalance = stabilityPool.getTotalBoldDeposits(); uint256 initialSPCollBalance = stabilityPool.getCollBalance(); @@ -250,7 +250,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1100e18 - 1); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); uint256 BInitialDebt = troveManager.getTroveEntireDebt(BTroveId); uint256 BInitialColl = troveManager.getTroveEntireColl(BTroveId); @@ -345,7 +345,7 @@ contract LiquidationsTest is DevTestSetup { // Price drops priceFeed.setPrice(1100e18 - 1); - (vars.price,) = priceFeed.fetchPrice(); + vars.price = priceFeed.fetchPrice(); vars.spBoldBalance = stabilityPool.getTotalBoldDeposits(); vars.spCollBalance = stabilityPool.getCollBalance(); diff --git a/contracts/test/liquidationsLST.t.sol b/contracts/test/liquidationsLST.t.sol index 73b6f1361..a81e42f5c 100644 --- a/contracts/test/liquidationsLST.t.sol +++ b/contracts/test/liquidationsLST.t.sol @@ -24,7 +24,7 @@ contract LiquidationsLSTTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev memory contracts; - (contracts, collateralRegistry, boldToken,,,,) = deployer.deployAndConnectContracts( + (contracts, collateralRegistry, boldToken,,,) = deployer.deployAndConnectContracts( TestDeployer.TroveManagerParams(160e16, 120e16, 10e16, 120e16, 5e16, 10e16) ); collToken = contracts.collToken; @@ -38,8 +38,11 @@ contract LiquidationsLSTTest is DevTestSetup { stabilityPool = contracts.stabilityPool; troveManager = contracts.troveManager; mockInterestRouter = contracts.interestRouter; + systemParams = contracts.systemParams; MCR = troveManager.get_MCR(); + MIN_ANNUAL_INTEREST_RATE = systemParams.MIN_ANNUAL_INTEREST_RATE(); + MIN_BOLD_IN_SP = systemParams.MIN_BOLD_IN_SP(); // Give some Coll to test accounts, and approve it to BorrowerOperations uint256 initialCollAmount = 10_000e18; @@ -96,7 +99,7 @@ contract LiquidationsLSTTest is DevTestSetup { // Price drops priceFeed.setPrice(1200e18 - 1); - (uint256 price,) = priceFeed.fetchPrice(); + uint256 price = priceFeed.fetchPrice(); InitialValues memory initialValues; initialValues.BDebt = troveManager.getTroveEntireDebt(BTroveId); diff --git a/contracts/test/multicollateral.t.sol b/contracts/test/multicollateral.t.sol index f7388dbaa..a4feab804 100644 --- a/contracts/test/multicollateral.t.sol +++ b/contracts/test/multicollateral.t.sol @@ -7,7 +7,7 @@ import "./TestContracts/DevTestSetup.sol"; contract MulticollateralTest is DevTestSetup { uint256 NUM_COLLATERALS = 4; - TestDeployer.LiquityContractsDev[] public contractsArray; + TestDeployer.LiquityContractsDev[] public contractsArray; function openMulticollateralTroveNoHints100pctWithIndex( uint256 _collIndex, @@ -75,7 +75,7 @@ contract MulticollateralTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken,,, WETH,) = + (_contractsArray, collateralRegistry, boldToken,,, WETH) = deployer.deployAndConnectContractsMultiColl(troveManagerParamsArray); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { @@ -109,6 +109,10 @@ contract MulticollateralTest is DevTestSetup { vm.stopPrank(); } } + + systemParams = contractsArray[0].systemParams; + REDEMPTION_FEE_FLOOR = systemParams.REDEMPTION_FEE_FLOOR(); + INITIAL_BASE_RATE = systemParams.INITIAL_BASE_RATE(); } function testMultiCollateralDeployment() public { @@ -744,6 +748,29 @@ contract MulticollateralTest is DevTestSetup { contract CsBold013 is TestAccounts { uint256 constant INITIAL_PRICE = 2_000 ether; + // TODO: Determine appropriate values for test(WETH, SETH) or remove test + // Collateral branch parameters (SETH = staked ETH, i.e. wstETH / rETH) + uint256 constant CCR_WETH = 150 * _1pct; + uint256 constant CCR_SETH = 160 * _1pct; + + uint256 constant MCR_WETH = 110 * _1pct; + uint256 constant MCR_SETH = 120 * _1pct; + + uint256 constant SCR_WETH = 110 * _1pct; + uint256 constant SCR_SETH = 120 * _1pct; + + // Batch CR buffer (same for all branches for now) + // On top of MCR to join a batch, or adjust inside a batch + uint256 constant BCR_ALL = 10 * _1pct; + + uint256 constant LIQUIDATION_PENALTY_SP_WETH = 5 * _1pct; + uint256 constant LIQUIDATION_PENALTY_SP_SETH = 5 * _1pct; + + uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_WETH = 10 * _1pct; + uint256 constant LIQUIDATION_PENALTY_REDISTRIBUTION_SETH = 20 * _1pct; + + uint256 constant MIN_ANNUAL_INTEREST_RATE = _1pct / 2; + IBoldToken boldToken; ICollateralRegistry collateralRegistry; IHintHelpers hintHelpers; @@ -794,7 +821,7 @@ contract CsBold013 is TestAccounts { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _branches; - (_branches, collateralRegistry, boldToken, hintHelpers,, weth,) = + (_branches, collateralRegistry, boldToken, hintHelpers,, weth) = deployer.deployAndConnectContractsMultiColl(params); for (uint256 i = 0; i < _branches.length; ++i) { diff --git a/contracts/test/rebalancingRedemptions.t.sol b/contracts/test/rebalancingRedemptions.t.sol new file mode 100644 index 000000000..eabe8d2d7 --- /dev/null +++ b/contracts/test/rebalancingRedemptions.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; + +contract RebalancingRedemptions is DevTestSetup { + using stdStorage for StdStorage; + + function test_redeemCollateralRebalancing_whenCallerIsNotLiquidityStrategy_shouldRevert() public { + vm.expectRevert("CollateralRegistry: Caller is not LiquidityStrategy"); + collateralRegistry.redeemCollateralRebalancing(100, 10, 1e18); + } + + function test_redeemCollateralRebalancing_whenAmountIsZero_shouldRevert() public { + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Amount must be greater than zero"); + collateralRegistry.redeemCollateralRebalancing(0, 10, 1e18); + } + + function test_redeemCollateralRebalancing_whenTroveOwnerFeeIsGreaterThan100_shouldRevert() public { + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Trove owner fee must be between 0% and 100%"); + collateralRegistry.redeemCollateralRebalancing(100, 10, 1e18 + 1); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategy_shouldRedeemAmountCorrectly() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + + uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A); + uint256 coll_B = troveManager.getTroveEntireColl(troveIDs.B); + uint256 coll_C = troveManager.getTroveEntireColl(troveIDs.C); + + uint256 debtToRedeem = debt_A + debt_B + debt_C/2; + + deal(address(boldToken), address(collateralRegistry.liquidityStrategy()), debtToRedeem); + + vm.startPrank(collateralRegistry.liquidityStrategy()); + // redemption fee is 50 bps scaled to 1e18 + collateralRegistry.redeemCollateralRebalancing(debtToRedeem, 10, 50 * 1e12); + + assertEq(troveManager.getTroveEntireDebt(troveIDs.A), 0); + assertEq(troveManager.getTroveEntireDebt(troveIDs.B), 0); + // plus 1 because of rounding down when calculating the debt to redeem + assertEq(troveManager.getTroveEntireDebt(troveIDs.C), debt_C/2 + 1 ); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategy_shouldLeaveCorrectFeeInTroves() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + uint256 price = priceFeed.getPrice(); + + // redemption fee is 50 bps scaled to 1e18 + uint256 fee = 50 * 1e12; + + uint256 debt_A = troveManager.getTroveEntireDebt(troveIDs.A); + uint256 debt_B = troveManager.getTroveEntireDebt(troveIDs.B); + uint256 debt_C = troveManager.getTroveEntireDebt(troveIDs.C); + + uint256 coll_A = troveManager.getTroveEntireColl(troveIDs.A); + uint256 coll_B = troveManager.getTroveEntireColl(troveIDs.B); + uint256 coll_C = troveManager.getTroveEntireColl(troveIDs.C); + + uint256 debtToRedeem = debt_A + debt_B + debt_C/2; + + uint256 expectedColl_A_After = calculateCorrespondingCollAfterRedemption(debt_A, coll_A, price, 50 * 1e12); + uint256 expectedColl_B_After = calculateCorrespondingCollAfterRedemption(debt_B, coll_B, price, 50 * 1e12); + uint256 expectedColl_C_After = calculateCorrespondingCollAfterRedemption(debt_C/2, coll_C, price, 50 * 1e12); + + deal(address(boldToken), address(collateralRegistry.liquidityStrategy()), debtToRedeem); + + vm.startPrank(collateralRegistry.liquidityStrategy()); + collateralRegistry.redeemCollateralRebalancing(debtToRedeem, 10, fee); + + + assertEq(troveManager.getTroveEntireColl(troveIDs.A), expectedColl_A_After); + assertEq(troveManager.getTroveEntireColl(troveIDs.B), expectedColl_B_After); + assertEq(troveManager.getTroveEntireColl(troveIDs.C), expectedColl_C_After); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategyAndCollateralIsNotRedeemable_shouldRevert() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + uint256 price = priceFeed.getPrice(); + + priceFeed.setPrice((price * 5e17)/1e18); // 50% below the initial price + + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Collateral is not redeemable"); + collateralRegistry.redeemCollateralRebalancing(100, 10, 50 * 1e12); + } + + function test_redeemCollateralRebalancing_whenCallerIsLiquidityStrategyAndFullAmountIsNotRedeemed_shouldRevert() public { + (,, ABCDEF memory troveIDs) = _setupForRedemptionAscendingInterest(); + + uint256 totalDebtSupply = boldToken.totalSupply(); + + vm.startPrank(collateralRegistry.liquidityStrategy()); + vm.expectRevert("CollateralRegistry: Redeemed amount does not match requested amount"); + collateralRegistry.redeemCollateralRebalancing(totalDebtSupply + 1, 10, 50 * 1e12); + } + + function calculateCorrespondingCollAfterRedemption(uint256 debtRedeemed, uint256 collInitial, uint256 price, uint256 fee) public pure returns (uint256 collateralAfter) { + uint256 correspondingColl = debtRedeemed * DECIMAL_PRECISION / price; + uint256 correspondingCollFee = (debtRedeemed * fee * DECIMAL_PRECISION) / (1e18 * price); + return collInitial - correspondingColl + correspondingCollFee; + } +} diff --git a/contracts/test/rebasingBatchShares.t.sol b/contracts/test/rebasingBatchShares.t.sol index e936821b5..417d5d03e 100644 --- a/contracts/test/rebasingBatchShares.t.sol +++ b/contracts/test/rebasingBatchShares.t.sol @@ -36,7 +36,6 @@ contract RebasingBatchShares is DevTestSetup { // TODO: Open A, Mint 1 extra (forgiven to A) _addOneDebtAndEnsureItDoesntMintShares(ATroveId, A); - /// @audit MED impact LatestBatchData memory afterBatch = troveManager.getLatestBatchData(address(B)); diff --git a/contracts/test/shutdown.t.sol b/contracts/test/shutdown.t.sol index 477dc5bbf..1d7c73439 100644 --- a/contracts/test/shutdown.t.sol +++ b/contracts/test/shutdown.t.sol @@ -34,12 +34,16 @@ contract ShutdownTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken,,, WETH,) = + (_contractsArray, collateralRegistry, boldToken,,, WETH) = deployer.deployAndConnectContractsMultiColl(troveManagerParamsArray); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { contractsArray.push(_contractsArray[c]); } + + // Initialize SystemParams-based variables + systemParams = contractsArray[0].systemParams; + SP_YIELD_SPLIT = systemParams.SP_YIELD_SPLIT(); // Set price feeds contractsArray[0].priceFeed.setPrice(2000e18); contractsArray[1].priceFeed.setPrice(200e18); @@ -74,6 +78,7 @@ contract ShutdownTest is DevTestSetup { borrowerOperations = contractsArray[0].borrowerOperations; troveManager = contractsArray[0].troveManager; priceFeed = contractsArray[0].priceFeed; + systemParams = contractsArray[0].systemParams; MCR = troveManager.get_MCR(); SCR = troveManager.get_SCR(); } @@ -421,13 +426,13 @@ contract ShutdownTest is DevTestSetup { // Min not reached vm.startPrank(A); - vm.expectRevert(abi.encodeWithSelector(TroveManager.MinCollNotReached.selector, 102e16)); - troveManager.urgentRedemption(1000e18, uintToArray(troveId), 103e16); + vm.expectRevert(abi.encodeWithSelector(TroveManager.MinCollNotReached.selector, 101e16)); + troveManager.urgentRedemption(1000e18, uintToArray(troveId), 102e16); vm.stopPrank(); // Min just reached vm.startPrank(A); - troveManager.urgentRedemption(1000e18, uintToArray(troveId), 102e16); + troveManager.urgentRedemption(1000e18, uintToArray(troveId), 101e16); vm.stopPrank(); } diff --git a/contracts/test/stabilityPool.t.sol b/contracts/test/stabilityPool.t.sol index cf5738c19..ab60d273a 100644 --- a/contracts/test/stabilityPool.t.sol +++ b/contracts/test/stabilityPool.t.sol @@ -1846,13 +1846,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -1891,13 +1891,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 10th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -2023,13 +2023,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); @@ -2158,13 +2158,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set console2.log(stabilityPool.P(), "stabilityPool.P()"); console2.log(_cheatP, "_cheatP"); @@ -2201,13 +2201,13 @@ contract SPTest is DevTestSetup { // Cheat 1: manipulate contract state to make value of P low vm.store( address(stabilityPool), - bytes32(uint256(10)), // 10th storage slot where P is stored + bytes32(uint256(60)), // 60th storage slot where P is stored bytes32(uint256(_cheatP)) ); - // Confirm that storage slot 10 is set - uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(10)))); - assertEq(storedVal, _cheatP, "value of slot 10 is not set"); + // Confirm that storage slot 60 is set + uint256 storedVal = uint256(vm.load(address(stabilityPool), bytes32(uint256(60)))); + assertEq(storedVal, _cheatP, "value of slot 60 is not set"); // Confirm that P specfically is set assertEq(stabilityPool.P(), _cheatP, "P is not set"); diff --git a/contracts/test/stabilityPoolUpgradeable.t.sol b/contracts/test/stabilityPoolUpgradeable.t.sol new file mode 100644 index 000000000..ec613c652 --- /dev/null +++ b/contracts/test/stabilityPoolUpgradeable.t.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {SPTest} from "./stabilityPool.t.sol"; +import {StabilityPool} from "../src/StabilityPool.sol"; +import {IStabilityPool} from "../src/Interfaces/IStabilityPool.sol"; +import {TransparentUpgradeableProxy, ITransparentUpgradeableProxy} from "openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; + +/* + * Tests for upgradeable StabilityPool with the P initialization fix. + */ +contract SPUpgradeableTest is SPTest { + bytes32 private constant IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 private constant ADMIN_SLOT = + 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + address proxyAdmin; + + function setUp() public override { + super.setUp(); + + address SPAddress = address(stabilityPool); + + StabilityPool spImplementation = new StabilityPool(true, systemParams); + + proxyAdmin = address(new ProxyAdmin()); + + TransparentUpgradeableProxy tempProxy = new TransparentUpgradeableProxy( + address(spImplementation), + proxyAdmin, + "" + ); + + bytes memory proxyBytecode = address(tempProxy).code; + + vm.etch(SPAddress, proxyBytecode); + + for (uint256 i = 0; i < 70; i++) { + vm.store(SPAddress, bytes32(i), bytes32(0)); + } + + vm.store( + SPAddress, + IMPLEMENTATION_SLOT, + bytes32(uint256(uint160(address(spImplementation)))) + ); + vm.store(SPAddress, ADMIN_SLOT, bytes32(uint256(uint160(proxyAdmin)))); + + vm.store(SPAddress, bytes32(uint256(0)), bytes32(uint256(0))); + + IStabilityPool(SPAddress).initialize(addressesRegistry); + } + + function testPValue() public view { + assertEq(stabilityPool.P(), stabilityPool.P_PRECISION()); + assertEq(stabilityPool.P(), 1e36); + } + + function testGetDepositorBoldGain_2SPDepositor1LiqFreshDeposit_Upgraded_EarnFairShareOfSPYield() + public + { + ABCDEF memory troveIDs = _setupForSPDepositAdjustments(); + ABCDEF[2] memory expectedShareOfReward; + + vm.warp(block.timestamp + 90 days + 1); + + uint256 pendingAggInterest_0 = activePool.calcPendingAggInterest(); + uint256 expectedSpYield_0 = (SP_YIELD_SPLIT * pendingAggInterest_0) / + 1e18; + + expectedShareOfReward[0].A = getShareofSPReward(A, expectedSpYield_0); + expectedShareOfReward[0].B = getShareofSPReward(B, expectedSpYield_0); + uint256 totalSPDeposits_0 = stabilityPool.getTotalBoldDeposits(); + + makeSPWithdrawalAndClaim(A, 500e18); + + // Upgrade SP to similar impl + address spImplementation = address( + new StabilityPool(true, systemParams) + ); + vm.prank(proxyAdmin); + ITransparentUpgradeableProxy(address(stabilityPool)).upgradeTo( + spImplementation + ); + + // A liquidates D + liquidate(A, troveIDs.D); + // Check SP has only 1e18 BOLD now, and A and B have small remaining deposits + assertEq( + stabilityPool.getTotalBoldDeposits(), + 1e18, + "SP total bold deposits should be 1e18" + ); + assertLt( + stabilityPool.getCompoundedBoldDeposit(A), + 1e18, + "A should have <1e18 deposit" + ); + assertLt( + stabilityPool.getCompoundedBoldDeposit(B), + 1e18, + "B should have <1e18 deposit" + ); + assertLe( + stabilityPool.getCompoundedBoldDeposit(B) + + stabilityPool.getCompoundedBoldDeposit(A), + 1e18, + "A & B deposits should sum to <=1e18" + ); + + // C and D makes fresh deposit + uint256 deposit_C = 1e18; + uint256 deposit_D = 1e18; + makeSPDepositAndClaim(C, deposit_C); + transferBold(C, D, deposit_D); + makeSPDepositAndClaim(D, deposit_D); + + // Check SP still has funds + uint256 totalSPDeposits_1 = stabilityPool.getTotalBoldDeposits(); + assertGt(totalSPDeposits_1, 0); + assertLt(totalSPDeposits_1, totalSPDeposits_0); + + // fast-forward time again and accrue interest + vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); + + uint256 pendingAggInterest_1 = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest_1, 0); + uint256 expectedSpYield_1 = (SP_YIELD_SPLIT * pendingAggInterest_1) / + 1e18; + + // Expected reward round 2 calculated with a different totalSPDeposits denominator. Expect A and B to earn a small share of + // this reward. + expectedShareOfReward[1].A = getShareofSPReward(A, expectedSpYield_1); + expectedShareOfReward[1].B = getShareofSPReward(B, expectedSpYield_1); + expectedShareOfReward[1].C = getShareofSPReward(C, expectedSpYield_1); + expectedShareOfReward[1].D = getShareofSPReward(D, expectedSpYield_1); + assertGt(expectedShareOfReward[1].A, 0); + assertGt(expectedShareOfReward[1].B, 0); + assertGt(expectedShareOfReward[1].C, 0); + assertGt(expectedShareOfReward[1].D, 0); + // A and B should get small rewards from reward 2, as their deposits were reduced to 1e18. + // Confirm A, B are smaller than C, D's rewards + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].D); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].D); + + // Confirm the expected shares sum up to the total expected yield + assertApproximatelyEqual( + expectedShareOfReward[1].A + + expectedShareOfReward[1].B + + expectedShareOfReward[1].C + + expectedShareOfReward[1].D, + expectedSpYield_1, + 1e4, + "expected shares should sum up to the total expected yield" + ); + + // A trove gets poked again, interst minted and yield paid to SP + applyPendingDebt(B, troveIDs.A); + + // Expect A to receive only their share of 2nd reward, since they already claimed first + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(A), + expectedShareOfReward[1].A, + 1e4, + "A should receive only 2nd reward" + ); + // Expect B to receive their share of 1st and 2nd rewards + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(B), + expectedShareOfReward[0].B + expectedShareOfReward[1].B, + 1e4, + "B should receive only their share of 1st and 2nd" + ); + // Expect C to receive a share of only 2nd reward + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(C), + expectedShareOfReward[1].C, + 1e4, + "C should receive a share of both reward 1 and 2" + ); + } + + function testGetDepositorBoldGain_2SPDepositor1LiqScaleChangeFreshDeposit_Upgraded_EarnFairShareOfSPYield() + public + { + ABCDEF memory troveIDs = _setupForSPDepositAdjustmentsBigTroves(); + ABCDEF[3] memory expectedShareOfReward; + + vm.warp(block.timestamp + 90 days + 1); + + uint256 pendingAggInterest_0 = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest_0, 0); + uint256 expectedSpYield_0 = (SP_YIELD_SPLIT * pendingAggInterest_0) / + 1e18; + + expectedShareOfReward[0].A = getShareofSPReward(A, expectedSpYield_0); + expectedShareOfReward[0].B = getShareofSPReward(B, expectedSpYield_0); + assertGt(expectedShareOfReward[0].A, 0); + assertGt(expectedShareOfReward[0].B, 0); + uint256 totalSPDeposits_0 = stabilityPool.getTotalBoldDeposits(); + + // Confirm the expected shares sum up to the total expected yield + assertApproximatelyEqual( + expectedShareOfReward[0].A + expectedShareOfReward[0].B, + expectedSpYield_0, + 1e3 + ); + + // A withdraws some deposit so that D's liq will trigger a scale change. + // This also mints interest and pays the yield to the SP + uint256 debtSPDelta = totalSPDeposits_0 - + troveManager.getTroveEntireDebt(troveIDs.D); + makeSPWithdrawalAndClaim(A, debtSPDelta - 1e12); + assertEq(stabilityPool.getDepositorYieldGain(A), 0); + assertEq(activePool.calcPendingAggInterest(), 0); + + assertEq(stabilityPool.currentScale(), 0); + + // A liquidates D + liquidate(A, troveIDs.D); + + // Upgrade SP to similar impl + address spImplementation = address( + new StabilityPool(true, systemParams) + ); + vm.prank(proxyAdmin); + ITransparentUpgradeableProxy(address(stabilityPool)).upgradeTo( + spImplementation + ); + + // Check scale increased + assertEq(stabilityPool.currentScale(), 1); + + // C and D makes fresh deposit + uint256 deposit_C = 1e27; + uint256 deposit_D = 1e27; + makeSPDepositAndClaim(C, deposit_C); + transferBold(C, D, deposit_D); + makeSPDepositAndClaim(D, deposit_D); + + // fast-forward time again and accrue interest + vm.warp(block.timestamp + STALE_TROVE_DURATION + 1); + + uint256 pendingAggInterest_1 = activePool.calcPendingAggInterest(); + assertGt(pendingAggInterest_1, 0); + uint256 expectedSpYield_1 = (SP_YIELD_SPLIT * pendingAggInterest_1) / + 1e18; + + // Expected reward round 2 calculated with a different totalSPDeposits denominator. + expectedShareOfReward[1].A = getShareofSPReward(A, expectedSpYield_1); + expectedShareOfReward[1].B = getShareofSPReward(B, expectedSpYield_1); + expectedShareOfReward[1].C = getShareofSPReward(C, expectedSpYield_1); + expectedShareOfReward[1].D = getShareofSPReward(D, expectedSpYield_1); + + // Expect A, B, C and D to get reward 2 + assertGt(expectedShareOfReward[1].A, 0); + assertGt(expectedShareOfReward[1].B, 0); + assertGt(expectedShareOfReward[1].C, 0); + assertGt(expectedShareOfReward[1].D, 0); + // ... though A, B's share should be smaller than C, D's share + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].A, expectedShareOfReward[1].D); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].C); + assertLt(expectedShareOfReward[1].B, expectedShareOfReward[1].D); + + // A trove gets poked again, interst minted and yield paid to SP + applyPendingDebt(B, troveIDs.A); + + // A only gets reward 2 since already claimed reward 1 + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(A), + expectedShareOfReward[1].A, + 1e15 + ); + + // B gets reward 1 + 2 + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(B), + expectedShareOfReward[0].B + expectedShareOfReward[1].B, + 1e14 + ); + // C, D get reward 2 + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(C), + expectedShareOfReward[1].C, + 1e14 + ); + assertApproximatelyEqual( + stabilityPool.getDepositorYieldGain(D), + expectedShareOfReward[1].D, + 1e14 + ); + } +} diff --git a/contracts/test/stabilityScaling.t.sol b/contracts/test/stabilityScaling.t.sol new file mode 100644 index 000000000..12e513f7f --- /dev/null +++ b/contracts/test/stabilityScaling.t.sol @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; + +contract StabilityScalingTest is DevTestSetup { + address liquidityStrategy; + + function setUp() public override { + super.setUp(); + liquidityStrategy = addressesRegistry.liquidityStrategy(); + deal(address(collToken), liquidityStrategy, 1e60); + vm.prank(liquidityStrategy); + collToken.approve(address(stabilityPool), type(uint256).max); + } + + function _setupStabilityPool(uint256 magnitude) internal { + uint256 amount = 1e18 * 10 ** magnitude; + deal(address(boldToken), A, amount); + deal(address(boldToken), B, amount); + makeSPDepositAndClaim(A, amount); + makeSPDepositAndClaim(B, amount); + } + + function _topUpStabilityPoolBold(uint256 amount) internal { + deal(address(boldToken), D, amount); + makeSPDepositAndClaim(D, amount); + } + + function _openLiquidatableTroves( + uint256 numTroves, + uint256 collAmount, + uint256 debtAmount, + uint256 interestRate, + uint256 liquidationPrice + ) internal returns (uint256[] memory) { + uint256[] memory troveIds = new uint256[](numTroves); + + // Set initial price high enough for troves to be opened safely + priceFeed.setPrice(4000e18); + + // Open troves for additional accounts + for (uint256 i = 0; i < numTroves; i++) { + address account = vm.addr(1000 + i); // Generate unique addresses + + // Fund and approve collateral + giveAndApproveColl(account, collAmount); + + deal(address(WETH), account, 1000e18); + + vm.startPrank(account); + WETH.approve(address(borrowerOperations), type(uint256).max); + vm.stopPrank(); + + // Open trove + uint256 troveId = openTroveNoHints100pct( + account, + collAmount, + debtAmount, + interestRate + ); + troveIds[i] = troveId; + } + + // Drop price to make troves liquidatable + if (liquidationPrice > 0) { + priceFeed.setPrice(liquidationPrice); + } + + return troveIds; + } + + function _openLiquidatableTrovesDefault( + uint256 numTroves + ) internal returns (uint256[] memory) { + uint256 collAmount = 1 ether; + uint256 debtAmount = 2000e18; + uint256 interestRate = 5e16; // 5% + uint256 liquidationPrice = 2000e18; // Makes troves liquidatable at 2000 price + + return + _openLiquidatableTroves( + numTroves, + collAmount, + debtAmount, + interestRate, + liquidationPrice + ); + } + + function testWithdrawalsWithLargeNumberOfScaleChanges() public { + // Users each deposit 1M bold to SP + _setupStabilityPool(6); + + uint256 previousScale = stabilityPool.currentScale(); + uint256 previousP = stabilityPool.P(); + uint256 previousTotalDeposits = stabilityPool.getTotalBoldDeposits(); + + // Open 200 liquidatable troves + uint256[] memory troveIds = _openLiquidatableTrovesDefault(200); + // liquidate 50 of them + for (uint256 j = 0; j < 50; j++) { + liquidate(D, troveIds[j]); + } + // Scale won't change + assertEq(stabilityPool.currentScale(), previousScale); + // P should decrease + assertGt(previousP, stabilityPool.P()); + // Total deposits should decrease + assertGt(previousTotalDeposits, stabilityPool.getTotalBoldDeposits()); + + // Update previous values + previousScale = stabilityPool.currentScale(); + previousP = stabilityPool.P(); + previousTotalDeposits = stabilityPool.getTotalBoldDeposits(); + + // Simulate rebalance 1000 times + for (uint256 i = 1; i <= 1000; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + } + + // Scale will increase + assertGt(stabilityPool.currentScale(), previousScale); + // Total deposits will stay the same because we top up SP with the same amount of bold swapped out + assertEq(previousTotalDeposits, stabilityPool.getTotalBoldDeposits()); + // Update previous values + previousScale = stabilityPool.currentScale(); + previousP = stabilityPool.P(); + previousTotalDeposits = stabilityPool.getTotalBoldDeposits(); + + // Liquidate the remaining troves - 1 + for (uint256 j = 50; j < 199; j++) { + liquidate(D, troveIds[j]); + } + // Scale won't change + assertEq(stabilityPool.currentScale(), previousScale); + // P should decrease + assertGt(previousP, stabilityPool.P()); + // Total deposits should decrease + assertGt(previousTotalDeposits, stabilityPool.getTotalBoldDeposits()); + + // Withdraw A and B from SP to observe the withdrawals + uint256 boldBalanceA = boldToken.balanceOf(A); + uint256 collBalanceA = collToken.balanceOf(A); + + // A deposited 1M bold to SP initially + assertEq(stabilityPool.deposits(A), 1_000_000e18); + + vm.startPrank(A); + // A tries to withdraw all their bold from SP + stabilityPool.withdrawFromSP(stabilityPool.deposits(A), true); + vm.stopPrank(); + + uint256 boldWithdrawA = boldToken.balanceOf(A) - boldBalanceA; + uint256 collWithdrawA = collToken.balanceOf(A) - collBalanceA; + + // A's full bold position is diminished by the rebalances and liquidations + // There is a small amount of bold left because of the Yield gain + assertLt(boldWithdrawA, 500e18); + + // A's coll gain is a combination of rebalance and liquidations + // In exchange for the 1M bold they lost, they should receive almost the same amount of collateral + // * because we executed swaps and liquidations at the same price (2000e18) + assertApproxEqAbs(collWithdrawA, 1_000_000e18 / 2000, 1e18); + } + + function testHowScaleChangesAffectsCollGains() public { + _setupStabilityPool(6); + + uint256 previousScale = stabilityPool.currentScale(); + uint256 previousCollGain = stabilityPool.getDepositorCollGain(A); + uint256 previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // Open 200 liquidatable troves + uint256[] memory troveIds = _openLiquidatableTrovesDefault(200); + + // liquidate 50 of them + for (uint256 j = 0; j < 50; j++) { + liquidate(D, troveIds[j]); + } + + // 50 troves were liquidated, each with 2000e18 debt + // Half of the total debt is offsetted by A's deposit since it had the half of the total deposits + uint256 deptOffset = (50 * 2_000e18) / 2; + // Liquidation price is 2000e18, so each trove has 1 ether of collateral + uint256 collGain = (50 * 1 ether) / 2; + assertApproxEqRel( + stabilityPool.getCompoundedBoldDeposit(A), + previousDeposit - deptOffset, + 1e16 + ); + assertApproxEqRel( + stabilityPool.getDepositorCollGain(A), + collGain - previousCollGain, + 1e16 + ); + + previousScale = stabilityPool.currentScale(); + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // Simulate rebalance until the edge of a scale change + for (uint256 i = 1; i <= 29; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + } + + assertEq(stabilityPool.currentScale(), previousScale); + + // at this point, initial depositors almost lost all their deposits + assertLt(stabilityPool.getCompoundedBoldDeposit(A), 1e18); + // their positions moved to collateral almost completely =~ 500e18 + assertApproxEqRel( + stabilityPool.getDepositorCollGain(A), + 1_000_000e18 / 2000, + 1e16 + ); + + // Deposit again + _setupStabilityPool(6); + + previousScale = stabilityPool.currentScale(); + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // Simulate rebalance until scale changes + for (uint256 i = 1; i <= 10; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 10; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + if (stabilityPool.currentScale() > previousScale) { + break; + } + } + + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // liquidate 50 more troves + // coll gains will be received for a positon that is opened on 1 scale above + for (uint256 j = 50; j < 100; j++) { + liquidate(D, troveIds[j]); + } + // depositors should have less deposits and more coll gain + assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); + assertGt(stabilityPool.getDepositorCollGain(A), previousCollGain); + + previousScale = stabilityPool.currentScale(); + + + // Simulate rebalance until scale changes + for (uint256 i = 1; i <= 100; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + if (stabilityPool.currentScale() > previousScale) { + break; + } + } + + + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // liquidate 50 more troves + // coll gains will be received for a positon that is opened on 2 scale above + for (uint256 j = 100; j < 150; j++) { + liquidate(D, troveIds[j]); + } + // depositors should have less deposits and more coll gain + // changes will be minimal since the share of the depositor is small now + assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); + assertGt(stabilityPool.getDepositorCollGain(A), previousCollGain); + + previousScale = stabilityPool.currentScale(); + + // Simulate rebalance until scale changes + for (uint256 i = 1; i <= 100; i++) { + uint boldBalance = stabilityPool.getTotalBoldDeposits(); + uint256 stableOut = (boldBalance) / 2; + // Swap out half of the bold in SP + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(stableOut / 2000, stableOut); + // Top up SP with the same amount of bold swapped out + _topUpStabilityPoolBold(stableOut); + if (stabilityPool.currentScale() > previousScale) { + break; + } + } + + previousCollGain = stabilityPool.getDepositorCollGain(A); + previousDeposit = stabilityPool.getCompoundedBoldDeposit(A); + + // liquidate 50 more troves + // positions will stop gaining coll because they are opened on 2 scale above + // and now too small to gain any coll + for (uint256 j = 150; j < 199; j++) { + liquidate(D, troveIds[j]); + } + // depositors should have less deposits and coll gain should be the same + assertLt(stabilityPool.getCompoundedBoldDeposit(A), previousDeposit); + assertEq(stabilityPool.getDepositorCollGain(A), previousCollGain); + } + + function testStabilityScaling_hugeDepositorDoesntLooseBold() public { + address depositor = makeAddr("earthUsdReserve"); + uint256 depositAmount = 2.417 ether * 1e12; // Total USD in circulation: 2.417 T + deal(address(boldToken), depositor, depositAmount); + makeSPDepositAndClaim(depositor, depositAmount); + + for (uint256 i = 0; i < 9; ++i) { + uint256 spBalance = boldToken.balanceOf(address(stabilityPool)); + uint256 boldOut = spBalance - 1_000e18; + uint256 collIn = boldOut / 2000; + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collIn, boldOut); + _topUpStabilityPoolBold(boldOut); + } + + uint256 collGain = stabilityPool.getDepositorCollGain(depositor); + uint256 compoundedBold = stabilityPool.getCompoundedBoldDeposit(depositor); + assertEq(stabilityPool.currentScale(), 9); + + assertEq(compoundedBold, 0); + assertApproxEqAbs(collGain, depositAmount / 2000, 1); + } +} diff --git a/contracts/test/swapCollateralForStable.t.sol b/contracts/test/swapCollateralForStable.t.sol new file mode 100644 index 000000000..91a255c57 --- /dev/null +++ b/contracts/test/swapCollateralForStable.t.sol @@ -0,0 +1,529 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.18; + +import "./TestContracts/DevTestSetup.sol"; +import "../src/StabilityPool.sol"; +import "../src/Interfaces/IStabilityPoolEvents.sol"; + +contract SwapCollateralForStableTest is DevTestSetup { + struct SwapTestVars { + uint256 spBoldBalance; + uint256 spCollBalance; + uint256 lsBoldBalance; + uint256 lsCollBalance; + uint256 depositor1CollBalance; + uint256 depositor1BoldBalance; + uint256 depositor2CollBalance; + uint256 depositor2BoldBalance; + uint256 depositor3CollBalance; + uint256 depositor3BoldBalance; + } + + address public liquidityStrategy = makeAddr("liquidityStrategy"); + + event P_Updated(uint256 _P); + event S_Updated(uint256 _S, uint256 _scale); + event ScaleUpdated(uint256 _currentScale); + event StabilityPoolCollBalanceUpdated(uint256 _newBalance); + event StabilityPoolBoldBalanceUpdated(uint256 _newBalance); + + function setUp() public override { + super.setUp(); + + deal(address(collToken), liquidityStrategy, 10_000e18); + + vm.prank(liquidityStrategy); + collToken.approve(address(stabilityPool), type(uint256).max); + + } + + function testLiquidityStrategy() public view { + assertEq(stabilityPool.liquidityStrategy(), liquidityStrategy); + } + + + function testSwapCollateralForStableRevertsWhenNotLiquidityStrategy() public { + vm.expectRevert("StabilityPool: Caller is not LiquidityStrategy"); + stabilityPool.swapCollateralForStable(1e18, 1e18); + } + + function testSwapCollateralForStableRevertsWhenSystemIsShutDown() public { + vm.mockCall( + address(troveManager), + abi.encodeWithSelector(ITroveManager.shutdownTime.selector), + abi.encode(block.timestamp - 1) + ); + + + vm.expectRevert("StabilityPool: System is shut down"); + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(1e18, 1e18); + vm.stopPrank(); + } + + function testSwapCollateralForStableRevertsWithInsufficientStableLiquidity() public { + uint256 stableAmount = 1000e18; + + deal(address(boldToken), A, stableAmount); + + makeSPDepositAndClaim(A, stableAmount); + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = stableAmount; + + + vm.startPrank(liquidityStrategy); + vm.expectRevert("P must never decrease to 0"); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + + + vm.expectRevert("Total Bold deposits must be >= MIN_BOLD_AFTER_REBALANCE"); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount - .1e18); + vm.stopPrank(); + } + + function testSwapCollateralForStableWithSurplus() public { + SwapTestVars memory initialValues; + + priceFeed.setPrice(2000e18); + vm.startPrank(A); + + borrowerOperations.openTrove( + A, + 0, + 2e18, + 2000e18, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove( + B, + 0, + 4e18, + 4000e18, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + + // B deposits to SP + makeSPDepositAndClaim(B, 4000e18); + + initialValues.depositor1CollBalance = collToken.balanceOf(B); + initialValues.depositor1BoldBalance = boldToken.balanceOf(B); + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spCollBalance = stabilityPool.getCollBalance(); + initialValues.lsBoldBalance = boldToken.balanceOf(liquidityStrategy); + initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); + + // Check SP has deposits + assertEq(initialValues.spBoldBalance, 4000e18, "SP should have Bold deposits"); + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 2000e18; + + // Simulate a rebalance by calling swapCollateralForStable as liquidity strategy + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Check SP Bold has decreased by swap amount + uint256 finalSPBoldBalance = stabilityPool.getTotalBoldDeposits(); + assertEq( + finalSPBoldBalance, + initialValues.spBoldBalance - stableSwapAmount, + "SP Bold balance should decrease by swap amount" + ); + + // Check SP Coll has increased + uint256 finalSPCollBalance = stabilityPool.getCollBalance(); + assertEq( + finalSPCollBalance, + initialValues.spCollBalance + collSwapAmount, + "SP Coll balance should increase by swapped collateral" + ); + + // Check LP has received Bold + uint256 finalLSBoldBalance = boldToken.balanceOf(liquidityStrategy); + assertEq(finalLSBoldBalance, initialValues.lsBoldBalance + stableSwapAmount); + + // Check LP has sent Coll + uint256 finalLSCollBalance = collToken.balanceOf(liquidityStrategy); + assertEq(finalLSCollBalance, initialValues.lsCollBalance - collSwapAmount); + + vm.prank(B); + stabilityPool.withdrawFromSP(finalSPBoldBalance - 1000e18, true); // subtract 1000e18 to avoid MIN_BOLD_AFTER_REBALANCE + + // Check B has received Bold + // Received bold is less than the initial deposit because of the rebalance + assertApproxEqAbs(boldToken.balanceOf(B), initialValues.depositor1BoldBalance + 1000e18, 1e18); + + // Check B has received Coll + // Even though the collateral was never deposited, depositor should have received the collateral from the rebalance + assertEq(collToken.balanceOf(B), initialValues.depositor1CollBalance + collSwapAmount); + + } + + function testSwapCollateralForStableWithLargerAmounts() public { + uint256 stableAmount = 1e32; + + deal(address(collToken), address(liquidityStrategy), 1e32); + deal(address(boldToken), A, stableAmount); + + makeSPDepositAndClaim(A, stableAmount); + + uint256 collSwapAmount = 1e30; + uint256 stableSwapAmount = stableAmount - 1000e18; // avoid MIN_BOLD_AFTER_REBALANCE + + SwapTestVars memory initialValues; + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spCollBalance = stabilityPool.getCollBalance(); + initialValues.lsBoldBalance = boldToken.balanceOf(liquidityStrategy); + initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); + + + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Verify balances updated correctly with larger amounts + assertEq(stabilityPool.getTotalBoldDeposits(), initialValues.spBoldBalance - stableSwapAmount); + assertEq(stabilityPool.getCollBalance(), initialValues.spCollBalance + collSwapAmount); + assertEq(boldToken.balanceOf(liquidityStrategy), initialValues.lsBoldBalance + stableSwapAmount); + assertEq(collToken.balanceOf(liquidityStrategy), initialValues.lsCollBalance - collSwapAmount); + } + + function testSwapCollateralForStableWithMultipleDepositors() public { + uint256 depositA = 1000e18; // 1/6 + uint256 depositB = 2000e18; // 1/3 + uint256 depositC = 3000e18; // 1/2 + + deal(address(boldToken), A, depositA); + deal(address(boldToken), B, depositB); + deal(address(boldToken), C, depositC); + + // Multiple depositors + makeSPDepositAndClaim(A, depositA); + makeSPDepositAndClaim(B, depositB); + makeSPDepositAndClaim(C, depositC); + + + SwapTestVars memory initialValues; + initialValues.spBoldBalance = stabilityPool.getTotalBoldDeposits(); + initialValues.spCollBalance = stabilityPool.getCollBalance(); + initialValues.lsBoldBalance = boldToken.balanceOf(liquidityStrategy); + initialValues.lsCollBalance = collToken.balanceOf(liquidityStrategy); + initialValues.depositor1CollBalance = collToken.balanceOf(A); + initialValues.depositor1BoldBalance = boldToken.balanceOf(A); + initialValues.depositor2CollBalance = collToken.balanceOf(B); + initialValues.depositor2BoldBalance = boldToken.balanceOf(B); + initialValues.depositor3CollBalance = collToken.balanceOf(C); + initialValues.depositor3BoldBalance = boldToken.balanceOf(C); + + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 1000e18; + + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Verify SP balances + assertEq(stabilityPool.getTotalBoldDeposits(), initialValues.spBoldBalance - stableSwapAmount); + assertEq(stabilityPool.getCollBalance(), initialValues.spCollBalance + collSwapAmount); + + // Verify liquidity strategy balances + assertEq(boldToken.balanceOf(liquidityStrategy), initialValues.lsBoldBalance + stableSwapAmount); + assertEq(collToken.balanceOf(liquidityStrategy), initialValues.lsCollBalance - collSwapAmount); + + // Withdraw from all depositors to check they receive proportional collateral gains + vm.startPrank(A); + stabilityPool.withdrawFromSP(depositA, true); + vm.stopPrank(); + + vm.startPrank(B); + stabilityPool.withdrawFromSP(depositB, true); + vm.stopPrank(); + + vm.startPrank(C); + // since this is the last depositor, we need to take depriciation and MIN_BOLD_IN_SP into account + stabilityPool.withdrawFromSP(depositC - (stableSwapAmount / 2 + 1e18), true); + vm.stopPrank(); + + // A should gain 1/6 of the collateral because they deposited 1/6 of the total deposits + assertApproxEqAbs(collToken.balanceOf(A), initialValues.depositor1CollBalance + collSwapAmount / 6, 1e18); + + // A should loose 1/6 of the Bold because they deposited 1/6 of the total deposits + assertApproxEqAbs(boldToken.balanceOf(A), depositA - (initialValues.depositor1BoldBalance + stableSwapAmount / 6), 1e18); + + // B should receive 1/3 of the collateral because they deposited 1/3 of the total deposits + assertApproxEqAbs(collToken.balanceOf(B), initialValues.depositor2CollBalance + collSwapAmount / 3, 1e18); + + // B should loose 1/3 of the Bold because they deposited 1/3 of the total deposits + assertApproxEqAbs(boldToken.balanceOf(B), depositB - (initialValues.depositor2BoldBalance + stableSwapAmount / 3), 1e18); + + // C should receive 1/2 of the collateral because they deposited 1/2 of the total deposits + assertApproxEqAbs(collToken.balanceOf(C), initialValues.depositor3CollBalance + collSwapAmount / 2, 1e18); + + // C should loose 1/2 of the Bold because they deposited 1/2 of the total deposits + assertApproxEqAbs(boldToken.balanceOf(C), depositC - (initialValues.depositor3BoldBalance + stableSwapAmount / 2 + 1e18), 1e18); + } + + function testSwapCollateralForStableAfterLiquidation() public { + uint256 stableAmount = 2000e18; + uint256 collAmount = 2e18; + + priceFeed.setPrice(2000e18); + + // Create troves + vm.startPrank(A); + uint256 ATroveId = borrowerOperations.openTrove( + A, + 0, + collAmount, + stableAmount, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + vm.startPrank(B); + borrowerOperations.openTrove( + B, + 0, + 2 * collAmount, + stableAmount + 100e18, + 0, + 0, + MIN_ANNUAL_INTEREST_RATE, + 1000e18, + address(0), + address(0), + address(0) + ); + vm.stopPrank(); + + uint256 cBoldDeposit = 20_000e18; + deal(address(boldToken), C, cBoldDeposit); + // C deposits to SP + makeSPDepositAndClaim(C, cBoldDeposit); + + uint256 initialSPBold = stabilityPool.getTotalBoldDeposits(); + uint256 initialSPColl = stabilityPool.getCollBalance(); + + // Perform a swap first + uint256 collSwapAmount = 0.5e18; + uint256 stableSwapAmount = 1000e18; + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Now perform a liquidation + priceFeed.setPrice(900e18); // Drop price to trigger liquidation + + vm.startPrank(A); + troveManager.liquidate(ATroveId); + vm.stopPrank(); + + // Check that the liquidation worked correctly after the swap + uint256 finalSPBold = stabilityPool.getTotalBoldDeposits(); + uint256 finalSPColl = stabilityPool.getCollBalance(); + + // SP should have less Bold (due to liquidation offset + swap out) and more Coll (from liquidation + swap in) + assertApproxEqAbs(finalSPBold, initialSPBold - stableAmount - stableSwapAmount, 1e18); + assertApproxEqAbs(finalSPColl, initialSPColl + collAmount + collSwapAmount, 1e18); + + uint256 initialCColl = collToken.balanceOf(C); + + uint256 compoundedBoldDeposit = stabilityPool.getCompoundedBoldDeposit(C); + // C should be able to withdraw and receive both swap and liquidation gains + vm.startPrank(C); + stabilityPool.withdrawFromSP(compoundedBoldDeposit - 1e18, true); + vm.stopPrank(); + + // C should have received collateral from both the swap and the liquidation + assertApproxEqAbs(collToken.balanceOf(C), initialCColl + collSwapAmount + collAmount, 1e18); + + // C should have less Bold than deposited + assertApproxEqAbs(boldToken.balanceOf(C), cBoldDeposit - (stableSwapAmount + stableAmount), 1e18); + } + + function testSwapCollateralForStableEmitsCorrectEvents() public { + uint256 stableAmount = 10_000e18; + + uint256 collSwapAmount = 1000e18; + uint256 stableSwapAmount = stableAmount / 2; + + + deal(address(boldToken), A, stableAmount); + deal(address(collToken), liquidityStrategy, collSwapAmount); + + makeSPDepositAndClaim(A, stableAmount); + + + // Record initial balances for event verification + uint256 initialSPBold = stabilityPool.getTotalBoldDeposits(); + uint256 initialSPColl = stabilityPool.getCollBalance(); + + vm.startPrank(liquidityStrategy); + + // Expect events to be emitted in the correct order + // First: S_Updated (from _updateTrackingVariables) + // S_Updated value = P * _amountCollIn / totalBoldDeposits + // = 1e36 * 1000e18 / 10_000e18 = 1e35 + vm.expectEmit(true, true, true, true); + emit S_Updated(1e35, 0); + + // Second: P_Updated (from _updateTrackingVariables) + // P_Updated value = P * (totalBoldDeposits - _amountStableOut) / totalBoldDeposits + // = 1e36 * (10000e18 - 10000e18 / 2) / 10000e18 = 5e35 + vm.expectEmit(true, true, true, true); + emit P_Updated(5e35); + + // Third: StabilityPoolBoldBalanceUpdated (from _swapCollateralForStable) + vm.expectEmit(true, true, true, true); + emit StabilityPoolBoldBalanceUpdated(initialSPBold - stableSwapAmount); + + // Fourth: StabilityPoolCollBalanceUpdated (from _swapCollateralForStable) + vm.expectEmit(true, true, true, true); + emit StabilityPoolCollBalanceUpdated(initialSPColl + collSwapAmount); + + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + } + + function testSwapCollateralForStableWithScaleChanges() public { + // Create a scenario that will trigger scale change + uint256 stableAmount = 10_000_000_000_000e18; + uint256 collSwapAmount = 1_000_000e18; + uint256 stableSwapAmount = stableAmount - 1000e18; // Swap out all liquidity - MIN_BOLD_AFTER_REBALANCE + + deal(address(boldToken), A, stableAmount/2); + deal(address(boldToken), B, stableAmount/2); + deal(address(collToken), liquidityStrategy, collSwapAmount); + + makeSPDepositAndClaim(A, stableAmount/2); + makeSPDepositAndClaim(B, stableAmount/2); + + + uint256 initialScale = stabilityPool.currentScale(); + + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // 1e36 * (1e18 / 1e28) = 1e26 + // 1e26 < 1e27, so the scale should increase + + uint256 finalScale = stabilityPool.currentScale(); + assertGt(finalScale, initialScale); + assertEq(finalScale, 1); + + // P should be scaled back up 1e26 * 1e9 = 1e35 + assertEq(stabilityPool.P(), 1e35); + + // // Verify the swap still worked correctly regardless of scale changes + assertEq(stabilityPool.getTotalBoldDeposits(), stableAmount - stableSwapAmount); + assertEq(stabilityPool.getCollBalance(), collSwapAmount); + + uint256 compundedBoldDeposit = stabilityPool.getCompoundedBoldDeposit(A); + assertEq(compundedBoldDeposit, (stableAmount - stableSwapAmount) / 2); + + uint256 depositorCollGain = stabilityPool.getDepositorCollGain(A); + assertEq(depositorCollGain, collSwapAmount / 2 ); + } + + + function testSwapCollateralForStableAtMinimumDeposit() public { + uint256 stableAmount = 2000e18; + + deal(address(boldToken), A, stableAmount); + makeSPDepositAndClaim(A, stableAmount); + + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 1000e18; + + vm.startPrank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + vm.stopPrank(); + + // Should still work and leave at least MIN_BOLD_IN_SP + assertEq(stabilityPool.getTotalBoldDeposits(), 1_000e18); + assertEq(stabilityPool.getCollBalance(), collSwapAmount); + } + + function testSwapCollateralForStableWithYieldGains() public { + uint256 stableAmount = 2000e18; + uint256 yieldAmount = 100e18; + uint256 collSwapAmount = 1e18; + uint256 stableSwapAmount = 1000e18; + + // Setup initial deposits + deal(address(boldToken), A, stableAmount); + makeSPDepositAndClaim(A, stableAmount); + + deal(address(boldToken), B, stableAmount); + makeSPDepositAndClaim(B, stableAmount); + + // Generate some yield gains + vm.prank(address(activePool)); + stabilityPool.triggerBoldRewards(yieldAmount); + + uint256 initialYieldGainA = stabilityPool.getDepositorYieldGain(A); + assertEq(initialYieldGainA, yieldAmount / 2); + + uint256 initialYieldGainB = stabilityPool.getDepositorYieldGain(B); + assertEq(initialYieldGainB, yieldAmount / 2); + + // Perform rebalance + vm.prank(liquidityStrategy); + stabilityPool.swapCollateralForStable(collSwapAmount, stableSwapAmount); + + // Verify yield gain is preserved after swap + uint256 finalYieldGainA = stabilityPool.getDepositorYieldGain(A); + assertEq(finalYieldGainA, initialYieldGainA); + + uint256 finalYieldGainB = stabilityPool.getDepositorYieldGain(B); + assertEq(finalYieldGainB, initialYieldGainB); + + // Verify depositors can still claim yield gains + uint256 preBoldBalance = boldToken.balanceOf(A); + vm.prank(A); + stabilityPool.withdrawFromSP(0, true); + + uint256 postBoldBalance = boldToken.balanceOf(A); + assertEq(postBoldBalance - preBoldBalance, initialYieldGainA); + + + preBoldBalance = boldToken.balanceOf(B); + vm.prank(B); + stabilityPool.withdrawFromSP(0, true); + postBoldBalance = boldToken.balanceOf(B); + + assertEq(postBoldBalance - preBoldBalance, initialYieldGainB); + } +} diff --git a/contracts/test/systemParams.t.sol b/contracts/test/systemParams.t.sol new file mode 100644 index 000000000..326460d40 --- /dev/null +++ b/contracts/test/systemParams.t.sol @@ -0,0 +1,685 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import "./TestContracts/DevTestSetup.sol"; +import {SystemParams} from "../src/SystemParams.sol"; +import {ISystemParams} from "../src/Interfaces/ISystemParams.sol"; + +contract SystemParamsTest is DevTestSetup { + function testConstructorSetsAllParametersCorrectly() public { + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({ + minDebt: 2000e18 + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, // 5% + liquidationPenaltyRedistribution: 10e16 // 10% + }); + + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2 // 0.5% + }); + + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, // 0.5% + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1 + }); + + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 + }); + + SystemParams params = new SystemParams(false, + debtParams, + liquidationParams, + gasCompParams, + collateralParams, + interestParams, + redemptionParams, + poolParams + ); + + // Verify all parameters were set correctly + assertEq(params.MIN_DEBT(), 2000e18); + assertEq(params.LIQUIDATION_PENALTY_SP(), 5e16); + assertEq(params.LIQUIDATION_PENALTY_REDISTRIBUTION(), 10e16); + assertEq(params.COLL_GAS_COMPENSATION_DIVISOR(), 200); + assertEq(params.COLL_GAS_COMPENSATION_CAP(), 2 ether); + assertEq(params.ETH_GAS_COMPENSATION(), 0.0375 ether); + assertEq(params.CCR(), 150 * _1pct); + assertEq(params.SCR(), 110 * _1pct); + assertEq(params.MCR(), 110 * _1pct); + assertEq(params.BCR(), 10 * _1pct); + assertEq(params.MIN_ANNUAL_INTEREST_RATE(), _1pct / 2); + assertEq(params.REDEMPTION_FEE_FLOOR(), _1pct / 2); + assertEq(params.INITIAL_BASE_RATE(), _100pct); + assertEq(params.REDEMPTION_MINUTE_DECAY_FACTOR(), 998076443575628800); + assertEq(params.REDEMPTION_BETA(), 1); + assertEq(params.SP_YIELD_SPLIT(), 75 * _1pct); + assertEq(params.MIN_BOLD_IN_SP(), 1e18); + } + + // ========== DEBT VALIDATION TESTS ========== + + function testConstructorRevertsWhenMinDebtIsZero() public { + ISystemParams.DebtParams memory debtParams = ISystemParams.DebtParams({minDebt: 0}); + + vm.expectRevert(ISystemParams.InvalidMinDebt.selector); + new SystemParams(false, + debtParams, + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + // ========== LIQUIDATION VALIDATION TESTS ========== + + function testConstructorRevertsWhenSPPenaltyTooLow() public { + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 4 * _1pct, // Below hardcoded 5% minimum + liquidationPenaltyRedistribution: 10e16 + }); + + vm.expectRevert(ISystemParams.SPPenaltyTooLow.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenSPPenaltyGreaterThanRedistribution() public { + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 15e16, + liquidationPenaltyRedistribution: 10e16 // SP > Redistribution + }); + + vm.expectRevert(ISystemParams.SPPenaltyGtRedist.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenRedistPenaltyTooHigh() public { + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 21 * _1pct // Above hardcoded 20% maximum + }); + + vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenRedistPenaltyExceedsMCRBuffer() public { + // MCR = 110%, so buffer = 10%. Set redistribution penalty to 11% (exceeds buffer) + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 11 * _1pct // 11% > (110% - 100%) + }); + + vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorAllowsRedistPenaltyEqualToMCRBuffer() public { + // MCR = 110%, so buffer = 10%. Set redistribution penalty to exactly 10% + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 10 * _1pct // 10% == (110% - 100%) + }); + + // Should not revert + SystemParams params = new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + + assertEq(params.LIQUIDATION_PENALTY_REDISTRIBUTION(), 10 * _1pct); + } + + function testConstructorRevertsWhenRedistPenaltyExceedsMCRBufferWithDifferentMCR() public { + // MCR = 115%, so buffer = 15%. Set redistribution penalty to 16% (exceeds buffer) + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 115 * _1pct, + mcr: 115 * _1pct, + bcr: 10 * _1pct + }); + + ISystemParams.LiquidationParams memory liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 16 * _1pct // 16% > (115% - 100%) + }); + + vm.expectRevert(ISystemParams.RedistPenaltyTooHigh.selector); + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + + liquidationParams = ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 15 * _1pct // 15% + }); + + new SystemParams(false, + _getValidDebtParams(), + liquidationParams, + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + // ========== GAS COMPENSATION VALIDATION TESTS ========== + + function testConstructorRevertsWhenGasCompDivisorZero() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 0, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenGasCompDivisorTooHigh() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 1001, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenGasCompCapZero() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 0, + ethGasCompensation: 0.0375 ether + }); + + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenGasCompCapTooHigh() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 11 ether, + ethGasCompensation: 0.0375 ether + }); + + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenETHGasCompZero() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0 + }); + + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenETHGasCompTooHigh() public { + ISystemParams.GasCompParams memory gasCompParams = ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 1.1 ether + }); + + vm.expectRevert(ISystemParams.InvalidGasCompensation.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + gasCompParams, + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + // ========== COLLATERAL VALIDATION TESTS ========== + + function testConstructorRevertsWhenCCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: _100pct, // <= 100% + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + + vm.expectRevert(ISystemParams.InvalidCCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenCCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 2 * _100pct, // >= 200% + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + + vm.expectRevert(ISystemParams.InvalidCCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenMCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: _100pct, // <= 100% + bcr: 10 * _1pct + }); + + vm.expectRevert(ISystemParams.InvalidMCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenMCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 2 * _100pct, // >= 200% + bcr: 10 * _1pct + }); + + vm.expectRevert(ISystemParams.InvalidMCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenBCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 4 * _1pct // < 5% + }); + + vm.expectRevert(ISystemParams.InvalidBCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenBCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 50 * _1pct // >= 50% + }); + + vm.expectRevert(ISystemParams.InvalidBCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenSCRTooLow() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: _100pct, // <= 100% + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + + vm.expectRevert(ISystemParams.InvalidSCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenSCRTooHigh() public { + ISystemParams.CollateralParams memory collateralParams = ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 2 * _100pct, // >= 200% + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + + vm.expectRevert(ISystemParams.InvalidSCR.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + collateralParams, + _getValidInterestParams(), + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + // ========== INTEREST VALIDATION TESTS ========== + + function testConstructorRevertsWhenMinInterestRateGreaterThanMax() public { + ISystemParams.InterestParams memory interestParams = ISystemParams.InterestParams({ + minAnnualInterestRate: 300 * _1pct // min > MAX (250%) + }); + + vm.expectRevert(ISystemParams.MinInterestRateGtMax.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + interestParams, + _getValidRedemptionParams(), + _getValidPoolParams() + ); + } + + + // ========== REDEMPTION VALIDATION TESTS ========== + + function testConstructorRevertsWhenRedemptionFeeFloorTooHigh() public { + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _100pct + 1, + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1 + }); + + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + redemptionParams, + _getValidPoolParams() + ); + } + + function testConstructorRevertsWhenInitialBaseRateTooHigh() public { + ISystemParams.RedemptionParams memory redemptionParams = ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, + initialBaseRate: 1001 * _1pct, // > 1000% + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1 + }); + + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + redemptionParams, + _getValidPoolParams() + ); + } + + // ========== STABILITY POOL VALIDATION TESTS ========== + + function testConstructorRevertsWhenSPYieldSplitTooHigh() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: _100pct + 1, + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 + }); + + vm.expectRevert(ISystemParams.InvalidFeeValue.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); + } + + function testConstructorRevertsWhenMinBoldInSPLessThan1e18() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18 - 1, // < 1e18 + minBoldAfterRebalance: 1_000e18 + }); + + vm.expectRevert(ISystemParams.InvalidMinBoldInSP.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); + } + + function testConstructorRevertsWhenMinBoldAfterRebalanceLessThanMinBoldInSP() public { + ISystemParams.StabilityPoolParams memory poolParams = ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18, + minBoldAfterRebalance: 1e18 - 1 // < 1e18 + }); + + vm.expectRevert(ISystemParams.InvalidMinBoldInSP.selector); + new SystemParams(false, + _getValidDebtParams(), + _getValidLiquidationParams(), + _getValidGasCompParams(), + _getValidCollateralParams(), + _getValidInterestParams(), + _getValidRedemptionParams(), + poolParams + ); + } + + // ========== HELPER FUNCTIONS ========== + + function _getValidDebtParams() internal pure returns (ISystemParams.DebtParams memory) { + return ISystemParams.DebtParams({minDebt: 2000e18}); + } + + function _getValidLiquidationParams() internal pure returns (ISystemParams.LiquidationParams memory) { + return ISystemParams.LiquidationParams({ + liquidationPenaltySP: 5e16, + liquidationPenaltyRedistribution: 10e16 + }); + } + + function _getValidGasCompParams() internal pure returns (ISystemParams.GasCompParams memory) { + return ISystemParams.GasCompParams({ + collGasCompensationDivisor: 200, + collGasCompensationCap: 2 ether, + ethGasCompensation: 0.0375 ether + }); + } + + function _getValidCollateralParams() internal pure returns (ISystemParams.CollateralParams memory) { + return ISystemParams.CollateralParams({ + ccr: 150 * _1pct, + scr: 110 * _1pct, + mcr: 110 * _1pct, + bcr: 10 * _1pct + }); + } + + function _getValidInterestParams() internal pure returns (ISystemParams.InterestParams memory) { + return ISystemParams.InterestParams({ + minAnnualInterestRate: _1pct / 2 + }); + } + + function _getValidRedemptionParams() internal pure returns (ISystemParams.RedemptionParams memory) { + return ISystemParams.RedemptionParams({ + redemptionFeeFloor: _1pct / 2, + initialBaseRate: _100pct, + redemptionMinuteDecayFactor: 998076443575628800, + redemptionBeta: 1 + }); + } + + function _getValidPoolParams() internal pure returns (ISystemParams.StabilityPoolParams memory) { + return ISystemParams.StabilityPoolParams({ + spYieldSplit: 75 * _1pct, + minBoldInSP: 1e18, + minBoldAfterRebalance: 1_000e18 + }); + } +} \ No newline at end of file diff --git a/contracts/test/troveManager.t.sol b/contracts/test/troveManager.t.sol index 621383f88..a63b80915 100644 --- a/contracts/test/troveManager.t.sol +++ b/contracts/test/troveManager.t.sol @@ -217,4 +217,19 @@ contract TroveManagerTest is DevTestSetup { liquidatedTroves[1] = troveIDs.B; troveManager.batchLiquidateTroves(liquidatedTroves); } + + function testLiquidationRevertsWhenL2SequencerIsDown() public { + priceFeed.setPrice(2000e18); + uint256 ATroveId = openTroveNoHints100pct(A, 100 ether, 100_000e18, 1e17); + uint256 BTroveId = openTroveNoHints100pct(B, 100 ether, 100_000e18, 1e17); + + + priceFeed.setPrice(1_000e18); + priceFeed.setL2SequencerUp(false); + + vm.startPrank(A); + vm.expectRevert(TroveManager.L2SequencerDown.selector); + troveManager.liquidate(ATroveId); + vm.stopPrank(); + } } diff --git a/contracts/test/troveNFT.t.sol b/contracts/test/troveNFT.t.sol index 5a1287b4d..03ef11ee0 100644 --- a/contracts/test/troveNFT.t.sol +++ b/contracts/test/troveNFT.t.sol @@ -75,7 +75,7 @@ contract troveNFTTest is DevTestSetup { TestDeployer deployer = new TestDeployer(); TestDeployer.LiquityContractsDev[] memory _contractsArray; - (_contractsArray, collateralRegistry, boldToken,,, WETH,) = + (_contractsArray, collateralRegistry, boldToken,,, WETH) = deployer.deployAndConnectContractsMultiColl(troveManagerParamsArray); // Unimplemented feature (...):Copying of type struct LiquityContracts memory[] memory to storage not yet supported. for (uint256 c = 0; c < NUM_COLLATERALS; c++) { @@ -109,6 +109,8 @@ contract troveNFTTest is DevTestSetup { } } + systemParams = contractsArray[0].systemParams; + troveIds = new uint256[](NUM_VARIANTS); // 0 = WETH diff --git a/contracts/test/zapperGasComp.t.sol b/contracts/test/zapperGasComp.t.sol deleted file mode 100644 index f78368fca..000000000 --- a/contracts/test/zapperGasComp.t.sol +++ /dev/null @@ -1,888 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "./TestContracts/DevTestSetup.sol"; -import "./TestContracts/WETH.sol"; -import "src/Zappers/GasCompZapper.sol"; - -contract ZapperGasCompTest is DevTestSetup { - function setUp() public override { - // Start tests at a non-zero timestamp - vm.warp(block.timestamp + 600); - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F, G) = ( - accountsList[0], - accountsList[1], - accountsList[2], - accountsList[3], - accountsList[4], - accountsList[5], - accountsList[6] - ); - - WETH = new WETH9(); - - TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](2); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - troveManagerParams[1] = TestDeployer.TroveManagerParams(160e16, 120e16, 10e16, 120e16, 5e16, 10e16); - - TestDeployer deployer = new TestDeployer(); - TestDeployer.LiquityContractsDev[] memory contractsArray; - TestDeployer.Zappers[] memory zappersArray; - (contractsArray, collateralRegistry, boldToken,,, zappersArray) = - deployer.deployAndConnectContracts(troveManagerParams, WETH); - - // Set price feeds - contractsArray[1].priceFeed.setPrice(2000e18); - - // Set first branch as default - addressesRegistry = contractsArray[1].addressesRegistry; - borrowerOperations = contractsArray[1].borrowerOperations; - troveManager = contractsArray[1].troveManager; - troveNFT = contractsArray[1].troveNFT; - collToken = contractsArray[1].collToken; - gasCompZapper = zappersArray[1].gasCompZapper; - - // Give some Collateral to test accounts - uint256 initialCollateralAmount = 10_000e18; - - // A to F - for (uint256 i = 0; i < 6; i++) { - // Give some raw ETH to test accounts - deal(accountsList[i], initialCollateralAmount); - // Give and approve some coll token to test accounts - deal(address(collToken), accountsList[i], initialCollateralAmount); - vm.startPrank(accountsList[i]); - collToken.approve(address(gasCompZapper), initialCollateralAmount); - vm.stopPrank(); - } - } - - function testCanOpenTrove() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - ETH_GAS_COMPENSATION, "ETH bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore - collAmount, "Coll bal mismatch"); - } - - function testCanOpenTroveWithBatchManager() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 0, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - ETH_GAS_COMPENSATION, "ETH bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore - collAmount, "Coll bal mismatch"); - assertEq(borrowerOperations.interestBatchManagerOf(troveId), B, "Wrong batch manager"); - (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); - assertEq(tmBatchManagerAddress, B, "Wrong batch manager (TM)"); - } - - function testCanNotOpenTroveWithBatchManagerAndInterest() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vm.expectRevert("GCZ: Cannot choose interest if joining a batch"); - gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - } - - function testCanAddColl() external { - uint256 collAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 collAmount2 = 5 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 collBalanceBefore = collToken.balanceOf(A); - vm.startPrank(A); - gasCompZapper.addColl(troveId, collAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 + collAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore - collAmount2, "Coll bal mismatch"); - } - - function testCanWithdrawColl() external { - uint256 collAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 collAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 collBalanceBefore = collToken.balanceOf(A); - vm.startPrank(A); - gasCompZapper.withdrawColl(troveId, collAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 - collAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore + collAmount2, "Coll bal mismatch"); - } - - function testCanontWithdrawCollIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 collAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.withdrawColl(troveId, collAmount2); - vm.stopPrank(); - } - - function testCanNotAddReceiverWithoutRemoveManager() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Try to add a receiver for the zapper without remove manager - vm.startPrank(A); - vm.expectRevert(AddRemoveManagers.EmptyManager.selector); - gasCompZapper.setRemoveManagerWithReceiver(troveId, address(0), B); - vm.stopPrank(); - } - - function testCanRepayBold() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper, and send bold - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - boldToken.transfer(B, boldAmount2); - vm.stopPrank(); - - // Approve and repay - vm.startPrank(B); - boldToken.approve(address(gasCompZapper), boldAmount2); - gasCompZapper.repayBold(troveId, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 - boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA - boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCanWithdrawBold() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - gasCompZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCannotWithdrawBoldIfZapperIsNotReceiver() external { - uint256 collAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustTroveWithdrawCollAndBold() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - gasCompZapper.adjustTrove(troveId, collAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 - collAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA + collAmount2, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCannotAdjustTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.adjustTrove(troveId, collAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustTroveAddCollAndWithdrawBold() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - gasCompZapper.adjustTrove(troveId, collAmount2, true, boldAmount2, true, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), collAmount1 + collAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB - collAmount2, "B Coll bal mismatch"); - } - - function testCannotAdjustTroveAddCollAndWithdrawBoldIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.adjustTrove(troveId, collAmount2, true, boldAmount2, true, boldAmount2); - vm.stopPrank(); - } - - // TODO: more adjustment combinations - function testCanAdjustZombieTroveWithdrawCollAndBold() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - uint256 troveCollBefore = troveManager.getTroveEntireColl(troveId); - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 collBalanceBeforeA = collToken.balanceOf(A); - uint256 collBalanceBeforeB = collToken.balanceOf(B); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - gasCompZapper.adjustZombieTrove(troveId, collAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore - collAmount2, "Trove coll mismatch"); - assertApproxEqAbs(troveManager.getTroveEntireDebt(troveId), 2 * boldAmount2, 2e18, "Trove debt mismatch"); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBeforeA + collAmount2, "A Coll bal mismatch"); - assertEq(boldToken.balanceOf(B), 0, "B BOLD bal mismatch"); - assertEq(collToken.balanceOf(B), collBalanceBeforeB, "B Coll bal mismatch"); - } - - function testCannotAdjustZombieTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 collAmount1 = 10 ether; - uint256 collAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount1, - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - gasCompZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), B); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.adjustZombieTrove(troveId, collAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - } - - function testCanCloseTrove() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), ETH_GAS_COMPENSATION); - deal(address(collToken), B, 100 ether); - collToken.approve(address(borrowerOperations), 100 ether); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - boldToken.approve(address(gasCompZapper), type(uint256).max); - gasCompZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), 0, "Coll mismatch"); - assertEq(troveManager.getTroveEntireDebt(troveId), 0, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), 0, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore, "ETH bal mismatch"); - assertEq(collToken.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - } - - function testCannotCloseTroveIfZapperIsNotReceiver() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), ETH_GAS_COMPENSATION); - deal(address(collToken), B, 100 ether); - collToken.approve(address(borrowerOperations), 100 ether); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(gasCompZapper), C); - boldToken.approve(address(gasCompZapper), type(uint256).max); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - gasCompZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - } - - function testExcessRepaymentByAdjustGoesBackToUser() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = collToken.balanceOf(A); - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - - // Adjust trove: remove 1 ETH and try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(gasCompZapper), type(uint256).max); - gasCompZapper.adjustTrove(troveId, 1 ether, false, 9000e18, false, 0); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(gasCompZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(A.balance, ethBalanceBefore, "ETH bal mismatch"); - assertEq(address(gasCompZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(collToken.balanceOf(A), collBalanceBefore + 1 ether, "Coll bal mismatch"); - assertEq(collToken.balanceOf(address(gasCompZapper)), 0, "Zapper Coll bal should be zero"); - } - - function testExcessRepaymentByRepayGoesBackToUser() external { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: collAmount, - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = gasCompZapper.openTroveWithRawETH{value: ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - uint256 collBalanceBefore = collToken.balanceOf(A); - - // Adjust trove: try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(gasCompZapper), type(uint256).max); - gasCompZapper.repayBold(troveId, 9000e18); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(gasCompZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(address(gasCompZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(collToken.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertEq(collToken.balanceOf(address(gasCompZapper)), 0, "Zapper Coll bal should be zero"); - } - - // TODO: tests for add/remove managers of zapper contract -} diff --git a/contracts/test/zapperLeverage.t.sol b/contracts/test/zapperLeverage.t.sol deleted file mode 100644 index d59bfccb0..000000000 --- a/contracts/test/zapperLeverage.t.sol +++ /dev/null @@ -1,2064 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "./TestContracts/DevTestSetup.sol"; -import "./TestContracts/WETH.sol"; -import "src/Zappers/Modules/Exchanges/Curve/ICurvePool.sol"; -import "src/Zappers/Modules/Exchanges/CurveExchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Pool.sol"; -import "src/Zappers/Modules/Exchanges/UniV3Exchange.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/INonfungiblePositionManager.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IUniswapV3Factory.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/IQuoterV2.sol"; -import "src/Zappers/Modules/Exchanges/UniswapV3/ISwapRouter.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3Exchange.sol"; -import "src/Zappers/Modules/Exchanges/HybridCurveUniV3ExchangeHelpers.sol"; -import "src/Zappers/Interfaces/IFlashLoanProvider.sol"; -import "src/Zappers/Modules/FlashLoans/Balancer/vault/IVault.sol"; - -import "src/Zappers/Modules/Exchanges/Curve/ICurveStableswapNGFactory.sol"; - -contract ZapperLeverageMainnet is DevTestSetup { - using StringFormatting for uint256; - - IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); - - // Curve - uint128 constant BOLD_TOKEN_INDEX = 0; - uint256 constant COLL_TOKEN_INDEX = 1; - uint128 constant USDC_INDEX = 1; - - // UniV3 - INonfungiblePositionManager constant uniV3PositionManager = - INonfungiblePositionManager(0xC36442b4a4522E871399CD717aBDD847Ab11FE88); - IUniswapV3Factory constant uniswapV3Factory = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984); - IQuoterV2 constant uniV3Quoter = IQuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e); - ISwapRouter constant uniV3Router = ISwapRouter(0xE592427A0AEce92De3Edee1F18E0157C05861564); - uint24 constant UNIV3_FEE = 3000; // 0.3% - uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% - uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - - uint256 constant NUM_COLLATERALS = 3; - - IZapper[] baseZapperArray; - ILeverageZapper[] leverageZapperCurveArray; - ILeverageZapper[] leverageZapperUniV3Array; - ILeverageZapper[] leverageZapperHybridArray; - - HybridCurveUniV3ExchangeHelpers hybridCurveUniV3ExchangeHelpers; - - ICurveStableswapNGPool usdcCurvePool; - - TestDeployer.LiquityContracts[] contractsArray; - - struct OpenTroveVars { - uint256 price; - uint256 flashLoanAmount; - uint256 expectedBoldAmount; - uint256 maxNetDebt; - uint256 effectiveBoldAmount; - uint256 value; - uint256 troveId; - } - - struct LeverVars { - uint256 price; - uint256 currentCR; - uint256 currentLR; - uint256 currentCollAmount; - uint256 flashLoanAmount; - uint256 expectedBoldAmount; - uint256 maxNetDebtIncrease; - uint256 effectiveBoldAmount; - } - - struct TestVars { - uint256 collAmount; - uint256 initialLeverageRatio; - uint256 troveId; - uint256 initialDebt; - uint256 newLeverageRatio; - uint256 resultingCollateralRatio; - uint256 flashLoanAmount; - uint256 price; - uint256 boldBalanceBeforeA; - uint256 ethBalanceBeforeA; - uint256 collBalanceBeforeA; - uint256 boldBalanceBeforeZapper; - uint256 ethBalanceBeforeZapper; - uint256 collBalanceBeforeZapper; - uint256 boldBalanceBeforeExchange; - uint256 ethBalanceBeforeExchange; - uint256 collBalanceBeforeExchange; - } - - enum ExchangeType { - Curve, - UniV3, - HybridCurveUniV3 - } - - function setUp() public override { - uint256 forkBlock = 21328610; - - try vm.envString("MAINNET_RPC_URL") returns (string memory rpcUrl) { - vm.createSelectFork(rpcUrl, forkBlock); - } catch { - vm.skip(true); - } - - // Start tests at a non-zero timestamp - vm.warp(block.timestamp + 600); - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F, G) = ( - accountsList[0], - accountsList[1], - accountsList[2], - accountsList[3], - accountsList[4], - accountsList[5], - accountsList[6] - ); - - WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - - TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = - new TestDeployer.TroveManagerParams[](NUM_COLLATERALS); - troveManagerParamsArray[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - troveManagerParamsArray[c] = TestDeployer.TroveManagerParams(160e16, 120e16, 10e16, 120e16, 5e16, 10e16); - } - - TestDeployer deployer = new TestDeployer(); - TestDeployer.DeploymentResultMainnet memory result = - deployer.deployAndConnectContractsMainnet(troveManagerParamsArray); - collateralRegistry = result.collateralRegistry; - boldToken = result.boldToken; - // Record contracts - baseZapperArray.push(result.zappersArray[0].wethZapper); - for (uint256 c = 1; c < NUM_COLLATERALS; c++) { - baseZapperArray.push(result.zappersArray[c].gasCompZapper); - } - for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - contractsArray.push(result.contractsArray[c]); - leverageZapperCurveArray.push(result.zappersArray[c].leverageZapperCurve); - leverageZapperUniV3Array.push(result.zappersArray[c].leverageZapperUniV3); - leverageZapperHybridArray.push(result.zappersArray[c].leverageZapperHybrid); - } - - // Bootstrap Curve pools - fundCurveV2Pools(result.contractsArray, result.zappersArray); - - // Bootstrap UniV3 pools - fundUniV3Pools(result.contractsArray); - - // Give some Collateral to test accounts - uint256 initialCollateralAmount = 10_000e18; - - // A to F - for (uint256 c = 0; c < NUM_COLLATERALS; c++) { - for (uint256 i = 0; i < 6; i++) { - // Give some raw ETH to test accounts - deal(accountsList[i], initialCollateralAmount); - // Give and approve some coll token to test accounts - deal(address(contractsArray[c].collToken), accountsList[i], initialCollateralAmount); - vm.startPrank(accountsList[i]); - contractsArray[c].collToken.approve(address(baseZapperArray[c]), initialCollateralAmount); - contractsArray[c].collToken.approve(address(leverageZapperCurveArray[c]), initialCollateralAmount); - contractsArray[c].collToken.approve(address(leverageZapperUniV3Array[c]), initialCollateralAmount); - contractsArray[c].collToken.approve(address(leverageZapperHybridArray[c]), initialCollateralAmount); - vm.stopPrank(); - } - } - - // exchange helpers - hybridCurveUniV3ExchangeHelpers = new HybridCurveUniV3ExchangeHelpers( - USDC, - WETH, - usdcCurvePool, - USDC_INDEX, // USDC Curve pool index - BOLD_TOKEN_INDEX, // BOLD Curve pool index - UNIV3_FEE_USDC_WETH, - UNIV3_FEE_WETH_COLL, - uniV3Quoter - ); - } - - function fundCurveV2Pools( - TestDeployer.LiquityContracts[] memory _contractsArray, - TestDeployer.Zappers[] memory _zappersArray - ) internal { - uint256 boldAmount; - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - (uint256 price,) = _contractsArray[i].priceFeed.fetchPrice(); - ICurvePool curvePool = CurveExchange(address(_zappersArray[i].leverageZapperCurve.exchange())).curvePool(); - - // Add liquidity - uint256 collAmount = 1000 ether; - boldAmount = collAmount * price / DECIMAL_PRECISION; - deal(address(_contractsArray[i].collToken), A, collAmount); - deal(address(boldToken), A, boldAmount); - vm.startPrank(A); - // approve - _contractsArray[i].collToken.approve(address(curvePool), collAmount); - boldToken.approve(address(curvePool), boldAmount); - uint256[2] memory amounts; - amounts[0] = boldAmount; - amounts[1] = collAmount; - curvePool.add_liquidity(amounts, 0); - vm.stopPrank(); - } - - // Add liquidity to USDC-BOLD - usdcCurvePool = HybridCurveUniV3Exchange(address(_zappersArray[0].leverageZapperHybrid.exchange())).curvePool(); - uint256 usdcAmount = 1e15; // 1B with 6 decimals - boldAmount = usdcAmount * 1e12; // from 6 to 18 decimals - deal(address(USDC), A, usdcAmount); - deal(address(boldToken), A, boldAmount); - vm.startPrank(A); - // approve - USDC.approve(address(usdcCurvePool), usdcAmount); - boldToken.approve(address(usdcCurvePool), boldAmount); - uint256[] memory amountsDynamic = new uint256[](2); - amountsDynamic[0] = boldAmount; - amountsDynamic[1] = usdcAmount; - // add liquidity - usdcCurvePool.add_liquidity(amountsDynamic, 0); - vm.stopPrank(); - } - - function fundUniV3Pools(TestDeployer.LiquityContracts[] memory _contractsArray) internal { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - (uint256 price,) = _contractsArray[i].priceFeed.fetchPrice(); - //console2.log(price, "price"); - - // tokens and amounts - uint256 collAmount = 1000 ether; - uint256 boldAmount = collAmount * price / DECIMAL_PRECISION; - address[2] memory tokens; - uint256[2] memory amounts; - if (address(boldToken) < address(_contractsArray[i].collToken)) { - //console2.log("b < c"); - tokens[0] = address(boldToken); - tokens[1] = address(_contractsArray[i].collToken); - amounts[0] = boldAmount; - amounts[1] = collAmount; - } else { - //console2.log("c < b"); - tokens[0] = address(_contractsArray[i].collToken); - tokens[1] = address(boldToken); - amounts[0] = collAmount; - amounts[1] = boldAmount; - } - - // Add liquidity - - vm.startPrank(A); - - // deal and approve - deal(address(_contractsArray[i].collToken), A, collAmount); - deal(address(boldToken), A, boldAmount); - _contractsArray[i].collToken.approve(address(uniV3PositionManager), collAmount); - boldToken.approve(address(uniV3PositionManager), boldAmount); - - // mint new position - address uniV3PoolAddress = - uniswapV3Factory.getPool(address(boldToken), address(_contractsArray[i].collToken), UNIV3_FEE); - //console2.log(uniV3PoolAddress, "uniV3PoolAddress"); - int24 TICK_SPACING = IUniswapV3Pool(uniV3PoolAddress).tickSpacing(); - (, int24 tick,,,,,) = IUniswapV3Pool(uniV3PoolAddress).slot0(); - int24 tickLower = (tick - 6000) / TICK_SPACING * TICK_SPACING; - int24 tickUpper = (tick + 6000) / TICK_SPACING * TICK_SPACING; - INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({ - token0: tokens[0], - token1: tokens[1], - fee: UNIV3_FEE, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: amounts[0], - amount1Desired: amounts[1], - amount0Min: 0, - amount1Min: 0, - recipient: A, - deadline: block.timestamp - }); - - uniV3PositionManager.mint(params); - - vm.stopPrank(); - } - } - - // Implementing `onERC721Received` so this contract can receive custody of erc721 tokens - function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { - return this.onERC721Received.selector; - } - - struct OpenLeveragedTroveWithIndexParams { - ILeverageZapper leverageZapper; - IERC20 collToken; - uint256 index; - uint256 collAmount; - uint256 leverageRatio; - IPriceFeed priceFeed; - ExchangeType exchangeType; - uint256 branch; - address batchManager; - } - - function openLeveragedTroveWithIndex(OpenLeveragedTroveWithIndexParams memory _inputParams) - internal - returns (uint256, uint256) - { - OpenTroveVars memory vars; - (vars.price,) = _inputParams.priceFeed.fetchPrice(); - - // This should be done in the frontend - vars.flashLoanAmount = - _inputParams.collAmount * (_inputParams.leverageRatio - DECIMAL_PRECISION) / DECIMAL_PRECISION; - vars.expectedBoldAmount = vars.flashLoanAmount * vars.price / DECIMAL_PRECISION; - vars.maxNetDebt = vars.expectedBoldAmount * 105 / 100; // slippage - vars.effectiveBoldAmount = _getBoldAmountToSwap( - _inputParams.exchangeType, - _inputParams.branch, - vars.expectedBoldAmount, - vars.maxNetDebt, - vars.flashLoanAmount, - _inputParams.collToken - ); - - ILeverageZapper.OpenLeveragedTroveParams memory params = ILeverageZapper.OpenLeveragedTroveParams({ - owner: A, - ownerIndex: _inputParams.index, - collAmount: _inputParams.collAmount, - flashLoanAmount: vars.flashLoanAmount, - boldAmount: vars.effectiveBoldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: _inputParams.batchManager == address(0) ? 5e16 : 0, - batchManager: _inputParams.batchManager, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vars.value = _inputParams.branch > 0 ? ETH_GAS_COMPENSATION : _inputParams.collAmount + ETH_GAS_COMPENSATION; - _inputParams.leverageZapper.openLeveragedTroveWithRawETH{value: vars.value}(params); - vars.troveId = addressToTroveIdThroughZapper(address(_inputParams.leverageZapper), A, _inputParams.index); - vm.stopPrank(); - - return (vars.troveId, vars.effectiveBoldAmount); - } - - function _setInitialBalances(ILeverageZapper _leverageZapper, uint256 _branch, TestVars memory vars) - internal - view - { - vars.boldBalanceBeforeA = boldToken.balanceOf(A); - vars.ethBalanceBeforeA = A.balance; - vars.collBalanceBeforeA = contractsArray[_branch].collToken.balanceOf(A); - vars.boldBalanceBeforeZapper = boldToken.balanceOf(address(_leverageZapper)); - vars.ethBalanceBeforeZapper = address(_leverageZapper).balance; - vars.collBalanceBeforeZapper = contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)); - vars.boldBalanceBeforeExchange = boldToken.balanceOf(address(_leverageZapper.exchange())); - vars.ethBalanceBeforeExchange = address(_leverageZapper.exchange()).balance; - vars.collBalanceBeforeExchange = - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())); - } - - function testCanOpenTroveWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanOpenTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i, address(0)); - } - } - - function testCanOpenTroveWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanOpenTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i, address(0)); - } - } - - function testCanOpenTroveWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCanOpenTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i, address(0)); - } - } - - function testCanOpenTroveAndJoinBatchWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _registerBatchManager(B, i); - _testCanOpenTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i, B); - } - } - - function testCanOpenTroveAndJoinBatchWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - if (i == 2) continue; // TODO!! - _registerBatchManager(B, i); - _testCanOpenTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i, B); - } - } - - function testCanOpenTroveAndJoinBatchWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _registerBatchManager(B, i); - _testCanOpenTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i, B); - } - } - - function _registerBatchManager(address _account, uint256 _branch) internal { - vm.startPrank(_account); - contractsArray[_branch].borrowerOperations.registerBatchManager( - uint128(1e16), uint128(20e16), uint128(5e16), uint128(25e14), MIN_INTEREST_RATE_CHANGE_PERIOD - ); - vm.stopPrank(); - } - - function _testCanOpenTrove( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch, - address _batchManager - ) internal { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.newLeverageRatio = 2e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - _setInitialBalances(_leverageZapper, _branch, vars); - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.newLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = _batchManager; - uint256 expectedMinNetDebt; - (vars.troveId, expectedMinNetDebt) = openLeveragedTroveWithIndex(openTroveParams); - - // Checks - (vars.price,) = contractsArray[_branch].priceFeed.fetchPrice(); - // owner - assertEq(contractsArray[_branch].troveNFT.ownerOf(vars.troveId), A, "Wrong owner"); - // troveId - assertGt(vars.troveId, 0, "Trove id should be set"); - // coll - assertEq( - getTroveEntireColl(contractsArray[_branch].troveManager, vars.troveId), - vars.collAmount * vars.newLeverageRatio / DECIMAL_PRECISION, - "Coll mismatch" - ); - // debt - uint256 expectedMaxNetDebt = expectedMinNetDebt * 105 / 100; - uint256 troveEntireDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - assertGe(troveEntireDebt, expectedMinNetDebt, "Debt too low"); - assertLe(troveEntireDebt, expectedMaxNetDebt, "Debt too high"); - // CR - uint256 ICR = contractsArray[_branch].troveManager.getCurrentICR(vars.troveId, vars.price); - assertTrue(ICR >= vars.resultingCollateralRatio || vars.resultingCollateralRatio - ICR < 3e16, "Wrong CR"); - // token balances - assertEq(boldToken.balanceOf(A), vars.boldBalanceBeforeA, "BOLD bal mismatch"); - assertEq( - boldToken.balanceOf(address(_leverageZapper)), vars.boldBalanceBeforeZapper, "Zapper should not keep BOLD" - ); - assertEq( - boldToken.balanceOf(address(_leverageZapper.exchange())), - vars.boldBalanceBeforeExchange, - "Exchange should not keep BOLD" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)), - vars.collBalanceBeforeZapper, - "Zapper should not keep Coll" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())), - vars.collBalanceBeforeExchange, - "Exchange should not keep Coll" - ); - assertEq(address(_leverageZapper).balance, vars.ethBalanceBeforeZapper, "Zapper should not keep ETH"); - assertEq( - address(_leverageZapper.exchange()).balance, vars.ethBalanceBeforeExchange, "Exchange should not keep ETH" - ); - if (_branch > 0) { - // LST - assertEq(A.balance, vars.ethBalanceBeforeA - ETH_GAS_COMPENSATION, "ETH bal mismatch"); - assertGe( - contractsArray[_branch].collToken.balanceOf(A), - vars.collBalanceBeforeA - vars.collAmount, - "Coll bal mismatch" - ); - } else { - assertEq(A.balance, vars.ethBalanceBeforeA - ETH_GAS_COMPENSATION - vars.collAmount, "ETH bal mismatch"); - assertGe(contractsArray[_branch].collToken.balanceOf(A), vars.collBalanceBeforeA, "Coll bal mismatch"); - } - } - - function testOnlyFlashLoanProviderCanCallOpenTroveCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallOpenTroveCallback(leverageZapperCurveArray[i]); - } - } - - function testOnlyFlashLoanProviderCanCallOpenTroveCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallOpenTroveCallback(leverageZapperUniV3Array[i]); - } - } - - function _testOnlyFlashLoanProviderCanCallOpenTroveCallback(ILeverageZapper _leverageZapper) internal { - ILeverageZapper.OpenLeveragedTroveParams memory params = ILeverageZapper.OpenLeveragedTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 10 ether, - flashLoanAmount: 10 ether, - boldAmount: 10000e18, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vm.expectRevert("LZ: Caller not FlashLoan provider"); - IFlashLoanReceiver(address(_leverageZapper)).receiveFlashLoanOnOpenLeveragedTrove(params, 10 ether); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - // Lever up - - struct LeverUpParams { - ILeverageZapper leverageZapper; - IERC20 collToken; - uint256 troveId; - uint256 leverageRatio; - ITroveManager troveManager; - IPriceFeed priceFeed; - ExchangeType exchangeType; - uint256 branch; - } - - function _getLeverUpFlashLoanAndBoldAmount(LeverUpParams memory _params) internal returns (uint256, uint256) { - LeverVars memory vars; - (vars.price,) = _params.priceFeed.fetchPrice(); - vars.currentCR = _params.troveManager.getCurrentICR(_params.troveId, vars.price); - vars.currentLR = _params.leverageZapper.leverageRatioToCollateralRatio(vars.currentCR); - assertGt(_params.leverageRatio, vars.currentLR, "Leverage ratio should increase"); - vars.currentCollAmount = getTroveEntireColl(_params.troveManager, _params.troveId); - vars.flashLoanAmount = vars.currentCollAmount * _params.leverageRatio / vars.currentLR - vars.currentCollAmount; - vars.expectedBoldAmount = vars.flashLoanAmount * vars.price / DECIMAL_PRECISION; - vars.maxNetDebtIncrease = vars.expectedBoldAmount * 105 / 100; // slippage - // The actual bold we need, capped by the slippage above, to get flash loan amount - vars.effectiveBoldAmount = _getBoldAmountToSwap( - _params.exchangeType, - _params.branch, - vars.expectedBoldAmount, - vars.maxNetDebtIncrease, - vars.flashLoanAmount, - _params.collToken - ); - - return (vars.flashLoanAmount, vars.effectiveBoldAmount); - } - - function leverUpTrove(LeverUpParams memory _params) internal returns (uint256, uint256) { - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(_params); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: _params.troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - vm.startPrank(A); - _params.leverageZapper.leverUpTrove(params); - vm.stopPrank(); - - return (flashLoanAmount, effectiveBoldAmount); - } - - function testCanLeverUpTroveWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverUpTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCanLeverUpTroveWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverUpTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCanLeverUpTroveWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCanLeverUpTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testCanLeverUpTrove(ILeverageZapper _leverageZapper, ExchangeType _exchangeType, uint256 _branch) - internal - { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.initialLeverageRatio = 2e18; - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.initialLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (vars.troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - vars.initialDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - - vars.newLeverageRatio = 2.5e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - _setInitialBalances(_leverageZapper, _branch, vars); - - LeverUpParams memory params; - params.leverageZapper = _leverageZapper; - params.collToken = contractsArray[_branch].collToken; - params.troveId = vars.troveId; - params.leverageRatio = vars.newLeverageRatio; - params.troveManager = contractsArray[_branch].troveManager; - params.priceFeed = contractsArray[_branch].priceFeed; - params.exchangeType = _exchangeType; - params.branch = _branch; - uint256 expectedMinLeverUpNetDebt; - (vars.flashLoanAmount, expectedMinLeverUpNetDebt) = leverUpTrove(params); - - // Checks - (vars.price,) = contractsArray[_branch].priceFeed.fetchPrice(); - // coll - uint256 coll = getTroveEntireColl(contractsArray[_branch].troveManager, vars.troveId); - uint256 collExpected = vars.collAmount * vars.newLeverageRatio / DECIMAL_PRECISION; - assertTrue(coll >= collExpected || collExpected - coll <= 4e17, "Coll mismatch"); - // debt - uint256 expectedMinNetDebt = vars.initialDebt + expectedMinLeverUpNetDebt; - uint256 expectedMaxNetDebt = expectedMinNetDebt * 105 / 100; - uint256 troveEntireDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - assertGe(troveEntireDebt, expectedMinNetDebt, "Debt too low"); - assertLe(troveEntireDebt, expectedMaxNetDebt, "Debt too high"); - // CR - uint256 ICR = contractsArray[_branch].troveManager.getCurrentICR(vars.troveId, vars.price); - assertTrue(ICR >= vars.resultingCollateralRatio || vars.resultingCollateralRatio - ICR < 2e16, "Wrong CR"); - // token balances - assertEq(boldToken.balanceOf(A), vars.boldBalanceBeforeA, "BOLD bal mismatch"); - assertEq(A.balance, vars.ethBalanceBeforeA, "ETH bal mismatch"); - assertGe(contractsArray[_branch].collToken.balanceOf(A), vars.collBalanceBeforeA, "Coll bal mismatch"); - assertEq( - boldToken.balanceOf(address(_leverageZapper)), vars.boldBalanceBeforeZapper, "Zapper should not keep BOLD" - ); - assertEq( - boldToken.balanceOf(address(_leverageZapper.exchange())), - vars.boldBalanceBeforeExchange, - "Exchange should not keep BOLD" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)), - vars.collBalanceBeforeZapper, - "Zapper should not keep Coll" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())), - vars.collBalanceBeforeExchange, - "Exchange should not keep Coll" - ); - assertEq(address(_leverageZapper).balance, vars.ethBalanceBeforeZapper, "Zapper should not keep ETH"); - assertEq( - address(_leverageZapper.exchange()).balance, vars.ethBalanceBeforeExchange, "Exchange should not keep ETH" - ); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testCannotLeverUpTroveWithCurveIfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverUpTroveIfZapperIsNotReceiver(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCannotLeverUpTroveWithUniV3IfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverUpTroveIfZapperIsNotReceiver(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCannotLeverUpTroveWithHybridIfZapperIsNotReceiver() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCannotLeverUpTroveIfZapperIsNotReceiver(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testCannotLeverUpTroveIfZapperIsNotReceiver( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.initialLeverageRatio = 2e18; - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.initialLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (vars.troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - vars.initialDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - - vars.newLeverageRatio = 2.5e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = vars.troveId; - getterParams.leverageRatio = vars.newLeverageRatio; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: vars.troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - vm.startPrank(A); - // Change receiver in BO - contractsArray[_branch].borrowerOperations.setRemoveManagerWithReceiver( - vars.troveId, address(_leverageZapper), C - ); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - _leverageZapper.leverUpTrove(params); - vm.stopPrank(); - } - - function testOnlyFlashLoanProviderCanCallLeverUpCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverUpCallback(leverageZapperCurveArray[i]); - } - } - - function testOnlyFlashLoanProviderCanCallLeverUpCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverUpCallback(leverageZapperUniV3Array[i]); - } - } - - function _testOnlyFlashLoanProviderCanCallLeverUpCallback(ILeverageZapper _leverageZapper) internal { - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: addressToTroveIdThroughZapper(address(_leverageZapper), A), - flashLoanAmount: 10 ether, - boldAmount: 10000e18, - maxUpfrontFee: 1000e18 - }); - vm.startPrank(A); - vm.expectRevert("LZ: Caller not FlashLoan provider"); - IFlashLoanReceiver(address(_leverageZapper)).receiveFlashLoanOnLeverUpTrove(params, 10 ether); - vm.stopPrank(); - } - - function testOnlyOwnerOrManagerCanLeverUpWithCurveFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromZapper(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithUniV3FromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromZapper(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithHybridFromZapper() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromZapper(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testOnlyOwnerOrManagerCanLeverUpFromZapper( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = troveId; - getterParams.leverageRatio = 2.5e18; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - // B tries to lever up A’s trove - vm.startPrank(B); - vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); - _leverageZapper.leverUpTrove(params); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverUpWithCurveFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithUniV3FromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithHybridFromBalancerFLProvider() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverUpFromBalancerFLProvider( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 1; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = troveId; - getterParams.leverageRatio = 2.5e18; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - // B tries to lever up A’s trove calling our flash loan provider module - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - vm.startPrank(B); - vm.expectRevert(); // reverts without data because it calls back B - flashLoanProvider.makeFlashLoan( - contractsArray[_branch].collToken, - flashLoanAmount, - IFlashLoanProvider.Operation.LeverUpTrove, - abi.encode(params) - ); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverUpWithCurveFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithUniV3FromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverUpWithHybridFromBalancerVault() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverUpFromBalancerVault( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 2; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever up A’s trove calling Balancer Vault directly - LeverUpParams memory getterParams; - getterParams.leverageZapper = _leverageZapper; - getterParams.collToken = contractsArray[_branch].collToken; - getterParams.troveId = troveId; - getterParams.leverageRatio = 2.5e18; - getterParams.troveManager = contractsArray[_branch].troveManager; - getterParams.priceFeed = contractsArray[_branch].priceFeed; - getterParams.exchangeType = _exchangeType; - getterParams.branch = _branch; - (uint256 flashLoanAmount, uint256 effectiveBoldAmount) = _getLeverUpFlashLoanAndBoldAmount(getterParams); - - ILeverageZapper.LeverUpTroveParams memory params = ILeverageZapper.LeverUpTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - boldAmount: effectiveBoldAmount, - maxUpfrontFee: 1000e18 - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = contractsArray[_branch].collToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = flashLoanAmount; - bytes memory userData = abi.encode(address(_leverageZapper), IFlashLoanProvider.Operation.LeverUpTrove, params); - IVault vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - vm.startPrank(B); - vm.expectRevert("Flash loan not properly initiated"); - vault.flashLoan(IFlashLoanRecipient(address(flashLoanProvider)), tokens, amounts, userData); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - // Lever down - - function _getLeverDownFlashLoanAndBoldAmount( - ILeverageZapper _leverageZapper, - uint256 _troveId, - uint256 _leverageRatio, - ITroveManager _troveManager, - IPriceFeed _priceFeed - ) internal returns (uint256, uint256) { - (uint256 price,) = _priceFeed.fetchPrice(); - - uint256 currentCR = _troveManager.getCurrentICR(_troveId, price); - uint256 currentLR = _leverageZapper.leverageRatioToCollateralRatio(currentCR); - assertLt(_leverageRatio, currentLR, "Leverage ratio should decrease"); - uint256 currentCollAmount = getTroveEntireColl(_troveManager, _troveId); - uint256 flashLoanAmount = currentCollAmount - currentCollAmount * _leverageRatio / currentLR; - uint256 expectedBoldAmount = flashLoanAmount * price / DECIMAL_PRECISION; - uint256 minBoldDebt = expectedBoldAmount * 95 / 100; // slippage - - return (flashLoanAmount, minBoldDebt); - } - - function leverDownTrove( - ILeverageZapper _leverageZapper, - uint256 _troveId, - uint256 _leverageRatio, - ITroveManager _troveManager, - IPriceFeed _priceFeed - ) internal returns (uint256) { - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minBoldDebt) = - _getLeverDownFlashLoanAndBoldAmount(_leverageZapper, _troveId, _leverageRatio, _troveManager, _priceFeed); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: _troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - vm.startPrank(A); - _leverageZapper.leverDownTrove(params); - vm.stopPrank(); - - return flashLoanAmount; - } - - function testCanLeverDownTroveWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverDownTrove(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCanLeverDownTroveWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanLeverDownTrove(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCanLeverDownTroveWithHybrid() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCanLeverDownTrove(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testCanLeverDownTrove(ILeverageZapper _leverageZapper, ExchangeType _exchangeType, uint256 _branch) - internal - { - TestVars memory vars; - vars.collAmount = 10 ether; - vars.initialLeverageRatio = 2e18; - - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = vars.collAmount; - openTroveParams.leverageRatio = vars.initialLeverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (vars.troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - vars.initialDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - - vars.newLeverageRatio = 1.5e18; - vars.resultingCollateralRatio = _leverageZapper.leverageRatioToCollateralRatio(vars.newLeverageRatio); - - _setInitialBalances(_leverageZapper, _branch, vars); - - vars.flashLoanAmount = leverDownTrove( - _leverageZapper, - vars.troveId, - vars.newLeverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - // Checks - (vars.price,) = contractsArray[_branch].priceFeed.fetchPrice(); - // coll - uint256 coll = getTroveEntireColl(contractsArray[_branch].troveManager, vars.troveId); - uint256 collExpected = vars.collAmount * vars.newLeverageRatio / DECIMAL_PRECISION; - assertTrue(coll >= collExpected || collExpected - coll <= 22e16, "Coll mismatch"); - // debt - uint256 expectedMinNetDebt = - vars.initialDebt - vars.flashLoanAmount * vars.price / DECIMAL_PRECISION * 101 / 100; - uint256 expectedMaxNetDebt = expectedMinNetDebt * 105 / 100; - uint256 troveEntireDebt = getTroveEntireDebt(contractsArray[_branch].troveManager, vars.troveId); - assertGe(troveEntireDebt, expectedMinNetDebt, "Debt too low"); - assertLe(troveEntireDebt, expectedMaxNetDebt, "Debt too high"); - // CR - // When getting flashloan amount, we allow the min debt to deviate up to 5% - // That deviation can translate into CR, specially for UniV3 exchange which is the less efficient - // With UniV3, the quoter gives a price “too good”, meaning we exchange less, so the deleverage is lower - uint256 CRTolerance = _exchangeType == ExchangeType.UniV3 ? 9e16 : 17e15; - uint256 ICR = contractsArray[_branch].troveManager.getCurrentICR(vars.troveId, vars.price); - assertTrue( - ICR >= vars.resultingCollateralRatio || vars.resultingCollateralRatio - ICR < CRTolerance, "Wrong CR" - ); - // token balances - assertEq(boldToken.balanceOf(A), vars.boldBalanceBeforeA, "BOLD bal mismatch"); - assertEq(A.balance, vars.ethBalanceBeforeA, "ETH bal mismatch"); - assertGe(contractsArray[_branch].collToken.balanceOf(A), vars.collBalanceBeforeA, "Coll bal mismatch"); - assertEq( - boldToken.balanceOf(address(_leverageZapper)), vars.boldBalanceBeforeZapper, "Zapper should not keep BOLD" - ); - assertEq( - boldToken.balanceOf(address(_leverageZapper.exchange())), - vars.boldBalanceBeforeExchange, - "Exchange should not keep BOLD" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper)), - vars.collBalanceBeforeZapper, - "Zapper should not keep Coll" - ); - assertEq( - contractsArray[_branch].collToken.balanceOf(address(_leverageZapper.exchange())), - vars.collBalanceBeforeExchange, - "Exchange should not keep Coll" - ); - assertEq(address(_leverageZapper).balance, vars.ethBalanceBeforeZapper, "Zapper should not keep ETH"); - assertEq( - address(_leverageZapper.exchange()).balance, vars.ethBalanceBeforeExchange, "Exchange should not keep ETH" - ); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testCannotLeverDownWithCurveFromZapperIfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverDownFromZapperIfZapperIsNotReceiver(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testCannotLeverDownWithUniV3FromZapperIfZapperIsNotReceiver() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotLeverDownFromZapperIfZapperIsNotReceiver(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testCannotLeverDownWithHybridFromZapperIfZapperIsNotReceiver() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testCannotLeverDownFromZapperIfZapperIsNotReceiver( - leverageZapperUniV3Array[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testCannotLeverDownFromZapperIfZapperIsNotReceiver( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - vm.startPrank(A); - // Change receiver in BO - contractsArray[_branch].borrowerOperations.setRemoveManagerWithReceiver(troveId, address(_leverageZapper), C); - - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - _leverageZapper.leverDownTrove(params); - vm.stopPrank(); - } - - function testOnlyFlashLoanProviderCanCallLeverDownCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverDownCallback(leverageZapperCurveArray[i]); - } - } - - function testOnlyFlashLoanProviderCanCallLeverDownCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallLeverDownCallback(leverageZapperUniV3Array[i]); - } - } - - function _testOnlyFlashLoanProviderCanCallLeverDownCallback(ILeverageZapper _leverageZapper) internal { - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: addressToTroveIdThroughZapper(address(_leverageZapper), A), - flashLoanAmount: 10 ether, - minBoldAmount: 10000e18 - }); - vm.startPrank(A); - vm.expectRevert("LZ: Caller not FlashLoan provider"); - IFlashLoanReceiver(address(_leverageZapper)).receiveFlashLoanOnLeverDownTrove(params, 10 ether); - vm.stopPrank(); - } - - function testOnlyOwnerOrManagerCanLeverDownWithCurveFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromZapper(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithUniV3FromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromZapper(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithHybridFromZapper() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromZapper(leverageZapperUniV3Array[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testOnlyOwnerOrManagerCanLeverDownFromZapper( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 0; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever up A’s trove - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - vm.startPrank(B); - vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); - _leverageZapper.leverDownTrove(params); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_leverageZapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverDownWithCurveFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - leverageZapperCurveArray[i], ExchangeType.Curve, i - ); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithUniV3FromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - leverageZapperUniV3Array[i], ExchangeType.UniV3, i - ); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithHybridFromBalancerFLProvider() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverDownFromBalancerFLProvider( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 1; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever down A’s trove calling our flash loan provider module - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - vm.startPrank(B); - vm.expectRevert(); // reverts without data because it calls back B - flashLoanProvider.makeFlashLoan( - contractsArray[_branch].collToken, - flashLoanAmount, - IFlashLoanProvider.Operation.LeverDownTrove, - abi.encode(params) - ); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanLeverDownWithCurveFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault(leverageZapperCurveArray[i], ExchangeType.Curve, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithUniV3FromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - } - - function testOnlyOwnerOrManagerCanLeverDownWithHybridFromBalancerVault() external { - // Not enough liquidity for ETHx - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault( - leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i - ); - } - } - - function _testOnlyOwnerOrManagerCanLeverDownFromBalancerVault( - ILeverageZapper _leverageZapper, - ExchangeType _exchangeType, - uint256 _branch - ) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 leverageRatio = 2e18; - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = 2; - openTroveParams.collAmount = collAmount; - openTroveParams.leverageRatio = leverageRatio; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - // B tries to lever down A’s trove calling Balancer Vault directly - (uint256 flashLoanAmount, uint256 minBoldDebt) = _getLeverDownFlashLoanAndBoldAmount( - _leverageZapper, - troveId, - 1.5e18, // _leverageRatio, - contractsArray[_branch].troveManager, - contractsArray[_branch].priceFeed - ); - - ILeverageZapper.LeverDownTroveParams memory params = ILeverageZapper.LeverDownTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minBoldAmount: minBoldDebt - }); - IFlashLoanProvider flashLoanProvider = _leverageZapper.flashLoanProvider(); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = contractsArray[_branch].collToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = flashLoanAmount; - bytes memory userData = - abi.encode(address(_leverageZapper), IFlashLoanProvider.Operation.LeverDownTrove, params); - IVault vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - vm.startPrank(B); - vm.expectRevert("Flash loan not properly initiated"); - vault.flashLoan(IFlashLoanRecipient(address(flashLoanProvider)), tokens, amounts, userData); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - // Close trove - - function testCanCloseTroveWithBaseZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanCloseTrove(baseZapperArray[i], i); - } - } - - function testCanCloseTroveWithLeverageCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanCloseTrove(IZapper(leverageZapperCurveArray[i]), i); - } - } - - function testCanCloseTroveWithLeverageUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCanCloseTrove(IZapper(leverageZapperUniV3Array[i]), i); - } - } - - function testCanCloseTroveWithLeverageHybrid() external { - for (uint256 i = 0; i < 3; i++) { - _testCanCloseTrove(IZapper(leverageZapperHybridArray[i]), i); - } - } - - function _getCloseFlashLoanAmount(uint256 _troveId, ITroveManager _troveManager, IPriceFeed _priceFeed) - internal - returns (uint256, uint256) - { - (uint256 price,) = _priceFeed.fetchPrice(); - - uint256 currentDebt = getTroveEntireDebt(_troveManager, _troveId); - uint256 currentColl = getTroveEntireColl(_troveManager, _troveId); - uint256 flashLoanAmount = currentDebt * DECIMAL_PRECISION / price * 105 / 100; // slippage - - return (flashLoanAmount, currentColl - flashLoanAmount); - } - - function closeTrove(IZapper _zapper, uint256 _troveId, ITroveManager _troveManager, IPriceFeed _priceFeed) - internal - { - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(_troveId, _troveManager, _priceFeed); - - vm.startPrank(A); - _zapper.closeTroveFromCollateral(_troveId, flashLoanAmount, minExpectedCollateral); - vm.stopPrank(); - } - - function openTrove( - IZapper _zapper, - address _account, - uint256 _index, - uint256 _collAmount, - uint256 _boldAmount, - bool _lst - ) internal returns (uint256) { - return openTrove(_zapper, _account, _index, _collAmount, _boldAmount, _lst, MIN_ANNUAL_INTEREST_RATE); - } - - function openTrove( - IZapper _zapper, - address _account, - uint256 _index, - uint256 _collAmount, - uint256 _boldAmount, - bool _lst, - uint256 _interestRate - ) internal returns (uint256) { - IZapper.OpenTroveParams memory openParams = IZapper.OpenTroveParams({ - owner: _account, - ownerIndex: _index, - collAmount: _collAmount, - boldAmount: _boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: _interestRate, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - - vm.startPrank(_account); - uint256 value = _lst ? ETH_GAS_COMPENSATION : _collAmount + ETH_GAS_COMPENSATION; - uint256 troveId = _zapper.openTroveWithRawETH{value: value}(openParams); - vm.stopPrank(); - - return troveId; - } - - function _testCanCloseTrove(IZapper _zapper, uint256 _branch) internal { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // open a 2nd trove so we can close the 1st one - openTrove(_zapper, B, 0, 100 ether, 10000e18, lst); - - uint256 boldBalanceBefore = boldToken.balanceOf(A); - uint256 collBalanceBefore = contractsArray[_branch].collToken.balanceOf(A); - uint256 ethBalanceBefore = A.balance; - (uint256 price,) = contractsArray[_branch].priceFeed.fetchPrice(); - uint256 debtInColl = - getTroveEntireDebt(contractsArray[_branch].troveManager, troveId) * DECIMAL_PRECISION / price; - - // Close trove - closeTrove(_zapper, troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - assertEq(getTroveEntireColl(contractsArray[_branch].troveManager, troveId), 0, "Coll mismatch"); - assertEq(getTroveEntireDebt(contractsArray[_branch].troveManager, troveId), 0, "Debt mismatch"); - assertGe(boldToken.balanceOf(A), boldBalanceBefore, "BOLD bal should not decrease"); - assertLe(boldToken.balanceOf(A), boldBalanceBefore * 105 / 100, "BOLD bal can only increase by slippage margin"); - if (lst) { - assertGe(contractsArray[_branch].collToken.balanceOf(A), collBalanceBefore, "Coll bal should not decrease"); - assertApproxEqAbs( - contractsArray[_branch].collToken.balanceOf(A), - collBalanceBefore + collAmount - debtInColl, - 3e17, - "Coll bal mismatch" - ); - assertEq(A.balance, ethBalanceBefore + ETH_GAS_COMPENSATION, "ETH bal mismatch"); - } else { - assertEq(contractsArray[_branch].collToken.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertGe(A.balance, ethBalanceBefore, "ETH bal should not decrease"); - assertApproxEqAbs( - A.balance, ethBalanceBefore + collAmount + ETH_GAS_COMPENSATION - debtInColl, 3e17, "ETH bal mismatch" - ); - } - } - - function testCannotCloseTroveWithBaseZapperIfLessCollThanExpected() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotCloseTroveIfLessCollThanExpected(baseZapperArray[i], i); - } - } - - function testCannotCloseTroveWithLeverageCurveIfLessCollThanExpected() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotCloseTroveIfLessCollThanExpected(IZapper(leverageZapperCurveArray[i]), i); - } - } - - function testCannotCloseTroveWithLeverageUniV3IfLessCollThanExpected() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testCannotCloseTroveIfLessCollThanExpected(IZapper(leverageZapperUniV3Array[i]), i); - } - } - - function testCannotCloseTroveWithLeverageHybridIfLessCollThanExpected() external { - for (uint256 i = 0; i < 3; i++) { - _testCannotCloseTroveIfLessCollThanExpected(IZapper(leverageZapperHybridArray[i]), i); - } - } - - function _testCannotCloseTroveIfLessCollThanExpected(IZapper _zapper, uint256 _branch) internal { - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // open a 2nd trove so we can close the 1st one - openTrove(_zapper, B, 0, 100 ether, 10000e18, lst); - - // Try to close trove - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - string memory revertReason = lst ? "GCZ: Not enough collateral received" : "WZ: Not enough collateral received"; - vm.startPrank(A); - vm.expectRevert(bytes(revertReason)); - _zapper.closeTroveFromCollateral(troveId, flashLoanAmount, minExpectedCollateral * 2); - vm.stopPrank(); - } - - function testCannotCloseTroveIfFrontRunByRedemption() external { - // Make sure redemption rate is not 100% - vm.warp(block.timestamp + 18 hours); - - IZapper zapper = IZapper(leverageZapperHybridArray[0]); - - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - // open a 2nd trove so we can close the A's one, with higher interest so it doesn't get redeemed - openTrove(zapper, B, 0, 100 ether, 10000e18, false, 1e17); - - uint256 troveId = openTrove(zapper, A, 0, collAmount, boldAmount, false); - - // Try to close trove - // This should be done in the frontend - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[0].troveManager, contractsArray[0].priceFeed); - - // Now attacker redeems from trove and increases Bold price - vm.startPrank(B); - // Redemption - collateralRegistry.redeemCollateral(10000e18, 0, 1e18); - uint256 troveDebt = getTroveEntireDebt(contractsArray[0].troveManager, troveId); - uint256 troveColl = getTroveEntireColl(contractsArray[0].troveManager, troveId); - assertLt(troveDebt, boldAmount, "Trove debt should have decreased"); - assertLt(troveColl, collAmount, "Trove coll should have decreased"); - - // Swap WETH to USDC to increase price - uint256 swapWETHAmount = 10000e18; - deal(address(WETH), B, swapWETHAmount); - WETH.approve(address(uniV3Router), swapWETHAmount); - bytes memory path = abi.encodePacked(WETH, UNIV3_FEE_USDC_WETH, USDC); - ISwapRouter.ExactInputParams memory params = ISwapRouter.ExactInputParams({ - path: path, - recipient: B, - deadline: block.timestamp, - amountIn: swapWETHAmount, - amountOutMinimum: 0 - }); - - uniV3Router.exactInput(params); - vm.stopPrank(); - - vm.startPrank(A); - vm.expectRevert("WZ: Not enough collateral received"); - zapper.closeTroveFromCollateral(troveId, flashLoanAmount, minExpectedCollateral); - vm.stopPrank(); - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithBaseZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(baseZapperArray[i], i); - } - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithCurve() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(leverageZapperCurveArray[i], i); - } - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithUniV3() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyFlashLoanProviderCanCallCloseTroveCallbackWithHybrid() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyFlashLoanProviderCanCallCloseTroveCallback(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyFlashLoanProviderCanCallCloseTroveCallback(IZapper _zapper, uint256 _branch) internal { - IZapper.CloseTroveParams memory params = IZapper.CloseTroveParams({ - troveId: addressToTroveIdThroughZapper(address(_zapper), A), - flashLoanAmount: 10 ether, - minExpectedCollateral: 0, - receiver: address(0) // Set later - }); - - bool lst = _branch > 0; - string memory revertReason = lst ? "GCZ: Caller not FlashLoan provider" : "WZ: Caller not FlashLoan provider"; - vm.startPrank(A); - vm.expectRevert(bytes(revertReason)); - IFlashLoanReceiver(address(_zapper)).receiveFlashLoanOnCloseTroveFromCollateral(params, 10 ether); - vm.stopPrank(); - } - - function testOnlyOwnerOrManagerCanCloseTroveWithBaseZapperFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(baseZapperArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithCurveFromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(leverageZapperCurveArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithUniV3FromZapper() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithHybridFromZapper() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromZapper(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyOwnerOrManagerCanCloseTroveFromZapper(IZapper _zapper, uint256 _branch) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // B tries to close A’s trove - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - vm.startPrank(B); - vm.expectRevert(AddRemoveManagers.NotOwnerNorRemoveManager.selector); - _zapper.closeTroveFromCollateral(troveId, flashLoanAmount, minExpectedCollateral); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(_zapper.flashLoanProvider().receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanCloseTroveWithBaseZapperFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(baseZapperArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithCurveFromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(leverageZapperCurveArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithUniV3FromBalancerFLProvider() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithHybridFromBalancerFLProvider() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyOwnerOrManagerCanCloseTroveFromBalancerFLProvider(IZapper _zapper, uint256 _branch) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // B tries to close A’s trove calling our flash loan provider module - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - IZapper.CloseTroveParams memory params = IZapper.CloseTroveParams({ - troveId: troveId, - flashLoanAmount: flashLoanAmount, - minExpectedCollateral: minExpectedCollateral, - receiver: address(0) // Set later - }); - IFlashLoanProvider flashLoanProvider = _zapper.flashLoanProvider(); - vm.startPrank(B); - vm.expectRevert(); // reverts without data because it calls back B - flashLoanProvider.makeFlashLoan( - contractsArray[_branch].collToken, - flashLoanAmount, - IFlashLoanProvider.Operation.CloseTrove, - abi.encode(params) - ); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testOnlyOwnerOrManagerCanCloseTroveWithBaseZapperFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(baseZapperArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithCurveFromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(leverageZapperCurveArray[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithUniV3FromBalancerVault() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(leverageZapperUniV3Array[i], i); - } - } - - function testOnlyOwnerOrManagerCanCloseTroveWithHybridFromBalancerVault() external { - for (uint256 i = 0; i < 3; i++) { - _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(leverageZapperHybridArray[i], i); - } - } - - function _testOnlyOwnerOrManagerCanCloseTroveFromBalancerVault(IZapper _zapper, uint256 _branch) internal { - // Open trove - uint256 collAmount = 10 ether; - uint256 boldAmount = 10000e18; - - bool lst = _branch > 0; - uint256 troveId = openTrove(_zapper, A, 0, collAmount, boldAmount, lst); - - // B tries to close A’s trove calling Balancer Vault directly - (uint256 flashLoanAmount, uint256 minExpectedCollateral) = - _getCloseFlashLoanAmount(troveId, contractsArray[_branch].troveManager, contractsArray[_branch].priceFeed); - - IFlashLoanProvider flashLoanProvider = _zapper.flashLoanProvider(); - IERC20[] memory tokens = new IERC20[](1); - tokens[0] = contractsArray[_branch].collToken; - uint256[] memory amounts = new uint256[](1); - amounts[0] = flashLoanAmount; - bytes memory userData = abi.encode( - address(_zapper), IFlashLoanProvider.Operation.CloseTrove, troveId, flashLoanAmount, minExpectedCollateral - ); - IVault vault = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8); - vm.startPrank(B); - vm.expectRevert("Flash loan not properly initiated"); - vault.flashLoan(IFlashLoanRecipient(address(flashLoanProvider)), tokens, amounts, userData); - vm.stopPrank(); - - // Check receiver is back to zero - assertEq(address(flashLoanProvider.receiver()), address(0), "Receiver should be zero"); - } - - function testApprovalIsNotReset() external { - for (uint256 i = 0; i < NUM_COLLATERALS; i++) { - _testApprovalIsNotReset(leverageZapperCurveArray[i], ExchangeType.Curve, i); - _testApprovalIsNotReset(leverageZapperUniV3Array[i], ExchangeType.UniV3, i); - } - for (uint256 i = 0; i < 3; i++) { - _testApprovalIsNotReset(leverageZapperHybridArray[i], ExchangeType.HybridCurveUniV3, i); - } - } - - function _testApprovalIsNotReset(ILeverageZapper _leverageZapper, ExchangeType _exchangeType, uint256 _branch) - internal - { - // Open non leveraged trove - openTrove(_leverageZapper, A, uint256(_exchangeType) * 2, 10 ether, 10000e18, _branch > 0); - - // Now try to open leveraged trove, it should still work - OpenLeveragedTroveWithIndexParams memory openTroveParams; - openTroveParams.leverageZapper = _leverageZapper; - openTroveParams.collToken = contractsArray[_branch].collToken; - openTroveParams.index = uint256(_exchangeType) * 2 + 1; - openTroveParams.collAmount = 10 ether; - openTroveParams.leverageRatio = 1.5 ether; - openTroveParams.priceFeed = contractsArray[_branch].priceFeed; - openTroveParams.exchangeType = _exchangeType; - openTroveParams.branch = _branch; - openTroveParams.batchManager = address(0); - (uint256 troveId,) = openLeveragedTroveWithIndex(openTroveParams); - - assertGt(getTroveEntireColl(contractsArray[_branch].troveManager, troveId), 0); - assertGt(getTroveEntireDebt(contractsArray[_branch].troveManager, troveId), 0); - } - - // helper price functions - - // Helper to get the actual bold we need, capped by a max value, to get flash loan amount - function _getBoldAmountToSwap( - ExchangeType _exchangeType, - uint256 _branch, - uint256 _boldAmount, - uint256 _maxBoldAmount, - uint256 _minCollAmount, - IERC20 _collToken - ) internal returns (uint256) { - if (_exchangeType == ExchangeType.Curve) { - return _getBoldAmountToSwapCurve(_branch, _boldAmount, _maxBoldAmount, _minCollAmount); - } - - if (_exchangeType == ExchangeType.UniV3) { - return _getBoldAmountToSwapUniV3(_maxBoldAmount, _minCollAmount, _collToken); - } - - return _getBoldAmountToSwapHybrid(_maxBoldAmount, _minCollAmount, _collToken); - } - - function _getBoldAmountToSwapCurve( - uint256 _branch, - uint256 _boldAmount, - uint256 _maxBoldAmount, - uint256 _minCollAmount - ) internal view returns (uint256) { - ICurvePool curvePool = CurveExchange(address(leverageZapperCurveArray[_branch].exchange())).curvePool(); - - uint256 step = (_maxBoldAmount - _boldAmount) / 5; // In max 5 iterations we should reach the target, unless price is lower - uint256 dy; - // TODO: Optimizations: binary search, change the step depending on last dy, ... - // Or check if there’s any helper implemented anywhere - uint256 lastBoldAmount = _maxBoldAmount + step; - do { - lastBoldAmount -= step; - dy = curvePool.get_dy(BOLD_TOKEN_INDEX, COLL_TOKEN_INDEX, lastBoldAmount); - } while (dy > _minCollAmount && lastBoldAmount > step); - - uint256 boldAmountToSwap = dy >= _minCollAmount ? lastBoldAmount : lastBoldAmount + step; - require(boldAmountToSwap <= _maxBoldAmount, "Bold amount required too high"); - - return boldAmountToSwap; - } - - // See: https://docs.uniswap.org/contracts/v3/reference/periphery/interfaces/IQuoterV2 - // These functions are not marked view because they rely on calling non-view functions and reverting to compute the result. - // They are also not gas efficient and should not be called on-chain. - function _getBoldAmountToSwapUniV3(uint256 _maxBoldAmount, uint256 _minCollAmount, IERC20 _collToken) - internal /* view */ - returns (uint256) - { - IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({ - tokenIn: address(boldToken), - tokenOut: address(_collToken), - amount: _minCollAmount, - fee: UNIV3_FEE, - sqrtPriceLimitX96: 0 - }); - (uint256 amountIn,,,) = uniV3Quoter.quoteExactOutputSingle(params); - require(amountIn <= _maxBoldAmount, "Price too high"); - - return amountIn; - } - - function _getBoldAmountToSwapHybrid(uint256 _maxBoldAmount, uint256 _minCollAmount, IERC20 _collToken) - internal /* view */ - returns (uint256) - { - // Uniswap - uint256 wethAmount; - IQuoterV2.QuoteExactOutputSingleParams memory quoterParams; - // Coll <- WETH - if (address(WETH) != address(_collToken)) { - quoterParams = IQuoterV2.QuoteExactOutputSingleParams({ - tokenIn: address(WETH), - tokenOut: address(_collToken), - amount: _minCollAmount, - fee: UNIV3_FEE_WETH_COLL, - sqrtPriceLimitX96: 0 - }); - (wethAmount,,,) = uniV3Quoter.quoteExactOutputSingle(quoterParams); - } else { - wethAmount = _minCollAmount; - } - // WETH <- USDC - quoterParams = IQuoterV2.QuoteExactOutputSingleParams({ - tokenIn: address(USDC), - tokenOut: address(WETH), - amount: wethAmount, - fee: UNIV3_FEE_USDC_WETH, - sqrtPriceLimitX96: 0 - }); - (uint256 usdcAmount,,,) = uniV3Quoter.quoteExactOutputSingle(quoterParams); - - // Curve - // USDC <- BOLD - uint256 boldAmountToSwap = usdcCurvePool.get_dx(int128(BOLD_TOKEN_INDEX), int128(USDC_INDEX), usdcAmount); - require(boldAmountToSwap <= _maxBoldAmount, "Bold amount required too high"); - - boldAmountToSwap = Math.min(boldAmountToSwap * 101 / 100, _maxBoldAmount); // TODO - - return boldAmountToSwap; - } - - // Helpers - function testHybridExchangeHelpers() public { - for (uint256 i = 0; i < 3; i++) { - (uint256 price,) = contractsArray[i].priceFeed.fetchPrice(); - //console2.log(i, "branch"); - //console2.log(price, "price"); - //console2.log(price, "amount"); - _testHybridExchangeHelpers(price, contractsArray[i].collToken, 1 ether, 1e16); // 1% slippage - //console2.log(price * 1e3, "amount"); - _testHybridExchangeHelpers(price * 1e3, contractsArray[i].collToken, 1 ether, 1e16); // 1% slippage - //console2.log(price * 1e6, "amount"); - _testHybridExchangeHelpers(price * 1e6, contractsArray[i].collToken, 1 ether, 1e16); // 1% slippage - } - } - - function _testHybridExchangeHelpers( - uint256 _boldAmount, - IERC20 _collToken, - uint256 _desiredCollAmount, - uint256 _acceptedSlippage - ) internal { - (uint256 collAmount, uint256 slippage) = - hybridCurveUniV3ExchangeHelpers.getCollFromBold(_boldAmount, _collToken, _desiredCollAmount); - //console2.log(collAmount, "collAmount"); - //console2.log(slippage, "slippage"); - assertGe(collAmount, (DECIMAL_PRECISION - slippage) * _desiredCollAmount / DECIMAL_PRECISION); - assertLe(slippage, _acceptedSlippage); - } - - function testHybridExchangeHelpersNoDeviation() public { - (uint256 price,) = contractsArray[0].priceFeed.fetchPrice(); - (uint256 collAmount, uint256 slippage) = - hybridCurveUniV3ExchangeHelpers.getCollFromBold(price, contractsArray[0].collToken, 0); - assertGt(collAmount, 0); - assertEq(slippage, 0); - } -} diff --git a/contracts/test/zapperWETH.t.sol b/contracts/test/zapperWETH.t.sol deleted file mode 100644 index 7eebf7b2e..000000000 --- a/contracts/test/zapperWETH.t.sol +++ /dev/null @@ -1,932 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.18; - -import "./TestContracts/DevTestSetup.sol"; -import "./TestContracts/WETH.sol"; -import "src/Zappers/WETHZapper.sol"; - -contract ZapperWETHTest is DevTestSetup { - function setUp() public override { - // Start tests at a non-zero timestamp - vm.warp(block.timestamp + 600); - - accounts = new Accounts(); - createAccounts(); - - (A, B, C, D, E, F, G) = ( - accountsList[0], - accountsList[1], - accountsList[2], - accountsList[3], - accountsList[4], - accountsList[5], - accountsList[6] - ); - - WETH = new WETH9(); - - TestDeployer.TroveManagerParams[] memory troveManagerParams = new TestDeployer.TroveManagerParams[](1); - troveManagerParams[0] = TestDeployer.TroveManagerParams(150e16, 110e16, 10e16, 110e16, 5e16, 10e16); - - TestDeployer deployer = new TestDeployer(); - TestDeployer.LiquityContractsDev[] memory contractsArray; - TestDeployer.Zappers[] memory zappersArray; - (contractsArray, collateralRegistry, boldToken,,, zappersArray) = - deployer.deployAndConnectContracts(troveManagerParams, WETH); - - // Set price feeds - contractsArray[0].priceFeed.setPrice(2000e18); - - // Give some Collateral to test accounts - uint256 initialCollateralAmount = 10_000e18; - - // A to F - for (uint256 i = 0; i < 6; i++) { - // Give some raw ETH to test accounts - deal(accountsList[i], initialCollateralAmount); - } - - // Set first branch as default - addressesRegistry = contractsArray[0].addressesRegistry; - borrowerOperations = contractsArray[0].borrowerOperations; - troveManager = contractsArray[0].troveManager; - troveNFT = contractsArray[0].troveNFT; - wethZapper = zappersArray[0].wethZapper; - } - - function testCanOpenTrove() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - (ethAmount + ETH_GAS_COMPENSATION), "ETH bal mismatch"); - } - - function testCanOpenTroveWithBatchManager() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 0, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - assertEq(troveNFT.ownerOf(troveId), A, "Wrong owner"); - assertGt(troveId, 0, "Trove id should be set"); - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - (ethAmount + ETH_GAS_COMPENSATION), "ETH bal mismatch"); - assertEq(borrowerOperations.interestBatchManagerOf(troveId), B, "Wrong batch manager"); - (,,,,,,,, address tmBatchManagerAddress,) = troveManager.Troves(troveId); - assertEq(tmBatchManagerAddress, B, "Wrong batch manager (TM)"); - } - - function testCanNotOpenTroveWithBatchManagerAndInterest() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - registerBatchManager(B); - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: B, - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - vm.expectRevert("WZ: Cannot choose interest if joining a batch"); - wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - } - - function testCanAddColl() external { - uint256 ethAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 ethAmount2 = 5 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - vm.startPrank(A); - wethZapper.addCollWithRawETH{value: ethAmount2}(troveId); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 + ethAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore - ethAmount2, "ETH bal mismatch"); - } - - function testCanWithdrawColl() external { - uint256 ethAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 ethAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - vm.startPrank(A); - wethZapper.withdrawCollToRawETH(troveId, ethAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 - ethAmount2, "Coll mismatch"); - assertGt(troveManager.getTroveEntireDebt(troveId), boldAmount, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), boldAmount, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore + ethAmount2, "ETH bal mismatch"); - } - - function testCannotWithdrawCollIfZapperIsNotReceiver() external { - uint256 ethAmount1 = 10 ether; - uint256 boldAmount = 10000e18; - uint256 ethAmount2 = 1 ether; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: 5e16, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.withdrawCollToRawETH(troveId, ethAmount2); - vm.stopPrank(); - } - - function testCanNotAddReceiverWithoutRemoveManager() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Try to add a receiver for the zapper without remove manager - vm.startPrank(A); - vm.expectRevert(AddRemoveManagers.EmptyManager.selector); - wethZapper.setRemoveManagerWithReceiver(troveId, address(0), B); - vm.stopPrank(); - } - - function testCanRepayBold() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add a remove manager for the zapper, and send bold - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - boldToken.transfer(B, boldAmount2); - vm.stopPrank(); - - // Approve and repay - vm.startPrank(B); - boldToken.approve(address(wethZapper), boldAmount2); - wethZapper.repayBold(troveId, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 - boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA - boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCanWithdrawBold() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - wethZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCannotWithdrawBoldIfZapperIsNotReceiver() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - // Withdraw bold - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.withdrawBold(troveId, boldAmount2, boldAmount2); - vm.stopPrank(); - } - - // TODO: more adjustment combinations - function testCanAdjustTroveWithdrawCollAndBold() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - wethZapper.adjustTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 - ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 + boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA + ethAmount2, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCannotAdjustTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.adjustTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustTroveAddCollAndBold() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - // A sends Bold to B - boldToken.transfer(B, boldAmount2); - vm.stopPrank(); - - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 boldBalanceBeforeB = boldToken.balanceOf(B); - uint256 ethBalanceBeforeB = B.balance; - - // Add an add manager for the zapper - vm.startPrank(A); - wethZapper.setAddManager(troveId, B); - vm.stopPrank(); - - // Adjust (add coll and Bold) - vm.startPrank(B); - boldToken.approve(address(wethZapper), boldAmount2); - wethZapper.adjustTroveWithRawETH{value: ethAmount2}(troveId, ethAmount2, true, boldAmount2, false, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), ethAmount1 + ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs( - troveManager.getTroveEntireDebt(troveId), boldAmount1 - boldAmount2, 2e18, "Trove debt mismatch" - ); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), boldBalanceBeforeB - boldAmount2, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB - ethAmount2, "B ETH bal mismatch"); - } - - function testCanAdjustZombieTroveWithdrawCollAndBold() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - uint256 troveCollBefore = troveManager.getTroveEntireColl(troveId); - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 ethBalanceBeforeB = B.balance; - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - wethZapper.adjustZombieTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore - ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs(troveManager.getTroveEntireDebt(troveId), 2 * boldAmount2, 2e18, "Trove debt mismatch"); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA + ethAmount2, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), 0, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB, "B ETH bal mismatch"); - } - - function testCannotAdjustZombieTroveWithdrawCollAndBoldIfZapperIsNotReceiver() external { - uint256 ethAmount1 = 10 ether; - uint256 ethAmount2 = 1 ether; - uint256 boldAmount1 = 10000e18; - uint256 boldAmount2 = 1000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount1, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount1 + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(boldAmount1 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - // Adjust (withdraw coll and Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.adjustZombieTroveWithRawETH(troveId, ethAmount2, false, boldAmount2, true, 0, 0, boldAmount2); - vm.stopPrank(); - } - - function testCanAdjustZombieTroveAddCollAndWithdrawBold() external { - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: 10000e18, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: 10 ether + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // Add a remove manager for the zapper - vm.startPrank(A); - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - vm.stopPrank(); - - uint256 ethAmount2 = 1 ether; - uint256 boldAmount2 = 1000e18; - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(10000e18 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - uint256 troveCollBefore = troveManager.getTroveEntireColl(troveId); - uint256 boldBalanceBeforeA = boldToken.balanceOf(A); - uint256 ethBalanceBeforeA = A.balance; - uint256 ethBalanceBeforeB = B.balance; - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - wethZapper.adjustZombieTroveWithRawETH{value: ethAmount2}( - troveId, ethAmount2, true, boldAmount2, true, 0, 0, boldAmount2 - ); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), troveCollBefore + ethAmount2, "Trove coll mismatch"); - assertApproxEqAbs(troveManager.getTroveEntireDebt(troveId), 2 * boldAmount2, 2e18, "Trove debt mismatch"); - assertEq(boldToken.balanceOf(A), boldBalanceBeforeA + boldAmount2, "A BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBeforeA, "A ETH bal mismatch"); - assertEq(boldToken.balanceOf(B), 0, "B BOLD bal mismatch"); - assertEq(B.balance, ethBalanceBeforeB - ethAmount2, "B ETH bal mismatch"); - } - - function testCannotAdjustZombieTroveAddCollAndWithdrawBoldIfZapperIsNotReceiver() external { - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: 10000e18, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: 10 ether + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - vm.startPrank(A); - // Add a remove manager for the zapper - wethZapper.setRemoveManagerWithReceiver(troveId, B, A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - vm.stopPrank(); - - uint256 ethAmount2 = 1 ether; - uint256 boldAmount2 = 1000e18; - - // Redeem to make trove zombie - vm.startPrank(A); - collateralRegistry.redeemCollateral(10000e18 - boldAmount2, 10, 1e18); - vm.stopPrank(); - - // Adjust (add coll and withdraw Bold) - vm.startPrank(B); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.adjustZombieTroveWithRawETH{value: ethAmount2}( - troveId, ethAmount2, true, boldAmount2, true, 0, 0, boldAmount2 - ); - vm.stopPrank(); - } - - function testCanCloseTrove() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - uint256 ethBalanceBefore = A.balance; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, 100 ether + ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), 100 ether + ETH_GAS_COMPENSATION); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - boldToken.approve(address(wethZapper), type(uint256).max); - wethZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - - assertEq(troveManager.getTroveEntireColl(troveId), 0, "Coll mismatch"); - assertEq(troveManager.getTroveEntireDebt(troveId), 0, "Debt mismatch"); - assertEq(boldToken.balanceOf(A), 0, "BOLD bal mismatch"); - assertEq(A.balance, ethBalanceBefore, "ETH bal mismatch"); - } - - function testCannotCloseTroveIfZapperIsNotReceiver() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - // open a 2nd trove so we can close the 1st one, and send Bold to account for interest and fee - vm.startPrank(B); - deal(address(WETH), B, 100 ether + ETH_GAS_COMPENSATION); - WETH.approve(address(borrowerOperations), 100 ether + ETH_GAS_COMPENSATION); - borrowerOperations.openTrove( - B, - 0, // index, - 100 ether, // coll, - 10000e18, //boldAmount, - 0, // _upperHint - 0, // _lowerHint - MIN_ANNUAL_INTEREST_RATE, // annualInterestRate, - 10000e18, // upfrontFee - address(0), - address(0), - address(0) - ); - boldToken.transfer(A, troveManager.getTroveEntireDebt(troveId) - boldAmount); - vm.stopPrank(); - - vm.startPrank(A); - // Change receiver in BO - borrowerOperations.setRemoveManagerWithReceiver(troveId, address(wethZapper), C); - - boldToken.approve(address(wethZapper), type(uint256).max); - vm.expectRevert("BZ: Zapper is not receiver for this trove"); - wethZapper.closeTroveToRawETH(troveId); - vm.stopPrank(); - } - - function testExcessRepaymentByAdjustGoesBackToUser() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 ethBalanceBefore = A.balance; - uint256 collBalanceBefore = WETH.balanceOf(A); - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - - // Adjust trove: remove 1 ETH and try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(wethZapper), type(uint256).max); - wethZapper.adjustTroveWithRawETH(troveId, 1 ether, false, 9000e18, false, 0); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(wethZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(A.balance, ethBalanceBefore + 1 ether, "ETH bal mismatch"); - assertEq(address(wethZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(WETH.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertEq(WETH.balanceOf(address(wethZapper)), 0, "Zapper Coll bal should be zero"); - } - - function testExcessRepaymentByRepayGoesBackToUser() external { - uint256 ethAmount = 10 ether; - uint256 boldAmount = 10000e18; - - IZapper.OpenTroveParams memory params = IZapper.OpenTroveParams({ - owner: A, - ownerIndex: 0, - collAmount: 0, // not needed - boldAmount: boldAmount, - upperHint: 0, - lowerHint: 0, - annualInterestRate: MIN_ANNUAL_INTEREST_RATE, - batchManager: address(0), - maxUpfrontFee: 1000e18, - addManager: address(0), - removeManager: address(0), - receiver: address(0) - }); - vm.startPrank(A); - uint256 troveId = wethZapper.openTroveWithRawETH{value: ethAmount + ETH_GAS_COMPENSATION}(params); - vm.stopPrank(); - - uint256 boldDebtBefore = troveManager.getTroveEntireDebt(troveId); - uint256 collBalanceBefore = WETH.balanceOf(A); - - // Adjust trove: try to repay 9k (only will repay ~8k, up to MIN_DEBT) - vm.startPrank(A); - boldToken.approve(address(wethZapper), type(uint256).max); - wethZapper.repayBold(troveId, 9000e18); - vm.stopPrank(); - - assertEq(boldToken.balanceOf(A), boldAmount + MIN_DEBT - boldDebtBefore, "BOLD bal mismatch"); - assertEq(boldToken.balanceOf(address(wethZapper)), 0, "Zapper BOLD bal should be zero"); - assertEq(address(wethZapper).balance, 0, "Zapper ETH bal should be zero"); - assertEq(WETH.balanceOf(A), collBalanceBefore, "Coll bal mismatch"); - assertEq(WETH.balanceOf(address(wethZapper)), 0, "Zapper Coll bal should be zero"); - } - - // TODO: tests for add/remove managers of zapper contract -}