diff --git a/.env.example b/.env.example index 18bef32..f71649f 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,41 @@ # Contracts admin. ADMIN= -# Rebalance caller. -REBALANCE_CALLER= +# Address that can increase/decerease LP conversion rate. +ADJUSTER= # Deposits to Liquidity Hub are only allowed till this limit is reached. ASSETS_LIMIT= # Liquidity mining tiers. Multiplier will be divided by 100. So 175 will result in 1.75x. # There is no limit to the number of tiers, but has to be atleast one. -TIER_1_DAYS=90 -TIER_1_MULTIPLIER=100 -TIER_2_DAYS=180 -TIER_2_MULTIPLIER=150 -TIER_3_DAYS=360 -TIER_3_MULTIPLIER=200 -BASE_SEPOLIA_PRIVATE_KEY= +TIER_1_SECONDS=7776000 +TIER_1_MULTIPLIER=30 +TIER_2_SECONDS=15552000 +TIER_2_MULTIPLIER=80 +TIER_3_SECONDS=31104000 +TIER_3_MULTIPLIER=170 +# Rebalance caller. +REBALANCE_CALLER= +# Liquidity Pool parameters. +# Value 500 will result in health factor 5. +MIN_HEALTH_FACTOR=500 +# Value 20 will result in LTV 20%. +DEFAULT_LTV=20 +MPC_ADDRESS= +WITHDRAW_PROFIT= +# General deployment parameters. +DEPLOYER_PRIVATE_KEY= VERIFY=false +BASE_SEPOLIA_RPC= +ETHEREUM_SEPOLIA_RPC= +ARBITRUM_SEPOLIA_RPC= +ETHERSCAN_BASE_SEPOLIA= +ETHERSCAN_ETHEREUM_SEPOLIA= +ETHERSCAN_ARBITRUM_SEPOLIA= +# Upgrade and configuration update parameters. +LIQUIDITY_POOL= +REBALANCER= +# Testing parameters. +FORK_PROVIDER=https://eth-mainnet.public.blastapi.io +USDC_OWNER_ADDRESS=0x7713974908Be4BEd47172370115e8b1219F4A5f0 +RPL_OWNER_ADDRESS=0xdC7b28976d6eb13082a5Be7C66f9dCFE0115738f +UNI_OWNER_ADDRESS=0x46f34C24A7bA7a2Ac6DD76c3F09B32D41C144d08 +PRIME_OWNER_ADDRESS=0x1D0065D367DA1919cD597d25F91a97B6039428C5 diff --git a/.solhint.json b/.solhint.json index 48bce0d..79a2b84 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,9 +1,11 @@ { "extends": "solhint:recommended", "rules": { - "max-line-length": ["off", 120], + "max-line-length": ["warn", 120], "func-visibility": ["warn", {"ignoreConstructors": true}], "func-name-mixedcase": ["off"], - "one-contract-per-file": ["off"] + "one-contract-per-file": ["off"], + "no-inline-assembly": ["off"], + "avoid-low-level-calls": ["off"] } } diff --git a/contracts/Deps.sol b/contracts/Deps.sol index 439edc4..7eba42a 100644 --- a/contracts/Deps.sol +++ b/contracts/Deps.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.28; -import { TransparentUpgradeableProxy } from '@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol'; +/* solhint-disable no-unused-import */ +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; diff --git a/contracts/LiquidityHub.sol b/contracts/LiquidityHub.sol index 6ca99ea..c51ec22 100644 --- a/contracts/LiquidityHub.sol +++ b/contracts/LiquidityHub.sol @@ -10,11 +10,11 @@ import { Math } from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import {AccessControlUpgradeable} from '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; -import {ERC7201Helper} from './utils/ERC7201Helper.sol'; -import {IManagedToken} from './interfaces/IManagedToken.sol'; -import {ILiquidityPool} from './interfaces/ILiquidityPool.sol'; -import {ILiquidityHub} from './interfaces/ILiquidityHub.sol'; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ERC7201Helper} from "./utils/ERC7201Helper.sol"; +import {IManagedToken} from "./interfaces/IManagedToken.sol"; +import {ILiquidityPool} from "./interfaces/ILiquidityPool.sol"; +import {ILiquidityHub} from "./interfaces/ILiquidityHub.sol"; contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgradeable { using Math for uint256; @@ -37,12 +37,12 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade uint256 assetsLimit; } - bytes32 private constant StorageLocation = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700; + bytes32 private constant STORAGE_LOCATION = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700; constructor(address shares, address liquidityPool) { ERC7201Helper.validateStorageLocation( - StorageLocation, - 'sprinter.storage.LiquidityHub' + STORAGE_LOCATION, + "sprinter.storage.LiquidityHub" ); if (shares == address(0)) revert ZeroAddress(); if (liquidityPool == address(0)) revert ZeroAddress(); @@ -251,7 +251,7 @@ contract LiquidityHub is ILiquidityHub, ERC4626Upgradeable, AccessControlUpgrade function _getStorage() private pure returns (LiquidityHubStorage storage $) { assembly { - $.slot := StorageLocation + $.slot := STORAGE_LOCATION } } } diff --git a/contracts/LiquidityMining.sol b/contracts/LiquidityMining.sol index 648de20..d28f004 100644 --- a/contracts/LiquidityMining.sol +++ b/contracts/LiquidityMining.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.28; import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract LiquidityMining is ERC20, Ownable { diff --git a/contracts/LiquidityPool.sol b/contracts/LiquidityPool.sol new file mode 100644 index 0000000..2409b48 --- /dev/null +++ b/contracts/LiquidityPool.sol @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {ERC7201Helper} from "./utils/ERC7201Helper.sol"; +import {IAavePoolAddressesProvider} from "./interfaces/IAavePoolAddressesProvider.sol"; +import {IAavePool, AaveDataTypes, NO_REFERRAL, INTEREST_RATE_MODE_VARIABLE} from "./interfaces/IAavePool.sol"; +import {IAaveOracle} from "./interfaces/IAaveOracle.sol"; +import {ILiquidityPool} from "./interfaces/ILiquidityPool.sol"; +import {IAavePoolDataProvider} from "./interfaces/IAavePoolDataProvider.sol"; + +contract LiquidityPool is ILiquidityPool, AccessControlUpgradeable, EIP712Upgradeable { + using SafeERC20 for IERC20; + using ECDSA for bytes32; + using Math for uint256; + using BitMaps for BitMaps.BitMap; + + uint256 private constant MULTIPLIER = 1e18; + bytes32 private constant BORROW_TYPEHASH = keccak256( + "Borrow(" + "address borrowToken," + "uint256 amount," + "address target," + "bytes targetCallData," + "uint256 nonce," + "uint256 deadline" + ")" + ); + + IERC20 immutable public COLLATERAL; + IAavePoolAddressesProvider immutable public AAVE_POOL_PROVIDER; + + /// @custom:storage-location erc7201:sprinter.storage.LiquidityPool + struct LiquidityPoolStorage { + mapping(address token => uint256 ltv) borrowTokenLTV; + BitMaps.BitMap usedNonces; + address mpcAddress; + uint256 minHealthFactor; + uint256 defaultLTV; + } + + bytes32 private constant STORAGE_LOCATION = 0x457f6fd6dd83195f8bfff9ee98f2df1d90fadb996523baa2b453217997285e00; + + bytes32 public constant LIQUIDITY_ADMIN_ROLE = "LIQUIDITY_ADMIN_ROLE"; + bytes32 public constant WITHDRAW_PROFIT_ROLE = "WITHDRAW_PROFIT_ROLE"; + + error ZeroAddress(); + error InvalidSignature(); + error TokenLtvExceeded(); + error NoCollateral(); + error HealthFactorTooLow(); + error TargetCallFailed(); + error NothingToRepay(); + error CannotWithdrawProfitCollateral(); + error ExpiredSignature(); + error NonceAlreadyUsed(); + error NotEnoughBalance(); + error CollateralNotSupported(); + + event SuppliedToAave(uint256 amount); + event BorrowTokenLTVSet(address token, uint256 oldLTV, uint256 newLTV); + event HealthFactorSet(uint256 oldHealthFactor, uint256 newHealthFactor); + event DefaultLTVSet(uint256 oldDefaultLTV, uint256 newDefaultLTV); + event Borrowed(address borrowToken, uint256 amount, address caller, address target, bytes targetCallData); + event Repaid(address borrowToken, uint256 repaidAmount); + event WithdrawnFromAave(address to, uint256 amount); + event ProfitWithdrawn(address token, address to, uint256 amount); + + constructor(address liquidityToken, address aavePoolProvider) { + ERC7201Helper.validateStorageLocation( + STORAGE_LOCATION, + "sprinter.storage.LiquidityPool" + ); + require(liquidityToken != address(0), ZeroAddress()); + COLLATERAL = IERC20(liquidityToken); + require(aavePoolProvider != address(0), ZeroAddress()); + AAVE_POOL_PROVIDER = IAavePoolAddressesProvider(aavePoolProvider); + IAavePoolDataProvider poolDataProvider = IAavePoolDataProvider(AAVE_POOL_PROVIDER.getPoolDataProvider()); + (,,,,,bool usageAsCollateralEnabled,,,,) = poolDataProvider.getReserveConfigurationData(address(COLLATERAL)); + require(usageAsCollateralEnabled, CollateralNotSupported()); + _disableInitializers(); + } + + function initialize( + address admin, + uint256 minHealthFactor, + uint256 defaultLTV_, + address mpcAddress_ + ) external initializer() { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + __EIP712_init("LiquidityPool", "1.0.0"); + LiquidityPoolStorage storage $ = _getStorage(); + $.minHealthFactor = minHealthFactor; + $.defaultLTV = defaultLTV_; + $.mpcAddress = mpcAddress_; + } + + function deposit() external override { + // called after receiving deposit in USDC + // transfer all USDC balance to AAVE + uint256 amount = COLLATERAL.balanceOf(address(this)); + require(amount > 0, NoCollateral()); + IAavePool pool = IAavePool(AAVE_POOL_PROVIDER.getPool()); + (, uint256 repaidAmount) = _repay(address(COLLATERAL), pool, true); + amount -= repaidAmount; + if (amount == 0) return; + COLLATERAL.forceApprove(address(pool), amount); + pool.supply(address(COLLATERAL), amount, address(this), NO_REFERRAL); + emit SuppliedToAave(amount); + } + + function borrow( + address borrowToken, + uint256 amount, + address target, + bytes calldata targetCallData, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) external { + // - Validate MPC signature + _validateMPCSignature(borrowToken, amount, target, targetCallData, nonce, deadline, signature); + + // - Borrow the requested source token from the lending protocol against available USDC liquidity. + IAavePool pool = IAavePool(AAVE_POOL_PROVIDER.getPool()); + pool.borrow( + borrowToken, + amount, + INTEREST_RATE_MODE_VARIABLE, + NO_REFERRAL, + address(this) + ); + + // - Check health factor for user after borrow (can be read from aave, getUserAccountData) + (,,,,,uint256 currentHealthFactor) = pool.getUserAccountData(address(this)); + require(currentHealthFactor >= _getStorage().minHealthFactor, HealthFactorTooLow()); + + // check ltv for token + _checkTokenLTV(pool, borrowToken); + // - Approve the borrowed funds for transfer to the recipient specified in the MPC signature. + IERC20(borrowToken).forceApprove(target, amount); + // - Invoke the recipient's address with calldata provided in the MPC signature to complete + // the operation securely. + (bool success,) = target.call(targetCallData); + require(success, TargetCallFailed()); + emit Borrowed(borrowToken, amount, msg.sender, target, targetCallData); + } + + function repay(address[] calldata borrowTokens) external { + // Repay token to aave + bool success; + IAavePool pool = IAavePool(AAVE_POOL_PROVIDER.getPool()); + for (uint256 i = 0; i < borrowTokens.length; i++) { + (success,) = _repay(borrowTokens[i], pool, success); + } + require(success, NothingToRepay()); + } + + // Admin functions + + function withdraw(address to, uint256 amount) external override onlyRole(LIQUIDITY_ADMIN_ROLE) returns (uint256) { + // get USDC from AAVE + IAavePool pool = IAavePool(AAVE_POOL_PROVIDER.getPool()); + uint256 withdrawn = pool.withdraw(address(COLLATERAL), amount, to); + // health factor after withdraw + (,,,,,uint256 currentHealthFactor) = pool.getUserAccountData(address(this)); + require(currentHealthFactor >= _getStorage().minHealthFactor, HealthFactorTooLow()); + emit WithdrawnFromAave(to, withdrawn); + return withdrawn; + } + + function withdrawProfit( + address token, + address to, + uint256 amount + ) external onlyRole(WITHDRAW_PROFIT_ROLE) returns (uint256) { + // check that not collateral + require(token != address(COLLATERAL), CannotWithdrawProfitCollateral()); + IAavePool pool = IAavePool(AAVE_POOL_PROVIDER.getPool()); + _repay(token, pool, true); + uint256 available = IERC20(token).balanceOf(address(this)); + require(available > 0 && (amount <= available || amount == type(uint256).max), NotEnoughBalance()); + uint256 amountToWithdraw = amount; + if (amount == type(uint256).max) { + amountToWithdraw = available; + } + // withdraw from this contract + IERC20(token).safeTransfer(to, amountToWithdraw); + emit ProfitWithdrawn(token, to, amountToWithdraw); + return amountToWithdraw; + } + + function setBorrowTokenLTV(address token, uint256 ltv) external onlyRole(DEFAULT_ADMIN_ROLE) { + LiquidityPoolStorage storage $ = _getStorage(); + uint256 oldLTV = $.borrowTokenLTV[token]; + $.borrowTokenLTV[token] = ltv; + emit BorrowTokenLTVSet(token, oldLTV, ltv); + } + + function setDefaultLTV(uint256 defaultLTV_) external onlyRole(DEFAULT_ADMIN_ROLE) { + LiquidityPoolStorage storage $ = _getStorage(); + uint256 oldDefaultLTV = $.defaultLTV; + $.defaultLTV = defaultLTV_; + emit DefaultLTVSet(oldDefaultLTV, defaultLTV_); + } + + function setHealthFactor(uint256 minHealthFactor) external onlyRole(DEFAULT_ADMIN_ROLE) { + LiquidityPoolStorage storage $ = _getStorage(); + uint256 oldHealthFactor = $.minHealthFactor; + $.minHealthFactor = minHealthFactor; + emit HealthFactorSet(oldHealthFactor, minHealthFactor); + } + + // Internal functions + + function _getStorage() private pure returns (LiquidityPoolStorage storage $) { + assembly { + $.slot := STORAGE_LOCATION + } + } + + function _validateMPCSignature( + address borrowToken, + uint256 amount, + address target, + bytes calldata targetCallData, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) private { + bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( + BORROW_TYPEHASH, + borrowToken, + amount, + target, + keccak256(targetCallData), + nonce, + deadline + ))); + address signer = digest.recover(signature); + LiquidityPoolStorage storage $ = _getStorage(); + require(signer == $.mpcAddress, InvalidSignature()); + require($.usedNonces.get(nonce) == false, NonceAlreadyUsed()); + $.usedNonces.set(nonce); + require(notPassed(deadline), ExpiredSignature()); + } + + function _checkTokenLTV(IAavePool pool, address borrowToken) private view { + LiquidityPoolStorage storage $ = _getStorage(); + uint256 ltv = $.borrowTokenLTV[borrowToken]; + if (ltv == 0) ltv = $.defaultLTV; + + AaveDataTypes.ReserveData memory collateralData = pool.getReserveData(address(COLLATERAL)); + uint256 totalCollateral = IERC20(collateralData.aTokenAddress).balanceOf(address(this)); + require(totalCollateral > 0, NoCollateral()); + + AaveDataTypes.ReserveData memory borrowTokenData = pool.getReserveData(borrowToken); + uint256 totalBorrowed = IERC20(borrowTokenData.variableDebtTokenAddress).balanceOf(address(this)); + + IAaveOracle oracle = IAaveOracle(AAVE_POOL_PROVIDER.getPriceOracle()); + address[] memory assets = new address[](2); + assets[0] = borrowToken; + assets[1] = address(COLLATERAL); + + uint256[] memory prices = oracle.getAssetsPrices(assets); + + uint256 collateralDecimals = IERC20Metadata(address(COLLATERAL)).decimals(); + uint256 borrowDecimals = IERC20Metadata(borrowToken).decimals(); + + uint256 collateralUnit = 10 ** collateralDecimals; + uint256 borrowUnit = 10 ** borrowDecimals; + + uint256 totalBorrowPrice = totalBorrowed * prices[0]; + uint256 collateralPrice = totalCollateral * prices[1]; + + uint256 currentLtv = totalBorrowPrice * MULTIPLIER * collateralUnit / (collateralPrice * borrowUnit); + require(currentLtv <= ltv, TokenLtvExceeded()); + } + + function _repay(address borrowToken, IAavePool pool, bool successInput) + internal + returns(bool success, uint256 repaidAmount) + { + success = successInput; + AaveDataTypes.ReserveData memory borrowTokenData = pool.getReserveData(borrowToken); + if (borrowTokenData.variableDebtTokenAddress == address(0)) return (success, 0); + uint256 totalBorrowed = IERC20(borrowTokenData.variableDebtTokenAddress).balanceOf(address(this)); + if (totalBorrowed == 0) return (success, 0); + uint256 borrowTokenBalance = IERC20(borrowToken).balanceOf(address(this)); + if (borrowTokenBalance == 0) return (success, 0); + IERC20(borrowToken).forceApprove(address(pool), borrowTokenBalance); + repaidAmount = pool.repay( + borrowToken, + borrowTokenBalance, + 2, + address(this) + ); + emit Repaid(borrowToken, repaidAmount); + success = true; + } + + // View functions + + function defaultLTV() public view returns (uint256) { + return _getStorage().defaultLTV; + } + + function healthFactor() public view returns (uint256) { + return _getStorage().minHealthFactor; + } + + function mpcAddress() public view returns (address) { + return _getStorage().mpcAddress; + } + + function borrowTokenLTV(address token) public view returns (uint256) { + return _getStorage().borrowTokenLTV[token]; + } + + function timeNow() internal view returns (uint32) { + return uint32(block.timestamp); + } + + function passed(uint256 timestamp) internal view returns (bool) { + return timeNow() > timestamp; + } + + function notPassed(uint256 timestamp) internal view returns (bool) { + return !passed(timestamp); + } +} diff --git a/contracts/ManagedToken.sol b/contracts/ManagedToken.sol index 18367da..5457055 100644 --- a/contracts/ManagedToken.sol +++ b/contracts/ManagedToken.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.28; import {ERC20, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import {IManagedToken} from './interfaces/IManagedToken.sol'; +import {IManagedToken} from "./interfaces/IManagedToken.sol"; contract ManagedToken is IManagedToken, ERC20Permit { address immutable public MANAGER; diff --git a/contracts/Rebalancer.sol b/contracts/Rebalancer.sol index 8b38e64..fb6d7b4 100644 --- a/contracts/Rebalancer.sol +++ b/contracts/Rebalancer.sol @@ -3,11 +3,11 @@ pragma solidity 0.8.28; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; -import {AccessControlUpgradeable} from '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; -import {ERC7201Helper} from './utils/ERC7201Helper.sol'; -import {ILiquidityPool} from './interfaces/ILiquidityPool.sol'; -import {IRebalancer} from './interfaces/IRebalancer.sol'; -import {ICCTPTokenMessenger, ICCTPMessageTransmitter} from './interfaces/ICCTP.sol'; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ERC7201Helper} from "./utils/ERC7201Helper.sol"; +import {ILiquidityPool} from "./interfaces/ILiquidityPool.sol"; +import {IRebalancer} from "./interfaces/IRebalancer.sol"; +import {ICCTPTokenMessenger, ICCTPMessageTransmitter} from "./interfaces/ICCTP.sol"; contract Rebalancer is IRebalancer, AccessControlUpgradeable { using BitMaps for BitMaps.BitMap; @@ -35,7 +35,7 @@ contract Rebalancer is IRebalancer, AccessControlUpgradeable { BitMaps.BitMap allowedRoutes; } - bytes32 private constant StorageLocation = 0x81fbb040176d3bdbf3707b380997ee0038798f9e3ad0bae77fff3621ef225c00; + bytes32 private constant STORAGE_LOCATION = 0x81fbb040176d3bdbf3707b380997ee0038798f9e3ad0bae77fff3621ef225c00; constructor( address liquidityPool, @@ -43,8 +43,8 @@ contract Rebalancer is IRebalancer, AccessControlUpgradeable { address cctpMessageTransmitter ) { ERC7201Helper.validateStorageLocation( - StorageLocation, - 'sprinter.storage.Rebalancer' + STORAGE_LOCATION, + "sprinter.storage.Rebalancer" ); if (liquidityPool == address(0)) revert ZeroAddress(); if (cctpTokenMessenger == address(0)) revert ZeroAddress(); @@ -179,7 +179,7 @@ contract Rebalancer is IRebalancer, AccessControlUpgradeable { function _getStorage() private pure returns (RebalancerStorage storage $) { assembly { - $.slot := StorageLocation + $.slot := STORAGE_LOCATION } } } diff --git a/contracts/interfaces/IAaveOracle.sol b/contracts/interfaces/IAaveOracle.sol new file mode 100644 index 0000000..675770f --- /dev/null +++ b/contracts/interfaces/IAaveOracle.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title IAaveOracle + * @author Aave + * @notice Defines the basic interface for the Aave Oracle + */ +interface IAaveOracle { + /** + * @notice Returns a list of prices from a list of assets addresses + * @param assets The list of assets addresses + * @return The prices of the given assets + */ + function getAssetsPrices(address[] calldata assets) external view returns (uint256[] memory); +} \ No newline at end of file diff --git a/contracts/interfaces/IAavePool.sol b/contracts/interfaces/IAavePool.sol new file mode 100644 index 0000000..6fe08bf --- /dev/null +++ b/contracts/interfaces/IAavePool.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +library AaveDataTypes { + struct ReserveConfigurationMap { + //bit 0-15: LTV + //bit 16-31: Liq. threshold + //bit 32-47: Liq. bonus + //bit 48-55: Decimals + //bit 56: reserve is active + //bit 57: reserve is frozen + //bit 58: borrowing is enabled + //bit 59: stable rate borrowing enabled + //bit 60: asset is paused + //bit 61: borrowing in isolation mode is enabled + //bit 62: siloed borrowing enabled + //bit 63: flashloaning enabled + //bit 64-79: reserve factor + //bit 80-115 borrow cap in whole tokens, borrowCap == 0 => no cap + //bit 116-151 supply cap in whole tokens, supplyCap == 0 => no cap + //bit 152-167 liquidation protocol fee + //bit 168-175 eMode category + //bit 176-211 unbacked mint cap in whole tokens, unbackedMintCap == 0 => minting disabled + //bit 212-251 debt ceiling for isolation mode with (ReserveConfiguration::DEBT_CEILING_DECIMALS) decimals + //bit 252-255 unused + + uint256 data; + } + + struct ReserveData { + //stores the reserve configuration + ReserveConfigurationMap configuration; + //the liquidity index. Expressed in ray + uint128 liquidityIndex; + //the current supply rate. Expressed in ray + uint128 currentLiquidityRate; + //variable borrow index. Expressed in ray + uint128 variableBorrowIndex; + //the current variable borrow rate. Expressed in ray + uint128 currentVariableBorrowRate; + //the current stable borrow rate. Expressed in ray + uint128 currentStableBorrowRate; + //timestamp of last update + uint40 lastUpdateTimestamp; + //the id of the reserve. Represents the position in the list of the active reserves + uint16 id; + //aToken address + address aTokenAddress; + //stableDebtToken address + address stableDebtTokenAddress; + //variableDebtToken address + address variableDebtTokenAddress; + //address of the interest rate strategy + address interestRateStrategyAddress; + //the current treasury balance, scaled + uint128 accruedToTreasury; + //the outstanding unbacked aTokens minted through the bridging feature + uint128 unbacked; + //the outstanding debt borrowed against this asset in isolation mode + uint128 isolationModeTotalDebt; + } +} + +uint16 constant NO_REFERRAL = 0; +uint256 constant INTEREST_RATE_MODE_VARIABLE = 2; + +/** + * @title IPool + * @author Aave + * @notice Defines the basic interface for an Aave Pool. + */ +interface IAavePool { + + /** + * @notice Supplies an `amount` of underlying asset into the reserve, receiving in return overlying aTokens. + * - E.g. User supplies 100 USDC and gets in return 100 aUSDC + * @param asset The address of the underlying asset to supply + * @param amount The amount to be supplied + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + */ + function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external; + + /** + * @notice Supply with transfer approval of asset to be supplied done via permit function + * see: https://eips.ethereum.org/EIPS/eip-2612 and https://eips.ethereum.org/EIPS/eip-713 + * @param asset The address of the underlying asset to supply + * @param amount The amount to be supplied + * @param onBehalfOf The address that will receive the aTokens, same as msg.sender if the user + * wants to receive them on his own wallet, or a different address if the beneficiary of aTokens + * is a different wallet + * @param deadline The deadline timestamp that the permit is valid + * @param referralCode Code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param permitV The V parameter of ERC712 permit sig + * @param permitR The R parameter of ERC712 permit sig + * @param permitS The S parameter of ERC712 permit sig + */ + function supplyWithPermit( + address asset, + uint256 amount, + address onBehalfOf, + uint16 referralCode, + uint256 deadline, + uint8 permitV, + bytes32 permitR, + bytes32 permitS + ) external; + + /** + * @notice Withdraws an `amount` of underlying asset from the reserve, burning the equivalent aTokens owned + * E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC + * @param asset The address of the underlying asset to withdraw + * @param amount The underlying amount to be withdrawn + * - Send the value type(uint256).max in order to withdraw the whole aToken balance + * @param to The address that will receive the underlying, same as msg.sender if the user + * wants to receive it on his own wallet, or a different address if the beneficiary is a + * different wallet + * @return The final amount withdrawn + */ + function withdraw(address asset, uint256 amount, address to) external returns (uint256); + + /** + * @notice Allows users to borrow a specific `amount` of the reserve underlying asset, provided that the borrower + * already supplied enough collateral, or he was given enough allowance by a credit delegator on the + * corresponding debt token (StableDebtToken or VariableDebtToken) + * - E.g. User borrows 100 USDC passing as `onBehalfOf` his own address, receiving the 100 USDC in his wallet + * and 100 stable/variable debt tokens, depending on the `interestRateMode` + * @param asset The address of the underlying asset to borrow + * @param amount The amount to be borrowed + * @param interestRateMode The interest rate mode at which the user wants to borrow: 1 for Stable, 2 for Variable + * @param referralCode The code used to register the integrator originating the operation, for potential rewards. + * 0 if the action is executed directly by the user, without any middle-man + * @param onBehalfOf The address of the user who will receive the debt. Should be the address of the borrower itself + * calling the function if he wants to borrow against his own collateral, or the address of the credit delegator + * if he has been given credit delegation allowance + */ + function borrow( + address asset, + uint256 amount, + uint256 interestRateMode, + uint16 referralCode, + address onBehalfOf + ) external; + + /** + * @notice Repays a borrowed `amount` on a specific reserve, burning the equivalent debt tokens owned + * - E.g. User repays 100 USDC, burning 100 variable/stable debt tokens of the `onBehalfOf` address + * @param asset The address of the borrowed underlying asset previously borrowed + * @param amount The amount to repay + * - Send the value type(uint256).max in order to repay the whole debt for `asset` on the specific `debtMode` + * @param interestRateMode The interest rate mode at of the debt the user wants to repay: 1 for Stable, 2 for Variable + * @param onBehalfOf The address of the user who will get his debt reduced/removed. Should be the address of the + * user calling the function if he wants to reduce/remove his own debt, or the address of any other + * other borrower whose debt should be removed + * @return The final amount repaid + */ + function repay( + address asset, + uint256 amount, + uint256 interestRateMode, + address onBehalfOf + ) external returns (uint256); + + /** + * @notice Returns the user account data across all the reserves + * @param user The address of the user + * @return totalCollateralBase The total collateral of the user in the base currency used by the price feed + * @return totalDebtBase The total debt of the user in the base currency used by the price feed + * @return availableBorrowsBase The borrowing power left of the user in the base currency used by the price feed + * @return currentLiquidationThreshold The liquidation threshold of the user + * @return ltv The loan to value of The user + * @return healthFactor The current health factor of the user + */ + function getUserAccountData( + address user + ) + external + view + returns ( + uint256 totalCollateralBase, + uint256 totalDebtBase, + uint256 availableBorrowsBase, + uint256 currentLiquidationThreshold, + uint256 ltv, + uint256 healthFactor + ); + + /** + * @notice Returns the state and configuration of the reserve + * @param asset The address of the underlying asset of the reserve + * @return The state and configuration data of the reserve + */ + function getReserveData(address asset) external view returns (AaveDataTypes.ReserveData memory); +} diff --git a/contracts/interfaces/IAavePoolAddressesProvider.sol b/contracts/interfaces/IAavePoolAddressesProvider.sol new file mode 100644 index 0000000..e37b603 --- /dev/null +++ b/contracts/interfaces/IAavePoolAddressesProvider.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.10; + +/** + * @title PoolAddressesProvider + * @author Aave + * @notice Main registry of addresses part of or connected to the protocol, including permissioned roles + * @dev Acts as factory of proxies and admin of those, so with right to change its implementations + * @dev Owned by the Aave Governance + */ +interface IAavePoolAddressesProvider { + function getPool() external view returns (address); + function getPriceOracle() external view returns (address); + function getPoolDataProvider() external view returns (address); +} diff --git a/contracts/interfaces/IAavePoolDataProvider.sol b/contracts/interfaces/IAavePoolDataProvider.sol new file mode 100644 index 0000000..e765f06 --- /dev/null +++ b/contracts/interfaces/IAavePoolDataProvider.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +/** + * @title IPoolDataProvider + * @author Aave + * @notice Defines the basic interface of a PoolDataProvider + */ +interface IAavePoolDataProvider { + /** + * @notice Returns the configuration data of the reserve + * @dev Not returning borrow and supply caps for compatibility, nor pause flag + * @param asset The address of the underlying asset of the reserve + * @return decimals The number of decimals of the reserve + * @return ltv The ltv of the reserve + * @return liquidationThreshold The liquidationThreshold of the reserve + * @return liquidationBonus The liquidationBonus of the reserve + * @return reserveFactor The reserveFactor of the reserve + * @return usageAsCollateralEnabled True if the usage as collateral is enabled, false otherwise + * @return borrowingEnabled True if borrowing is enabled, false otherwise + * @return stableBorrowRateEnabled True if stable rate borrowing is enabled, false otherwise + * @return isActive True if it is active, false otherwise + * @return isFrozen True if it is frozen, false otherwise + */ + function getReserveConfigurationData( + address asset + ) + external + view + returns ( + uint256 decimals, + uint256 ltv, + uint256 liquidationThreshold, + uint256 liquidationBonus, + uint256 reserveFactor, + bool usageAsCollateralEnabled, + bool borrowingEnabled, + bool stableBorrowRateEnabled, + bool isActive, + bool isFrozen + ); +} diff --git a/contracts/interfaces/ILiquidityPool.sol b/contracts/interfaces/ILiquidityPool.sol index 7301d8c..b87b163 100644 --- a/contracts/interfaces/ILiquidityPool.sol +++ b/contracts/interfaces/ILiquidityPool.sol @@ -6,7 +6,7 @@ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; interface ILiquidityPool { function deposit() external; - function withdraw(address to, uint256 amount) external; + function withdraw(address to, uint256 amount) external returns (uint256 withdrawn); function COLLATERAL() external returns (IERC20); } diff --git a/contracts/testing/MockTarget.sol b/contracts/testing/MockTarget.sol new file mode 100644 index 0000000..5c4458c --- /dev/null +++ b/contracts/testing/MockTarget.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract MockTarget { + using SafeERC20 for IERC20; + + event DataReceived(bytes data); + + function fulfill(IERC20 token, uint256 amount, bytes calldata data) external { + token.safeTransferFrom(msg.sender, address(this), amount); + emit DataReceived(data); + } +} diff --git a/contracts/testing/TestLiquidityPool.sol b/contracts/testing/TestLiquidityPool.sol index 4885539..4f4c090 100644 --- a/contracts/testing/TestLiquidityPool.sol +++ b/contracts/testing/TestLiquidityPool.sol @@ -7,19 +7,22 @@ import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; contract TestLiquidityPool is ILiquidityPool, AccessControl { IERC20 public immutable COLLATERAL; + bytes32 public constant LIQUIDITY_ADMIN_ROLE = "LIQUIDITY_ADMIN_ROLE"; event Deposit(); constructor(IERC20 collateral) { COLLATERAL = collateral; _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); + _grantRole(LIQUIDITY_ADMIN_ROLE, _msgSender()); } function deposit() external override { emit Deposit(); } - function withdraw(address to, uint256 amount) external override onlyRole(DEFAULT_ADMIN_ROLE) { + function withdraw(address to, uint256 amount) external override onlyRole(LIQUIDITY_ADMIN_ROLE) returns (uint256) { SafeERC20.safeTransfer(COLLATERAL, to, amount); + return amount; } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 2087b9d..c1027de 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,10 +1,175 @@ -import {HardhatUserConfig} from "hardhat/config"; +import {HardhatUserConfig, task} from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; -import {networkConfig, Network} from "./network.config"; +import {networkConfig, Network, Provider} from "./network.config"; +import {TypedDataDomain, resolveAddress} from "ethers"; +import { + LiquidityPool, Rebalancer, +} from "./typechain-types"; +import { + assert, isSet, ProviderSolidity, DomainSolidity, +} from "./scripts/common"; -function isSet(param?: string) { - return param && param.length > 0; -} +import dotenv from "dotenv"; + +dotenv.config(); + +task("grant-role", "Grant some role on some AccessControl") +.addParam("contract", "AccessControl-like contract address") +.addParam("role", "Human readable role to be converted to bytes32") +.addParam("actor", "Wallet address that should get the role") +.setAction(async ({contract, role, actor}: {contract: string, role: string, actor: string}, hre) => { + const [admin] = await hre.ethers.getSigners(); + + const target = await hre.ethers.getContractAt("AccessControl", contract, admin); + + await target.grantRole(hre.ethers.encodeBytes32String(role), actor); + console.log(`Role ${role} granted to ${actor} on ${contract}.`); +}); + +task("set-default-ltv", "Update Liquidity Pool config") +.addOptionalParam("pool", "Liquidity Pool address") +.addOptionalParam("ltv", "New default LTV value") +.setAction(async ({pool, ltv}: {pool?: string, ltv?: string}, hre) => { + const [admin] = await hre.ethers.getSigners(); + + const targetAddress = await resolveAddress(pool || process.env.LIQUIDITY_POOL || ""); + const target = (await hre.ethers.getContractAt("LiquidityPool", targetAddress, admin)) as LiquidityPool; + + const newLtv = ltv || "200000000000000000"; + await target.setDefaultLTV(newLtv); + console.log(`Default LTV set to ${newLtv} on ${targetAddress}.`); +}); + +task("set-token-ltv", "Update Liquidity Pool config") +.addParam("token", "Token to update LTV for") +.addOptionalParam("pool", "Liquidity Pool address") +.addOptionalParam("ltv", "New LTV value") +.setAction(async ({token, pool, ltv}: {token: string, pool?: string, ltv?: string}, hre) => { + const [admin] = await hre.ethers.getSigners(); + + const targetAddress = await resolveAddress(pool || process.env.LIQUIDITY_POOL || ""); + const target = (await hre.ethers.getContractAt("LiquidityPool", targetAddress, admin)) as LiquidityPool; + + const newLtv = ltv || "200000000000000000"; + await target.setBorrowTokenLTV(token, newLtv); + console.log(`Token ${token} LTV set to ${newLtv} on ${targetAddress}.`); +}); + +task("set-min-health-factor", "Update Liquidity Pool config") +.addOptionalParam("pool", "Liquidity Pool address") +.addOptionalParam("healthfactor", "New min health factor value") +.setAction(async ({pool, healthfactor}: {pool?: string, healthfactor?: string}, hre) => { + const [admin] = await hre.ethers.getSigners(); + + const targetAddress = await resolveAddress(pool || process.env.LIQUIDITY_POOL || ""); + const target = (await hre.ethers.getContractAt("LiquidityPool", targetAddress, admin)) as LiquidityPool; + + const newHealthFactor = healthfactor || "5000000000000000000"; + await target.setHealthFactor(newHealthFactor); + console.log(`Min health factor set to ${newHealthFactor} on ${targetAddress}.`); +}); + +task("set-routes", "Update Rebalancer config") +.addOptionalParam("rebalancer", "Rebalancer address") +.addOptionalParam("domains", "Comma separated list of domain names") +.addOptionalParam("providers", "Comma separated list of provider names") +.addOptionalParam("allowed", "Allowed or denied") +.setAction(async (args: {rebalancer?: string, providers?: string, domains?: string, allowed?: string}, hre) => { + const config = networkConfig[hre.network.name as Network]; + + const [admin] = await hre.ethers.getSigners(); + + const targetAddress = await resolveAddress(args.rebalancer || process.env.REBALANCER || ""); + const target = (await hre.ethers.getContractAt("Rebalancer", targetAddress, admin)) as Rebalancer; + + const domains = args.domains && args.domains.split(",") || config.Routes?.Domains || []; + const domainsSolidity = domains.map(el => { + assert(Object.values(Network).includes(el as Network), `Invalid domain ${el}`); + return DomainSolidity[el as Network]; + }); + const providers = args.providers && args.providers.split(",") || config.Routes?.Providers || []; + const providersSolidity = providers.map(el => { + assert(Object.values(Provider).includes(el as Provider), `Invalid provider ${el}`); + return ProviderSolidity[el as Provider]; + }); + assert(!args.allowed || args.allowed === "false" || args.allowed === "true", "Unexpected allowed value"); + const allowed = args.allowed ? args.allowed === "true" : true; + await target.setRoute(domainsSolidity, providersSolidity, allowed); + console.log(`Following routes are ${allowed ? "" : "dis"}allowed on ${targetAddress}.`); + console.table({domains, providers}); +}); + +task("sign-borrow", "Sign a Liquidity Pool borrow request for testing purposes") +.addOptionalParam("token", "Token to borrow") +.addOptionalParam("amount", "Amount to borrow in base units") +.addOptionalParam("target", "Target address to approve and call") +.addOptionalParam("data", "Data to call target with") +.addOptionalParam("nonce", "Reuse protection nonce") +.addOptionalParam("deadline", "Expiry protection timestamp") +.addOptionalParam("pool", "Liquidity Pool address") +.setAction(async (args: { + token?: string, + amount?: string, + target?: string, + data?: string, + nonce?: string, + deadline?: string, + pool?: string, +}, hre) => { + const config = networkConfig[hre.network.name as Network]; + + const [signer] = await hre.ethers.getSigners(); + + const name = "LiquidityPool"; + const version = "1.0.0"; + + const pool = args.pool || process.env.LIQUIDITY_POOL; + const domain: TypedDataDomain = { + name, + version, + chainId: hre.network.config.chainId, + verifyingContract: pool, + }; + + const types = { + Borrow: [ + {name: "borrowToken", type: "address"}, + {name: "amount", type: "uint256"}, + {name: "target", type: "address"}, + {name: "targetCallData", type: "bytes"}, + {name: "nonce", type: "uint256"}, + {name: "deadline", type: "uint256"}, + ], + }; + + const token = await hre.ethers.getContractAt("IERC20", hre.ethers.ZeroAddress, signer); + const borrowToken = args.token || config.USDC; + const amount = args.amount || "1000000"; + const target = args.target || borrowToken; + const data = args.data || (await token.transfer.populateTransaction(signer.address, amount)).data; + const nonce = args.nonce || `${Date.now()}`; + const deadline = args.deadline || "2000000000"; + const value = { + borrowToken, + amount, + target, + targetCallData: data, + nonce, + deadline, + }; + + const sig = await signer.signTypedData(domain, types, value); + + console.log(`borrowToken: ${borrowToken}`); + console.log(`amount: ${amount}`); + console.log(`target: ${target}`); + console.log(`targetCallData: ${data}`); + console.log(`nonce: ${nonce}`); + console.log(`deadline: ${deadline}`); + console.log(`signature: ${sig}`); +}); + +const accounts: string[] = isSet(process.env.DEPLOYER_PRIVATE_KEY) ? [process.env.DEPLOYER_PRIVATE_KEY || ""] : []; const config: HardhatUserConfig = { solidity: { @@ -22,13 +187,34 @@ const config: HardhatUserConfig = { }, [Network.BASE_SEPOLIA]: { chainId: networkConfig.BASE_SEPOLIA.chainId, - url: "https://sepolia.base.org", - accounts: - isSet(process.env.BASE_SEPOLIA_PRIVATE_KEY) ? [process.env.BASE_SEPOLIA_PRIVATE_KEY || ""] : [], + url: process.env.BASE_SEPOLIA_RPC || "https://sepolia.base.org", + accounts, + }, + [Network.ETHEREUM_SEPOLIA]: { + chainId: networkConfig.ETHEREUM_SEPOLIA.chainId, + url: process.env.ETHEREUM_SEPOLIA_RPC || "", + accounts, + }, + [Network.ARBITRUM_SEPOLIA]: { + chainId: networkConfig.ARBITRUM_SEPOLIA.chainId, + url: process.env.ARBITRUM_SEPOLIA_RPC || "https://sepolia-rollup.arbitrum.io/rpc", + accounts, + }, + hardhat: { + forking: { + url: process.env.FORK_PROVIDER || "https://eth-mainnet.public.blastapi.io", + }, }, }, sourcify: { - enabled: true, + enabled: false, + }, + etherscan: { + apiKey: { + baseSepolia: process.env.ETHERSCAN_BASE_SEPOLIA || "", + sepolia: process.env.ETHERSCAN_ETHEREUM_SEPOLIA || "", + arbitrumSepolia: process.env.ETHERSCAN_ARBITRUM_SEPOLIA || "", + }, }, }; diff --git a/network.config.ts b/network.config.ts index 86ec23d..22fc1c7 100644 --- a/network.config.ts +++ b/network.config.ts @@ -1,3 +1,5 @@ +import * as AAVEPools from "@bgd-labs/aave-address-book"; + export enum Network { ETHEREUM = "ETHEREUM", AVALANCHE = "AVALANCHE", @@ -34,6 +36,7 @@ export interface NetworkConfig { Routes?: RoutesConfig; IsTest: boolean; IsHub: boolean; + Aave?: string; }; type NetworksConfig = { @@ -54,6 +57,7 @@ export const networkConfig: NetworksConfig = { Domains: [Network.BASE], Providers: [Provider.CCTP], }, + Aave: AAVEPools.AaveV3Ethereum.POOL_ADDRESSES_PROVIDER, }, AVALANCHE: { chainId: 43114, @@ -64,6 +68,7 @@ export const networkConfig: NetworksConfig = { USDC: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", IsTest: false, IsHub: false, + Aave: AAVEPools.AaveV3Avalanche.POOL_ADDRESSES_PROVIDER, }, OP_MAINNET: { chainId: 10, @@ -74,6 +79,7 @@ export const networkConfig: NetworksConfig = { USDC: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", IsTest: false, IsHub: false, + Aave: AAVEPools.AaveV3Optimism.POOL_ADDRESSES_PROVIDER, }, ARBITRUM_ONE: { chainId: 42161, @@ -84,6 +90,7 @@ export const networkConfig: NetworksConfig = { USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", IsTest: false, IsHub: false, + Aave: AAVEPools.AaveV3Arbitrum.POOL_ADDRESSES_PROVIDER, }, BASE: { chainId: 8453, @@ -98,6 +105,7 @@ export const networkConfig: NetworksConfig = { Domains: [Network.ETHEREUM], Providers: [Provider.CCTP], }, + Aave: AAVEPools.AaveV3Base.POOL_ADDRESSES_PROVIDER, }, POLYGON_MAINNET: { chainId: 137, @@ -108,6 +116,7 @@ export const networkConfig: NetworksConfig = { USDC: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", IsTest: false, IsHub: false, + Aave: AAVEPools.AaveV3Polygon.POOL_ADDRESSES_PROVIDER, }, ETHEREUM_SEPOLIA: { chainId: 11155111, @@ -119,9 +128,10 @@ export const networkConfig: NetworksConfig = { IsTest: true, IsHub: false, Routes: { - Domains: [Network.BASE_SEPOLIA], - Providers: [Provider.CCTP], + Domains: [Network.BASE_SEPOLIA, Network.ARBITRUM_SEPOLIA], + Providers: [Provider.CCTP, Provider.CCTP], }, + // Aave: AAVEPools.AaveV3Sepolia.POOL_ADDRESSES_PROVIDER, // Uses not official USDC. }, AVALANCHE_FUJI: { chainId: 43113, @@ -132,6 +142,7 @@ export const networkConfig: NetworksConfig = { USDC: "0x5425890298aed601595a70ab815c96711a31bc65", IsTest: true, IsHub: false, + Aave: AAVEPools.AaveV3Fuji.POOL_ADDRESSES_PROVIDER, }, OP_SEPOLIA: { chainId: 11155420, @@ -142,6 +153,7 @@ export const networkConfig: NetworksConfig = { USDC: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", IsTest: true, IsHub: false, + Aave: AAVEPools.AaveV3OptimismSepolia.POOL_ADDRESSES_PROVIDER, }, ARBITRUM_SEPOLIA: { chainId: 421614, @@ -152,6 +164,11 @@ export const networkConfig: NetworksConfig = { USDC: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", IsTest: true, IsHub: false, + Routes: { + Domains: [Network.BASE_SEPOLIA, Network.ETHEREUM_SEPOLIA], + Providers: [Provider.CCTP, Provider.CCTP], + }, + Aave: AAVEPools.AaveV3ArbitrumSepolia.POOL_ADDRESSES_PROVIDER, }, BASE_SEPOLIA: { chainId: 84532, @@ -163,9 +180,10 @@ export const networkConfig: NetworksConfig = { IsTest: true, IsHub: true, Routes: { - Domains: [Network.ETHEREUM_SEPOLIA], - Providers: [Provider.CCTP], + Domains: [Network.ETHEREUM_SEPOLIA, Network.ARBITRUM_SEPOLIA], + Providers: [Provider.CCTP, Provider.CCTP], }, + Aave: AAVEPools.AaveV3BaseSepolia.POOL_ADDRESSES_PROVIDER, }, POLYGON_AMOY: { chainId: 80002, diff --git a/package-lock.json b/package-lock.json index 910bbb5..dd6146a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "LGPL-3.0", "dependencies": { + "@bgd-labs/aave-address-book": "^4.10.0", "@openzeppelin/contracts": "^5.1.0", "@openzeppelin/contracts-upgradeable": "^5.1.0" }, @@ -56,6 +57,15 @@ "node": ">=6.9.0" } }, + "node_modules/@bgd-labs/aave-address-book": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@bgd-labs/aave-address-book/-/aave-address-book-4.10.0.tgz", + "integrity": "sha512-+WtMRtXLFpic/PLhNn32FIFzNFIgqlgC5Z9RLOsPgbdkvDDAW83w1emoxqxbW0jYGGNcYJUTTrtXqlhDeQGwkg==", + "license": "MIT", + "workspaces": [ + "ui" + ] + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", diff --git a/package.json b/package.json index 40485cd..f283032 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,15 @@ "deploy": "hardhat run ./scripts/deploy.ts", "deploy-local": "hardhat run ./scripts/deploy.ts --network localhost", "deploy-basesepolia": "hardhat run ./scripts/deploy.ts --network BASE_SEPOLIA", + "deploy-ethereumsepolia": "hardhat run ./scripts/deploy.ts --network ETHEREUM_SEPOLIA", + "deploy-arbitrumsepolia": "hardhat run ./scripts/deploy.ts --network ARBITRUM_SEPOLIA", + "upgrade-liquiditypool": "hardhat run ./scripts/upgradeLiquidityPool.ts", + "upgrade-liquiditypool-basesepolia": "hardhat run ./scripts/upgradeLiquidityPool.ts --network BASE_SEPOLIA", + "upgrade-liquiditypool-ethereumsepolia": "hardhat run ./scripts/upgradeLiquidityPool.ts --network ETHEREUM_SEPOLIA", "node": "hardhat node", "hardhat": "hardhat", "lint": "npm run lint:solidity && npm run lint:ts", - "lint:solidity": "solhint contracts/**/*.sol", + "lint:solidity": "solhint 'contracts/**/*.sol'", "lint:ts": "eslint", "test": "hardhat test --typecheck", "test:deploy": "ts-node --files ./scripts/deploy.ts" @@ -42,6 +47,7 @@ "typescript-eslint": "^8.19.1" }, "dependencies": { + "@bgd-labs/aave-address-book": "^4.10.0", "@openzeppelin/contracts": "^5.1.0", "@openzeppelin/contracts-upgradeable": "^5.1.0" } diff --git a/scripts/common.ts b/scripts/common.ts new file mode 100644 index 0000000..28a83ab --- /dev/null +++ b/scripts/common.ts @@ -0,0 +1,36 @@ +export function assert(condition: boolean, message: string): void { + if (condition) return; + throw new Error(message); +} + +export function sleep(msec: number): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve(true), msec); + }); +} + +export function isSet(input?: string): boolean { + if (input) { + return input.length > 0; + } + return false; +} + +export const ProviderSolidity = { + CCTP: 0n, +}; + +export const DomainSolidity = { + ETHEREUM: 0n, + AVALANCHE: 1n, + OP_MAINNET: 2n, + ARBITRUM_ONE: 3n, + BASE: 4n, + POLYGON_MAINNET: 5n, + ETHEREUM_SEPOLIA: 6n, + AVALANCHE_FUJI: 7n, + OP_SEPOLIA: 8n, + ARBITRUM_SEPOLIA: 9n, + BASE_SEPOLIA: 10n, + POLYGON_AMOY: 11n, +}; diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 0c47a31..069ce96 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,36 +1,53 @@ import dotenv from "dotenv"; dotenv.config(); - import hre from "hardhat"; import {isAddress, MaxUint256, getBigInt} from "ethers"; -import {getContractAt, getCreateAddress, deploy, ZERO_BYTES32} from "../test/helpers"; -import {assert, getVerifier, isSet, ProviderSolidity, DomainSolidity} from "./helpers"; +import {toBytes32} from "../test/helpers"; +import { + getVerifier, deployProxy, getProxyCreateAddress, +} from "./helpers"; +import { + assert, isSet, ProviderSolidity, DomainSolidity, +} from "./common"; import { - TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin, - TestLiquidityPool, SprinterLiquidityMining, TestCCTPTokenMessenger, TestCCTPMessageTransmitter, - Rebalancer, + TestUSDC, SprinterUSDCLPShare, LiquidityHub, + SprinterLiquidityMining, TestCCTPTokenMessenger, TestCCTPMessageTransmitter, + Rebalancer, LiquidityPool, } from "../typechain-types"; import {networkConfig, Network, Provider, NetworkConfig} from "../network.config"; -const DAY = 60n * 60n * 24n; - async function main() { + // Rework granting admin roles on deployments so that deployer does not have to be admin. const [deployer] = await hre.ethers.getSigners(); const admin: string = isAddress(process.env.ADMIN) ? process.env.ADMIN : deployer.address; - const rebalanceCaller: string = isAddress(process.env.REBALANCE_CALLER) ? - process.env.REBALANCE_CALLER : deployer.address; const adjuster: string = isAddress(process.env.ADJUSTER) ? process.env.ADJUSTER : deployer.address; const maxLimit: bigint = MaxUint256 / 10n ** 12n; const assetsLimit: bigint = getBigInt(process.env.ASSETS_LIMIT || maxLimit); + const rebalanceCaller: string = isAddress(process.env.REBALANCE_CALLER) ? + process.env.REBALANCE_CALLER : deployer.address; + + const mpcAddress: string = isAddress(process.env.MPC_ADDRESS) ? + process.env.MPC_ADDRESS : deployer.address; + const withdrawProfit: string = isAddress(process.env.WITHDRAW_PROFIT) ? + process.env.WITHDRAW_PROFIT : deployer.address; + const minHealthFactor: bigint = getBigInt(process.env.MIN_HEALTH_FACTOR || 500n) * 10n ** 18n / 100n; + const defaultLTV: bigint = getBigInt(process.env.DEFAULT_LTV || 20n) * 10n ** 18n / 100n; + + const LIQUIDITY_ADMIN_ROLE = toBytes32("LIQUIDITY_ADMIN_ROLE"); + const WITHDRAW_PROFIT_ROLE = toBytes32("WITHDRAW_PROFIT_ROLE"); + + const verifier = getVerifier(); + let config: NetworkConfig; if (Object.values(Network).includes(hre.network.name as Network)) { config = networkConfig[hre.network.name as Network]; } else { - const testUSDC = (await deploy("TestUSDC", deployer, {})) as TestUSDC; - const cctpTokenMessenger = (await deploy("TestCCTPTokenMessenger", deployer, {})) as TestCCTPTokenMessenger; + console.log("TEST: Using TEST USDC and CCTP"); + const testUSDC = (await verifier.deploy("TestUSDC", deployer)) as TestUSDC; + const cctpTokenMessenger = (await verifier.deploy("TestCCTPTokenMessenger", deployer)) as TestCCTPTokenMessenger; const cctpMessageTransmitter = ( - await deploy("TestCCTPMessageTransmitter", deployer, {}) + await verifier.deploy("TestCCTPMessageTransmitter", deployer) ) as TestCCTPMessageTransmitter; config = { @@ -48,45 +65,55 @@ async function main() { }; } - console.log("TEST: Using TEST Liquidity Pool"); - const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, config.USDC)) as TestLiquidityPool; + let liquidityPool: LiquidityPool; + if (config.Aave) { + const {target, targetAdmin: liquidityPoolAdmin} = await deployProxy( + verifier.deploy, + "LiquidityPool", + deployer, + admin, + [config.USDC, config.Aave], + [ + admin, + minHealthFactor, + defaultLTV, + mpcAddress, + ], + ); + liquidityPool = target; + console.log(`LiquidityPoolProxyAdmin: ${liquidityPoolAdmin.target}`); + } else { + console.log("TEST: Using TEST Liquidity Pool"); + liquidityPool = (await verifier.deploy("TestLiquidityPool", deployer, {}, [config.USDC])) as LiquidityPool; + } const rebalancerVersion = config.IsTest ? "TestRebalancer" : "Rebalancer"; - const rebalancerImpl = ( - await deploy(rebalancerVersion, deployer, {}, - liquidityPool.target, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter - ) - ) as Rebalancer; - const rebalancerInit = (await rebalancerImpl.initialize.populateTransaction( + const {target: rebalancer, targetAdmin: rebalancerAdmin} = await deployProxy( + verifier.deploy, + rebalancerVersion, + deployer, admin, - rebalanceCaller, - config.Routes ? config.Routes.Domains.map(el => DomainSolidity[el]) : [], - config.Routes ? config.Routes.Providers.map(el => ProviderSolidity[el]) : [] - )).data; - const rebalancerProxy = (await deploy( - "TransparentUpgradeableProxy", deployer, {}, - rebalancerImpl.target, admin, rebalancerInit - )) as TransparentUpgradeableProxy; - const rebalancer = (await getContractAt("Rebalancer", rebalancerProxy.target, deployer)) as Rebalancer; - const rebalancerProxyAdminAddress = await getCreateAddress(rebalancerProxy, 1); - const rebalancerAdmin = (await getContractAt("ProxyAdmin", rebalancerProxyAdminAddress, deployer)) as ProxyAdmin; - - const DEFAULT_ADMIN_ROLE = ZERO_BYTES32; - - console.log("TEST: Using default admin role for Rebalancer on Pool"); - await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, rebalancer.target); - - const verifier = getVerifier(); + [liquidityPool, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter], + [ + admin, + rebalanceCaller, + config.Routes ? config.Routes.Domains.map(el => DomainSolidity[el]) : [], + config.Routes ? config.Routes.Providers.map(el => ProviderSolidity[el]) : [], + ], + ); + + await liquidityPool.grantRole(LIQUIDITY_ADMIN_ROLE, rebalancer); + await liquidityPool.grantRole(WITHDRAW_PROFIT_ROLE, withdrawProfit); if (config.IsHub) { const tiers = []; for (let i = 1;; i++) { - if (!isSet(process.env[`TIER_${i}_DAYS`])) { + if (!isSet(process.env[`TIER_${i}_SECONDS`])) { break; } - const period = BigInt(process.env[`TIER_${i}_DAYS`] || "0") * DAY; + const period = BigInt(process.env[`TIER_${i}_SECONDS`] || "0"); const multiplier = BigInt(process.env[`TIER_${i}_MULTIPLIER`] || "0"); tiers.push({period, multiplier}); } @@ -97,40 +124,40 @@ async function main() { const startingNonce = await deployer.getNonce(); - const liquidityHubAddress = await getCreateAddress(deployer, startingNonce + 2); - const lpToken = ( - await verifier.deploy("SprinterUSDCLPShare", deployer, {nonce: startingNonce + 0}, liquidityHubAddress) - ) as SprinterUSDCLPShare; - - const liquidityHubImpl = ( - await verifier.deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target, liquidityPool.target) - ) as LiquidityHub; - const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction( - config.USDC, admin, adjuster, assetsLimit - )).data; - const liquidityHubProxy = (await verifier.deploy( - "TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2}, - liquidityHubImpl.target, admin, liquidityHubInit - )) as TransparentUpgradeableProxy; - const liquidityHub = (await getContractAt("LiquidityHub", liquidityHubAddress, deployer)) as LiquidityHub; - const liquidityHubProxyAdminAddress = await getCreateAddress(liquidityHubProxy, 1); - const liquidityHubAdmin = (await getContractAt("ProxyAdmin", liquidityHubProxyAdminAddress)) as ProxyAdmin; - - assert(liquidityHubAddress == liquidityHubProxy.target, "LiquidityHub address mismatch"); + const liquidityHubAddress = await getProxyCreateAddress(deployer, startingNonce + 1); + const lpToken = (await verifier.deploy( + "SprinterUSDCLPShare", + deployer, + {}, + [liquidityHubAddress], + "contracts/SprinterUSDCLPShare.sol:SprinterUSDCLPShare" + )) as SprinterUSDCLPShare; + + const {target: liquidityHub, targetAdmin: liquidityHubAdmin} = await deployProxy( + verifier.deploy, + "LiquidityHub", + deployer, + admin, + [lpToken, liquidityPool], + [config.USDC, admin, adjuster, assetsLimit], + ); + + assert(liquidityHubAddress == liquidityHub.target, "LiquidityHub address mismatch"); const liquidityMining = ( - await deploy("SprinterLiquidityMining", deployer, {}, admin, liquidityHub.target, tiers) + await verifier.deploy("SprinterLiquidityMining", deployer, {}, [admin, liquidityHub, tiers]) ) as SprinterLiquidityMining; - console.log("TEST: Using default admin role for Hub on Pool"); - await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, liquidityHub.target); + await liquidityPool.grantRole(LIQUIDITY_ADMIN_ROLE, liquidityHub); - console.log(); console.log(`SprinterUSDCLPShare: ${lpToken.target}`); console.log(`LiquidityHub: ${liquidityHub.target}`); console.log(`LiquidityHubProxyAdmin: ${liquidityHubAdmin.target}`); console.log(`SprinterLiquidityMining: ${liquidityMining.target}`); console.log("Tiers:"); - console.table(tiers); + console.table(tiers.map(el => { + const multiplier = `${el.multiplier / 100n}.${el.multiplier % 100n}x`; + return {seconds: Number(el.period), multiplier}; + })); } console.log(`Admin: ${admin}`); @@ -138,14 +165,10 @@ async function main() { console.log(`USDC: ${config.USDC}`); console.log(`Rebalancer: ${rebalancer.target}`); console.log(`RebalancerProxyAdmin: ${rebalancerAdmin.target}`); - if (config.Routes) { - console.log("Routes:"); - console.table(config.Routes); - } + console.log("Routes:"); + console.table(config.Routes || {}); - if (process.env.VERIFY === "true") { - await verifier.verify(); - } + await verifier.verify(process.env.VERIFY === "true"); } main(); diff --git a/scripts/helpers.ts b/scripts/helpers.ts index fc6f9f2..77f7fac 100644 --- a/scripts/helpers.ts +++ b/scripts/helpers.ts @@ -1,66 +1,148 @@ import hre from "hardhat"; -import {Signer} from "ethers"; -import {deploy} from "../test/helpers"; - -export function assert(condition: boolean, message: string): void { - if (condition) return; - throw new Error(message); -}; - -export function sleep(msec: number): Promise { - return new Promise((resolve) => { - setTimeout(() => resolve(true), msec); - }); -}; - -export function isSet(input?: string): boolean { - if (input) { - return input.length > 0; - } - return false; -}; +import {Signer, BaseContract, AddressLike, resolveAddress, ContractTransaction} from "ethers"; +import {deploy, getContractAt, getCreateAddress} from "../test/helpers"; +import { + TransparentUpgradeableProxy, ProxyAdmin, +} from "../typechain-types"; +import {sleep} from "./common"; export function getVerifier() { interface VerificationInput { address: string; constructorArguments: any[]; + contract?: string; } const contracts: VerificationInput[] = []; return { - deploy: async (contractName: string, signer: Signer, txParams: object, ...params: any[]) => { - const contract = await deploy(contractName, signer, txParams, ...params); + deploy: async ( + contractName: string, + deployer: Signer, + txParams: object = {}, + params: any[] = [], + contractVerificationName?: string, + ): Promise => { + const contract = await deploy(contractName, deployer, txParams, ...params); contracts.push({ address: await contract.getAddress(), - constructorArguments: params, + constructorArguments: await Promise.all(params.map(async (el) => { + // Resolving all Addressable into string addresses. + try { + return await resolveAddress(el); + } catch { + return el; + } + })), + contract: contractVerificationName, }); return contract; }, - verify: async () => { - console.log("Waiting half a minute to start verification"); - await sleep(30000); - for (const contract of contracts) { - await hre.run("verify:verify", contract); + verify: async (performVerification: boolean) => { + if (performVerification) { + console.log("Waiting half a minute to start verification"); + await sleep(30000); + for (const contract of contracts) { + try { + await hre.run("verify:verify", contract); + } catch(error) { + console.error(error); + console.log(`Failed to verify: ${contract.address}`); + console.log(JSON.stringify(contract.constructorArguments)); + } + } + } else { + console.log(); + console.log("Verification skipped"); + for (const contract of contracts) { + console.log(`Contract: ${contract.address}`); + if (contract.contract) { + console.log(`Name: ${contract.contract}`); + } + if (contract.constructorArguments.length > 0) { + console.log("Constructor args:"); + console.log(JSON.stringify(contract.constructorArguments, (key, value) => { + if ((typeof value) == "bigint") { + return value.toString(); + } + return value; + })); + } + console.log(); + } } }, }; -}; +} -export const ProviderSolidity = { - CCTP: 0n, -}; +interface Initializable extends BaseContract { + initialize: { + populateTransaction: (...params: any[]) => Promise + } +} + +export async function deployProxy( + deployFunc: (contractName: string, deployer: Signer, txParams: object, ...params: any[]) => Promise, + contractName: string, + deployer: Signer, + upgradeAdmin: AddressLike, + contructorArgs: any[], + initArgs: any[] +): Promise<{target: ContractType; targetAdmin: ProxyAdmin;}> { + const targetImpl = ( + await deployFunc(contractName, deployer, {}, contructorArgs) + ) as ContractType; + const targetInit = (await targetImpl.initialize.populateTransaction(...initArgs)).data; + const targetProxy = (await deployFunc( + "TransparentUpgradeableProxy", deployer, {}, + [targetImpl.target, await resolveAddress(upgradeAdmin), targetInit] + )) as TransparentUpgradeableProxy; + const target = (await getContractAt(contractName, targetProxy, deployer)) as ContractType; + const targetProxyAdminAddress = await getCreateAddress(targetProxy, 1); + const targetAdmin = (await getContractAt("ProxyAdmin", targetProxyAdminAddress)) as ProxyAdmin; + return {target, targetAdmin}; +} + +export async function upgradeProxy( + deployFunc: (contractName: string, deployer: Signer, txParams: object, ...params: any[]) => Promise, + contractName: string, + deployer: Signer, + contructorArgs: any[], + proxyAddress: AddressLike +): Promise<{target?: ContractType; txRequired: boolean}> { + const targetImpl = ( + await deployFunc(contractName, deployer, {}, contructorArgs) + ) as ContractType; + console.log(`New ${contractName} implementation deployed to ${await resolveAddress(targetImpl)}`); + const targetProxyAdminAddress = await resolveAddress( + "0x" + + (await hre.ethers.provider.getStorage( + proxyAddress, "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103") + ).slice(-40) + ); + const targetAdmin = (await getContractAt("ProxyAdmin", targetProxyAdminAddress, deployer)) as ProxyAdmin; + const adminOwner = await targetAdmin.owner(); + if (adminOwner == await resolveAddress(deployer)) { + console.log(`Sending ${contractName} upgrade transaction.`); + const upgradeTx = await targetAdmin.upgradeAndCall(proxyAddress, targetImpl, "0x"); + console.log(upgradeTx.hash); + console.log(`${contractName} upgraded.`); + const target = (await getContractAt(contractName, proxyAddress, deployer)) as ContractType; + return {target, txRequired: false}; + } else { + const tx = await targetAdmin.upgradeAndCall.populateTransaction( + proxyAddress, targetImpl, "0x", {from: adminOwner} + ); + console.log(`Simulating ${contractName} upgrade.`); + await hre.ethers.provider.call(tx); + console.log("Success."); + console.log(`To finalize upgrade send the following transaction from ProxyAdmin owner: ${adminOwner}`); + console.log(`To: ${tx.to}`); + console.log("Value: 0"); + console.log(`Data: ${tx.data}`); + return {txRequired: true}; + } +} -export const DomainSolidity = { - ETHEREUM: 0n, - AVALANCHE: 1n, - OP_MAINNET: 2n, - ARBITRUM_ONE: 3n, - BASE: 4n, - POLYGON_MAINNET: 5n, - ETHEREUM_SEPOLIA: 6n, - AVALANCHE_FUJI: 7n, - OP_SEPOLIA: 8n, - ARBITRUM_SEPOLIA: 9n, - BASE_SEPOLIA: 10n, - POLYGON_AMOY: 11n, -}; +export async function getProxyCreateAddress(deployer: Signer, startingNonce: number) { + return await getCreateAddress(deployer, startingNonce + 1); +} diff --git a/scripts/upgradeLiquidityPool.ts b/scripts/upgradeLiquidityPool.ts new file mode 100644 index 0000000..b927487 --- /dev/null +++ b/scripts/upgradeLiquidityPool.ts @@ -0,0 +1,27 @@ +import dotenv from "dotenv"; +dotenv.config(); +import hre from "hardhat"; +import {resolveAddress} from "ethers"; +import {getVerifier, upgradeProxy} from "./helpers"; +import {LiquidityPool} from "../typechain-types"; +import {networkConfig, Network} from "../network.config"; + +async function main() { + const [deployer] = await hre.ethers.getSigners(); + const liquidityPoolAddress = await resolveAddress(process.env.LIQUIDITY_POOL || ""); + + const verifier = getVerifier(); + + const config = networkConfig[hre.network.name as Network]; + await upgradeProxy( + verifier.deploy, + "LiquidityPool", + deployer, + [config.USDC, config.Aave], + liquidityPoolAddress, + ); + + await verifier.verify(process.env.VERIFY === "true"); +} + +main(); diff --git a/test/LiquidityHub.ts b/test/LiquidityHub.ts index 49c1555..2fb5762 100644 --- a/test/LiquidityHub.ts +++ b/test/LiquidityHub.ts @@ -6,7 +6,7 @@ import hre from "hardhat"; import {Signature, resolveAddress, MaxUint256, getBigInt} from "ethers"; import { getCreateAddress, getContractAt, deploy, - ZERO_ADDRESS, ZERO_BYTES32, + ZERO_ADDRESS, toBytes32, } from "./helpers"; import { TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin, @@ -20,7 +20,7 @@ describe("LiquidityHub", function () { const deployAll = async () => { const [deployer, admin, user, user2, user3] = await hre.ethers.getSigners(); - const DEFAULT_ADMIN_ROLE = ZERO_BYTES32; + const LIQUIDITY_ADMIN_ROLE = toBytes32("LIQUIDITY_ADMIN_ROLE"); const usdc = (await deploy("TestUSDC", deployer, {})) as TestUSDC; const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, usdc.target)) as TestLiquidityPool; @@ -49,7 +49,7 @@ describe("LiquidityHub", function () { const liquidityHubProxyAdminAddress = await getCreateAddress(liquidityHubProxy, 1); const liquidityHubAdmin = (await getContractAt("ProxyAdmin", liquidityHubProxyAdminAddress, admin)) as ProxyAdmin; - await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, liquidityHub.target); + await liquidityPool.grantRole(LIQUIDITY_ADMIN_ROLE, liquidityHub.target); return {deployer, admin, user, user2, user3, usdc, lpToken, liquidityHub, liquidityHubProxy, liquidityHubAdmin, USDC, LP, liquidityPool}; diff --git a/test/LiquidityPool.ts b/test/LiquidityPool.ts new file mode 100644 index 0000000..b97077d --- /dev/null +++ b/test/LiquidityPool.ts @@ -0,0 +1,1151 @@ +import { + loadFixture, time +} from "@nomicfoundation/hardhat-toolbox/network-helpers"; +import {expect} from "chai"; +import hre from "hardhat"; +import { + getCreateAddress, getContractAt, deploy, signBorrow +} from "./helpers"; +import {encodeBytes32String, MaxUint256} from "ethers"; +import { + MockTarget, LiquidityPool, TransparentUpgradeableProxy, ProxyAdmin +} from "../typechain-types"; + +async function now() { + return BigInt(await time.latest()); +} + +describe("LiquidityPool", function () { + const deployAll = async () => { + const [deployer, admin, user, user2, mpc_signer] = await hre.ethers.getSigners(); + + const AAVE_POOL_PROVIDER = "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e"; + const aavePoolAddressesProvider = await hre.ethers.getContractAt("IAavePoolAddressesProvider", AAVE_POOL_PROVIDER); + const aavePoolAddress = await aavePoolAddressesProvider.getPool(); + const aavePool = await hre.ethers.getContractAt("IAavePool", aavePoolAddress); + + const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; + const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; + if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); + const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); + const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ADDRESS); + + const collateralData = await aavePool.getReserveData(USDC_ADDRESS); + const aToken = await hre.ethers.getContractAt("ERC20", collateralData[8]); + const usdcDebtToken = await hre.ethers.getContractAt("ERC20", collateralData[10]); + + const RPL_ADDRESS = "0xD33526068D116cE69F19A9ee46F0bd304F21A51f"; + const RPL_OWNER_ADDRESS = process.env.RPL_OWNER_ADDRESS!; + if (!RPL_OWNER_ADDRESS) throw new Error("Env variables not configured (RPL_OWNER_ADDRESS missing)"); + const rpl = await hre.ethers.getContractAt("ERC20", RPL_ADDRESS); + const rplOwner = await hre.ethers.getImpersonatedSigner(RPL_OWNER_ADDRESS); + const rplData = await aavePool.getReserveData(RPL_ADDRESS); + const rplDebtToken = await hre.ethers.getContractAt("ERC20", rplData[10]); + + const UNI_ADDRESS = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"; + const UNI_OWNER_ADDRESS = process.env.UNI_OWNER_ADDRESS!; + if (!UNI_OWNER_ADDRESS) throw new Error("Env variables not configured (UNI_OWNER_ADDRESS missing)"); + const uni = await hre.ethers.getContractAt("ERC20", UNI_ADDRESS); + const uniOwner = await hre.ethers.getImpersonatedSigner(UNI_OWNER_ADDRESS); + const uniData = await aavePool.getReserveData(UNI_ADDRESS); + const uniDebtToken = await hre.ethers.getContractAt("ERC20", uniData[10]); + + // PRIME token used as not supported by aave + const NON_SUPPORTED_TOKEN_ADDRESS = "0xb23d80f5FefcDDaa212212F028021B41DEd428CF"; + const NON_SUPPORTED_TOKEN_OWNER_ADDRESS = process.env.PRIME_OWNER_ADDRESS!; + if (!NON_SUPPORTED_TOKEN_OWNER_ADDRESS) + throw new Error("Env variables not configured (PRIME_OWNER_ADDRESS missing)"); + const nonSupportedToken = await hre.ethers.getContractAt("ERC20", NON_SUPPORTED_TOKEN_ADDRESS); + const nonSupportedTokenOwner = await hre.ethers.getImpersonatedSigner(NON_SUPPORTED_TOKEN_OWNER_ADDRESS); + + const USDC_DEC = 10n ** (await usdc.decimals()); + const RPL_DEC = 10n ** (await rpl.decimals()); + const UNI_DEC = 10n ** (await uni.decimals()); + + const startingNonce = await deployer.getNonce(); + + const liquidityPoolAddress = await getCreateAddress(deployer, startingNonce + 1); + const liquidityPoolImpl = ( + await deploy("LiquidityPool", deployer, {nonce: startingNonce}, + usdc.target, AAVE_POOL_PROVIDER + ) + ) as LiquidityPool; + // Initialize health factor as 5 (500%) + const healthFactor = 500n * 10n ** 18n / 100n; + // Initialize token LTV as 5% + const defaultLtv = 5n * 10n ** 18n / 100n; + const liquidityPoolInit = + (await liquidityPoolImpl.initialize.populateTransaction( + admin.address, healthFactor, defaultLtv, mpc_signer.address + )).data; + const liquidityPoolProxy = (await deploy( + "TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 1}, + liquidityPoolImpl.target, admin, liquidityPoolInit + )) as TransparentUpgradeableProxy; + const liquidityPool = (await getContractAt("LiquidityPool", liquidityPoolAddress, deployer)) as LiquidityPool; + const liquidityPoolProxyAdminAddress = await getCreateAddress(liquidityPoolProxy, 1); + const liquidityPoolAdmin = (await getContractAt("ProxyAdmin", liquidityPoolProxyAdminAddress, admin)) as ProxyAdmin; + + const mockTarget = ( + await deploy("MockTarget", deployer, {nonce: startingNonce + 2}) + ) as MockTarget; + + const LIQUIDITY_ADMIN_ROLE = encodeBytes32String("LIQUIDITY_ADMIN_ROLE"); + await liquidityPool.connect(admin).grantRole(LIQUIDITY_ADMIN_ROLE, admin.address); + + const WITHDRAW_PROFIT_ROLE = encodeBytes32String("WITHDRAW_PROFIT_ROLE"); + await liquidityPool.connect(admin).grantRole(WITHDRAW_PROFIT_ROLE, admin.address); + + return {deployer, admin, user, user2, mpc_signer, usdc, usdcOwner, rpl, rplOwner, uni, uniOwner, + liquidityPool, liquidityPoolProxy, liquidityPoolAdmin, mockTarget, USDC_DEC, RPL_DEC, UNI_DEC, AAVE_POOL_PROVIDER, + healthFactor, defaultLtv, aavePool, aToken, rplDebtToken, uniDebtToken, usdcDebtToken, + nonSupportedToken, nonSupportedTokenOwner}; + }; + + describe("Initialization", function () { + it("Should initialize the contract with correct values", async function () { + const { + liquidityPool, usdc, AAVE_POOL_PROVIDER, healthFactor, defaultLtv, mpc_signer + } = await loadFixture(deployAll); + expect(await liquidityPool.COLLATERAL()) + .to.be.eq(usdc.target); + expect(await liquidityPool.AAVE_POOL_PROVIDER()) + .to.be.eq(AAVE_POOL_PROVIDER); + expect(await liquidityPool.healthFactor()) + .to.be.eq(healthFactor); + expect(await liquidityPool.defaultLTV()) + .to.be.eq(defaultLtv); + expect(await liquidityPool.mpcAddress()) + .to.be.eq(mpc_signer); + }); + + it("Should NOT deploy the contract if token cannot be used as collateral", async function () { + const { + deployer, AAVE_POOL_PROVIDER, rpl, liquidityPool + } = await loadFixture(deployAll); + const startingNonce = await deployer.getNonce(); + await expect(deploy("LiquidityPool", deployer, {nonce: startingNonce}, + rpl.target, AAVE_POOL_PROVIDER + )).to.be.revertedWithCustomError(liquidityPool, "CollateralNotSupported"); + }); + }); + + describe("Borrow, supply, repay, withdraw", function () { + it("Should deposit to aave", async function () { + const {liquidityPool, usdc, usdcOwner, USDC_DEC, aToken} = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThanOrEqual(amount - 1n); + }); + + it("Should borrow a token", async function () { + const { + liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, user, user2, mpc_signer, usdcOwner + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(rpl.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await rpl.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + }); + + it("Should calculate token ltv if decimals of token and collateral are different", async function () { + const { + liquidityPool, usdc, uni, mpc_signer, user, user2, usdcOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 3n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(uni.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await uni.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + expect(await uni.allowance(liquidityPool.target, user2.address)).to.eq(amountToBorrow); + }); + + it("Should make a contract call to the recipient", async function () { + const { + liquidityPool, mockTarget, usdc, USDC_DEC, rpl, RPL_DEC, user, mpc_signer, usdcOwner + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 3n * RPL_DEC; + + const additionalData = "0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"; + + const callData = await mockTarget.fulfill.populateTransaction(rpl.target, amountToBorrow, additionalData); + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + mockTarget.target as string, + callData.data, + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + mockTarget.target, + callData.data, + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(rpl.target, amountToBorrow, user.address, mockTarget.target, callData.data) + .and.to.emit(mockTarget, "DataReceived").withArgs(additionalData); + expect(await rpl.balanceOf(liquidityPool.target)).to.eq(0); + expect(await rpl.balanceOf(mockTarget.target)).to.eq(amountToBorrow); + }); + + it("Should borrow collateral", async function () { + const { + liquidityPool, usdc, USDC_DEC, aToken, user, user2, mpc_signer, usdcOwner + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * USDC_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + usdc.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + usdc.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(usdc.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await usdc.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThanOrEqual(amountCollateral - 1n); + }); + + it("Should repay a debt", async function () { + const { + liquidityPool, usdc, uni, mpc_signer, user, user2, usdcOwner, uniOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 3n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed"); + expect(await uni.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + await uni.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + + await expect(liquidityPool.connect(user).repay([uni.target])) + .to.emit(liquidityPool, "Repaid"); + expect(await uni.balanceOf(liquidityPool.target)).to.be.lessThan(amountToBorrow); + }); + + it("Should deposit to aave multiple times", async function () { + const {liquidityPool, usdc, usdcOwner, USDC_DEC, aToken} = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThanOrEqual(amount * 2n - 1n); + }); + + it("Should borrow and repay different tokens", async function () { + const { + liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, uni, user, user2, mpc_signer, usdcOwner, uniOwner, + rplOwner, rplDebtToken, uniDebtToken + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 1n * RPL_DEC; + + const signature1 = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature1)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(rpl.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await rpl.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + const signature2 = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337, + 1n + ); + + await expect(liquidityPool.connect(user).borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 1n, + 2000000000n, + signature2)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(uni.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await uni.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + // advance time by one hour + await time.increase(3600); + + const uniDebtBefore = await uniDebtToken.balanceOf(liquidityPool.target); + const rplDebtBefore = await rplDebtToken.balanceOf(liquidityPool.target); + expect(uniDebtBefore).to.be.greaterThan(amountToBorrow); + expect(rplDebtBefore).to.be.greaterThan(amountToBorrow); + + await expect(liquidityPool.connect(user).repay([uni.target])) + .to.emit(liquidityPool, "Repaid"); + const uniDebtAfter1 = await uniDebtToken.balanceOf(liquidityPool.target); + expect(uniDebtAfter1).to.be.lessThan(uniDebtBefore); + + await uni.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + await expect(liquidityPool.connect(user).repay([uni.target])) + .to.emit(liquidityPool, "Repaid"); + const uniDebtAfter2 = await uniDebtToken.balanceOf(liquidityPool.target); + expect(uniDebtAfter2).to.eq(0); + + await expect(liquidityPool.connect(user).repay([rpl.target])) + .to.emit(liquidityPool, "Repaid"); + const rplDebtAfter1 = await rplDebtToken.balanceOf(liquidityPool.target); + expect(rplDebtAfter1).to.be.lessThan(rplDebtBefore); + + await rpl.connect(rplOwner).transfer(liquidityPool.target, amountToBorrow); + await expect(liquidityPool.connect(user).repay([rpl.target])) + .to.emit(liquidityPool, "Repaid"); + const rplDebtAfter2 = await rplDebtToken.balanceOf(liquidityPool.target); + expect(rplDebtAfter2).to.eq(0); + }); + + it("Should repay if some tokens don't have debt", async function () { + const { + liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, uni, user, user2, mpc_signer, usdcOwner, uniOwner, + rplOwner, rplDebtToken, uniDebtToken + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + + const signature1 = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature1)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(rpl.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await rpl.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + // advance time by one hour + await time.increase(3600); + + const rplDebtBefore = await rplDebtToken.balanceOf(liquidityPool.target); + expect(rplDebtBefore).to.be.greaterThan(amountToBorrow); + + await uni.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + await rpl.connect(rplOwner).transfer(liquidityPool.target, amountToBorrow); + + await expect(liquidityPool.connect(user).repay([uni.target, rpl.target])) + .to.emit(liquidityPool, "Repaid"); + const uniDebtAfter = await uniDebtToken.balanceOf(liquidityPool.target); + expect(uniDebtAfter).to.eq(0); + const rplDebtAfter = await rplDebtToken.balanceOf(liquidityPool.target); + expect(rplDebtAfter).to.eq(0); + }); + + it("Should repay collateral", async function () { + const { + liquidityPool, usdc, USDC_DEC, user, user2, mpc_signer, usdcOwner, uniOwner, + usdcDebtToken, uniDebtToken + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * USDC_DEC; + + const signature1 = await signBorrow( + mpc_signer, + liquidityPool.target as string, + usdc.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + usdc.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature1)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(usdc.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await usdc.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + // advance time by one hour + await time.increase(3600); + + const usdcDebtBefore = await usdcDebtToken.balanceOf(liquidityPool.target); + expect(usdcDebtBefore).to.be.greaterThan(amountToBorrow); + + await usdc.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + + await expect(liquidityPool.connect(user).repay([usdc.target])) + .to.emit(liquidityPool, "Repaid"); + const usdcDebtAfter = await uniDebtToken.balanceOf(liquidityPool.target); + expect(usdcDebtAfter).to.eq(0); + }); + + it("Should withdraw collateral from aave", async function () { + const {liquidityPool, usdc, usdcOwner, USDC_DEC, aToken, user, admin} = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThanOrEqual(amount - 1n); + + await expect(liquidityPool.connect(admin).withdraw(user.address, amount)) + .to.emit(liquidityPool, "WithdrawnFromAave").withArgs(user.address, amount); + expect(await usdc.balanceOf(user.address)).to.be.eq(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThan(0); + + // Using type(uint256).max as amount to withdraw all available amount + await expect(liquidityPool.connect(admin).withdraw( + user.address, MaxUint256 + )) + .to.emit(liquidityPool, "WithdrawnFromAave"); + expect(await usdc.balanceOf(user.address)).to.be.greaterThan(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.eq(0); + }); + + it("Should withdraw profit from the pool", async function () { + const {liquidityPool, uni, UNI_DEC, uniOwner, admin, user} = await loadFixture(deployAll); + const amount = 2n * UNI_DEC; + await uni.connect(uniOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.connect(admin).withdrawProfit(uni.target, user.address, amount)) + .to.emit(liquidityPool, "ProfitWithdrawn").withArgs(uni.target, user.address, amount); + expect(await uni.balanceOf(user.address)).to.eq(amount); + }); + + it("Should repay before withdrawing profit", async function () { + const { + liquidityPool, usdc, uni, mpc_signer, user, user2, admin, usdcOwner, uniOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 3n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed"); + expect(await uni.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + await uni.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + + const amountToWithdraw = 2n * UNI_DEC; + + await expect(liquidityPool.connect(admin).withdrawProfit(uni.target, user.address, amountToWithdraw)) + .to.emit(liquidityPool, "Repaid") + .and.to.emit(liquidityPool, "ProfitWithdrawn").withArgs(uni.target, user.address, amountToWithdraw); + expect(await uni.balanceOf(user.address)).to.eq(amountToWithdraw); + expect(await uni.balanceOf(liquidityPool.target)).to.be.greaterThan(0); + }); + + it("Should withdraw all available balance as profit ", async function () { + const {liquidityPool, uni, UNI_DEC, uniOwner, admin, user} = await loadFixture(deployAll); + const amount = 2n * UNI_DEC; + await uni.connect(uniOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.connect(admin).withdrawProfit(uni.target, user.address, MaxUint256)) + .to.emit(liquidityPool, "ProfitWithdrawn").withArgs(uni.target, user.address, amount); + expect(await uni.balanceOf(user.address)).to.eq(amount); + }); + + it("Should withdraw non-supported token", async function () { + const { + liquidityPool, nonSupportedToken, nonSupportedTokenOwner, admin, user, UNI_DEC + } = await loadFixture(deployAll); + const amount = 2n * UNI_DEC; + await nonSupportedToken.connect(nonSupportedTokenOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.connect(admin).withdrawProfit(nonSupportedToken.target, user.address, amount)) + .to.emit(liquidityPool, "ProfitWithdrawn").withArgs(nonSupportedToken.target, user.address, amount); + expect(await nonSupportedToken.balanceOf(user.address)).to.eq(amount); + }); + + it("Should repay before deposit", async function () { + const { + liquidityPool, usdc, USDC_DEC, user, user2, mpc_signer, usdcOwner, uniOwner, + usdcDebtToken, uniDebtToken + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * USDC_DEC; + + const signature1 = await signBorrow( + mpc_signer, + liquidityPool.target as string, + usdc.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + usdc.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature1)) + .to.emit(liquidityPool, "Borrowed") + .withArgs(usdc.target, amountToBorrow, user.address, user2.address, "0x"); + expect(await usdc.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + // advance time by one hour + await time.increase(3600); + + const usdcDebtBefore = await usdcDebtToken.balanceOf(liquidityPool.target); + expect(usdcDebtBefore).to.be.greaterThan(amountToBorrow); + + await usdc.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + + await expect(liquidityPool.connect(user).deposit()) + .to.emit(liquidityPool, "Repaid") + .and.to.emit(liquidityPool, "SuppliedToAave"); + const usdcDebtAfter = await uniDebtToken.balanceOf(liquidityPool.target); + expect(usdcDebtAfter).to.eq(0); + expect(await usdc.balanceOf(liquidityPool.target)).to.eq(0); + }); + + it.skip("Should deposit, borrow and repay multiple times", async function () { + // increase time + }); + + it("Should NOT deposit if no collateral on contract", async function () { + const {liquidityPool} = await loadFixture(deployAll); + await expect(liquidityPool.deposit()) + .to.be.revertedWithCustomError(liquidityPool, "NoCollateral"); + }); + + it("Should NOT borrow if MPC signature is wrong", async function () { + const {liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, user, user2, usdcOwner} = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + const signature = await signBorrow( + user, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.be.revertedWithCustomError(liquidityPool, "InvalidSignature"); + }); + + it("Should NOT borrow if MPC signature nonce is reused", async function () { + const { + liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, user, user2, usdcOwner, mpc_signer + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature); + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.be.revertedWithCustomError(liquidityPool, "NonceAlreadyUsed"); + }); + + it("Should NOT borrow if MPC signature is expired", async function () { + const { + liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, user, user2, usdcOwner, mpc_signer + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + const deadline = (await now()) - 1n; + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337, + 0n, + deadline, + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + user2, + "0x", + 0n, + deadline, + signature)) + .to.be.revertedWithCustomError(liquidityPool, "ExpiredSignature"); + }); + + it("Should NOT borrow if token ltv is exceeded", async function () { + const { + liquidityPool, usdc, uni, mpc_signer, user, user2, usdcOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 10n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.be.revertedWithCustomError(liquidityPool, "TokenLtvExceeded"); + }); + + it("Should NOT borrow if health factor is too low", async function () { + const { + liquidityPool, admin, usdc, uni, mpc_signer, user, user2, usdcOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + await expect(liquidityPool.connect(admin).setHealthFactor(4000n * 10n ** 18n / 100n)) + .to.emit(liquidityPool, "HealthFactorSet"); + + const amountToBorrow = 3n * UNI_DEC; + + const signature2 = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature2)) + .to.be.revertedWithCustomError(liquidityPool, "HealthFactorTooLow"); + }); + + it("Should NOT borrow if target call fails", async function () { + const { + liquidityPool, mockTarget, usdc, USDC_DEC, rpl, RPL_DEC, user, mpc_signer, usdcOwner + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + + const additionalData = "0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"; + + const callData = await mockTarget.fulfill.populateTransaction(rpl.target, amountToBorrow, additionalData); + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + rpl.target as string, + callData.data, + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + rpl.target, + callData.data, + 0n, + 2000000000n, + signature)) + .to.be.revertedWithCustomError(liquidityPool, "TargetCallFailed"); + }); + + it("Should NOT repay if all tokens don't have debt or balance", async function () { + const { + liquidityPool, usdc, USDC_DEC, rpl, RPL_DEC, uni, user, mockTarget, mpc_signer, usdcOwner, uniOwner + } = await loadFixture(deployAll); + + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 2n * RPL_DEC; + + const additionalData = "0x123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0"; + + const callData = await mockTarget.fulfill.populateTransaction(rpl.target, amountToBorrow, additionalData); + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + rpl.target as string, + amountToBorrow.toString(), + mockTarget.target as string, + callData.data, + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + rpl.target, + amountToBorrow, + mockTarget.target, + callData.data, + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed") + .and.to.emit(mockTarget, "DataReceived").withArgs(additionalData); + expect(await rpl.balanceOf(liquidityPool.target)).to.eq(0); + expect(await rpl.balanceOf(mockTarget.target)).to.eq(amountToBorrow); + + await uni.connect(uniOwner).transfer(liquidityPool.target, amountToBorrow); + + // No balance for rpl, no dept for uni + await expect(liquidityPool.connect(user).repay([uni.target, rpl.target])) + .to.be.revertedWithCustomError(liquidityPool, "NothingToRepay"); + }); + + it("Should NOT repay unsupported tokens", async function () { + const {liquidityPool, user} = await loadFixture(deployAll); + const unsupportedToken = await hre.ethers.getContractAt("ERC20", "0x53fFFB19BAcD44b82e204d036D579E86097E5D09"); + + // No balance for rpl, no dept for uni + await expect(liquidityPool.connect(user).repay([unsupportedToken.target])) + .to.be.revertedWithCustomError(liquidityPool, "NothingToRepay"); + }); + + it("Should NOT withdraw collateral if not enough on aave", async function () { + const {liquidityPool, usdc, USDC_DEC, usdcOwner, aToken, user, admin} = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThanOrEqual(amount - 1n); + + await expect(liquidityPool.connect(admin).withdraw(user.address, amount * 2n)) + .to.be.reverted; + }); + + it("Should NOT withdraw collateral if health factor is too low", async function () { + const { + liquidityPool, usdc, usdcOwner, USDC_DEC, user, admin, uni, mpc_signer, UNI_DEC, user2 + } = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + + const amountToBorrow = 3n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.connect(user).borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed"); + + await expect(liquidityPool.connect(admin).withdraw(user.address, 900000000n)) + .to.be.revertedWithCustomError(liquidityPool, "HealthFactorTooLow"); + }); + + it("Should NOT withdraw collateral by unauthorized user", async function () { + const {liquidityPool, usdc, usdcOwner, USDC_DEC, aToken, user} = await loadFixture(deployAll); + const amount = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amount); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); + expect(await aToken.balanceOf(liquidityPool.target)).to.be.greaterThanOrEqual(amount - 1n); + + await expect(liquidityPool.connect(user).withdraw(user.address, amount * 2n)) + .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); + }); + + it("Should NOT withdraw profit for collateral", async function () { + const {liquidityPool, usdc, admin, user} = await loadFixture(deployAll); + const amount = 1000n; + await expect(liquidityPool.connect(admin).withdrawProfit(usdc.target, user.address, amount)) + .to.be.revertedWithCustomError(liquidityPool, "CannotWithdrawProfitCollateral"); + }); + + it("Should NOT withdraw profit if not enough balance without repay", async function () { + const {liquidityPool, uni, UNI_DEC, uniOwner, admin, user} = await loadFixture(deployAll); + const amount = 2n * UNI_DEC; + await uni.connect(uniOwner).transfer(liquidityPool.target, amount - 1n); + await expect(liquidityPool.connect(admin).withdrawProfit(uni.target, user.address, amount)) + .to.be.revertedWithCustomError(liquidityPool, "NotEnoughBalance"); + }); + + it("Should NOT withdraw profit if not enough balance after repay", async function () { + const { + liquidityPool, usdc, uni, mpc_signer, user, user2, admin, usdcOwner, uniOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 3n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed"); + expect(await uni.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + const amountToWithdraw = 2n * UNI_DEC; + + // Repaying will require more tokens than were borrowed so there will be not enough to withdraw + await uni.connect(uniOwner).transfer(liquidityPool.target, amountToWithdraw); + + await expect(liquidityPool.connect(admin).withdrawProfit(uni.target, user.address, amountToWithdraw)) + .to.be.revertedWithCustomError(liquidityPool, "NotEnoughBalance"); + }); + + it("Should NOT withdraw profit for all balance if balance is 0 after repay", async function () { + const { + liquidityPool, usdc, uni, mpc_signer, user, user2, admin, usdcOwner, USDC_DEC, UNI_DEC + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; // $1000 + await usdc.connect(usdcOwner).transfer(liquidityPool.target, amountCollateral); + await expect(liquidityPool.deposit()) + .to.emit(liquidityPool, "SuppliedToAave"); + + const amountToBorrow = 3n * UNI_DEC; + + const signature = await signBorrow( + mpc_signer, + liquidityPool.target as string, + uni.target as string, + amountToBorrow.toString(), + user2.address, + "0x", + 31337 + ); + + await expect(liquidityPool.borrow( + uni.target, + amountToBorrow, + user2, + "0x", + 0n, + 2000000000n, + signature)) + .to.emit(liquidityPool, "Borrowed"); + expect(await uni.balanceOf(liquidityPool.target)).to.eq(amountToBorrow); + + // Repaying will require more tokens than were borrowed so there will be not enough to withdraw + await expect(liquidityPool.connect(admin).withdrawProfit(uni.target, user.address, MaxUint256)) + .to.be.revertedWithCustomError(liquidityPool, "NotEnoughBalance"); + }); + + it("Should NOT withdraw profit by unauthorized user", async function () { + const {liquidityPool, uni, user} = await loadFixture(deployAll); + const amount = 1000n; + await expect(liquidityPool.connect(user).withdrawProfit(uni.target, user.address, amount)) + .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); + }); + }); + + describe("Admin functions", function () { + it("Should allow admin to set default token LTV", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + const oldDefaultLTV = await liquidityPool.defaultLTV(); + const defaultLtv = 1000; + await expect(liquidityPool.connect(admin).setDefaultLTV(defaultLtv)) + .to.emit(liquidityPool, "DefaultLTVSet").withArgs(oldDefaultLTV, defaultLtv); + expect(await liquidityPool.defaultLTV()) + .to.eq(defaultLtv); + }); + + it("Should NOT allow others to set default token LTV", async function () { + const {liquidityPool, user} = await loadFixture(deployAll); + const defaultLtv = 1000; + await expect(liquidityPool.connect(user).setDefaultLTV(defaultLtv)) + .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); + }); + + it("Should allow admin to set token LTV for each token", async function () { + const {liquidityPool, admin, uni} = await loadFixture(deployAll); + const oldLTV = await liquidityPool.borrowTokenLTV(uni.target); + const ltv = 1000; + await expect(liquidityPool.connect(admin).setBorrowTokenLTV(uni.target, ltv)) + .to.emit(liquidityPool, "BorrowTokenLTVSet").withArgs(uni.target, oldLTV, ltv); + expect(await liquidityPool.borrowTokenLTV(uni.target)) + .to.eq(ltv); + }); + + it("Should NOT allow others to set token LTV for each token", async function () { + const {liquidityPool, user, uni} = await loadFixture(deployAll); + const ltv = 1000; + await expect(liquidityPool.connect(user).setBorrowTokenLTV(uni.target, ltv)) + .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); + }); + + it("Should allow admin to set minimal health factor", async function () { + const {liquidityPool, admin} = await loadFixture(deployAll); + const oldHealthFactor = await liquidityPool.healthFactor(); + const healthFactor = 300n * 10n ** 18n / 100n; + await expect(liquidityPool.connect(admin).setHealthFactor(healthFactor)) + .to.emit(liquidityPool, "HealthFactorSet").withArgs(oldHealthFactor, healthFactor); + expect(await liquidityPool.healthFactor()) + .to.eq(healthFactor); + }); + + it("Should NOT allow others to set minimal health factor", async function () { + const {liquidityPool, user} = await loadFixture(deployAll); + const healthFactor = 500n * 10n ** 18n / 100n; + await expect(liquidityPool.connect(user).setHealthFactor(healthFactor)) + .to.be.revertedWithCustomError(liquidityPool, "AccessControlUnauthorizedAccount"); + }); + }); +}); diff --git a/test/Rebalancer.ts b/test/Rebalancer.ts index 5335e99..2b2f3e1 100644 --- a/test/Rebalancer.ts +++ b/test/Rebalancer.ts @@ -10,7 +10,7 @@ import { } from "./helpers"; import { ProviderSolidity as Provider, DomainSolidity as Domain, -} from "../scripts/helpers"; +} from "../scripts/common"; import { TestUSDC, TransparentUpgradeableProxy, ProxyAdmin, TestLiquidityPool, Rebalancer, TestCCTPTokenMessenger, TestCCTPMessageTransmitter, @@ -25,6 +25,7 @@ describe("Rebalancer", function () { const DEFAULT_ADMIN_ROLE = ZERO_BYTES32; const REBALANCER_ROLE = toBytes32("REBALANCER_ROLE"); + const LIQUIDITY_ADMIN_ROLE = toBytes32("LIQUIDITY_ADMIN_ROLE"); const usdc = (await deploy("TestUSDC", deployer, {})) as TestUSDC; const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, usdc.target)) as TestLiquidityPool; @@ -51,7 +52,7 @@ describe("Rebalancer", function () { const rebalancerProxyAdminAddress = await getCreateAddress(rebalancerProxy, 1); const rebalancerAdmin = (await getContractAt("ProxyAdmin", rebalancerProxyAdminAddress, admin)) as ProxyAdmin; - await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, rebalancer.target); + await liquidityPool.grantRole(LIQUIDITY_ADMIN_ROLE, rebalancer.target); return { deployer, admin, rebalanceUser, user, usdc, diff --git a/test/SprinterLiquidityMining.ts b/test/SprinterLiquidityMining.ts index 2a9f754..412ab0b 100644 --- a/test/SprinterLiquidityMining.ts +++ b/test/SprinterLiquidityMining.ts @@ -6,7 +6,7 @@ import hre from "hardhat"; import {Signature, resolveAddress, MaxUint256, getBigInt} from "ethers"; import { getCreateAddress, getContractAt, deploy, - ZERO_ADDRESS, ZERO_BYTES32, + ZERO_ADDRESS, toBytes32, } from "./helpers"; import { TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin, @@ -24,7 +24,7 @@ describe("SprinterLiquidityMining", function () { const deployAll = async () => { const [deployer, admin, user, user2, user3] = await hre.ethers.getSigners(); - const DEFAULT_ADMIN_ROLE = ZERO_BYTES32; + const LIQUIDITY_ADMIN_ROLE = toBytes32("LIQUIDITY_ADMIN_ROLE"); const usdc = (await deploy("TestUSDC", deployer, {})) as TestUSDC; const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, usdc.target)) as TestLiquidityPool; @@ -63,7 +63,7 @@ describe("SprinterLiquidityMining", function () { await deploy("SprinterLiquidityMining", deployer, {}, admin.address, liquidityHub.target, tiers) ) as SprinterLiquidityMining; - await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, liquidityHub.target); + await liquidityPool.grantRole(LIQUIDITY_ADMIN_ROLE, liquidityHub.target); return {deployer, admin, user, user2, user3, usdc, lpToken, liquidityHub, liquidityHubProxy, liquidityHubAdmin, USDC, LP, liquidityPool, liquidityMining}; diff --git a/test/helpers.ts b/test/helpers.ts index f32c275..e8b4ae4 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,5 @@ import hre from "hardhat"; -import {AddressLike, resolveAddress, Signer, BaseContract, zeroPadBytes, toUtf8Bytes} from "ethers"; +import {AddressLike, resolveAddress, Signer, BaseContract, zeroPadBytes, toUtf8Bytes, TypedDataDomain} from "ethers"; export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; export const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; @@ -14,7 +14,7 @@ export async function getContractAt(contractName: string, address: AddressLike, return hre.ethers.getContractAt(contractName, await resolveAddress(address), signer); } -export async function deploy(contractName: string, signer: Signer, txParams: object, ...params: any[]): +export async function deploy(contractName: string, signer: Signer, txParams: object = {}, ...params: any[]): Promise { const factory = await hre.ethers.getContractFactory(contractName, signer); @@ -34,3 +34,47 @@ export function divCeil(a: bigint, b: bigint): bigint { } return a / b + 1n; } + +export async function signBorrow( + signer: Signer, + verifyingContract: string, + borrowToken: string, + amount: string, + target: string, + targetCallData: string, + chainId: number = 1, + nonce: bigint = 0n, + deadline: bigint = 2000000000n +) { + const name = "LiquidityPool"; + const version = "1.0.0"; + + const domain: TypedDataDomain = { + name, + version, + chainId, + verifyingContract + }; + + const types = { + Borrow: [ + {name: "borrowToken", type: "address"}, + {name: "amount", type: "uint256"}, + {name: "target", type: "address"}, + {name: "targetCallData", type: "bytes"}, + {name: "nonce", type: "uint256"}, + {name: "deadline", type: "uint256"}, + ], + }; + + const value = { + borrowToken: borrowToken.toLowerCase(), + amount, + target: target.toLowerCase(), + targetCallData, + nonce, + deadline, + }; + + return signer.signTypedData(domain, types, value); +}