From cb27b3bccf9c204fdc874b534e06259385acb110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Sat, 2 Nov 2024 15:35:33 -0400 Subject: [PATCH 1/8] gas optimize and consolidate data loading --- script/deploy/Deploy.WETHGateway.s.sol | 8 +- src/core/VaultManagerV6.sol | 370 +++++++++++++++++++++++++ test/fork/v5/BaseV5.sol | 168 +++++++++++ test/fork/v5/v5.Liquidate.t.sol | 151 ++++++++++ test/fork/v6/BaseV6.sol | 168 +++++++++++ test/fork/v6/v6.Liquidate.t.sol | 151 ++++++++++ 6 files changed, 1009 insertions(+), 7 deletions(-) create mode 100644 src/core/VaultManagerV6.sol create mode 100644 test/fork/v5/BaseV5.sol create mode 100644 test/fork/v5/v5.Liquidate.t.sol create mode 100644 test/fork/v6/BaseV6.sol create mode 100644 test/fork/v6/v6.Liquidate.t.sol diff --git a/script/deploy/Deploy.WETHGateway.s.sol b/script/deploy/Deploy.WETHGateway.s.sol index f80f6fc9..0c0395b4 100644 --- a/script/deploy/Deploy.WETHGateway.s.sol +++ b/script/deploy/Deploy.WETHGateway.s.sol @@ -10,13 +10,7 @@ contract DeployVault is Script, Parameters { function run() public { vm.startBroadcast(); // ---------------------- - new WETHGateway( - MAINNET_V2_DYAD, - MAINNET_DNFT, - MAINNET_WETH, - MAINNET_V2_VAULT_MANAGER, - MAINNET_V2_WETH_VAULT - ); + new WETHGateway(MAINNET_V2_DYAD, MAINNET_DNFT, MAINNET_WETH, MAINNET_V2_VAULT_MANAGER, MAINNET_V2_WETH_VAULT); vm.stopBroadcast(); // ---------------------------- } diff --git a/src/core/VaultManagerV6.sol b/src/core/VaultManagerV6.sol new file mode 100644 index 00000000..1e790165 --- /dev/null +++ b/src/core/VaultManagerV6.sol @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {DNft} from "./DNft.sol"; +import {Dyad} from "./Dyad.sol"; +import {VaultLicenser} from "./VaultLicenser.sol"; +import {Vault} from "./Vault.sol"; +import {DyadXP} from "../staking/DyadXP.sol"; +import {IVaultManagerV5} from "../interfaces/IVaultManagerV5.sol"; +import {DyadHooks} from "./DyadHooks.sol"; +import "../interfaces/IExtension.sol"; + +import {FixedPointMathLib} from "@solmate/src/utils/FixedPointMathLib.sol"; +import {ERC20} from "@solmate/src/tokens/ERC20.sol"; +import {SafeTransferLib} from "@solmate/src/utils/SafeTransferLib.sol"; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/// @custom:oz-upgrades-from src/core/VaultManagerV4.sol:VaultManagerV4 +contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable { + using EnumerableSet for EnumerableSet.AddressSet; + using FixedPointMathLib for uint256; + using SafeTransferLib for ERC20; + + uint256 public constant MAX_VAULTS = 6; + uint256 public constant MIN_COLLAT_RATIO = 1.5e18; // 150% // Collaterization + uint256 public constant LIQUIDATION_REWARD = 0.2e18; // 20% + + address public constant KEROSENE_VAULT = 0x4808e4CC6a2Ba764778A0351E1Be198494aF0b43; + + DNft public dNft; + Dyad public dyad; + VaultLicenser public vaultLicenser; + + mapping(uint256 id => EnumerableSet.AddressSet vaults) internal vaults; + mapping(uint256 id => uint256 block) private lastDeposit; + + DyadXP public dyadXP; + + /// @notice Extensions authorized for use in the system, with bitmap of enabled hooks + mapping(address => uint256) private _systemExtensions; + + /// @notice Extensions authorized by a user for use on their notes + mapping(address user => EnumerableSet.AddressSet) private _authorizedExtensions; + + modifier isValidDNft(uint256 id) { + if (dNft.ownerOf(id) == address(0)) revert InvalidDNft(); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() public reinitializer(6) { + // Nothing to initialize right now + } + + /// @inheritdoc IVaultManagerV5 + function add(uint256 id, address vault) external { + _authorizeCall(id); + if (!vaultLicenser.isLicensed(vault)) revert VaultNotLicensed(); + if (vaults[id].length() >= MAX_VAULTS) revert TooManyVaults(); + if (vaults[id].add(vault)) { + emit Added(id, vault); + } + } + + /// @inheritdoc IVaultManagerV5 + function remove(uint256 id, address vault) external { + _authorizeCall(id); + if (vaults[id].remove(vault)) { + if (Vault(vault).id2asset(id) > 0) { + if (vaultLicenser.isLicensed(vault)) { + _checkExoValueAndCollatRatio(id); + } + } + emit Removed(id, vault); + } + } + + /// @inheritdoc IVaultManagerV5 + function deposit(uint256 id, address vault, uint256 amount) external isValidDNft(id) { + _authorizeCall(id); + lastDeposit[id] = block.number; + Vault _vault = Vault(vault); + _vault.asset().safeTransferFrom(msg.sender, vault, amount); + _vault.deposit(id, amount); + + if (vault == KEROSENE_VAULT) { + dyadXP.afterKeroseneDeposited(id, amount); + } + } + + /// @inheritdoc IVaultManagerV5 + function withdraw(uint256 id, address vault, uint256 amount, address to) public { + uint256 extensionFlags = _authorizeCall(id); + if (lastDeposit[id] == block.number) revert CanNotWithdrawInSameBlock(); + if (vault == KEROSENE_VAULT) dyadXP.beforeKeroseneWithdrawn(id, amount); + Vault(vault).withdraw(id, to, amount); // changes `exo` or `kero` value and `cr` + if (DyadHooks.hookEnabled(extensionFlags, DyadHooks.AFTER_WITHDRAW)) { + IAfterWithdrawHook(msg.sender).afterWithdraw(id, vault, amount, to); + } + _checkExoValueAndCollatRatio(id); + } + + //// @inheritdoc IVaultManagerV5 + function mintDyad(uint256 id, uint256 amount, address to) external { + uint256 extensionFlags = _authorizeCall(id); + dyad.mint(id, to, amount); // changes `mintedDyad` and `cr` + if (DyadHooks.hookEnabled(extensionFlags, DyadHooks.AFTER_MINT)) { + IAfterMintHook(msg.sender).afterMint(id, amount, to); + } + _checkExoValueAndCollatRatio(id); + emit MintDyad(id, amount, to); + } + + /// @notice Checks the exogenous collateral value and collateral ratio for the specified note. + /// @dev Reverts if the exogenous collateral value is less than the minted dyad or the collateral + /// ratio is below the minimum. + function _checkExoValueAndCollatRatio(uint256 id) internal view { + (, uint256 exoValue,, uint256 cr, uint256 mintedDyad) = _totalVaultValuesAndCr(id); + if (exoValue < mintedDyad) { + revert NotEnoughExoCollat(); + } + if (cr < MIN_COLLAT_RATIO) { + revert CrTooLow(); + } + } + + /// @inheritdoc IVaultManagerV5 + function burnDyad(uint256 id, uint256 amount) public isValidDNft(id) { + dyad.burn(id, msg.sender, amount); + emit BurnDyad(id, amount, msg.sender); + } + + /// @notice Liquidates the specified note, transferring collateral to the specified note. + /// @param id The note id + /// @param to The address to transfer the collateral to + /// @param amount The amount of dyad to liquidate + function liquidate(uint256 id, uint256 to, uint256 amount) + external + isValidDNft(id) + isValidDNft(to) + returns (address[] memory, uint256[] memory) + { + (uint256[] memory vaultsValues, uint256 exoValue, uint256 keroValue, uint256 cr, uint256 debt) = + _totalVaultValuesAndCr(id); + + if (cr >= MIN_COLLAT_RATIO) revert CrTooHigh(); + dyad.burn(id, msg.sender, amount); // changes `debt` and `cr` + + lastDeposit[to] = block.number; // `move` acts like a deposit + + uint256 numberOfVaults = vaults[id].length(); + address[] memory vaultAddresses = new address[](numberOfVaults); + uint256[] memory vaultAmounts = new uint256[](numberOfVaults); + + uint256 totalValue = exoValue + keroValue; + if (totalValue == 0) return (vaultAddresses, vaultAmounts); + + for (uint256 i = 0; i < numberOfVaults; i++) { + Vault vault = Vault(vaults[id].at(i)); + vaultAddresses[i] = address(vault); + if (vaultLicenser.isLicensed(address(vault))) { + uint256 depositAmount = vault.id2asset(id); + if (depositAmount == 0) continue; + uint256 value = vaultsValues[i]; + if (value == 0) continue; + uint256 asset; + if (cr < LIQUIDATION_REWARD + 1e18 && debt != amount) { + uint256 cappedCr = cr < 1e18 ? 1e18 : cr; + uint256 liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); + uint256 liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); + uint256 allAsset = depositAmount.mulWadUp(liquidationAssetShare); + asset = allAsset.mulWadDown(amount).divWadDown(debt); + } else { + uint256 share = value.divWadDown(totalValue); + uint256 amountShare = share.mulWadUp(amount); + uint256 reward_rate = amount.divWadDown(debt).mulWadDown(LIQUIDATION_REWARD); + uint256 valueToMove = amountShare + amountShare.mulWadUp(reward_rate); + uint256 cappedValue = valueToMove > value ? value : valueToMove; + asset = cappedValue * (10 ** (vault.oracle().decimals() + vault.asset().decimals())) + / vault.assetPrice() / 1e18; + } + vaultAmounts[i] = asset; + if (address(vault) == KEROSENE_VAULT) { + dyadXP.beforeKeroseneWithdrawn(id, asset); + } + vault.move(id, to, asset); + if (address(vault) == KEROSENE_VAULT) { + dyadXP.afterKeroseneDeposited(to, asset); + } + } + } + + emit Liquidate(id, msg.sender, to, amount); + + return (vaultAddresses, vaultAmounts); + } + + /// @notice Returns the collateral ratio for the specified note. + /// @param id The note id + function collatRatio(uint256 id) external view returns (uint256) { + (,,, uint256 cr,) = _totalVaultValuesAndCr(id); + return cr; + } + + /// @dev Internal function for computing collateral ratio. Reading `mintedDyad` and `totalValue` + /// is expensive. If we already have these values loaded, we can re-use the cached values. + /// @param mintedDyad The amount of dyad minted for the note + /// @param totalValue The total value of all exogenous collateral for the note + function _collatRatio( + uint256 mintedDyad, + uint256 totalValue // in USD + ) internal pure returns (uint256) { + if (mintedDyad == 0) return type(uint256).max; + return totalValue.divWadDown(mintedDyad); + } + + /// @notice Returns the total value of all exogenous and kerosene collateral for the specified note. + /// @param id The note id + function getTotalValue( // in USD + uint256 id) + external + view + returns (uint256) + { + (, uint256 exoValue, uint256 keroValue,,) = _totalVaultValuesAndCr(id); + return exoValue + keroValue; + } + + /// @notice Returns the USD value of all exogenous and kerosene collateral for the specified note. + /// @param id The note id + function getVaultsValues( // in USD + uint256 id) + external + view + returns ( + uint256 exoValue, // exo := exogenous (non-kerosene) + uint256 keroValue + ) + { + (, exoValue, keroValue,,) = _totalVaultValuesAndCr(id); + return (exoValue, keroValue); + } + + // ----------------- MISC ----------------- // + /// @notice Returns the registered vaults for the specified note + /// @param id The note id + function getVaults(uint256 id) external view returns (address[] memory) { + return vaults[id].values(); + } + + /// @notice Returns whether the specified vault is registered for the specified note + /// @param id The note id + /// @param vault The vault address + function hasVault(uint256 id, address vault) external view returns (bool) { + return vaults[id].contains(vault); + } + + /// @notice Authorizes an extension for use by current user + /// @dev Can not authorize an extension that is not a registered and enabled system extension, + /// but can deauthorize it + /// @param extension The extension address + /// @param isAuthorized Whether the extension is authorized + function authorizeExtension(address extension, bool isAuthorized) external { + bool authorizationChanged = false; + if (isAuthorized) { + if (!DyadHooks.hookEnabled(_systemExtensions[extension], DyadHooks.EXTENSION_ENABLED)) { + revert Unauthorized(); + } + authorizationChanged = _authorizedExtensions[msg.sender].add(extension); + } else { + authorizationChanged = _authorizedExtensions[msg.sender].remove(extension); + } + if (authorizationChanged) { + emit UserExtensionAuthorized(msg.sender, extension, isAuthorized); + } + } + + /// @notice Authorizes an extension for use in the system + /// @param extension The extension address + /// @param isAuthorized Whether the extension is authorized + function authorizeSystemExtension(address extension, bool isAuthorized) external onlyOwner { + uint256 hooks; + if (isAuthorized) { + hooks = DyadHooks.enableExtension(IExtension(extension).getHookFlags()); + } else { + hooks = DyadHooks.disableExtension(_systemExtensions[extension]); + } + _systemExtensions[extension] = hooks; + emit SystemExtensionAuthorized(extension, hooks); + } + + /// @notice Returns whether the specified extension is authorized for use in the system + /// @param extension The extension address + function isSystemExtension(address extension) external view returns (bool) { + return DyadHooks.hookEnabled(_systemExtensions[extension], DyadHooks.EXTENSION_ENABLED); + } + + /// @notice Returns the authorized extensions for the specified user + /// @param user The user address + function authorizedExtensions(address user) external view returns (address[] memory) { + return _authorizedExtensions[user].values(); + } + + /// @notice Returns whether the specified extension is authorized for use by the specified user + /// @param user The user address + /// @param extension The extension address + function isExtensionAuthorized(address user, address extension) public view returns (bool) { + return _authorizedExtensions[user].contains(extension); + } + + function _totalVaultValuesAndCr(uint256 id) + private + view + returns (uint256[] memory vaultValues, uint256 exoValue, uint256 keroValue, uint256 cr, uint256 mintedDyad) + { + uint256 numberOfVaults = vaults[id].length(); + vaultValues = new uint256[](numberOfVaults); + + for (uint256 i = 0; i < numberOfVaults; i++) { + Vault vault = Vault(vaults[id].at(i)); + if (vaultLicenser.isLicensed(address(vault))) { + uint256 value = vault.getUsdValue(id); + vaultValues[i] = value; + if (vaultLicenser.isKerosene(address(vault))) { + keroValue += value; + } else { + exoValue += value; + } + } + } + + mintedDyad = dyad.mintedDyad(id); + uint256 totalValue = exoValue + keroValue; + cr = _collatRatio(mintedDyad, totalValue); + + return (vaultValues, exoValue, keroValue, cr, mintedDyad); + } + + // ----------------- UPGRADABILITY ----------------- // + /// @dev UUPS upgrade authorization - only owner can upgrade + function _authorizeUpgrade(address) internal view override onlyOwner {} + + /// @dev Authorizes that the caller is either the owner of the specified note, or a system extension + /// that is both enabled and authorized by the owner of the note. Returns the extension flags if + /// the caller is an authorized extension. Reverts if the caller is not authorized. + /// @param id The note id + function _authorizeCall(uint256 id) internal view returns (uint256) { + address dnftOwner = dNft.ownerOf(id); + if (dnftOwner != msg.sender) { + uint256 extensionFlags = _systemExtensions[msg.sender]; + if (!DyadHooks.hookEnabled(extensionFlags, DyadHooks.EXTENSION_ENABLED)) { + revert Unauthorized(); + } + if (!_authorizedExtensions[dnftOwner].contains(msg.sender)) { + revert Unauthorized(); + } + return extensionFlags; + } + return 0; + } +} diff --git a/test/fork/v5/BaseV5.sol b/test/fork/v5/BaseV5.sol new file mode 100644 index 00000000..749c9030 --- /dev/null +++ b/test/fork/v5/BaseV5.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + +import {Parameters} from "../../../src/params/Parameters.sol"; +import {Licenser} from "../../../src/core/Licenser.sol"; +import {Modifiers} from "../../Modifiers.sol"; +import {IVault} from "../../../src/interfaces/IVault.sol"; +import {VaultManagerV5} from "../../../src/core/VaultManagerV5.sol"; + +import {FixedPointMathLib} from "@solmate/src/utils/FixedPointMathLib.sol"; +import {ERC20} from "@solmate/src/tokens/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; +import {Vault} from "../../../src/core/Vault.sol"; +import {KeroseneVault} from "../../../src/core/VaultKerosene.sol"; +import {DNft} from "../../../src/core/DNft.sol"; +import {Dyad} from "../../../src/core/Dyad.sol"; +import {VaultWstEth} from "../../../src/core/Vault.wsteth.sol"; + +struct Contracts { + DNft dNft; + Dyad dyad; + VaultManagerV5 vaultManager; + Vault ethVault; + VaultWstEth wstEth; + KeroseneVault keroseneVault; +} + +contract BaseTestV5 is Test, Modifiers, Parameters { + using stdStorage for StdStorage; + using FixedPointMathLib for uint256; + + Contracts contracts; + ERC20 weth; + + uint256 ETH_TO_USD; // 1e8 + uint256 MIN_COLLAT_RATIO; + + uint256 RANDOM_NUMBER_0 = 471966444; + + uint256 alice0; + uint256 alice1; + uint256 bob0; + uint256 bob1; + + address alice; + address bob = address(0x42); + + function setUp() public { + + vm.createSelectFork(vm.envString("RPC_URL"), 20930795); + + VaultManagerV5 impl = new VaultManagerV5(); + vm.prank(MAINNET_FEE_RECIPIENT); + VaultManagerV5(MAINNET_V2_VAULT_MANAGER).upgradeToAndCall( + address(impl), abi.encodeWithSelector(impl.initialize.selector) + ); + + weth = ERC20(MAINNET_WETH); + alice = address(this); + + contracts = Contracts({ + dNft: DNft(MAINNET_DNFT), + dyad: Dyad(MAINNET_V2_DYAD), + vaultManager: VaultManagerV5(MAINNET_V2_VAULT_MANAGER), + ethVault: Vault(MAINNET_V2_WETH_VAULT), + wstEth: VaultWstEth(MAINNET_V2_WSTETH_VAULT), + keroseneVault: KeroseneVault(MAINNET_V2_KEROSENE_V2_VAULT) + }); + + ETH_TO_USD = contracts.ethVault.assetPrice(); + MIN_COLLAT_RATIO = contracts.vaultManager.MIN_COLLAT_RATIO(); + } + + // --- alice --- + modifier mintAlice0() { + alice0 = mintDNft(address(this)); + _; + } + + modifier mintAlice1() { + alice1 = mintDNft(address(this)); + _; + } + // --- bob --- + + modifier mintBob0() { + bob0 = mintDNft(bob); + _; + } + + modifier mintBob1() { + bob1 = mintDNft(bob); + _; + } + + function mintDNft(address owner) public returns (uint256 id) { + uint256 startPrice = contracts.dNft.START_PRICE(); + uint256 priceIncrease = contracts.dNft.PRICE_INCREASE(); + uint256 publicMints = contracts.dNft.publicMints(); + uint256 price = startPrice + (priceIncrease * publicMints); + vm.deal(address(this), price); + id = contracts.dNft.mintNft{value: price}(owner); + } + + // -- helpers -- + function _ethToUSD(uint256 eth) public view returns (uint256) { + return eth * ETH_TO_USD / 1e8; + } + + function getMintedDyad(uint256 id) public view returns (uint256) { + return contracts.dyad.mintedDyad(id); + } + + function getCR(uint256 id) public view returns (uint256) { + return contracts.vaultManager.collatRatio(id); + } + + // -- storage manipulation -- + function _changeAsset(uint256 id, IVault vault, uint256 amount) public { + stdstore.target(address(vault)).sig("id2asset(uint256)").with_key(id).checked_write(amount); + } + + // Manually set the Collaterization Ratio of a dNft by changing the the asset + // of the vault. + function changeCollatRatio(uint256 id, IVault vault, uint256 newCr) public { + uint256 debt = getMintedDyad(id); + uint256 value = newCr.mulWadDown(debt); + uint256 asset = + value * (10 ** (vault.oracle().decimals() + vault.asset().decimals())) / vault.assetPrice() / 1e18; + + _changeAsset(id, vault, asset); + } + + // --- modifiers --- + modifier changeAsset(uint256 id, IVault vault, uint256 amount) { + _changeAsset(id, vault, amount); + _; + } + + modifier deposit(uint256 id, IVault vault, uint256 amount) { + address owner = contracts.dNft.ownerOf(id); + vm.startPrank(owner); + + ERC20 asset = vault.asset(); + deal(address(asset), owner, amount); + asset.approve(address(contracts.vaultManager), amount); + contracts.vaultManager.deposit(id, address(vault), amount); + + vm.stopPrank(); + _; + } + + modifier addVault(uint256 id, IVault vault) { + vm.prank(contracts.dNft.ownerOf(id)); + contracts.vaultManager.add(id, address(vault)); + _; + } + + // --- RECEIVER --- + receive() external payable {} + + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return 0x150b7a02; + } +} diff --git a/test/fork/v5/v5.Liquidate.t.sol b/test/fork/v5/v5.Liquidate.t.sol new file mode 100644 index 00000000..57478e57 --- /dev/null +++ b/test/fork/v5/v5.Liquidate.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {BaseTestV5} from "./BaseV5.sol"; +import {VaultManagerV3} from "../../../src/core/VaultManagerV3.sol"; +import {Parameters} from "../../../src/params/Parameters.sol"; +import {DeployVaultManagerV3} from "../../../script/deploy/DeployVaultManagerV3.s.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +contract LiquidateTest is BaseTestV5 { + modifier mintDyad(uint256 id, uint256 amount) { + vm.prank(contracts.dNft.ownerOf(id)); + contracts.vaultManager.mintDyad(id, amount, address(this)); + _; + } + + modifier liquidate(uint256 id, uint256 to, address liquidator) { + deal(address(contracts.dyad), liquidator, _ethToUSD(getMintedDyad(id))); + vm.prank(liquidator); + contracts.vaultManager.liquidate(id, to, getMintedDyad(id)); + _; + } + + function test_LiquidateWithManyVaults() + public + // alice + mintAlice0 + // eth vault + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + // wstEth vault + addVault(alice0, contracts.wstEth) + deposit(alice0, contracts.wstEth, 100 ether) + // kerosene vault + addVault(alice0, contracts.keroseneVault) + deposit(alice0, contracts.keroseneVault, 5 ether) + mintDyad(alice0, _ethToUSD(10 ether)) + // change assets + changeAsset(alice0, contracts.ethVault, 1 ether) + changeAsset(alice0, contracts.wstEth, 1 ether) + changeAsset(alice0, contracts.keroseneVault, 1 ether) + // bob + mintBob0 + liquidate(alice0, bob0, bob) + {} + + function testFuzz_LiquidateWithManyVaults(uint256 amount) + public + // alice + mintAlice0 + // eth vault + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + // wstEth vault + addVault(alice0, contracts.wstEth) + deposit(alice0, contracts.wstEth, 100 ether) + // kerosene vault + addVault(alice0, contracts.keroseneVault) + deposit(alice0, contracts.keroseneVault, 5 ether) + mintDyad(alice0, _ethToUSD(10 ether)) + // change assets + changeAsset(alice0, contracts.ethVault, 1 ether) + changeAsset(alice0, contracts.wstEth, 1 ether) + changeAsset(alice0, contracts.keroseneVault, 1 ether) + // bob + mintBob0 + { + amount = bound(amount, 1, _ethToUSD(10 ether)); + contracts.vaultManager.liquidate(alice0, bob0, amount); + } + + function test_LiquidateNoCollateralLeftForLiquidatee() + public + // alice + mintAlice0 + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + mintDyad(alice0, _ethToUSD(1 ether)) + changeAsset(alice0, contracts.ethVault, 1.2 ether) + // bob + mintBob0 + liquidate(alice0, bob0, bob) + { + uint256 ethAfter_Liquidator = contracts.ethVault.id2asset(bob0); + uint256 ethAfter_Liquidatee = contracts.ethVault.id2asset(alice0); + uint256 dyadAfter_Liquidatee = contracts.dyad.mintedDyad(alice0); + + assertTrue(ethAfter_Liquidator > 0); + assertTrue(ethAfter_Liquidatee == 0); + + assertEq(getMintedDyad(alice0), 0); + assertEq(getCR(alice0), type(uint256).max); + assertEq(dyadAfter_Liquidatee, 0); + } + + function test_LiquidateSomeCollateralLeft() + public + // alice + mintAlice0 + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + mintDyad(alice0, _ethToUSD(1 ether)) + changeAsset(alice0, contracts.ethVault, 1.4 ether) + // bob + mintBob0 + liquidate(alice0, bob0, bob) + { + uint256 ethAfter_Liquidator = contracts.ethVault.id2asset(bob0); + uint256 ethAfter_Liquidatee = contracts.ethVault.id2asset(alice0); + uint256 dyadAfter_Liquidatee = contracts.dyad.mintedDyad(alice0); + + assertTrue(ethAfter_Liquidator > 0); + assertTrue(ethAfter_Liquidatee > 0); + + assertEq(getMintedDyad(alice0), 0); + assertEq(getCR(alice0), type(uint256).max); + assertEq(dyadAfter_Liquidatee, 0); + } + + function test_LiquidatePartial() + public + // alice + mintAlice0 + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + addVault(alice0, contracts.wstEth) + deposit(alice0, contracts.wstEth, 100 ether) + mintDyad(alice0, _ethToUSD(50 ether)) + changeAsset(alice0, contracts.ethVault, 50 ether) + changeAsset(alice0, contracts.wstEth, 10 ether) + // bob + mintBob0 + { + uint256 crBefore = getCR(alice0); + console.log("crBefore: ", crBefore / 1e15); + + uint256 debtBefore = getMintedDyad(alice0); + console.log("debtBefore: ", debtBefore / 1e18); + + contracts.vaultManager.liquidate(alice0, bob0, _ethToUSD(10 ether)); + + uint256 crAfter = getCR(alice0); + console.log("crAfter: ", crAfter / 1e15); + + uint256 debtAfter = getMintedDyad(alice0); + console.log("debtAfter: ", debtAfter / 1e18); + } +} diff --git a/test/fork/v6/BaseV6.sol b/test/fork/v6/BaseV6.sol new file mode 100644 index 00000000..2e8dd0cd --- /dev/null +++ b/test/fork/v6/BaseV6.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + +import {Parameters} from "../../../src/params/Parameters.sol"; +import {Licenser} from "../../../src/core/Licenser.sol"; +import {Modifiers} from "../../Modifiers.sol"; +import {IVault} from "../../../src/interfaces/IVault.sol"; +import {VaultManagerV6} from "../../../src/core/VaultManagerV6.sol"; + +import {FixedPointMathLib} from "@solmate/src/utils/FixedPointMathLib.sol"; +import {ERC20} from "@solmate/src/tokens/ERC20.sol"; +import {Test} from "forge-std/Test.sol"; +import {Vault} from "../../../src/core/Vault.sol"; +import {KeroseneVault} from "../../../src/core/VaultKerosene.sol"; +import {DNft} from "../../../src/core/DNft.sol"; +import {Dyad} from "../../../src/core/Dyad.sol"; +import {VaultWstEth} from "../../../src/core/Vault.wsteth.sol"; + +struct Contracts { + DNft dNft; + Dyad dyad; + VaultManagerV6 vaultManager; + Vault ethVault; + VaultWstEth wstEth; + KeroseneVault keroseneVault; +} + +contract BaseTestV6 is Test, Modifiers, Parameters { + using stdStorage for StdStorage; + using FixedPointMathLib for uint256; + + Contracts contracts; + ERC20 weth; + + uint256 ETH_TO_USD; // 1e8 + uint256 MIN_COLLAT_RATIO; + + uint256 RANDOM_NUMBER_0 = 471966444; + + uint256 alice0; + uint256 alice1; + uint256 bob0; + uint256 bob1; + + address alice; + address bob = address(0x42); + + function setUp() public { + + vm.createSelectFork(vm.envString("RPC_URL"), 21109036); + + VaultManagerV6 impl = new VaultManagerV6(); + vm.prank(MAINNET_FEE_RECIPIENT); + VaultManagerV6(MAINNET_V2_VAULT_MANAGER).upgradeToAndCall( + address(impl), abi.encodeWithSelector(impl.initialize.selector) + ); + + weth = ERC20(MAINNET_WETH); + alice = address(this); + + contracts = Contracts({ + dNft: DNft(MAINNET_DNFT), + dyad: Dyad(MAINNET_V2_DYAD), + vaultManager: VaultManagerV6(MAINNET_V2_VAULT_MANAGER), + ethVault: Vault(MAINNET_V2_WETH_VAULT), + wstEth: VaultWstEth(MAINNET_V2_WSTETH_VAULT), + keroseneVault: KeroseneVault(MAINNET_V2_KEROSENE_V2_VAULT) + }); + + ETH_TO_USD = contracts.ethVault.assetPrice(); + MIN_COLLAT_RATIO = contracts.vaultManager.MIN_COLLAT_RATIO(); + } + + // --- alice --- + modifier mintAlice0() { + alice0 = mintDNft(address(this)); + _; + } + + modifier mintAlice1() { + alice1 = mintDNft(address(this)); + _; + } + // --- bob --- + + modifier mintBob0() { + bob0 = mintDNft(bob); + _; + } + + modifier mintBob1() { + bob1 = mintDNft(bob); + _; + } + + function mintDNft(address owner) public returns (uint256 id) { + uint256 startPrice = contracts.dNft.START_PRICE(); + uint256 priceIncrease = contracts.dNft.PRICE_INCREASE(); + uint256 publicMints = contracts.dNft.publicMints(); + uint256 price = startPrice + (priceIncrease * publicMints); + vm.deal(address(this), price); + id = contracts.dNft.mintNft{value: price}(owner); + } + + // -- helpers -- + function _ethToUSD(uint256 eth) public view returns (uint256) { + return eth * ETH_TO_USD / 1e8; + } + + function getMintedDyad(uint256 id) public view returns (uint256) { + return contracts.dyad.mintedDyad(id); + } + + function getCR(uint256 id) public view returns (uint256) { + return contracts.vaultManager.collatRatio(id); + } + + // -- storage manipulation -- + function _changeAsset(uint256 id, IVault vault, uint256 amount) public { + stdstore.target(address(vault)).sig("id2asset(uint256)").with_key(id).checked_write(amount); + } + + // Manually set the Collaterization Ratio of a dNft by changing the the asset + // of the vault. + function changeCollatRatio(uint256 id, IVault vault, uint256 newCr) public { + uint256 debt = getMintedDyad(id); + uint256 value = newCr.mulWadDown(debt); + uint256 asset = + value * (10 ** (vault.oracle().decimals() + vault.asset().decimals())) / vault.assetPrice() / 1e18; + + _changeAsset(id, vault, asset); + } + + // --- modifiers --- + modifier changeAsset(uint256 id, IVault vault, uint256 amount) { + _changeAsset(id, vault, amount); + _; + } + + modifier deposit(uint256 id, IVault vault, uint256 amount) { + address owner = contracts.dNft.ownerOf(id); + vm.startPrank(owner); + + ERC20 asset = vault.asset(); + deal(address(asset), owner, amount); + asset.approve(address(contracts.vaultManager), amount); + contracts.vaultManager.deposit(id, address(vault), amount); + + vm.stopPrank(); + _; + } + + modifier addVault(uint256 id, IVault vault) { + vm.prank(contracts.dNft.ownerOf(id)); + contracts.vaultManager.add(id, address(vault)); + _; + } + + // --- RECEIVER --- + receive() external payable {} + + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return 0x150b7a02; + } +} diff --git a/test/fork/v6/v6.Liquidate.t.sol b/test/fork/v6/v6.Liquidate.t.sol new file mode 100644 index 00000000..e80efbc6 --- /dev/null +++ b/test/fork/v6/v6.Liquidate.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {BaseTestV6} from "./BaseV6.sol"; +import {VaultManagerV3} from "../../../src/core/VaultManagerV3.sol"; +import {Parameters} from "../../../src/params/Parameters.sol"; +import {DeployVaultManagerV3} from "../../../script/deploy/DeployVaultManagerV3.s.sol"; +import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +contract LiquidateTest is BaseTestV6 { + modifier mintDyad(uint256 id, uint256 amount) { + vm.prank(contracts.dNft.ownerOf(id)); + contracts.vaultManager.mintDyad(id, amount, address(this)); + _; + } + + modifier liquidate(uint256 id, uint256 to, address liquidator) { + deal(address(contracts.dyad), liquidator, _ethToUSD(getMintedDyad(id))); + vm.prank(liquidator); + contracts.vaultManager.liquidate(id, to, getMintedDyad(id)); + _; + } + + function test_LiquidateWithManyVaults() + public + // alice + mintAlice0 + // eth vault + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + // wstEth vault + addVault(alice0, contracts.wstEth) + deposit(alice0, contracts.wstEth, 100 ether) + // kerosene vault + addVault(alice0, contracts.keroseneVault) + deposit(alice0, contracts.keroseneVault, 5 ether) + mintDyad(alice0, _ethToUSD(10 ether)) + // change assets + changeAsset(alice0, contracts.ethVault, 1 ether) + changeAsset(alice0, contracts.wstEth, 1 ether) + changeAsset(alice0, contracts.keroseneVault, 1 ether) + // bob + mintBob0 + liquidate(alice0, bob0, bob) + {} + + function testFuzz_LiquidateWithManyVaults(uint256 amount) + public + // alice + mintAlice0 + // eth vault + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + // wstEth vault + addVault(alice0, contracts.wstEth) + deposit(alice0, contracts.wstEth, 100 ether) + // kerosene vault + addVault(alice0, contracts.keroseneVault) + deposit(alice0, contracts.keroseneVault, 5 ether) + mintDyad(alice0, _ethToUSD(10 ether)) + // change assets + changeAsset(alice0, contracts.ethVault, 1 ether) + changeAsset(alice0, contracts.wstEth, 1 ether) + changeAsset(alice0, contracts.keroseneVault, 1 ether) + // bob + mintBob0 + { + amount = bound(amount, 1, _ethToUSD(10 ether)); + contracts.vaultManager.liquidate(alice0, bob0, amount); + } + + function test_LiquidateNoCollateralLeftForLiquidatee() + public + // alice + mintAlice0 + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + mintDyad(alice0, _ethToUSD(1 ether)) + changeAsset(alice0, contracts.ethVault, 1.2 ether) + // bob + mintBob0 + liquidate(alice0, bob0, bob) + { + uint256 ethAfter_Liquidator = contracts.ethVault.id2asset(bob0); + uint256 ethAfter_Liquidatee = contracts.ethVault.id2asset(alice0); + uint256 dyadAfter_Liquidatee = contracts.dyad.mintedDyad(alice0); + + assertTrue(ethAfter_Liquidator > 0); + assertTrue(ethAfter_Liquidatee == 0); + + assertEq(getMintedDyad(alice0), 0); + assertEq(getCR(alice0), type(uint256).max); + assertEq(dyadAfter_Liquidatee, 0); + } + + function test_LiquidateSomeCollateralLeft() + public + // alice + mintAlice0 + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + mintDyad(alice0, _ethToUSD(1 ether)) + changeAsset(alice0, contracts.ethVault, 1.4 ether) + // bob + mintBob0 + liquidate(alice0, bob0, bob) + { + uint256 ethAfter_Liquidator = contracts.ethVault.id2asset(bob0); + uint256 ethAfter_Liquidatee = contracts.ethVault.id2asset(alice0); + uint256 dyadAfter_Liquidatee = contracts.dyad.mintedDyad(alice0); + + assertTrue(ethAfter_Liquidator > 0); + assertTrue(ethAfter_Liquidatee > 0); + + assertEq(getMintedDyad(alice0), 0); + assertEq(getCR(alice0), type(uint256).max); + assertEq(dyadAfter_Liquidatee, 0); + } + + function test_LiquidatePartial() + public + // alice + mintAlice0 + addVault(alice0, contracts.ethVault) + deposit(alice0, contracts.ethVault, 100 ether) + addVault(alice0, contracts.wstEth) + deposit(alice0, contracts.wstEth, 100 ether) + mintDyad(alice0, _ethToUSD(50 ether)) + changeAsset(alice0, contracts.ethVault, 50 ether) + changeAsset(alice0, contracts.wstEth, 10 ether) + // bob + mintBob0 + { + uint256 crBefore = getCR(alice0); + console.log("crBefore: ", crBefore / 1e15); + + uint256 debtBefore = getMintedDyad(alice0); + console.log("debtBefore: ", debtBefore / 1e18); + + contracts.vaultManager.liquidate(alice0, bob0, _ethToUSD(10 ether)); + + uint256 crAfter = getCR(alice0); + console.log("crAfter: ", crAfter / 1e15); + + uint256 debtAfter = getMintedDyad(alice0); + console.log("debtAfter: ", debtAfter / 1e18); + } +} From e993204c014e1a39a2793c6c627744b53d6440bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Sun, 3 Nov 2024 13:59:05 -0500 Subject: [PATCH 2/8] fmt --- test/fork/v5/BaseV5.sol | 5 ++--- test/fork/v6/BaseV6.sol | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/fork/v5/BaseV5.sol b/test/fork/v5/BaseV5.sol index 749c9030..be57c4c7 100644 --- a/test/fork/v5/BaseV5.sol +++ b/test/fork/v5/BaseV5.sol @@ -14,7 +14,7 @@ import {FixedPointMathLib} from "@solmate/src/utils/FixedPointMathLib.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; import {Test} from "forge-std/Test.sol"; import {Vault} from "../../../src/core/Vault.sol"; -import {KeroseneVault} from "../../../src/core/VaultKerosene.sol"; +import {KeroseneVault} from "../../../src/core/VaultKerosene.sol"; import {DNft} from "../../../src/core/DNft.sol"; import {Dyad} from "../../../src/core/Dyad.sol"; import {VaultWstEth} from "../../../src/core/Vault.wsteth.sol"; @@ -49,7 +49,6 @@ contract BaseTestV5 is Test, Modifiers, Parameters { address bob = address(0x42); function setUp() public { - vm.createSelectFork(vm.envString("RPC_URL"), 20930795); VaultManagerV5 impl = new VaultManagerV5(); @@ -60,7 +59,7 @@ contract BaseTestV5 is Test, Modifiers, Parameters { weth = ERC20(MAINNET_WETH); alice = address(this); - + contracts = Contracts({ dNft: DNft(MAINNET_DNFT), dyad: Dyad(MAINNET_V2_DYAD), diff --git a/test/fork/v6/BaseV6.sol b/test/fork/v6/BaseV6.sol index 2e8dd0cd..f5913b4b 100644 --- a/test/fork/v6/BaseV6.sol +++ b/test/fork/v6/BaseV6.sol @@ -49,7 +49,6 @@ contract BaseTestV6 is Test, Modifiers, Parameters { address bob = address(0x42); function setUp() public { - vm.createSelectFork(vm.envString("RPC_URL"), 21109036); VaultManagerV6 impl = new VaultManagerV6(); From 687df5f2ea4572e6bd90023a3496de522be59aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Sun, 3 Nov 2024 14:59:41 -0500 Subject: [PATCH 3/8] additional refactor --- src/core/VaultManagerV6.sol | 56 +++++++++++++++++++++++---------- test/fork/v6/v6.Liquidate.t.sol | 8 ++--- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/core/VaultManagerV6.sol b/src/core/VaultManagerV6.sol index 1e790165..ab1eb6ac 100644 --- a/src/core/VaultManagerV6.sol +++ b/src/core/VaultManagerV6.sol @@ -152,7 +152,9 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable (uint256[] memory vaultsValues, uint256 exoValue, uint256 keroValue, uint256 cr, uint256 debt) = _totalVaultValuesAndCr(id); - if (cr >= MIN_COLLAT_RATIO) revert CrTooHigh(); + if (cr >= MIN_COLLAT_RATIO) { + revert CrTooHigh(); + } dyad.burn(id, msg.sender, amount); // changes `debt` and `cr` lastDeposit[to] = block.number; // `move` acts like a deposit @@ -162,31 +164,22 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable uint256[] memory vaultAmounts = new uint256[](numberOfVaults); uint256 totalValue = exoValue + keroValue; - if (totalValue == 0) return (vaultAddresses, vaultAmounts); + if (totalValue == 0) { + return (vaultAddresses, vaultAmounts); + } - for (uint256 i = 0; i < numberOfVaults; i++) { + for (uint256 i; i < numberOfVaults; ++i) { Vault vault = Vault(vaults[id].at(i)); vaultAddresses[i] = address(vault); if (vaultLicenser.isLicensed(address(vault))) { - uint256 depositAmount = vault.id2asset(id); - if (depositAmount == 0) continue; uint256 value = vaultsValues[i]; if (value == 0) continue; + uint256 depositAmount = vault.id2asset(id); uint256 asset; if (cr < LIQUIDATION_REWARD + 1e18 && debt != amount) { - uint256 cappedCr = cr < 1e18 ? 1e18 : cr; - uint256 liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); - uint256 liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); - uint256 allAsset = depositAmount.mulWadUp(liquidationAssetShare); - asset = allAsset.mulWadDown(amount).divWadDown(debt); + asset = _getPartialLiquidationAmount(amount, debt, depositAmount, cr); } else { - uint256 share = value.divWadDown(totalValue); - uint256 amountShare = share.mulWadUp(amount); - uint256 reward_rate = amount.divWadDown(debt).mulWadDown(LIQUIDATION_REWARD); - uint256 valueToMove = amountShare + amountShare.mulWadUp(reward_rate); - uint256 cappedValue = valueToMove > value ? value : valueToMove; - asset = cappedValue * (10 ** (vault.oracle().decimals() + vault.asset().decimals())) - / vault.assetPrice() / 1e18; + asset = _getFullLiquidationAmount(amount, debt, depositAmount, value, totalValue); } vaultAmounts[i] = asset; if (address(vault) == KEROSENE_VAULT) { @@ -204,6 +197,35 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable return (vaultAddresses, vaultAmounts); } + function _getPartialLiquidationAmount(uint256 amount, uint256 debt, uint256 depositAmount, uint256 cr) + private + view + returns (uint256) + { + uint256 cappedCr = cr < 1e18 ? 1e18 : cr; + uint256 liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); + uint256 liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); + uint256 allAsset = depositAmount.mulWadUp(liquidationAssetShare); + return allAsset.mulWadDown(amount).divWadDown(debt); + } + + function _getFullLiquidationAmount( + uint256 amount, + uint256 debt, + uint256 depositAmount, + uint256 value, + uint256 totalValue + ) private view returns (uint256) { + uint256 amountShare = value.divWadDown(totalValue).mulWadUp(amount); // amount of asset in dollars represented by the share + uint256 rewardRate = amount.divWadDown(debt).mulWadDown(LIQUIDATION_REWARD); + uint256 valueToMove = amountShare + amountShare.mulWadUp(rewardRate); + uint256 asset = depositAmount.mulWadDown(valueToMove).divWadDown(value); + if (asset > depositAmount) { + asset = depositAmount; + } + return asset; + } + /// @notice Returns the collateral ratio for the specified note. /// @param id The note id function collatRatio(uint256 id) external view returns (uint256) { diff --git a/test/fork/v6/v6.Liquidate.t.sol b/test/fork/v6/v6.Liquidate.t.sol index e80efbc6..5f831d2d 100644 --- a/test/fork/v6/v6.Liquidate.t.sol +++ b/test/fork/v6/v6.Liquidate.t.sol @@ -88,8 +88,8 @@ contract LiquidateTest is BaseTestV6 { uint256 ethAfter_Liquidatee = contracts.ethVault.id2asset(alice0); uint256 dyadAfter_Liquidatee = contracts.dyad.mintedDyad(alice0); - assertTrue(ethAfter_Liquidator > 0); - assertTrue(ethAfter_Liquidatee == 0); + assertGt(ethAfter_Liquidator, 0); + assertEq(ethAfter_Liquidatee, 0); assertEq(getMintedDyad(alice0), 0); assertEq(getCR(alice0), type(uint256).max); @@ -112,8 +112,8 @@ contract LiquidateTest is BaseTestV6 { uint256 ethAfter_Liquidatee = contracts.ethVault.id2asset(alice0); uint256 dyadAfter_Liquidatee = contracts.dyad.mintedDyad(alice0); - assertTrue(ethAfter_Liquidator > 0); - assertTrue(ethAfter_Liquidatee > 0); + assertGt(ethAfter_Liquidator, 0); + assertGt(ethAfter_Liquidatee, 0); assertEq(getMintedDyad(alice0), 0); assertEq(getCR(alice0), type(uint256).max); From 201171936e12fd2960fbeaec4cf227482bef5ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Sun, 3 Nov 2024 19:49:36 -0500 Subject: [PATCH 4/8] liquidate kero last --- src/core/VaultManagerV6.sol | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/core/VaultManagerV6.sol b/src/core/VaultManagerV6.sol index ab1eb6ac..ea6c9416 100644 --- a/src/core/VaultManagerV6.sol +++ b/src/core/VaultManagerV6.sol @@ -168,27 +168,40 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable return (vaultAddresses, vaultAmounts); } + uint256 amountMoved; for (uint256 i; i < numberOfVaults; ++i) { Vault vault = Vault(vaults[id].at(i)); + if (address(vault) == KEROSENE_VAULT) { + continue; + } vaultAddresses[i] = address(vault); if (vaultLicenser.isLicensed(address(vault))) { uint256 value = vaultsValues[i]; if (value == 0) continue; uint256 depositAmount = vault.id2asset(id); uint256 asset; + uint256 amountShare; if (cr < LIQUIDATION_REWARD + 1e18 && debt != amount) { asset = _getPartialLiquidationAmount(amount, debt, depositAmount, cr); } else { - asset = _getFullLiquidationAmount(amount, debt, depositAmount, value, totalValue); + (asset, amountShare) = _getFullLiquidationAmount(amount, debt, depositAmount, value, exoValue); + amountMoved += amountShare; } vaultAmounts[i] = asset; - if (address(vault) == KEROSENE_VAULT) { - dyadXP.beforeKeroseneWithdrawn(id, asset); - } vault.move(id, to, asset); - if (address(vault) == KEROSENE_VAULT) { - dyadXP.afterKeroseneDeposited(to, asset); - } + } + } + + if(keroValue > 0) { + vaultAddresses[numberOfVaults - 1] = KEROSENE_VAULT; + if (amountMoved < amount) { + uint256 amountRemaining = amount - amountMoved; + uint256 keroDeposited = Vault(KEROSENE_VAULT).id2asset(id); + (uint256 keroToMove, ) = _getFullLiquidationAmount(amountRemaining, debt, keroDeposited, keroValue, keroValue); + vaultAmounts[numberOfVaults - 1] = keroToMove; + dyadXP.beforeKeroseneWithdrawn(id, keroToMove); + Vault(KEROSENE_VAULT).move(id, to, keroToMove); + dyadXP.afterKeroseneDeposited(to, keroToMove); } } @@ -215,15 +228,14 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable uint256 depositAmount, uint256 value, uint256 totalValue - ) private view returns (uint256) { - uint256 amountShare = value.divWadDown(totalValue).mulWadUp(amount); // amount of asset in dollars represented by the share + ) private view returns (uint256 asset, uint256 amountShare) { + amountShare = value.divWadDown(totalValue).mulWadUp(amount); // amount of asset in dollars represented by the share uint256 rewardRate = amount.divWadDown(debt).mulWadDown(LIQUIDATION_REWARD); uint256 valueToMove = amountShare + amountShare.mulWadUp(rewardRate); - uint256 asset = depositAmount.mulWadDown(valueToMove).divWadDown(value); + asset = depositAmount.mulWadDown(valueToMove).divWadDown(value); if (asset > depositAmount) { asset = depositAmount; } - return asset; } /// @notice Returns the collateral ratio for the specified note. From a216905c050268d780c270f9a3f1d9e586f30259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Coffee=E2=98=95=EF=B8=8F?= Date: Fri, 15 Nov 2024 11:07:37 -0500 Subject: [PATCH 5/8] liquidate tests --- src/core/VaultManagerV6.sol | 79 +++++++++++++-------------------- test/fork/v6/BaseV6.sol | 2 +- test/fork/v6/v6.Liquidate.t.sol | 37 +++++++++++++++ 3 files changed, 70 insertions(+), 48 deletions(-) diff --git a/src/core/VaultManagerV6.sol b/src/core/VaultManagerV6.sol index ea6c9416..74b0249c 100644 --- a/src/core/VaultManagerV6.sol +++ b/src/core/VaultManagerV6.sol @@ -168,41 +168,54 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable return (vaultAddresses, vaultAmounts); } + uint256 totalLiquidationReward; + if (cr < LIQUIDATION_REWARD + 1e18 && debt != amount) { + uint256 cappedCr = cr < 1e18 ? 1e18 : cr; + uint256 liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); + uint256 liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); + uint256 allAsset = totalValue.mulWadUp(liquidationAssetShare); + totalLiquidationReward = allAsset.mulWadDown(amount).divWadDown(debt); + } else { + totalLiquidationReward = amount + amount.mulWadDown(LIQUIDATION_REWARD); + if (totalLiquidationReward < totalValue) { + totalLiquidationReward = totalValue; + } + } + uint256 amountMoved; + uint256 keroIndex; for (uint256 i; i < numberOfVaults; ++i) { Vault vault = Vault(vaults[id].at(i)); + vaultAddresses[i] = address(vault); if (address(vault) == KEROSENE_VAULT) { + keroIndex = i; continue; } - vaultAddresses[i] = address(vault); if (vaultLicenser.isLicensed(address(vault))) { uint256 value = vaultsValues[i]; if (value == 0) continue; uint256 depositAmount = vault.id2asset(id); - uint256 asset; - uint256 amountShare; - if (cr < LIQUIDATION_REWARD + 1e18 && debt != amount) { - asset = _getPartialLiquidationAmount(amount, debt, depositAmount, cr); + uint256 percentageOfValue = value.divWadDown(exoValue); + uint256 valueToMove = percentageOfValue.mulWadDown(totalLiquidationReward); + uint256 asset = depositAmount.mulWadDown(valueToMove).divWadDown(value); + if (asset > depositAmount) { + asset = depositAmount; + amountMoved += value; } else { - (asset, amountShare) = _getFullLiquidationAmount(amount, debt, depositAmount, value, exoValue); - amountMoved += amountShare; + amountMoved += valueToMove; } - vaultAmounts[i] = asset; vault.move(id, to, asset); } } - if(keroValue > 0) { - vaultAddresses[numberOfVaults - 1] = KEROSENE_VAULT; - if (amountMoved < amount) { - uint256 amountRemaining = amount - amountMoved; - uint256 keroDeposited = Vault(KEROSENE_VAULT).id2asset(id); - (uint256 keroToMove, ) = _getFullLiquidationAmount(amountRemaining, debt, keroDeposited, keroValue, keroValue); - vaultAmounts[numberOfVaults - 1] = keroToMove; - dyadXP.beforeKeroseneWithdrawn(id, keroToMove); - Vault(KEROSENE_VAULT).move(id, to, keroToMove); - dyadXP.afterKeroseneDeposited(to, keroToMove); - } + if (keroValue > 0 && amountMoved < totalLiquidationReward) { + uint256 amountRemaining = totalLiquidationReward - amountMoved; + uint256 keroDeposited = Vault(KEROSENE_VAULT).id2asset(id); + uint256 keroToMove = keroDeposited.mulWadDown(amountRemaining).divWadDown(keroValue); + vaultAmounts[keroIndex] = keroToMove; + dyadXP.beforeKeroseneWithdrawn(id, keroToMove); + Vault(KEROSENE_VAULT).move(id, to, keroToMove); + dyadXP.afterKeroseneDeposited(to, keroToMove); } emit Liquidate(id, msg.sender, to, amount); @@ -210,34 +223,6 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable return (vaultAddresses, vaultAmounts); } - function _getPartialLiquidationAmount(uint256 amount, uint256 debt, uint256 depositAmount, uint256 cr) - private - view - returns (uint256) - { - uint256 cappedCr = cr < 1e18 ? 1e18 : cr; - uint256 liquidationEquityShare = (cappedCr - 1e18).mulWadDown(LIQUIDATION_REWARD); - uint256 liquidationAssetShare = (liquidationEquityShare + 1e18).divWadDown(cappedCr); - uint256 allAsset = depositAmount.mulWadUp(liquidationAssetShare); - return allAsset.mulWadDown(amount).divWadDown(debt); - } - - function _getFullLiquidationAmount( - uint256 amount, - uint256 debt, - uint256 depositAmount, - uint256 value, - uint256 totalValue - ) private view returns (uint256 asset, uint256 amountShare) { - amountShare = value.divWadDown(totalValue).mulWadUp(amount); // amount of asset in dollars represented by the share - uint256 rewardRate = amount.divWadDown(debt).mulWadDown(LIQUIDATION_REWARD); - uint256 valueToMove = amountShare + amountShare.mulWadUp(rewardRate); - asset = depositAmount.mulWadDown(valueToMove).divWadDown(value); - if (asset > depositAmount) { - asset = depositAmount; - } - } - /// @notice Returns the collateral ratio for the specified note. /// @param id The note id function collatRatio(uint256 id) external view returns (uint256) { diff --git a/test/fork/v6/BaseV6.sol b/test/fork/v6/BaseV6.sol index f5913b4b..326aa41b 100644 --- a/test/fork/v6/BaseV6.sol +++ b/test/fork/v6/BaseV6.sol @@ -49,7 +49,7 @@ contract BaseTestV6 is Test, Modifiers, Parameters { address bob = address(0x42); function setUp() public { - vm.createSelectFork(vm.envString("RPC_URL"), 21109036); + vm.createSelectFork(vm.envString("RPC_URL"), 21086097); VaultManagerV6 impl = new VaultManagerV6(); vm.prank(MAINNET_FEE_RECIPIENT); diff --git a/test/fork/v6/v6.Liquidate.t.sol b/test/fork/v6/v6.Liquidate.t.sol index 5f831d2d..2f845b85 100644 --- a/test/fork/v6/v6.Liquidate.t.sol +++ b/test/fork/v6/v6.Liquidate.t.sol @@ -9,6 +9,7 @@ import {VaultManagerV3} from "../../../src/core/VaultManagerV3.sol"; import {Parameters} from "../../../src/params/Parameters.sol"; import {DeployVaultManagerV3} from "../../../script/deploy/DeployVaultManagerV3.s.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {IVault} from "../../../src/interfaces/IVault.sol"; contract LiquidateTest is BaseTestV6 { modifier mintDyad(uint256 id, uint256 amount) { @@ -148,4 +149,40 @@ contract LiquidateTest is BaseTestV6 { uint256 debtAfter = getMintedDyad(alice0); console.log("debtAfter: ", debtAfter / 1e18); } + + function test_LiquidateFromV5() public { + IVault keroseneVault = IVault(MAINNET_V2_KEROSENE_V2_VAULT); + IVault apxVault = IVault(MAINNET_APXETH_VAULT); + + uint256 keroBefore = keroseneVault.id2asset(511); + uint256 apxBefore = apxVault.id2asset(511); + + console.log("debt before: ", contracts.dyad.mintedDyad(511)); + + vm.prank(0xBA1aAd8B27e841C8E3298e4ceadB0524b4d4B978); + (address[] memory vaults, uint256[] memory amounts) = + contracts.vaultManager.liquidate(511, 108, 3_238_833_231_264_062_800_000); // $3238 dyad + vm.assertEq(4, vaults.length); + + console.log("debt after: ", contracts.dyad.mintedDyad(511)); + + vm.assertEq(vaults[2], MAINNET_V2_KEROSENE_V2_VAULT); + vm.assertLt(amounts[2], 9643113711581927460479); + + vm.assertEq(vaults[3], MAINNET_APXETH_VAULT); + vm.assertGt(amounts[3], 1029566515236775113); + + uint256 keroAfter = keroseneVault.id2asset(511); + uint256 apxAfter = apxVault.id2asset(511); + + console.log("keroBefore: ", keroBefore); + console.log("apxBefore: ", apxBefore); + console.log("keroAfter: ", keroAfter); + console.log("apxAfter: ", apxAfter); + + console.log("keroLoss: ", keroBefore - keroAfter); + console.log("apxLoss: ", apxBefore - apxAfter); + + console.log("apxPrice: ", apxVault.assetPrice()); + } } From 1958894c0887f1c3be8c78b1b207d20e48b5af15 Mon Sep 17 00:00:00 2001 From: gnkz Date: Wed, 11 Dec 2024 10:15:39 -0300 Subject: [PATCH 6/8] feat: add kerosene valuer --- src/core/VaultManagerV6.sol | 44 +++++++-- src/staking/KeroseneValuer.sol | 115 ++++++++++++++++++++++ test/KeroseneValuer.t.sol | 174 +++++++++++++++++++++++++++++++++ test/fork/v6/BaseV6.sol | 12 ++- 4 files changed, 336 insertions(+), 9 deletions(-) create mode 100644 src/staking/KeroseneValuer.sol create mode 100644 test/KeroseneValuer.t.sol diff --git a/src/core/VaultManagerV6.sol b/src/core/VaultManagerV6.sol index 74b0249c..6b0d5723 100644 --- a/src/core/VaultManagerV6.sol +++ b/src/core/VaultManagerV6.sol @@ -9,6 +9,8 @@ import {DyadXP} from "../staking/DyadXP.sol"; import {IVaultManagerV5} from "../interfaces/IVaultManagerV5.sol"; import {DyadHooks} from "./DyadHooks.sol"; import "../interfaces/IExtension.sol"; +import {KeroseneValuer} from "../staking/KeroseneValuer.sol"; +import {KerosineManager} from "../core/KerosineManager.sol"; import {FixedPointMathLib} from "@solmate/src/utils/FixedPointMathLib.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; @@ -47,6 +49,9 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable /// @notice Extensions authorized by a user for use on their notes mapping(address user => EnumerableSet.AddressSet) private _authorizedExtensions; + KeroseneValuer public keroseneValuer; + KerosineManager public keroseneManager; + modifier isValidDNft(uint256 id) { if (dNft.ownerOf(id) == address(0)) revert InvalidDNft(); _; @@ -57,8 +62,13 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable _disableInitializers(); } - function initialize() public reinitializer(6) { - // Nothing to initialize right now + function initialize(address _keroseneValuer, address _keroseneManager) public reinitializer(6) { + keroseneValuer = KeroseneValuer(_keroseneValuer); + keroseneManager = KerosineManager(_keroseneManager); + } + + function setKeroseneValuer(address _newKeroseneValuer) external onlyOwner { + keroseneValuer = KeroseneValuer(_newKeroseneValuer); } /// @inheritdoc IVaultManagerV5 @@ -344,20 +354,42 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable uint256 numberOfVaults = vaults[id].length(); vaultValues = new uint256[](numberOfVaults); + uint256 keroseneVaultIndex; + uint256 noteKeroseneAmount; + for (uint256 i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); if (vaultLicenser.isLicensed(address(vault))) { - uint256 value = vault.getUsdValue(id); - vaultValues[i] = value; if (vaultLicenser.isKerosene(address(vault))) { - keroValue += value; + noteKeroseneAmount = vault.id2asset(id); + keroseneVaultIndex = i; + continue; } else { + uint256 value = vault.getUsdValue(id); + vaultValues[i] = value; exoValue += value; } } } - mintedDyad = dyad.mintedDyad(id); + uint256 tvl; + + address[] memory exoVaults = keroseneManager.getVaults(); + + uint256 numberOfExoVaults = exoVaults.length; + for (uint256 i = 0; i < numberOfExoVaults; i++) { + Vault vault = Vault(exoVaults[i]); + ERC20 asset = vault.asset(); + tvl += asset.balanceOf(address(vault)) * vault.assetPrice() * 1e18 / (10 ** asset.decimals()) + / (10 ** vault.oracle().decimals()); + } + + Dyad dyadCache = dyad; + + keroValue = (noteKeroseneAmount * keroseneValuer.deterministicValue(tvl, dyadCache.totalSupply())) / 1e8; + vaultValues[keroseneVaultIndex] = keroValue; + + mintedDyad = dyadCache.mintedDyad(id); uint256 totalValue = exoValue + keroValue; cr = _collatRatio(mintedDyad, totalValue); diff --git a/src/staking/KeroseneValuer.sol b/src/staking/KeroseneValuer.sol new file mode 100644 index 00000000..79e36797 --- /dev/null +++ b/src/staking/KeroseneValuer.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Owned} from "@solmate/src/auth/Owned.sol"; +import {ERC20} from "@solmate/src/tokens/ERC20.sol"; +import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import {Parameters} from "../params/Parameters.sol"; +import {Kerosine} from "../staking/Kerosine.sol"; +import {Dyad} from "../core/Dyad.sol"; +import {Vault} from "../core/Vault.sol"; + +contract KeroseneValuer is Owned { + using EnumerableSet for EnumerableSet.AddressSet; + using FixedPointMathLib for uint256; + + Kerosine public immutable KEROSINE; + + uint64 public dyadMultiplierSnapshot = 1e12; + uint64 public targetDyadMultiplier = 1e12; + + uint32 public dyadMultiplierSnapshotTimestamp; + uint32 public targetDyadMultiplierTimestamp; + + EnumerableSet.AddressSet private _excludedAddresses; + + event DyadMultiplierUpdated(uint64 previous, uint64 target, uint32 fromTimestamp, uint32 toTimestamp); + + error TargetMultiplierTooSmall(); + + constructor(Kerosine _kerosine) Owned(0xDeD796De6a14E255487191963dEe436c45995813) { + KEROSINE = _kerosine; + _excludedAddresses.add(0xDeD796De6a14E255487191963dEe436c45995813); // Team Multisig + _excludedAddresses.add(0x3962f6585946823440d274aD7C719B02b49DE51E); // Sablier Linear Lockup + } + + function setAddressExcluded(address _address, bool exclude) external onlyOwner { + if (exclude) { + _excludedAddresses.add(_address); + } else { + _excludedAddresses.remove(_address); + } + } + + function setTargetDyadMultiplier(uint64 _targetMultiplier, uint32 _duration) external onlyOwner { + if (_targetMultiplier < 1e12) { + revert TargetMultiplierTooSmall(); + } + + uint64 previousMultiplier = _getDyadSupplyMultiplier(); + + dyadMultiplierSnapshot = previousMultiplier; + targetDyadMultiplier = _targetMultiplier; + dyadMultiplierSnapshotTimestamp = uint32(block.timestamp); + targetDyadMultiplierTimestamp = uint32(block.timestamp) + _duration; + + emit DyadMultiplierUpdated( + previousMultiplier, _targetMultiplier, uint32(block.timestamp), uint32(block.timestamp) + _duration + ); + } + + function currentDyadMultiplier() external view returns (uint64) { + return _getDyadSupplyMultiplier(); + } + + function isExcludedAddress(address _address) external view returns (bool) { + return _excludedAddresses.contains(_address); + } + + function excludedAddresses() external view returns (address[] memory) { + return _excludedAddresses.values(); + } + + function deterministicValue(uint256 _tvl, uint256 _dyadTotalSupply) external view returns (uint256) { + uint256 dyadMultiplier = _getDyadSupplyMultiplier(); + + uint256 normalizedSupply = _dyadTotalSupply.mulDiv(dyadMultiplier, 1e12); + + if (normalizedSupply >= _tvl) { + return 0; + } + + uint256 adjustedKerosineSupply = KEROSINE.totalSupply(); + uint256 excludedAddressLength = _excludedAddresses.length(); + for (uint256 i = 0; i < excludedAddressLength; ++i) { + adjustedKerosineSupply -= KEROSINE.balanceOf(_excludedAddresses.at(i)); + } + + return (_tvl - normalizedSupply).mulDiv(1e8, adjustedKerosineSupply); + } + + function _getDyadSupplyMultiplier() internal view returns (uint64) { + uint32 targetTimestamp = targetDyadMultiplierTimestamp; + if (block.timestamp >= targetTimestamp) { + return targetDyadMultiplier; + } + + uint64 target = targetDyadMultiplier; + uint64 snapshot = dyadMultiplierSnapshot; + uint32 snapshotTimestamp = dyadMultiplierSnapshotTimestamp; + + uint32 timeDelta = targetTimestamp - snapshotTimestamp; + uint64 multiplierDelta = target > snapshot ? target - snapshot : snapshot - target; + + uint64 ratePerSecond = multiplierDelta / timeDelta; + + uint32 secondsPassed = uint32(block.timestamp) - snapshotTimestamp; + + if (target > snapshot) { + return snapshot + (secondsPassed * ratePerSecond); + } + + return snapshot - (secondsPassed * ratePerSecond); + } +} diff --git a/test/KeroseneValuer.t.sol b/test/KeroseneValuer.t.sol new file mode 100644 index 00000000..85c759ed --- /dev/null +++ b/test/KeroseneValuer.t.sol @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {KeroseneValuer} from "../src/staking/KeroseneValuer.sol"; +import {Kerosine} from "../src/staking/Kerosine.sol"; + +contract KeroseneValuerTest is Test { + address OWNER; + address ALICE = makeAddr("ALICE"); + + KeroseneValuer valuer; + Kerosine kerosene; + + function setUp() external { + kerosene = new Kerosine(); + + valuer = new KeroseneValuer(kerosene); + + OWNER = valuer.owner(); + } + + function test_get_multiplier() external view { + uint64 multiplier = valuer.currentDyadMultiplier(); + + // Default value + assertEq(multiplier, 1e12); + } + + function test_set_multiplier() external { + uint64 newMultiplier = 1.25e12; + uint32 duration = 2 hours; + + vm.prank(OWNER); + valuer.setTargetDyadMultiplier(newMultiplier, duration); + + assertEq(valuer.dyadMultiplierSnapshot(), 1e12); + assertEq(valuer.dyadMultiplierSnapshotTimestamp(), vm.getBlockTimestamp()); + assertEq(valuer.targetDyadMultiplier(), newMultiplier); + assertEq(valuer.targetDyadMultiplierTimestamp(), vm.getBlockTimestamp() + duration); + // Current multiplier stays the same right after update + assertEq(valuer.currentDyadMultiplier(), 1e12); + } + + function test_multiplier_increase() external { + uint64 newMultiplier = 2e12; + uint32 duration = 10 seconds; + + uint64 increasePerSecond = (newMultiplier - valuer.currentDyadMultiplier()) / duration; + + vm.prank(OWNER); + valuer.setTargetDyadMultiplier(newMultiplier, duration); + + uint64 multiplierSnapshot = valuer.dyadMultiplierSnapshot(); + + // after 1 second + vm.warp(vm.getBlockTimestamp() + 1 seconds); + assertEq(valuer.currentDyadMultiplier(), multiplierSnapshot + increasePerSecond); + + // after 8 seconds + vm.warp(vm.getBlockTimestamp() + 7 seconds); + assertEq(valuer.currentDyadMultiplier(), multiplierSnapshot + 8 * increasePerSecond); + + // after 10 seconds + vm.warp(vm.getBlockTimestamp() + 2 seconds); + assertEq(valuer.currentDyadMultiplier(), newMultiplier); + + // after some time + vm.warp(vm.getBlockTimestamp() + 1 hours); + assertEq(valuer.currentDyadMultiplier(), newMultiplier); + } + + function test_multiplier_decrease() external { + vm.prank(OWNER); + valuer.setTargetDyadMultiplier(2e12, 0); + + uint64 newMultiplier = 1.2e12; + uint32 duration = 10 seconds; + + uint64 decreasePerSecond = (valuer.currentDyadMultiplier() - newMultiplier) / duration; + + vm.prank(OWNER); + valuer.setTargetDyadMultiplier(newMultiplier, duration); + + uint64 multiplierSnapshot = valuer.dyadMultiplierSnapshot(); + + // after 1 second + vm.warp(vm.getBlockTimestamp() + 1 seconds); + assertEq(valuer.currentDyadMultiplier(), multiplierSnapshot - decreasePerSecond); + + // after 8 seconds + vm.warp(vm.getBlockTimestamp() + 7 seconds); + assertEq(valuer.currentDyadMultiplier(), multiplierSnapshot - 8 * decreasePerSecond); + + // after 10 seconds + vm.warp(vm.getBlockTimestamp() + 2 seconds); + assertEq(valuer.currentDyadMultiplier(), newMultiplier); + + // after some time + vm.warp(vm.getBlockTimestamp() + 1 hours); + assertEq(valuer.currentDyadMultiplier(), newMultiplier); + } + + function test_kerosine_deterministic_value() external { + uint64 multiplier = 1.25e12; + vm.prank(OWNER); + valuer.setTargetDyadMultiplier(multiplier, 0); + uint256 dyadSupply = 100_000_000e18; + + uint96[25] memory systemTvls = [ + 1_000_000_000e18, + 950_000_000e18, + 900_000_000e18, + 850_000_000e18, + 800_000_000e18, + 750_000_000e18, + 700_000_000e18, + 650_000_000e18, + 600_000_000e18, + 550_000_000e18, + 500_000_000e18, + 450_000_000e18, + 400_000_000e18, + 350_000_000e18, + 300_000_000e18, + 250_000_000e18, + 200_000_000e18, + 150_000_000e18, + 140_000_000e18, + 130_000_000e18, + 127_500_000e18, + 125_000_000e18, + 122_500_000e18, + 120_000_000e18, + 100_000_000e18 + ]; + + uint32[25] memory expectedPrices = [ + 0.875e8, + 0.825e8, + 0.775e8, + 0.725e8, + 0.675e8, + 0.625e8, + 0.575e8, + 0.525e8, + 0.475e8, + 0.425e8, + 0.375e8, + 0.325e8, + 0.275e8, + 0.225e8, + 0.175e8, + 0.125e8, + 0.075e8, + 0.025e8, + 0.015e8, + 0.005e8, + 0.0025e8, + 0.0, + 0.0, + 0.0, + 0.0 + ]; + + for (uint256 i; i < systemTvls.length; i++) { + uint256 expectedPrice = expectedPrices[i]; + + uint256 deterministicValue = valuer.deterministicValue(systemTvls[i], dyadSupply); + + assertEq(deterministicValue, expectedPrice); + } + } +} diff --git a/test/fork/v6/BaseV6.sol b/test/fork/v6/BaseV6.sol index 326aa41b..2a8ae007 100644 --- a/test/fork/v6/BaseV6.sol +++ b/test/fork/v6/BaseV6.sol @@ -9,6 +9,8 @@ import {Licenser} from "../../../src/core/Licenser.sol"; import {Modifiers} from "../../Modifiers.sol"; import {IVault} from "../../../src/interfaces/IVault.sol"; import {VaultManagerV6} from "../../../src/core/VaultManagerV6.sol"; +import {KeroseneValuer} from "../../../src/staking/KeroseneValuer.sol"; +import {Kerosine} from "../../../src/staking/Kerosine.sol"; import {FixedPointMathLib} from "@solmate/src/utils/FixedPointMathLib.sol"; import {ERC20} from "@solmate/src/tokens/ERC20.sol"; @@ -26,6 +28,7 @@ struct Contracts { Vault ethVault; VaultWstEth wstEth; KeroseneVault keroseneVault; + KeroseneValuer keroseneValuer; } contract BaseTestV6 is Test, Modifiers, Parameters { @@ -49,12 +52,14 @@ contract BaseTestV6 is Test, Modifiers, Parameters { address bob = address(0x42); function setUp() public { - vm.createSelectFork(vm.envString("RPC_URL"), 21086097); + vm.createSelectFork(vm.envString("RPC_URL"), 21_086_097); + + KeroseneValuer keroseneValuer = new KeroseneValuer(Kerosine(MAINNET_KEROSENE)); VaultManagerV6 impl = new VaultManagerV6(); vm.prank(MAINNET_FEE_RECIPIENT); VaultManagerV6(MAINNET_V2_VAULT_MANAGER).upgradeToAndCall( - address(impl), abi.encodeWithSelector(impl.initialize.selector) + address(impl), abi.encodeWithSelector(impl.initialize.selector, address(keroseneValuer)) ); weth = ERC20(MAINNET_WETH); @@ -66,7 +71,8 @@ contract BaseTestV6 is Test, Modifiers, Parameters { vaultManager: VaultManagerV6(MAINNET_V2_VAULT_MANAGER), ethVault: Vault(MAINNET_V2_WETH_VAULT), wstEth: VaultWstEth(MAINNET_V2_WSTETH_VAULT), - keroseneVault: KeroseneVault(MAINNET_V2_KEROSENE_V2_VAULT) + keroseneVault: KeroseneVault(MAINNET_V2_KEROSENE_V2_VAULT), + keroseneValuer: keroseneValuer }); ETH_TO_USD = contracts.ethVault.assetPrice(); From ad8f0a3da1ae28b674f32dd8e136856aa43fcd74 Mon Sep 17 00:00:00 2001 From: gnkz Date: Fri, 13 Dec 2024 10:23:25 -0300 Subject: [PATCH 7/8] fix: fix vault values --- src/core/VaultManagerV6.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/VaultManagerV6.sol b/src/core/VaultManagerV6.sol index 6b0d5723..a0c95699 100644 --- a/src/core/VaultManagerV6.sol +++ b/src/core/VaultManagerV6.sol @@ -386,8 +386,10 @@ contract VaultManagerV6 is IVaultManagerV5, UUPSUpgradeable, OwnableUpgradeable Dyad dyadCache = dyad; - keroValue = (noteKeroseneAmount * keroseneValuer.deterministicValue(tvl, dyadCache.totalSupply())) / 1e8; - vaultValues[keroseneVaultIndex] = keroValue; + if (noteKeroseneAmount > 0) { + keroValue = (noteKeroseneAmount * keroseneValuer.deterministicValue(tvl, dyadCache.totalSupply())) / 1e8; + vaultValues[keroseneVaultIndex] = keroValue; + } mintedDyad = dyadCache.mintedDyad(id); uint256 totalValue = exoValue + keroValue; From 5a0500539e19e8d865ac3f176a1ed65fcc6daad3 Mon Sep 17 00:00:00 2001 From: gnkz Date: Fri, 13 Dec 2024 10:23:42 -0300 Subject: [PATCH 8/8] test: initialize vault manager v6 --- test/fork/v6/BaseV6.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/fork/v6/BaseV6.sol b/test/fork/v6/BaseV6.sol index 2a8ae007..b60fb992 100644 --- a/test/fork/v6/BaseV6.sol +++ b/test/fork/v6/BaseV6.sol @@ -59,7 +59,8 @@ contract BaseTestV6 is Test, Modifiers, Parameters { VaultManagerV6 impl = new VaultManagerV6(); vm.prank(MAINNET_FEE_RECIPIENT); VaultManagerV6(MAINNET_V2_VAULT_MANAGER).upgradeToAndCall( - address(impl), abi.encodeWithSelector(impl.initialize.selector, address(keroseneValuer)) + address(impl), + abi.encodeWithSelector(impl.initialize.selector, address(keroseneValuer), MAINNET_V2_KEROSENE_MANAGER) ); weth = ERC20(MAINNET_WETH);