diff --git a/packages/contracts/.env.template b/packages/contracts/.env.template index bdf05e900..bf733b497 100644 --- a/packages/contracts/.env.template +++ b/packages/contracts/.env.template @@ -29,3 +29,4 @@ CMC_KEY= # Disables logging via `hre.log` DISABLE_LOGS='false' +SAFE_GLOBAL_API_KEY = \ No newline at end of file diff --git a/packages/contracts/.openzeppelin/mainnet.json b/packages/contracts/.openzeppelin/mainnet.json index 9714eeed8..61305e3de 100644 --- a/packages/contracts/.openzeppelin/mainnet.json +++ b/packages/contracts/.openzeppelin/mainnet.json @@ -10843,6 +10843,375 @@ "types": {}, "namespaces": {} } + }, + "1cfdfffe45bf5ad17e0bac4b0742818d847dfbff93cc1a39f6fde15f3dfbf887": { + "address": "0xA6406fcbCCe89B899344d3E0D004DfeAa225a259", + "txHash": "0xbdee983609a698b828ceec1f9c6c07d78cda8cd4c9f1b40eb14d4f672ad5edbf", + "layout": { + "solcVersion": "0.8.11", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_paused", + "offset": 0, + "slot": "101", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "102", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "_status", + "offset": 0, + "slot": "151", + "type": "t_uint256", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:38" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "ReentrancyGuardUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol:80" + }, + { + "label": "poolSharesToken", + "offset": 0, + "slot": "201", + "type": "t_contract(LenderCommitmentGroupShares)8341", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:113" + }, + { + "label": "principalToken", + "offset": 0, + "slot": "202", + "type": "t_contract(IERC20)3202", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:115" + }, + { + "label": "collateralToken", + "offset": 0, + "slot": "203", + "type": "t_contract(IERC20)3202", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:116" + }, + { + "label": "marketId", + "offset": 0, + "slot": "204", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:118" + }, + { + "label": "totalPrincipalTokensCommitted", + "offset": 0, + "slot": "205", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:121" + }, + { + "label": "totalPrincipalTokensWithdrawn", + "offset": 0, + "slot": "206", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:122" + }, + { + "label": "totalPrincipalTokensLended", + "offset": 0, + "slot": "207", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:124" + }, + { + "label": "totalPrincipalTokensRepaid", + "offset": 0, + "slot": "208", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:125" + }, + { + "label": "excessivePrincipalTokensRepaid", + "offset": 0, + "slot": "209", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:126" + }, + { + "label": "totalInterestCollected", + "offset": 0, + "slot": "210", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:128" + }, + { + "label": "liquidityThresholdPercent", + "offset": 0, + "slot": "211", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:130" + }, + { + "label": "collateralRatio", + "offset": 2, + "slot": "211", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:131" + }, + { + "label": "maxLoanDuration", + "offset": 4, + "slot": "211", + "type": "t_uint32", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:133" + }, + { + "label": "interestRateLowerBound", + "offset": 8, + "slot": "211", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:134" + }, + { + "label": "interestRateUpperBound", + "offset": 10, + "slot": "211", + "type": "t_uint16", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:135" + }, + { + "label": "activeBids", + "offset": 0, + "slot": "212", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:143" + }, + { + "label": "activeBidsAmountDueRemaining", + "offset": 0, + "slot": "213", + "type": "t_mapping(t_uint256,t_uint256)", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:144" + }, + { + "label": "tokenDifferenceFromLiquidations", + "offset": 0, + "slot": "214", + "type": "t_int256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:146" + }, + { + "label": "firstDepositMade", + "offset": 0, + "slot": "215", + "type": "t_bool", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:148" + }, + { + "label": "withdrawDelayTimeSeconds", + "offset": 0, + "slot": "216", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:149" + }, + { + "label": "poolOracleRoutes", + "offset": 0, + "slot": "217", + "type": "t_array(t_struct(PoolRouteConfig)11707_storage)dyn_storage", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:151" + }, + { + "label": "maxPrincipalPerCollateralAmount", + "offset": 0, + "slot": "218", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:154" + }, + { + "label": "lastUnpausedAt", + "offset": 0, + "slot": "219", + "type": "t_uint256", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:157" + }, + { + "label": "liquidationsPaused", + "offset": 0, + "slot": "220", + "type": "t_bool", + "contract": "LenderCommitmentGroup_Smart", + "src": "contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol:159" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_struct(PoolRouteConfig)11707_storage)dyn_storage": { + "label": "struct IUniswapPricingLibrary.PoolRouteConfig[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IERC20)3202": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(LenderCommitmentGroupShares)8341": { + "label": "contract LenderCommitmentGroupShares", + "numberOfBytes": "20" + }, + "t_int256": { + "label": "int256", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_struct(PoolRouteConfig)11707_storage": { + "label": "struct IUniswapPricingLibrary.PoolRouteConfig", + "members": [ + { + "label": "pool", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "zeroForOne", + "type": "t_bool", + "offset": 20, + "slot": "0" + }, + { + "label": "twapInterval", + "type": "t_uint32", + "offset": 21, + "slot": "0" + }, + { + "label": "token0Decimals", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "token1Decimals", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": {} + } } } } diff --git a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol index 0deed5b76..ad48c11b0 100644 --- a/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol +++ b/packages/contracts/contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol @@ -155,6 +155,8 @@ contract LenderCommitmentGroup_Smart is uint256 public lastUnpausedAt; + + bool public liquidationsPaused; event PoolInitialized( @@ -221,7 +223,7 @@ contract LenderCommitmentGroup_Smart is modifier onlySmartCommitmentForwarder() { require( msg.sender == address(SMART_COMMITMENT_FORWARDER), - "Can only be called by Smart Commitment Forwarder" + "oSCF" ); _; } @@ -310,20 +312,20 @@ contract LenderCommitmentGroup_Smart is interestRateLowerBound = _commitmentGroupConfig.interestRateLowerBound; interestRateUpperBound = _commitmentGroupConfig.interestRateUpperBound; - require(interestRateLowerBound <= interestRateUpperBound, "invalid _interestRateLowerBound"); + require(interestRateLowerBound <= interestRateUpperBound, "IRL"); liquidityThresholdPercent = _commitmentGroupConfig.liquidityThresholdPercent; collateralRatio = _commitmentGroupConfig.collateralRatio; - require( liquidityThresholdPercent <= 10000, "invalid _liquidityThresholdPercent"); + require( liquidityThresholdPercent <= 10000, "LTP"); for (uint256 i = 0; i < _poolOracleRoutes.length; i++) { poolOracleRoutes.push(_poolOracleRoutes[i]); } - require(poolOracleRoutes.length >= 1 && poolOracleRoutes.length <= 2, "invalid pool routes length"); + require(poolOracleRoutes.length >= 1 && poolOracleRoutes.length <= 2, "PRL"); poolSharesToken_ = _deployPoolSharesToken(); @@ -379,7 +381,7 @@ contract LenderCommitmentGroup_Smart is { require( address(poolSharesToken) == address(0), - "Pool shares already deployed" + "ST" ); poolSharesToken = new LenderCommitmentGroupShares( @@ -465,7 +467,7 @@ contract LenderCommitmentGroup_Smart is uint256 principalTokenBalanceAfter = principalToken.balanceOf(address(this)); - require( principalTokenBalanceAfter == principalTokenBalanceBefore + _amount, "Token balance was not added properly" ); + require( principalTokenBalanceAfter == principalTokenBalanceBefore + _amount, "B" ); sharesAmount_ = _valueOfUnderlying(_amount, sharesExchangeRate()); @@ -483,11 +485,11 @@ contract LenderCommitmentGroup_Smart is ); - require( sharesAmount_ >= _minSharesAmountOut, "Invalid: Min Shares AmountOut" ); + require( sharesAmount_ >= _minSharesAmountOut, "SM" ); if(!firstDepositMade){ - require(msg.sender == owner(), "Owner must initialize the pool with a deposit first."); - require( sharesAmount_>= 1e6, "Initial shares amount must be atleast 1e6" ); + require(msg.sender == owner(), "F."); + require( sharesAmount_>= 1e6, "SA" ); firstDepositMade = true; } @@ -530,7 +532,7 @@ contract LenderCommitmentGroup_Smart is require( _collateralTokenAddress == address(collateralToken), - "Mismatching collateral token" + "CT" ); //the interest rate must be at least as high has the commitment demands. The borrower can use a higher interest rate although that would not be beneficial to the borrower. require(_interestRate >= getMinInterestRate(_principalAmount), "Invalid interest rate"); @@ -539,7 +541,7 @@ contract LenderCommitmentGroup_Smart is require( getPrincipalAmountAvailableToBorrow() >= _principalAmount, - "Invalid loan max principal" + "LMP" ); @@ -552,7 +554,7 @@ contract LenderCommitmentGroup_Smart is require( _collateralAmount >= requiredCollateral, - "Insufficient Borrower Collateral" + "BC" ); principalToken.safeApprove(address(TELLER_V2), _principalAmount); @@ -641,7 +643,7 @@ contract LenderCommitmentGroup_Smart is _recipient ); - require( principalTokenValueToWithdraw >= _minAmountOut ,"Invalid: Min Amount Out"); + require( principalTokenValueToWithdraw >= _minAmountOut ,"W"); return principalTokenValueToWithdraw; } @@ -660,6 +662,7 @@ contract LenderCommitmentGroup_Smart is int256 _tokenAmountDifference ) external whenForwarderNotPaused whenNotPaused bidIsActiveForGroup(_bidId) nonReentrant onlyOracleApprovedAllowEOA { + require(!liquidationsPaused ); uint256 loanTotalPrincipalAmount = _getLoanTotalPrincipalAmount(_bidId); //only used for the auction delta amount @@ -682,8 +685,7 @@ contract LenderCommitmentGroup_Smart is require( - _tokenAmountDifference >= minAmountDifference, - "Insufficient tokenAmountDifference" + _tokenAmountDifference >= minAmountDifference ); @@ -814,7 +816,7 @@ contract LenderCommitmentGroup_Smart is } - function getTokenDifferenceFromLiquidations() public view returns (int256){ + function getTokenDifferenceFromLiquidations() external view returns (int256){ return tokenDifferenceFromLiquidations; @@ -833,11 +835,11 @@ contract LenderCommitmentGroup_Smart is ) public view virtual returns (int256 amountDifference_) { require( _loanDefaultedTimestamp > 0, - "Loan defaulted timestamp must be greater than zero" + "Ldt" ); require( block.timestamp > _loanDefaultedTimestamp, - "Loan defaulted timestamp must be in the past" + "Ldp" ); uint256 secondsSinceDefaulted = block.timestamp - @@ -1109,4 +1111,26 @@ contract LenderCommitmentGroup_Smart is setLastUnpausedAt(); _unpause(); } + + + + /** + * @notice Lets the DAO/owner of the protocol implement an emergency stop mechanism. + */ + function pauseLiquidations() public virtual onlyProtocolPauser { + require(!liquidationsPaused); + liquidationsPaused = true; + } + + /** + * @notice Lets the DAO/owner of the protocol undo a previously implemented emergency stop. + */ + function unpauseLiquidations() public virtual onlyProtocolPauser { + + require(liquidationsPaused); + + setLastUnpausedAt(); + liquidationsPaused = false; + } + } diff --git a/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol b/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol index a401a198e..4ad21a8f8 100644 --- a/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol +++ b/packages/contracts/contracts/mock/UniswapPricingHelperMock.sol @@ -2,8 +2,8 @@ pragma solidity >=0.8.0 <0.9.0; // SPDX-License-Identifier: MIT -import "forge-std/console.sol"; - +// import "forge-std/console.sol"; // Not needed for Hardhat deployment + import {IUniswapPricingLibrary} from "../interfaces/IUniswapPricingLibrary.sol"; diff --git a/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol b/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol index 2982346ae..e69603ea5 100644 --- a/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol +++ b/packages/contracts/contracts/price_oracles/UniswapPricingHelper.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; // SPDX-License-Identifier: MIT -import "forge-std/console.sol"; +//import "forge-std/console.sol"; import {IUniswapPricingLibrary} from "../interfaces/IUniswapPricingLibrary.sol"; @@ -50,11 +50,7 @@ contract UniswapPricingHelper uint256 pool1PriceRatio = getUniswapPriceRatioForPool( poolRoutes[1] ); - - - console.log("ratio"); - console.logUint(pool0PriceRatio); - console.logUint(pool1PriceRatio); + return FullMath.mulDiv( diff --git a/packages/contracts/deploy/upgrades/35_upgrade_lender_pools_v1.ts b/packages/contracts/deploy/upgrades/35_upgrade_lender_pools_v1.ts new file mode 100644 index 000000000..c02b121c2 --- /dev/null +++ b/packages/contracts/deploy/upgrades/35_upgrade_lender_pools_v1.ts @@ -0,0 +1,87 @@ +import { DeployFunction } from 'hardhat-deploy/dist/types' + + +import { get_ecosystem_contract_address } from "../../helpers/ecosystem-contracts-lookup" + + +const deployFn: DeployFunction = async (hre) => { + hre.log('----------') + hre.log('') + hre.log('Lendergroups: Proposing upgrade...') + + + + const lenderCommitmentGroupBeaconProxy = await hre.contracts.get('LenderCommitmentGroupBeacon') + + const tellerV2 = await hre.contracts.get('TellerV2') + const SmartCommitmentForwarder = await hre.contracts.get( + 'SmartCommitmentForwarder' + ) + const tellerV2Address = await tellerV2.getAddress() + + const smartCommitmentForwarderAddress = + await SmartCommitmentForwarder.getAddress() + + +let uniswapV3FactoryAddress: string = get_ecosystem_contract_address( hre.network.name, "uniswapV3Factory" ) ; + const uniswapPricingLibraryV2 = await hre.deployments.get('UniswapPricingLibraryV2') + + + +//this is why the owner of the beacon should be timelock controller ! +// so we can upgrade it like this . Using a proposal. This actually goes AROUND the proxy admin, interestingly. + await hre.upgrades.proposeBatchTimelock({ + title: 'LenderGroups: Liq Pause', + description: ` +# LenderGroups + +* A patch to add pausing of liquidations. +`, + _steps: [ + { + beacon: lenderCommitmentGroupBeaconProxy, + implFactory: await hre.ethers.getContractFactory('LenderCommitmentGroup_Smart', { + libraries: { + UniswapPricingLibraryV2: uniswapPricingLibraryV2.address, + }, + }), + + opts: { + // unsafeSkipStorageCheck: true, + unsafeAllow: [ + 'constructor', + 'state-variable-immutable', + 'external-library-linking', + ], + constructorArgs: [ + tellerV2Address, + smartCommitmentForwarderAddress, + uniswapV3FactoryAddress, + + ], + }, + }, + ], + }) + + hre.log('done.') + hre.log('') + hre.log('----------') + + return true +} + +// tags and deployment +deployFn.id = 'lender-commitment-group-beacon:upgrade-liq-pause' +deployFn.tags = ['lender-commitment-group-beacon'] +deployFn.dependencies = [ + 'teller-v2:deploy', + 'smart-commitment-forwarder:deploy', + 'teller-v2:uniswap-pricing-library-v2', + 'lender-commitment-group-beacon:deploy' +] + +deployFn.skip = async (hre) => { + return !hre.network.live || !['sepolia','polygon','mainnet'].includes(hre.network.name) +} +export default deployFn diff --git a/packages/contracts/deployments/mainnet/.migrations.json b/packages/contracts/deployments/mainnet/.migrations.json index cd415a62c..a29aafaa7 100644 --- a/packages/contracts/deployments/mainnet/.migrations.json +++ b/packages/contracts/deployments/mainnet/.migrations.json @@ -47,5 +47,6 @@ "uniswap-pricing-helper:deploy": 1754403885, "lender-commitment-group-beacon-v2:pricing-helper": 1755203064, "lender-commitment-forwarder:extensions:flash-swap-rollover:g2-upgrade": 1755273953, - "timelock-controller:update-delay-7200": 1757524329 + "timelock-controller:update-delay-7200": 1757524329, + "lender-commitment-group-beacon:upgrade-liq-pause": 1768407044 } \ No newline at end of file diff --git a/packages/contracts/deployments/mainnet/LenderCommitmentGroupBeacon.json b/packages/contracts/deployments/mainnet/LenderCommitmentGroupBeacon.json index b651b1158..25d9b94e5 100644 --- a/packages/contracts/deployments/mainnet/LenderCommitmentGroupBeacon.json +++ b/packages/contracts/deployments/mainnet/LenderCommitmentGroupBeacon.json @@ -1114,6 +1114,20 @@ ], "outputs": [] }, + { + "type": "function", + "name": "liquidationsPaused", + "constant": true, + "stateMutability": "view", + "payable": false, + "inputs": [], + "outputs": [ + { + "type": "bool", + "name": "" + } + ] + }, { "type": "function", "name": "liquidityThresholdPercent", @@ -1178,6 +1192,14 @@ "inputs": [], "outputs": [] }, + { + "type": "function", + "name": "pauseLiquidations", + "constant": false, + "payable": false, + "inputs": [], + "outputs": [] + }, { "type": "function", "name": "paused", @@ -1451,6 +1473,14 @@ "inputs": [], "outputs": [] }, + { + "type": "function", + "name": "unpauseLiquidations", + "constant": false, + "payable": false, + "inputs": [], + "outputs": [] + }, { "type": "function", "name": "withdrawDelayTimeSeconds", @@ -1480,6 +1510,6 @@ } ], "receipt": {}, - "numDeployments": 1, + "numDeployments": 8, "implementation": "0xf6E926D7282Ba2Dc1bd580dA36420d2067bEc4A3" } \ No newline at end of file diff --git a/packages/contracts/helpers/gnosis-safe-helpers.ts b/packages/contracts/helpers/gnosis-safe-helpers.ts index fbcd0f1ac..c53ae8a51 100644 --- a/packages/contracts/helpers/gnosis-safe-helpers.ts +++ b/packages/contracts/helpers/gnosis-safe-helpers.ts @@ -76,6 +76,9 @@ export class GnosisSafeAdminClient { private baseUrl: string = 'https://api.safe.global' constructor(config: { apiKey: string }) { + if (!config.apiKey) { + throw new Error('SAFE_GLOBAL_API_KEY is required. Get your API key at: https://app.safe.global/settings/setup') + } this.apiKey = config.apiKey } @@ -257,8 +260,8 @@ export class GnosisSafeAdminClient { transaction: SafeTransactionRequest, network: string ): Promise<{ safeTxHash: string }> { - const getNetwork = this.getNetworkPath([{network} as any]) - const url = `https://safe-transaction-${getNetwork}.safe.global/api/v2/safes/${transaction.safe}/multisig-transactions/` + const chainName = this.getNetworkPath([{network} as any]) + const url = `https://api.safe.global/tx-service/${chainName}/api/v2/safes/${transaction.safe}/multisig-transactions/` console.log(`submitTransaction ${url }`) @@ -267,17 +270,10 @@ export class GnosisSafeAdminClient { const headers: Record = { 'accept': 'application/json', - 'content-type': 'application/json' + 'content-type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}` } - - // Add Authorization header if API key is provided - if (this.apiKey) { - headers['Authorization'] = `Bearer ${this.apiKey}` - console.log('Using API key for authentication') - } else { - console.log('No API key provided') - } - + console.log('Request headers:', headers) const response = await fetch(url, { @@ -352,37 +348,45 @@ export class GnosisSafeAdminClient { } /* + Fetches the next nonce for a Safe using the new Safe API format with authentication. - ex - - https://safe-transaction-mainnet.safe.global/api/v1/safes/0xcd2E72aEBe2A203b84f46DEEC948E6465dB51c75/ - - - - Need to be VERY careful with this bc it doesnt properly work in rapid succession rn + Example endpoint: + https://api.safe.global/tx-service/eth/api/v2/safes/0xcd2E72aEBe2A203b84f46DEEC948E6465dB51c75/ + Requires API key authentication (get from https://app.safe.global/settings/setup) */ private async getNextNonce(safeAddress: string, network: string, offset: number = 0): Promise { - const getNetwork = this.getNetworkPath([{network} as any]) - const url = `https://safe-transaction-${getNetwork}.safe.global/api/v1/safes/${safeAddress}/` - - console.log(`getNextNonce ${url }`) + const chainName = this.getNetworkPath([{network} as any]) + const url = `https://api.safe.global/tx-service/${chainName}/api/v2/safes/${safeAddress}/` + console.log(`getNextNonce ${url}`) + + const headers: Record = { + 'accept': 'application/json', + 'content-type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}` + } const response = await fetch(url, { - headers: { - 'accept': 'application/json', - 'content-type': 'application/json' - } + headers }) if (!response.ok) { const errorText = await response.text() if (response.status === 404) { - console.warn(`Safe not found, using nonce 0. This might be a new Safe or incorrect network.`) - return 0 + // console.warn(`Safe not found, using nonce 0. This might be a new Safe or incorrect network.`) + // return 0 } + + + + //try to get getNextNonceV1 and return that .. ? + + + + // return 71 + offset // hack for now use this if needed x.x + throw new Error(`Failed to get Safe info: ${response.status} - ${errorText}`) } @@ -390,6 +394,48 @@ export class GnosisSafeAdminClient { return parseInt(safeInfo.nonce) + parseInt(offset) } + + private async getNextNonceV1(safeAddress: string, network: string, offset: number = 0): Promise { + + // https://api.safe.global/tx-service/eth/api/v1/safes/0x9E3bfee4C6b4D28b5113E4786A1D9812eB3D2Db6/ + + // this works , oddly enough + + + const chainName = this.getNetworkPath([{network} as any]) + let backup_url = `https://api.safe.global/tx-service/${chainName}/api/v1/safes/${safeAddress}/`; + + console.log(`getNextNonce ${url}`) + + const headers: Record = { + 'accept': 'application/json', + 'content-type': 'application/json' + + } + + const response = await fetch(backup_url, { + headers + }) + + + if (!response.ok) { + const errorText = await response.text() + if (response.status === 404) { + // console.warn(`Safe not found, using nonce 0. This might be a new Safe or incorrect network.`) + // return 0 + } + + // return 71 + offset // hack for now use this if needed x.x + + throw new Error(`Failed to get Safe info: ${response.status} - ${errorText}`) + } + + const safeInfo = await response.json() + return parseInt(safeInfo.nonce) + parseInt(offset) + } + + + private getChainId(network: string): number { const chainIds: Record = { 'mainnet': 1, @@ -462,12 +508,12 @@ export class GnosisSafeAdminClient { return safeTxHash } -/* - private getNetworkPathShorthand(contract: PartialContract | PartialContract[]): string { + private getNetworkPath(contract: PartialContract | PartialContract[]): string { const firstContract = Array.isArray(contract) ? contract[0] : contract const network = firstContract.network - - // For POST requests to api.safe.global/tx-service/{network}/ + + // Maps Hardhat network names to Safe API EIP3770 chain names + // Format: https://api.safe.global/tx-service/{chain}/api/v2/... const networkMap: Record = { 'mainnet': 'eth', 'sepolia': 'sep', @@ -478,32 +524,11 @@ export class GnosisSafeAdminClient { 'base': 'base', 'gnosis': 'gno', 'avalanche': 'avax', - 'bsc': 'bnb' - } - - return networkMap[network as string] || 'eth' - }*/ - - private getNetworkPath(contract: PartialContract | PartialContract[]): string { - const firstContract = Array.isArray(contract) ? contract[0] : contract - const network = firstContract.network - - // For GET requests to safe-transaction-{network}.safe.global/ - const networkMap: Record = { - 'mainnet': 'mainnet', - 'sepolia': 'sepolia', - 'goerli': 'goerli', - 'polygon': 'polygon', - 'arbitrum': 'arbitrum', - 'optimism': 'optimism', - 'base': 'base', - 'gnosis': 'gnosis', - 'avalanche': 'avalanche', - 'bsc': 'bsc', + 'bsc': 'bnb', 'katana': 'katana', } - - return networkMap[network as string] || 'mainnet' + + return networkMap[network as string] || 'eth' } /* diff --git a/packages/contracts/tests_fork/upgrade_poolv1_beacon_test.sol b/packages/contracts/tests_fork/upgrade_poolv1_beacon_test.sol new file mode 100644 index 000000000..ac5152d19 --- /dev/null +++ b/packages/contracts/tests_fork/upgrade_poolv1_beacon_test.sol @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "../tests/util/FoundryTest.sol"; +import "forge-std/console.sol"; +import "forge-std/StdJson.sol"; + +// Import contracts +import { LenderCommitmentGroup_Smart } from "../contracts/LenderCommitmentForwarder/extensions/LenderCommitmentGroup/LenderCommitmentGroup_Smart.sol"; +import { IBeacon } from "../contracts/openzeppelin/beacon/IBeacon.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { IProtocolPausingManager } from "../contracts/interfaces/IProtocolPausingManager.sol"; +import { IHasProtocolPausingManager } from "../contracts/interfaces/IHasProtocolPausingManager.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/* + +This test validates the upgrade of the LenderCommitmentGroup_Smart beacon implementation +to add pause/unpause liquidations functionality. + +To run this test: +1. Start anvil forking mainnet in a separate terminal: + anvil --fork-url https://eth-mainnet.g.alchemy.com/v2/ --fork-block-number + +2. Run the test: + forge test --match-test test_upgrade_pool_v1_beacon --rpc-url http://127.0.0.1:8545 -vvv + +*/ + +contract UpgradePoolV1BeaconTest is Test { + + using stdJson for string; + + string constant NETWORK_NAME = "mainnet"; + + // Mainnet addresses - loaded dynamically from deployments + address tellerV2Address; + address beaconAddress; + address smartCommitmentForwarderAddress; + address protocolPausingManagerAddress; + + IBeacon beacon; + address originalImplementation; + LenderCommitmentGroup_Smart newImplementation; + + // Get a deployed pool instance to test with (this pool should use the V1 beacon) + address constant TEST_POOL_ADDRESS = 0xAbBf23bE0A3D14A7cBD0df78D8eea4BdDF9544EF; // V1 pool + address constant UNISWAP_V3_FACTORY_ADDRESS = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + LenderCommitmentGroup_Smart testPool; + + function setUp() public { + console.log("Running fork test on chain:", block.chainid); + console.log("Block number:", block.number); + + // Load deployed addresses dynamically + tellerV2Address = getDeployedAddress("TellerV2"); + console.log("TellerV2 proxy:", tellerV2Address); + + beaconAddress = getDeployedAddress("LenderCommitmentGroupBeacon"); + console.log("LenderCommitmentGroupBeacon:", beaconAddress); + + smartCommitmentForwarderAddress = getDeployedAddress("SmartCommitmentForwarder"); + console.log("SmartCommitmentForwarder:", smartCommitmentForwarderAddress); + + protocolPausingManagerAddress = getDeployedAddress("ProtocolPausingManager"); + console.log("ProtocolPausingManager:", protocolPausingManagerAddress); + + // Verify we're connected to the beacon + require(beaconAddress.code.length > 0, "Beacon not found at address"); + beacon = IBeacon(beaconAddress); + + // Get the original implementation from the beacon + originalImplementation = beacon.implementation(); + console.log("Original V1 implementation:", originalImplementation); + require(originalImplementation.code.length > 0, "Original implementation has no code"); + + // Verify this is the expected implementation address + require(originalImplementation == 0xf6E926D7282Ba2Dc1bd580dA36420d2067bEc4A3, "Implementation address mismatch"); + + // Connect to test pool (if it exists) + if (TEST_POOL_ADDRESS.code.length > 0) { + testPool = LenderCommitmentGroup_Smart(TEST_POOL_ADDRESS); + console.log("Connected to test pool at:", TEST_POOL_ADDRESS); + } + } + + function test_upgrade_pool_v1_beacon() public { + console.log("Testing Pool V1 Beacon Upgrade..."); + + // Step 1: Deploy new implementation + console.log("Deploying new LenderCommitmentGroup_Smart implementation..."); + newImplementation = new LenderCommitmentGroup_Smart( + tellerV2Address, + smartCommitmentForwarderAddress, + UNISWAP_V3_FACTORY_ADDRESS + ); + console.log("New implementation deployed at:", address(newImplementation)); + + // Step 2: Use vm.etch to replace the implementation code at the original implementation address + console.log("Etching new implementation over original..."); + vm.etch(originalImplementation, address(newImplementation).code); + console.log("Implementation upgraded via etch"); + + // Step 3: Verify the upgrade was successful + console.log("Verifying upgrade..."); + address currentImplementation = beacon.implementation(); + console.log("Current implementation after etch:", currentImplementation); + + // The address should still be the same, but the code should be new + assertEq(currentImplementation, originalImplementation, "Implementation address should not change"); + assertTrue(currentImplementation.code.length > 0, "Implementation should have code"); + + console.log("Pool V1 Beacon upgrade test completed successfully!"); + } + + function test_new_pause_liquidations_functionality() public { + // First perform the upgrade + console.log("Testing new pause liquidations functionality..."); + + // Deploy new implementation + newImplementation = new LenderCommitmentGroup_Smart( + tellerV2Address, + smartCommitmentForwarderAddress, + UNISWAP_V3_FACTORY_ADDRESS + ); + + // Etch over the original implementation + vm.etch(originalImplementation, address(newImplementation).code); + + // Skip this test if we don't have a test pool + if (TEST_POOL_ADDRESS.code.length == 0) { + console.log("Skipping pause test - no test pool available"); + return; + } + + // Step 1: Check initial liquidationsPaused state + bool initialPauseState = testPool.liquidationsPaused(); + console.log("Initial liquidationsPaused state:", initialPauseState); + assertFalse(initialPauseState, "Liquidations should not be paused initially"); + + // Step 2: Get the protocol pausing manager from TellerV2 and find a pauser + IHasProtocolPausingManager tellerV2 = IHasProtocolPausingManager(tellerV2Address); + address pausingManager = tellerV2.getProtocolPausingManager(); + console.log("Protocol pausing manager:", pausingManager); + require(pausingManager.code.length > 0, "Pausing manager not found"); + + IProtocolPausingManager pausingMgr = IProtocolPausingManager(pausingManager); + + // Find a pauser address - we can get the owner of the pausing manager + address pauserAddress = getPauserAddress(pausingManager); + console.log("Using pauser address:", pauserAddress); + + // Step 3: Use vm.prank to impersonate the pauser and pause liquidations + vm.prank(pauserAddress); + testPool.pauseLiquidations(); + console.log("Called pauseLiquidations()"); + + // Step 4: Verify liquidations are now paused + bool pausedState = testPool.liquidationsPaused(); + console.log("Liquidations paused state after pause:", pausedState); + assertTrue(pausedState, "Liquidations should be paused"); + + // Step 5: Unpause liquidations + vm.prank(pauserAddress); + testPool.unpauseLiquidations(); + console.log("Called unpauseLiquidations()"); + + // Step 6: Verify liquidations are unpaused + bool unpausedState = testPool.liquidationsPaused(); + console.log("Liquidations paused state after unpause:", unpausedState); + assertFalse(unpausedState, "Liquidations should be unpaused"); + + console.log("New pause liquidations functionality verified!"); + } + + function getPauserAddress(address pausingManager) internal view returns (address) { + // The owner of the ProtocolPausingManager is always a pauser + // (see isPauser function: returns pauserRoleBearer[_account] || _account == owner()) + try OwnableUpgradeable(pausingManager).owner() returns (address owner) { + console.log("Pausing manager owner:", owner); + // Owner is always a pauser according to isPauser() logic + return owner; + } catch { + // If we can't get owner, return zero address and let test fail + console.log("Failed to get pausing manager owner"); + return address(0); + } + } + + function test_liquidation_reverts_when_paused() public { + console.log("Testing that liquidations revert when paused..."); + + // Deploy and etch new implementation + newImplementation = new LenderCommitmentGroup_Smart( + tellerV2Address, + smartCommitmentForwarderAddress, + UNISWAP_V3_FACTORY_ADDRESS + ); + vm.etch(originalImplementation, address(newImplementation).code); + + if (TEST_POOL_ADDRESS.code.length == 0) { + console.log("Skipping liquidation revert test - no test pool available"); + return; + } + + // Get the protocol pausing manager from TellerV2 + IHasProtocolPausingManager tellerV2 = IHasProtocolPausingManager(tellerV2Address); + address pausingManager = tellerV2.getProtocolPausingManager(); + console.log("Protocol pausing manager:", pausingManager); + + if (pausingManager.code.length == 0) { + console.log("Skipping - no pausing manager configured"); + return; + } + + // Get a pauser address + address pauserAddress = getPauserAddress(pausingManager); + console.log("Using pauser address:", pauserAddress); + + // Pause liquidations + vm.prank(pauserAddress); + testPool.pauseLiquidations(); + console.log("Liquidations paused"); + + // Verify liquidations are paused + assertTrue(testPool.liquidationsPaused(), "Liquidations should be paused"); + + // Try to liquidate - this should revert with "P" (paused) + // We'll use a dummy bid ID and expect it to revert with the pause check before any other checks + uint256 dummyBidId = 999999; + int256 dummyTokenDiff = 0; + + vm.expectRevert( ); + testPool.liquidateDefaultedLoanWithIncentive(dummyBidId, dummyTokenDiff); + + console.log("Liquidation correctly reverted when paused!"); + + // Unpause and verify we can proceed past the pause check + vm.prank(pauserAddress); + testPool.unpauseLiquidations(); + console.log("Liquidations unpaused"); + + assertFalse(testPool.liquidationsPaused(), "Liquidations should be unpaused"); + + console.log("Liquidation pause enforcement test completed successfully!"); + } + + function getDeployedAddress(string memory contractName) internal view returns (address) { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/deployments/", NETWORK_NAME, "/", contractName, ".json"); + string memory json = vm.readFile(path); + return json.readAddress(".address"); + } +}