diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 147b2d2b6..671843c37 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -18,6 +18,7 @@ jobs: secrets: ETHEREUM_PROVIDER_URL: ${{ secrets.ETHEREUM_PROVIDER_URL }} ARBITRUM_PROVIDER_URL: ${{ secrets.ARBITRUM_PROVIDER_URL }} + BASE_PROVIDER_URL: ${{ secrets.BASE_PROVIDER_URL }} with: test-options: '--no-match-path "test/fork/*"' fork-test-options: '--match-path "test/fork/*"' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d529307d..b42c3f1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: secrets: ETHEREUM_PROVIDER_URL: ${{ secrets.ETHEREUM_PROVIDER_URL }} ARBITRUM_PROVIDER_URL: ${{ secrets.ARBITRUM_PROVIDER_URL }} + BASE_PROVIDER_URL: ${{ secrets.BASE_PROVIDER_URL }} with: test-options: '--no-match-path "test/fork/*"' fork-test-options: '--match-path "test/fork/*"' diff --git a/.github/workflows/foundry-tests.yml b/.github/workflows/foundry-tests.yml index d4e1944a4..6e250da6b 100644 --- a/.github/workflows/foundry-tests.yml +++ b/.github/workflows/foundry-tests.yml @@ -7,6 +7,8 @@ on: required: true ARBITRUM_PROVIDER_URL: required: true + BASE_PROVIDER_URL: + required: true inputs: @@ -59,8 +61,8 @@ jobs: node-version: ${{ inputs.node-version }} - name: Set up node_modules cache - # from tag: v4.0.2 - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + # from tag: v4.2.3 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 with: path: "**/node_modules" key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} @@ -85,6 +87,7 @@ jobs: env: ETHEREUM_PROVIDER_URL: ${{ secrets.ETHEREUM_PROVIDER_URL }} ARBITRUM_PROVIDER_URL: ${{ secrets.ARBITRUM_PROVIDER_URL }} + BASE_PROVIDER_URL: ${{ secrets.BASE_PROVIDER_URL }} run: | forge test ${{ inputs.test-options }} -vvv id: test @@ -94,6 +97,7 @@ jobs: env: ETHEREUM_PROVIDER_URL: ${{ secrets.ETHEREUM_PROVIDER_URL }} ARBITRUM_PROVIDER_URL: ${{ secrets.ARBITRUM_PROVIDER_URL }} + BASE_PROVIDER_URL: ${{ secrets.BASE_PROVIDER_URL }} run: | forge test ${{ inputs.fork-test-options }} --fork-url ${{ secrets.ETHEREUM_PROVIDER_URL }} -vvv id: fork-test diff --git a/contracts/base/amm-usdc/interfaces/IAmmPoolsServiceUsdcBaseV1.sol b/contracts/base/amm-usdc/interfaces/IAmmPoolsServiceUsdcBaseV1.sol index 912d1c6f0..ad4f1e20d 100644 --- a/contracts/base/amm-usdc/interfaces/IAmmPoolsServiceUsdcBaseV1.sol +++ b/contracts/base/amm-usdc/interfaces/IAmmPoolsServiceUsdcBaseV1.sol @@ -7,4 +7,6 @@ interface IAmmPoolsServiceUsdcBaseV1 is IProvideLiquidityEvents { function provideLiquidityUsdcToAmmPoolUsdc(address beneficiary, uint256 assetAmount) external payable; function redeemFromAmmPoolUsdc(address beneficiary, uint256 ipTokenAmount) external; + + function rebalanceBetweenAmmTreasuryAndAssetManagementUsdc() external; } diff --git a/contracts/base/amm-usdc/services/AmmPoolsServiceUsdcBaseV1.sol b/contracts/base/amm-usdc/services/AmmPoolsServiceUsdcBaseV1.sol index 189cf7021..2bea7e7f9 100644 --- a/contracts/base/amm-usdc/services/AmmPoolsServiceUsdcBaseV1.sol +++ b/contracts/base/amm-usdc/services/AmmPoolsServiceUsdcBaseV1.sol @@ -49,4 +49,8 @@ contract AmmPoolsServiceUsdcBaseV1 is IAmmPoolsServiceUsdcBaseV1, AmmPoolsServic function redeemFromAmmPoolUsdc(address beneficiary, uint256 ipTokenAmount) external { _redeem(beneficiary, ipTokenAmount); } + + function rebalanceBetweenAmmTreasuryAndAssetManagementUsdc() external override { + _rebalanceBetweenAmmTreasuryAndAssetManagement(); + } } diff --git a/contracts/base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV2.sol b/contracts/base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV2.sol new file mode 100644 index 000000000..88fd2b938 --- /dev/null +++ b/contracts/base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV2.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import {IAmmPoolsServiceWstEthBaseV1} from "./IAmmPoolsServiceWstEthBaseV1.sol"; + +/// @title Interface of the AmmPoolsServiceWstEth contract V2. +interface IAmmPoolsServiceWstEthBaseV2 is IAmmPoolsServiceWstEthBaseV1 { + /// @notice Rebalances wstETH assets between the AmmTreasury and the AssetManagement, based on configuration stored + /// in the `AmmPoolsParamsValue.ammTreasuryAndAssetManagementRatio` field. + /// @dev Emits {Deposit} or {Withdraw} event from AssetManagement depends on current asset balance on AmmTreasury and AssetManagement. + /// @dev Emits {Transfer} from ERC20 asset. + function rebalanceBetweenAmmTreasuryAndAssetManagementWstEth() external; +} diff --git a/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV1.sol b/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV1.sol index 4a3cbf48c..ca21c2cf3 100644 --- a/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV1.sol +++ b/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV1.sol @@ -7,7 +7,7 @@ import {StorageLibBaseV1} from "../../libraries/StorageLibBaseV1.sol"; /// @dev It is not recommended to use service contract directly, should be used only through IporProtocolRouter. /// @dev Service can be safely used directly only if you are sure that methods will not touch any storage variables. -/// @dev Close Swap Service for wstEth pool - no asset management support +/// @dev Close Swap Service for wstEth pool - Asset Management IS NOT supported in this contract. contract AmmCloseSwapServiceWstEthBaseV1 is AmmCloseSwapServiceBaseV1, IAmmCloseSwapServiceWstEth { using IporContractValidator for address; diff --git a/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV2.sol b/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV2.sol new file mode 100644 index 000000000..8ba0be38b --- /dev/null +++ b/contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV2.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.26; + +import {IAmmCloseSwapServiceWstEth} from "../../../interfaces/IAmmCloseSwapServiceWstEth.sol"; +import {AmmCloseSwapServiceBaseV2} from "../../../base/amm/services/AmmCloseSwapServiceBaseV2.sol"; +import {StorageLibBaseV1} from "../../libraries/StorageLibBaseV1.sol"; +import {IporContractValidator} from "../../../libraries/IporContractValidator.sol"; +import {AmmTypes} from "../../../interfaces/types/AmmTypes.sol"; +import {IAmmCloseSwapLens} from "../../../interfaces/IAmmCloseSwapLens.sol"; + + +/// @dev It is not recommended to use service contract directly, should be used only through IporProtocolRouter. +/// @dev Service can be safely used directly only if you are sure that methods will not touch any storage variables. +/// @dev Close Swap Service for wstEth pool - Asset Management (PlasmaVault from Ipor Fusion) and rebalancing between AMM Treasury +/// and Asset Management (PlasmaVault from Ipor Fusion) IS supported in this contract. +contract AmmCloseSwapServiceWstEthBaseV2 is AmmCloseSwapServiceBaseV2, IAmmCloseSwapServiceWstEth { + using IporContractValidator for address; + + constructor( + IAmmCloseSwapLens.AmmCloseSwapServicePoolConfiguration memory poolCfg, + address iporOracle_ + ) AmmCloseSwapServiceBaseV2(poolCfg, iporOracle_) {} + + function closeSwapsWstEth( + address beneficiary, + uint256[] memory payFixedSwapIds, + uint256[] memory receiveFixedSwapIds, + AmmTypes.CloseSwapRiskIndicatorsInput calldata riskIndicatorsInput + ) + external + override + returns ( + AmmTypes.IporSwapClosingResult[] memory closedPayFixedSwaps, + AmmTypes.IporSwapClosingResult[] memory closedReceiveFixedSwaps + ) + { + (closedPayFixedSwaps, closedReceiveFixedSwaps) = _closeSwaps( + beneficiary, + payFixedSwapIds, + receiveFixedSwapIds, + riskIndicatorsInput + ); + } + + function emergencyCloseSwapsWstEth( + uint256[] memory payFixedSwapIds, + uint256[] memory receiveFixedSwapIds, + AmmTypes.CloseSwapRiskIndicatorsInput calldata riskIndicatorsInput + ) + external + override + returns ( + AmmTypes.IporSwapClosingResult[] memory closedPayFixedSwaps, + AmmTypes.IporSwapClosingResult[] memory closedReceiveFixedSwaps + ) + { + (closedPayFixedSwaps, closedReceiveFixedSwaps) = _emergencyCloseSwaps( + payFixedSwapIds, + receiveFixedSwapIds, + riskIndicatorsInput + ); + } + + function _getMessageSigner() internal view override returns (address) { + return StorageLibBaseV1.getMessageSignerStorage().value; + } +} diff --git a/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV1.sol b/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV1.sol index 90b44eb98..22690d4a7 100644 --- a/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV1.sol +++ b/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV1.sol @@ -15,6 +15,7 @@ import "../../../governance/AmmConfigurationManager.sol"; import "../../../base/interfaces/IAmmTreasuryBaseV1.sol"; /// @dev It is not recommended to use service contract directly, should be used only through IporProtocolRouter. +/// @dev Asset Management is NOT supported in this contract. Rebalancing between AMM Treasury and Asset Management is NOT supported in this contract. contract AmmPoolsServiceWstEthBaseV1 is IAmmPoolsServiceWstEthBaseV1 { using IporContractValidator for address; using SafeERC20 for IERC20; diff --git a/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV2.sol b/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV2.sol new file mode 100644 index 000000000..66dd4abfe --- /dev/null +++ b/contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV2.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.26; + +import {IAmmPoolsServiceWstEthBaseV2} from "../../../base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV2.sol"; +import {AmmPoolsServiceBaseV1} from "../../../base/amm/services/AmmPoolsServiceBaseV1.sol"; + +/// @dev It is not recommended to use service contract directly, should be used only through IporProtocolRouter. +/// @dev Asset Management is supported in this contract. Rebalancing between AMM Treasury and Asset Management IS supported in this contract. +contract AmmPoolsServiceWstEthBaseV2 is IAmmPoolsServiceWstEthBaseV2, AmmPoolsServiceBaseV1 { + constructor( + address asset_, + address ipToken_, + address ammTreasury_, + address ammStorage_, + address ammAssetManagement_, + address iporOracle_, + address iporProtocolRouter_, + uint256 redeemFeeRate_, + uint256 autoRebalanceThresholdMultiplier_ + ) + AmmPoolsServiceBaseV1( + asset_, + ipToken_, + ammTreasury_, + ammStorage_, + ammAssetManagement_, + iporOracle_, + iporProtocolRouter_, + redeemFeeRate_, + autoRebalanceThresholdMultiplier_ + ) + {} + + function provideLiquidityWstEth(address beneficiary, uint256 assetAmount) external payable override { + _provideLiquidity(beneficiary, assetAmount); + } + + function redeemFromAmmPoolWstEth(address beneficiary, uint256 ipTokenAmount) external override { + _redeem(beneficiary, ipTokenAmount); + } + + function rebalanceBetweenAmmTreasuryAndAssetManagementWstEth() external override { + _rebalanceBetweenAmmTreasuryAndAssetManagement(); + } +} diff --git a/contracts/base/amm/services/AmmPoolsServiceBaseV1.sol b/contracts/base/amm/services/AmmPoolsServiceBaseV1.sol index acde0bb1a..098d2e9d9 100644 --- a/contracts/base/amm/services/AmmPoolsServiceBaseV1.sol +++ b/contracts/base/amm/services/AmmPoolsServiceBaseV1.sol @@ -148,7 +148,7 @@ contract AmmPoolsServiceBaseV1 is IProvideLiquidityEvents { ); } - function rebalanceBetweenAmmTreasuryAndAssetManagement() external { + function _rebalanceBetweenAmmTreasuryAndAssetManagement() internal virtual { require( AmmConfigurationManager.isAppointedToRebalanceInAmm(asset, msg.sender), AmmPoolsErrors.CALLER_NOT_APPOINTED_TO_REBALANCE diff --git a/contracts/chains/base/router/IporProtocolRouterBase.sol b/contracts/chains/base/router/IporProtocolRouterBase.sol index acaa743b8..3c5c0954c 100644 --- a/contracts/chains/base/router/IporProtocolRouterBase.sol +++ b/contracts/chains/base/router/IporProtocolRouterBase.sol @@ -17,6 +17,7 @@ import {IAmmPoolsServiceUsdcBaseV1} from "../../../base/amm-usdc/interfaces/IAmm import {IAmmOpenSwapServiceWstEth} from "../../../interfaces/IAmmOpenSwapServiceWstEth.sol"; import {IAmmCloseSwapServiceWstEth} from "../../../interfaces/IAmmCloseSwapServiceWstEth.sol"; import {IAmmPoolsServiceWstEthBaseV1} from "../../../base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV1.sol"; +import {IAmmPoolsServiceWstEthBaseV2} from "../../../base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV2.sol"; import {IAmmOpenSwapServiceUsdcBaseV1} from "../../../base/amm-usdc/interfaces/IAmmOpenSwapServiceUsdcBaseV1.sol"; import {IAmmCloseSwapServiceUsdc} from "../../../interfaces/IAmmCloseSwapServiceUsdc.sol"; import {IAmmPoolsServiceUsdm} from "../../../amm-usdm/interfaces/IAmmPoolsServiceUsdm.sol"; @@ -117,7 +118,8 @@ contract IporProtocolRouterBase is IporProtocolRouterAbstract { return servicesCfg.ammCloseSwapService; } else if ( _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceWstEthBaseV1.provideLiquidityWstEth.selector) || - _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceWstEthBaseV1.redeemFromAmmPoolWstEth.selector) + _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceWstEthBaseV1.redeemFromAmmPoolWstEth.selector) || + _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceWstEthBaseV2.rebalanceBetweenAmmTreasuryAndAssetManagementWstEth.selector) ) { if (batchOperation == 0) { _nonReentrantBefore(); @@ -128,7 +130,8 @@ contract IporProtocolRouterBase is IporProtocolRouterAbstract { return servicesCfg.ammPoolsService; } else if ( _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceUsdcBaseV1.provideLiquidityUsdcToAmmPoolUsdc.selector) || - _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceUsdcBaseV1.redeemFromAmmPoolUsdc.selector) + _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceUsdcBaseV1.redeemFromAmmPoolUsdc.selector) || + _checkFunctionSigAndIsNotPause(sig, IAmmPoolsServiceUsdcBaseV1.rebalanceBetweenAmmTreasuryAndAssetManagementUsdc.selector) ) { if (batchOperation == 0) { _nonReentrantBefore(); diff --git a/contracts/interfaces/IAmmGovernanceService.sol b/contracts/interfaces/IAmmGovernanceService.sol index 6ea0b18c2..e5a36e659 100644 --- a/contracts/interfaces/IAmmGovernanceService.sol +++ b/contracts/interfaces/IAmmGovernanceService.sol @@ -56,7 +56,7 @@ interface IAmmGovernanceService { /// @param asset Address of asset representing specific pool /// @param newMaxLiquidityPoolBalance New max liquidity pool balance threshold. Value represented WITHOUT 18 decimals. /// @param newAutoRebalanceThreshold New auto rebalance threshold (for stablecoins represented in thousands). Value represented WITHOUT 18 decimals. For stablecoins value represents multiplication of 1000. - /// @param newAmmTreasuryAndAssetManagementRatio New AMM Treasury and Asset Management ratio, represented WITHOUT 18 decimals, value represents percentage with 2 decimals. Example: 65% = 6500, 99,99% = 9999 + /// @param newAmmTreasuryAndAssetManagementRatio New AMM Treasury and Asset Management ratio, represented WITHOUT 18 decimals, value represents percentage with 2 decimals. Example: 65% = 6500, 99,99% = 9999. The value determines what percentage of total funds remains in AMM treasury, while the rest goes to asset management. function setAmmPoolsParams( address asset, uint32 newMaxLiquidityPoolBalance, diff --git a/test/amm/AmmClosingSwaps.t.sol b/test/amm/AmmClosingSwaps.t.sol index 16b45dcc2..9738084dd 100644 --- a/test/amm/AmmClosingSwaps.t.sol +++ b/test/amm/AmmClosingSwaps.t.sol @@ -17,6 +17,7 @@ contract AmmClosingSwaps is TestCommons, DataUtils { IporProtocolFactory.IporProtocolConfig private _cfg; function setUp() public { + _admin = address(this); _buyer = _getUserAddress(1); _community = _getUserAddress(2); @@ -30,6 +31,89 @@ contract AmmClosingSwaps is TestCommons, DataUtils { _cfg.spread28DaysTestCase = BuilderUtils.Spread28DaysTestCase.CASE0; } + /** +* @dev This test verifies that a swap cannot be closed by a legitimate liquidator + * if the swap beneficiary is a blacklisted address. + * + * Conditions: + * 1. Swap asset: USDT or USDC. + * 2. Swap beneficiary: Blacklisted address. + * + * Run this test using: + * ``` + * forge test -vvvv --match-test testCannotCloseSwapByLiquidatorAfterMaturity + * ``` + * + * Test files: https://drive.google.com/file/d/1TjVhquYDqCowjRs_NLMdNF8-nkZGvm3D/view?usp=sharing + * + */ + function testCannotClosePayFixedAsLiquidatorAfterMaturity() public { + //given + _iporProtocol = _iporProtocolFactory.getUsdtInstance(_cfg); + MockTestnetToken asset = _iporProtocol.asset; + + uint256 liquidityAmount = 1_000_000 * 1e6; + uint256 totalAmount = 10_000 * 1e6; + uint256 acceptableFixedInterestRate = 10 * 10 ** 16; + uint256 leverage = 100 * 10 ** 18; + + // + // This address is a banned address from https://dune.com/phabc/usdt---banned-addresses + // + address maliciousSwapBeneficiary = 0xcaCa5575eB423183bA4B6EE3aA9fc2cB488aEEEE; + + asset.addToBlackList(maliciousSwapBeneficiary); + + asset.approve(address(_iporProtocol.router), liquidityAmount); + _iporProtocol.ammPoolsService.provideLiquidityUsdt(_admin, liquidityAmount); + + asset.transfer(_buyer, totalAmount); + + vm.prank(_buyer); + asset.approve(address(_iporProtocol.router), totalAmount); + + uint256 buyerBalanceBefore = _iporProtocol.asset.balanceOf(_buyer); + uint256 adminBalanceBefore = _iporProtocol.asset.balanceOf(_admin); + uint256 liquidatorBalanceBefore = _iporProtocol.asset.balanceOf(_liquidator); + + vm.startPrank(_buyer); + uint256 swapId = _iporProtocol.ammOpenSwapService.openSwapPayFixed28daysUsdt( + maliciousSwapBeneficiary, // @audit: Malicious user passes a banned address here. + totalAmount, + acceptableFixedInterestRate, + leverage, + getRiskIndicatorsInputs(0) + ); + vm.stopPrank(); + + vm.warp(100 + 28 days + 1 seconds); + + _iporProtocol.ammGovernanceService.addSwapLiquidator(address(_iporProtocol.asset), _liquidator); + + uint256[] memory swapPfIds = new uint256[](1); + swapPfIds[0] = 1; + uint256[] memory swapRfIds = new uint256[](0); + + //when + vm.startPrank(_liquidator); + _iporProtocol.ammCloseSwapServiceUsdt.closeSwapsUsdt( + _liquidator, + swapPfIds, + swapRfIds, + getCloseRiskIndicatorsInputs(address(_iporProtocol.asset), IporTypes.SwapTenor.DAYS_28) + ); + vm.stopPrank(); + + //then + uint256 buyerBalanceAfter = _iporProtocol.asset.balanceOf(_buyer); + uint256 adminBalanceAfter = _iporProtocol.asset.balanceOf(_admin); + uint256 liquidatorBalanceAfter = _iporProtocol.asset.balanceOf(_liquidator); + + // assertEq(buyerBalanceBefore - buyerBalanceAfter, 73075873); + // assertEq(adminBalanceAfter - adminBalanceBefore, 0); + assertEq(liquidatorBalanceAfter - liquidatorBalanceBefore, 25000000); + } + function testShouldAddSwapLiquidatorAsIporOwner() public { //given _iporProtocol = _iporProtocolFactory.getUsdtInstance(_cfg); diff --git a/test/chain-base/BaseForkAmmWstEthCloseSwaps.t.sol b/test/chain-base/BaseForkAmmWstEthCloseSwaps.t.sol new file mode 100644 index 000000000..ca1c9ca52 --- /dev/null +++ b/test/chain-base/BaseForkAmmWstEthCloseSwaps.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.26; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "forge-std/console2.sol"; +import "./BaseTestForkCommons.sol"; +import {IAmmCloseSwapServiceWstEth} from "../../contracts/interfaces/IAmmCloseSwapServiceWstEth.sol"; +import {AmmTypes} from "../../contracts/interfaces/types/AmmTypes.sol"; +import {AmmStorageBaseV1} from "../../contracts/base/amm/AmmStorageBaseV1.sol"; +import {IAmmPoolsServiceWstEthBaseV2} from "../../contracts/base/amm-wstEth/interfaces/IAmmPoolsServiceWstEthBaseV2.sol"; + +contract BaseForkAmmWstEthCloseSwapsTest is BaseTestForkCommons { + function setUp() public { + vm.createSelectFork(vm.envString("BASE_PROVIDER_URL"), 29096300); + } + + function testShouldClosePositionWstEthForWstEth28daysPayFixed() public { + //given + _init(); + address user = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb; + _setupUser(user, 1000 * 1e18); + uint256 totalAmount = 1 * 1e17; + + vm.warp(block.timestamp); + + AmmTypes.RiskIndicatorsInputs memory riskIndicatorsInputs = AmmTypes.RiskIndicatorsInputs({ + maxCollateralRatio: 50000000000000000, + maxCollateralRatioPerLeg: 50000000000000000, + maxLeveragePerLeg: 1000000000000000000000, + baseSpreadPerLeg: 3695000000000000, + fixedRateCapPerLeg: 20000000000000000, + demandSpreadFactor: 20, + expiration: block.timestamp + 1000, + signature: bytes("0x00") + }); + + riskIndicatorsInputs.signature = signRiskParams( + riskIndicatorsInputs, + wstETH, + uint256(IporTypes.SwapTenor.DAYS_28), + 0, + messageSignerPrivateKey + ); + + vm.prank(user); + uint256 swapId = IAmmOpenSwapServiceWstEth(iporProtocolRouterProxy).openSwapPayFixed28daysWstEth( + user, + wstETH, + totalAmount, + 1e18, + 10e18, + riskIndicatorsInputs + ); + + uint256[] memory swapPfIds = new uint256[](1); + swapPfIds[0] = swapId; + uint256[] memory swapRfIds = new uint256[](0); + + vm.warp(block.timestamp + 28 days + 1); + + AmmTypes.CloseSwapRiskIndicatorsInput memory closeRiskIndicatorsInputs = _prepareCloseSwapRiskIndicators( + IporTypes.SwapTenor.DAYS_28 + ); + + //when + vm.prank(user); + IAmmCloseSwapServiceWstEth(iporProtocolRouterProxy).closeSwapsWstEth( + user, + swapPfIds, + swapRfIds, + closeRiskIndicatorsInputs + ); + + //then + AmmTypesBaseV1.Swap memory swap = AmmStorageBaseV1(ammStorageWstEthProxy).getSwap( + AmmTypes.SwapDirection.PAY_FIXED_RECEIVE_FLOATING, + swapId + ); + + assertEq(user, swap.buyer, "swap.buyer"); + assertEq(0, uint256(swap.state), "swap.state"); + } + + function testShouldDepositToAssetManagementWstEth() public { + //given + _init(); + _setupUser(owner, 1000 * 1e18); + + // Transfer wstETH from Lido treasury to owner + vm.prank(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + IERC20(wstETH).transfer(owner, 1000 * 1e18); + + // First, transfer wstETH to the AMM treasury to ensure it has sufficient balance + vm.prank(owner); + IERC20(wstETH).transfer(ammTreasuryWstEthProxy, 100 * 1e18); + + uint256 assetManagementBalanceBefore = IERC4626(iporPlasmaVaultWstEth).totalAssets(); + + // when + vm.prank(owner); + IAmmGovernanceService(iporProtocolRouterProxy).depositToAssetManagement(wstETH, 100 * 1e18); + + //then + uint256 assetManagementBalanceAfter = IERC4626(iporPlasmaVaultWstEth).totalAssets(); + + assertGt( + assetManagementBalanceAfter, + assetManagementBalanceBefore, + "Asset management balance should increase after deposit" + ); + } + + function testShouldWithdrawFromAssetManagementWstEth() public { + //given + _init(); + _setupUser(owner, 1000 * 1e18); + + // Transfer wstETH from Lido treasury to owner + vm.prank(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb); + IERC20(wstETH).transfer(owner, 1000 * 1e18); + + // First, transfer wstETH to the AMM treasury to ensure it has sufficient balance + vm.prank(owner); + IERC20(wstETH).transfer(ammTreasuryWstEthProxy, 100 * 1e18); + + // First deposit some wstETH to asset management + vm.prank(owner); + IAmmGovernanceService(iporProtocolRouterProxy).depositToAssetManagement(wstETH, 100 * 1e18); + + vm.warp(block.timestamp + 1 seconds); + + uint256 assetManagementBalanceBefore = IERC4626(iporPlasmaVaultWstEth).totalAssets(); + + // when + vm.prank(owner); + IAmmGovernanceService(iporProtocolRouterProxy).withdrawFromAssetManagement(wstETH, 50 * 1e18); + + //then + uint256 assetManagementBalanceAfter = IERC4626(iporPlasmaVaultWstEth).totalAssets(); + + assertLt( + assetManagementBalanceAfter, + assetManagementBalanceBefore, + "Asset management balance should decrease after withdrawal" + ); + } + + function testShouldRebalanceBetweenAmmTreasuryAndAssetManagementWstEth() public { + //given + _init(); + + // Record balances before rebalancing + uint256 ammTreasuryBalanceBefore = IERC20(wstETH).balanceOf(ammTreasuryWstEthProxy); + uint256 assetManagementBalanceBefore = IERC4626(iporPlasmaVaultWstEth).totalAssets(); + + // Add the test contract as an appointed account to rebalance + vm.prank(owner); + IAmmGovernanceService(iporProtocolRouterProxy).addAppointedToRebalanceInAmm(wstETH, address(this)); + + // Set AMM pools parameters for rebalancing + vm.prank(owner); + IAmmGovernanceService(iporProtocolRouterProxy).setAmmPoolsParams( + wstETH, + 1000000000, // maxLiquidityPoolBalance + 1, // autoRebalanceThreshold + 100 // ammTreasuryAssetManagementRatio (15%) + ); + //when + IAmmPoolsServiceWstEthBaseV2(iporProtocolRouterProxy).rebalanceBetweenAmmTreasuryAndAssetManagementWstEth(); + + //then + uint256 ammTreasuryBalanceAfter = IERC20(wstETH).balanceOf(ammTreasuryWstEthProxy); + uint256 assetManagementBalanceAfter = IERC4626(iporPlasmaVaultWstEth).totalAssets(); + + // Verify that the asset management (plasma vault) balance increased + assertGt( + assetManagementBalanceAfter, + assetManagementBalanceBefore, + "Asset management balance should be higher after rebalancing" + ); + + console2.log("assetManagementBalanceAfter", assetManagementBalanceAfter); + console2.log("assetManagementBalanceBefore", assetManagementBalanceBefore); + } +} diff --git a/test/chain-base/BaseTestForkCommons.sol b/test/chain-base/BaseTestForkCommons.sol new file mode 100644 index 000000000..2e357348b --- /dev/null +++ b/test/chain-base/BaseTestForkCommons.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import "forge-std/Test.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {IWETH9} from "../../contracts/amm-eth/interfaces/IWETH9.sol"; + +import "../../contracts/interfaces/IAmmCloseSwapLens.sol"; +import "../../contracts/chains/ethereum/amm-commons/AmmSwapsLens.sol"; +import "../../contracts/chains/arbitrum/amm-wstEth/AmmOpenSwapServiceWstEth.sol"; +import "../../contracts/chains/arbitrum/amm-wstEth/AmmCloseSwapServiceWstEth.sol"; +import "../../contracts/amm/AmmPoolsService.sol"; +import "../../contracts/chains/arbitrum/amm-commons/AmmCloseSwapLensArbitrum.sol"; +import "../../contracts/chains/arbitrum/amm-commons/AmmGovernanceServiceArbitrum.sol"; +import {StorageLibArbitrum} from "../../contracts/chains/arbitrum/libraries/StorageLibArbitrum.sol"; + +import {AmmTreasuryBaseV2} from "../../contracts/base/amm/AmmTreasuryBaseV2.sol"; + + +import {AmmPoolsServiceWstEthBaseV2} from "../../contracts/base/amm-wstEth/services/AmmPoolsServiceWstEthBaseV2.sol"; + +import {AmmCloseSwapServiceWstEthBaseV2} from "../../contracts/base/amm-wstEth/services/AmmCloseSwapServiceWstEthBaseV2.sol"; + +import {IporProtocolRouterBase} from "../../contracts/chains/base/router/IporProtocolRouterBase.sol"; + + +interface IAccessManager { + function grantRole(uint64 roleId, address account, uint32 executionDelay) external; +} + +contract BaseTestForkCommons is Test { + + address private _defaultAddress = address(0x1234); + address constant owner = address(0xF6a9bd8F6DC537675D499Ac1CA14f2c55d8b5569); + address constant treasurer = address(0x888); + + address constant wstETH = 0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452; + address constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address constant ipwstETH = 0xff7907CDCA84DB03f09702A4A49C262908AF48Af; + address constant iporOracleProxy = 0x85564fb392e18A84A64343A3FB65839206936C0f; + address constant spreadWstEth = 0x3D21ADf3b0Ff5B3fDfFC8D5FFa6634Bd65949924; + address constant ammSwapsLens = 0x6834BdFe5864c6B1703B999D04B092229A322943; + address constant ammPoolsLens = 0xa4989A9225f6DD130e8Ce4a4b5ef7902c8c389dc; + address constant ammCloseSwapLens = 0xB9C7A519BA2d6213F9E77f334b6aD8d8A2749CB9; + address constant ammGovernanceService = 0x498eB532c9D3b4Cf20351b8767Dceb4B5D28FE4c; + address constant iporProtocolRouterProxy = 0x21d337eBF86E584e614ecC18A2B1144D3C375918; + + address constant ammTreasuryWstEthProxy = 0x09388e18d5C331449C6eF636726dD1fd007b8DDf; + address constant ammStorageWstEthProxy = 0x29399D76921e23314Ae259Cf5E17116f48AE65b7; + + address constant iporPlasmaVaultWstEth = 0xFe8b23B493579e5c3a0A3BC5BBF20662B3072DE6; + address constant iporFusionAccessManagerWstEth = 0x3033C274D3Ccc8d12B4Ea567F6F94849507c37aE; + + uint64 constant WHITELIST_ROLE = 800; + + uint256 public messageSignerPrivateKey; + address public messageSignerAddress; + + address public iporProtocolRouterImpl; + + + address public ammPoolsServiceWstEth; + address public ammOpenSwapServiceWstEth; + address public ammCloseSwapServiceWstEth; + + function _init() internal { + messageSignerPrivateKey = 0x12341234; + messageSignerAddress = vm.addr(messageSignerPrivateKey); + + _upgradeAmmTreasury(); + + _createAmmPoolsServicesWstEth(); + + ammOpenSwapServiceWstEth = 0xFbE094Bcc8731fa45Eb88850592248e5D6aC9472; + + // _createAmmOpenSwapServiceWstEth(); + _createAmmCloseSwapServiceWstEth(); + _setupAssetServices(); + + _updateIporRouterImplementation(); + + _setupIporProtocol(); + + vm.prank(owner); + IAccessManager(iporFusionAccessManagerWstEth).grantRole(WHITELIST_ROLE, ammTreasuryWstEthProxy, 0); + + } + + function _updateIporRouterImplementation() internal { + IporProtocolRouterBase.DeployedContractsBase memory deployedContracts = IporProtocolRouterBase + .DeployedContractsBase({ + ammSwapsLens: ammSwapsLens, + ammPoolsLens: ammPoolsLens, + ammCloseSwapLens: ammCloseSwapLens, + ammGovernanceService: ammGovernanceService, + + liquidityMiningLens: _defaultAddress, + powerTokenLens: _defaultAddress, + flowService: _defaultAddress, + stakeService: _defaultAddress, + + wstEth: wstETH, + usdc: USDC + }); + + + iporProtocolRouterImpl = address(new IporProtocolRouterBase(deployedContracts)); + + vm.startPrank(owner); + IporProtocolRouterBase(payable(iporProtocolRouterProxy)).upgradeTo(iporProtocolRouterImpl); + vm.stopPrank(); + } + + function _setupIporProtocol() internal { + + vm.startPrank(owner); + + IAmmGovernanceServiceArbitrum(iporProtocolRouterProxy).setMessageSigner(messageSignerAddress); + + IAmmGovernanceServiceArbitrum(iporProtocolRouterProxy).setAmmGovernancePoolConfiguration(wstETH, StorageLibArbitrum.AssetGovernancePoolConfigValue({ + decimals: IERC20MetadataUpgradeable(wstETH).decimals(), + ammStorage: ammStorageWstEthProxy, + ammTreasury: ammTreasuryWstEthProxy, + ammVault: iporPlasmaVaultWstEth, + ammPoolsTreasury: treasurer, + ammPoolsTreasuryManager: treasurer, + ammCharlieTreasury: treasurer, + ammCharlieTreasuryManager: treasurer + } + )); + + IAmmGovernanceServiceArbitrum(iporProtocolRouterProxy).setAssetLensData(wstETH, StorageLibArbitrum.AssetLensDataValue({ + decimals: IERC20MetadataUpgradeable(wstETH).decimals(), + ipToken: ipwstETH, + ammStorage: ammStorageWstEthProxy, + ammTreasury: ammTreasuryWstEthProxy, + ammVault: iporPlasmaVaultWstEth, + spread: spreadWstEth + })); + + IAmmGovernanceServiceArbitrum(iporProtocolRouterProxy).setAssetServices(wstETH, StorageLibArbitrum.AssetServicesValue({ + ammPoolsService: ammPoolsServiceWstEth, + ammOpenSwapService: ammOpenSwapServiceWstEth, + ammCloseSwapService: ammCloseSwapServiceWstEth + })); + + vm.stopPrank(); + + } + + function _setupAssetServices() internal { + vm.startPrank(0xF6a9bd8F6DC537675D499Ac1CA14f2c55d8b5569); + IAmmGovernanceServiceArbitrum(iporProtocolRouterProxy).setAssetServices(wstETH, StorageLibArbitrum.AssetServicesValue({ + ammPoolsService: ammPoolsServiceWstEth, + ammOpenSwapService: ammOpenSwapServiceWstEth, + ammCloseSwapService: ammCloseSwapServiceWstEth + })); + vm.stopPrank(); + } + + function _setupUser(address user, uint256 value) internal { + deal(user, 1_000_000e18); + + vm.startPrank(user); + IWETH9(wstETH).approve(iporProtocolRouterProxy, type(uint256).max); + vm.stopPrank(); + + } + + function _getUserAddress(uint256 number) internal returns (address) { + return vm.rememberKey(number); + } + + + function _createAmmPoolsServicesWstEth() private { + + ammPoolsServiceWstEth = address( + new AmmPoolsServiceWstEthBaseV2({ + asset_: wstETH, + ipToken_: ipwstETH, + ammTreasury_: ammTreasuryWstEthProxy, + ammStorage_: ammStorageWstEthProxy, + ammAssetManagement_: iporPlasmaVaultWstEth, + iporOracle_: iporOracleProxy, + iporProtocolRouter_: iporProtocolRouterProxy, + redeemFeeRate_: 5 * 1e15, + autoRebalanceThresholdMultiplier_ : 1 + }) + ); + + } + + + function _upgradeAmmTreasury() private { + + address ammTreasuryWstEthImpl = address(new AmmTreasuryBaseV2(wstETH, iporProtocolRouterProxy, ammStorageWstEthProxy, iporPlasmaVaultWstEth)); + + vm.prank(0xF6a9bd8F6DC537675D499Ac1CA14f2c55d8b5569); + AmmTreasuryBaseV2(ammTreasuryWstEthProxy).upgradeTo(ammTreasuryWstEthImpl); + + } + + + + // function _createAmmOpenSwapServiceWstEth() private { + // ammOpenSwapServiceWstEth = address( + // new AmmOpenSwapServiceWstEth({ + // poolCfg: AmmTypesBaseV1.AmmOpenSwapServicePoolConfiguration({ + // asset: wstETH, + // decimals: IERC20MetadataUpgradeable(wstETH).decimals(), + // ammStorage: ammStorageWstEthProxy, + // ammTreasury: ammTreasuryWstEthProxy, + // spread: spreadWstEth, + // iporPublicationFee: 10 * 1e15, + // maxSwapCollateralAmount: 100_000 * 1e18, + // liquidationDepositAmount: 1000, + // minLeverage: 10 * 1e18, + // openingFeeRate: 5e14, + // openingFeeTreasuryPortionRate: 5e17 + // }), + // iporOracle_: iporOracleProxy + // }) + // ); + // } + + function _createAmmCloseSwapServiceWstEth() private { + ammCloseSwapServiceWstEth = address( + new AmmCloseSwapServiceWstEthBaseV2({ + poolCfg: IAmmCloseSwapLens.AmmCloseSwapServicePoolConfiguration({ + asset: wstETH, + decimals: IERC20MetadataUpgradeable(wstETH).decimals(), + ammStorage: ammStorageWstEthProxy, + ammTreasury: ammTreasuryWstEthProxy, + assetManagement: iporPlasmaVaultWstEth, + spread: spreadWstEth, + unwindingFeeTreasuryPortionRate: 25e16, + unwindingFeeRate: 5 * 1e11, + maxLengthOfLiquidatedSwapsPerLeg: 10, + timeBeforeMaturityAllowedToCloseSwapByCommunity: 1 hours, + timeBeforeMaturityAllowedToCloseSwapByBuyerTenor28days: 1 days, + timeBeforeMaturityAllowedToCloseSwapByBuyerTenor60days: 2 days, + timeBeforeMaturityAllowedToCloseSwapByBuyerTenor90days: 3 days, + minLiquidationThresholdToCloseBeforeMaturityByCommunity: 995 * 1e15, + minLiquidationThresholdToCloseBeforeMaturityByBuyer: 99 * 1e16, + minLeverage: 10 * 1e18, + timeAfterOpenAllowedToCloseSwapWithUnwindingTenor28days: 1 days, + timeAfterOpenAllowedToCloseSwapWithUnwindingTenor60days: 2 days, + timeAfterOpenAllowedToCloseSwapWithUnwindingTenor90days: 3 days + }), + iporOracle_: iporOracleProxy + }) + ); + } + + function _getCurrentTimestamps( + address[] memory assets + ) internal view returns (uint32[] memory lastUpdateTimestamps) { + lastUpdateTimestamps = new uint32[](assets.length); + + uint32 lastUpdateTimestamp = uint32(block.timestamp); + + for (uint256 i = 0; i < assets.length; i++) { + lastUpdateTimestamps[i] = lastUpdateTimestamp; + } + } + + function signRiskParams( + AmmTypes.RiskIndicatorsInputs memory riskParamsInput, + address asset, + uint256 tenor, + uint256 direction, + uint256 privateKey + ) internal pure returns (bytes memory) { + // create digest: keccak256 gives us the first 32bytes after doing the hash + // so this is always 32 bytes. + bytes32 digest = keccak256( + abi.encodePacked( + riskParamsInput.maxCollateralRatio, + riskParamsInput.maxCollateralRatioPerLeg, + riskParamsInput.maxLeveragePerLeg, + riskParamsInput.baseSpreadPerLeg, + riskParamsInput.fixedRateCapPerLeg, + riskParamsInput.demandSpreadFactor, + riskParamsInput.expiration, + asset, + tenor, + direction + ) + ); + // r and s are the outputs of the ECDSA signature + // r,s and v are packed into the signature. It should be 65 bytes: 32 + 32 + 1 + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + + // pack v, r, s into 65bytes signature + // bytes memory signature = abi.encodePacked(r, s, v); + return abi.encodePacked(r, s, v); + } + + function _prepareCloseSwapRiskIndicators( + IporTypes.SwapTenor tenor + ) internal view returns (AmmTypes.CloseSwapRiskIndicatorsInput memory closeRiskIndicatorsInputs) { + AmmTypes.RiskIndicatorsInputs memory riskIndicatorsInputsPayFixed = AmmTypes.RiskIndicatorsInputs({ + maxCollateralRatio: 50000000000000000, + maxCollateralRatioPerLeg: 25000000000000000, + maxLeveragePerLeg: 1000000000000000000000, + baseSpreadPerLeg: 3695000000000000, + fixedRateCapPerLeg: 20000000000000000, + demandSpreadFactor: 20, + expiration: block.timestamp + 1000, + signature: bytes("0x00") + }); + + AmmTypes.RiskIndicatorsInputs memory riskIndicatorsInputsReceiveFixed = AmmTypes.RiskIndicatorsInputs({ + maxCollateralRatio: 50000000000000000, + maxCollateralRatioPerLeg: 25000000000000000, + maxLeveragePerLeg: 1000000000000000000000, + baseSpreadPerLeg: 3695000000000000, + fixedRateCapPerLeg: 20000000000000000, + demandSpreadFactor: 20, + expiration: block.timestamp + 1000, + signature: bytes("0x00") + }); + + riskIndicatorsInputsPayFixed.signature = signRiskParams( + riskIndicatorsInputsPayFixed, + address(wstETH), + uint256(tenor), + 0, + messageSignerPrivateKey + ); + riskIndicatorsInputsReceiveFixed.signature = signRiskParams( + riskIndicatorsInputsReceiveFixed, + address(wstETH), + uint256(tenor), + 1, + messageSignerPrivateKey + ); + + closeRiskIndicatorsInputs = AmmTypes.CloseSwapRiskIndicatorsInput({ + payFixed: riskIndicatorsInputsPayFixed, + receiveFixed: riskIndicatorsInputsReceiveFixed + }); + } + + function _prepareCloseSwapRiskIndicatorsHighFixedRateCaps( + IporTypes.SwapTenor tenor + ) internal view returns (AmmTypes.CloseSwapRiskIndicatorsInput memory closeRiskIndicatorsInputs) { + AmmTypes.RiskIndicatorsInputs memory riskIndicatorsInputsPayFixed = AmmTypes.RiskIndicatorsInputs({ + maxCollateralRatio: 50000000000000000, + maxCollateralRatioPerLeg: 25000000000000000, + maxLeveragePerLeg: 1000000000000000000000, + baseSpreadPerLeg: 3695000000000000, + fixedRateCapPerLeg: 300000000000000000, /// @dev 30% + demandSpreadFactor: 20, + expiration: block.timestamp + 1000, + signature: bytes("0x00") + }); + + AmmTypes.RiskIndicatorsInputs memory riskIndicatorsInputsReceiveFixed = AmmTypes.RiskIndicatorsInputs({ + maxCollateralRatio: 50000000000000000, + maxCollateralRatioPerLeg: 25000000000000000, + maxLeveragePerLeg: 1000000000000000000000, + baseSpreadPerLeg: 3695000000000000, + fixedRateCapPerLeg: 300000000000000000, /// @dev 30% + demandSpreadFactor: 20, + expiration: block.timestamp + 1000, + signature: bytes("0x00") + }); + + riskIndicatorsInputsPayFixed.signature = signRiskParams( + riskIndicatorsInputsPayFixed, + address(wstETH), + uint256(tenor), + 0, + messageSignerPrivateKey + ); + riskIndicatorsInputsReceiveFixed.signature = signRiskParams( + riskIndicatorsInputsReceiveFixed, + address(wstETH), + uint256(tenor), + 1, + messageSignerPrivateKey + ); + + closeRiskIndicatorsInputs = AmmTypes.CloseSwapRiskIndicatorsInput({ + payFixed: riskIndicatorsInputsPayFixed, + receiveFixed: riskIndicatorsInputsReceiveFixed + }); + } + + function getIndexToUpdate( + address asset, + uint indexValue + ) internal pure returns (IIporOracle.UpdateIndexParams[] memory) { + IIporOracle.UpdateIndexParams[] memory updateIndexParams = new IIporOracle.UpdateIndexParams[](1); + updateIndexParams[0] = IIporOracle.UpdateIndexParams({ + asset: asset, + indexValue: indexValue, + updateTimestamp: 0, + quasiIbtPrice: 0 + }); + return updateIndexParams; + } +} diff --git a/test/mocks/tokens/MockTestnetToken.sol b/test/mocks/tokens/MockTestnetToken.sol index 30ee5e905..a10eaa35f 100644 --- a/test/mocks/tokens/MockTestnetToken.sol +++ b/test/mocks/tokens/MockTestnetToken.sol @@ -5,6 +5,8 @@ import "contracts/security/IporOwnable.sol"; contract MockTestnetToken is ERC20, IporOwnable { uint8 private _customDecimals; + mapping (address => bool) public isBlackListed; + constructor( string memory name, @@ -28,6 +30,30 @@ contract MockTestnetToken is ERC20, IporOwnable { _mint(account, amount); } + function addToBlackList(address account) external virtual onlyOwner { + isBlackListed[account] = true; + } + + function removeFromBlackList(address account) external virtual onlyOwner { + isBlackListed[account] = false; + } + + function transfer(address to, uint256 amount) public override returns (bool) { + if (isBlackListed[msg.sender]) { + revert("Blacklisted address"); + } + return super.transfer(to, amount); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + if (isBlackListed[from]) { + revert("Blacklisted address"); + } + return super.transferFrom(from, to, amount); + } + + + /// @dev used only for Compound Share Token function accrueInterest() public returns (uint256) {} }