diff --git a/script/Deployer.sol b/script/Deployer.sol index c56a3a4d..2f1680c8 100644 --- a/script/Deployer.sol +++ b/script/Deployer.sol @@ -32,6 +32,9 @@ contract Deployer is Script { PauseAdmin public pauseAdmin; DelayedAdmin public delayedAdmin; Gateway public gateway; + address public liquidityPoolFactory; + address public restrictionManagerFactory; + address public trancheTokenFactory; function deployInvestmentManager(address deployer) public { // If no salt is provided, a pseudo-random salt is generated, @@ -43,9 +46,9 @@ contract Deployer is Script { userEscrow = new UserEscrow(); root = new Root{salt: salt}(address(escrow), delay, deployer); - address liquidityPoolFactory = address(new LiquidityPoolFactory(address(root))); - address restrictionManagerFactory = address(new RestrictionManagerFactory(address(root))); - address trancheTokenFactory = address(new TrancheTokenFactory{salt: salt}(address(root), deployer)); + liquidityPoolFactory = address(new LiquidityPoolFactory(address(root))); + restrictionManagerFactory = address(new RestrictionManagerFactory(address(root))); + trancheTokenFactory = address(new TrancheTokenFactory{salt: salt}(address(root), deployer)); investmentManager = new InvestmentManager(address(escrow), address(userEscrow)); poolManager = new PoolManager(address(escrow), liquidityPoolFactory, restrictionManagerFactory, trancheTokenFactory); diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index c5ec1aa4..3eb2b70e 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -585,29 +585,6 @@ contract PoolManagerTest is TestSetup { assertEq(LiquidityPool(lPool_).balanceOf(address(escrow)), amount); } - function testLiquidityPoolMigration() public { - address oldLiquidityPool_ = deploySimplePool(); - - LiquidityPool oldLiquidityPool = LiquidityPool(oldLiquidityPool_); - uint64 poolId = oldLiquidityPool.poolId(); - bytes16 trancheId = oldLiquidityPool.trancheId(); - address currency = address(oldLiquidityPool.asset()); - - LiquidityPoolFactory newLiquidityPoolFactory = new LiquidityPoolFactory(address(root)); - - // rewire factory contracts - newLiquidityPoolFactory.rely(address(poolManager)); - poolManager.file("liquidityPoolFactory", address(newLiquidityPoolFactory)); - - // Remove old liquidity pool - poolManager.removeLiquidityPool(poolId, trancheId, currency); - assertEq(poolManager.getLiquidityPool(poolId, trancheId, currency), address(0)); - - // Deploy new liquidity pool - address newLiquidityPool = poolManager.deployLiquidityPool(poolId, trancheId, currency); - assertEq(poolManager.getLiquidityPool(poolId, trancheId, currency), newLiquidityPool); - } - // helpers function hasDuplicates(bytes16[4] calldata array) internal pure returns (bool) { uint256 length = array.length; diff --git a/test/migrations/InvestRedeemFlow.t.sol b/test/migrations/InvestRedeemFlow.t.sol new file mode 100644 index 00000000..d3c7de6e --- /dev/null +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "../TestSetup.t.sol"; +import {LiquidityPool} from "src/LiquidityPool.sol"; +import {MathLib} from "src/util/MathLib.sol"; + +contract InvestRedeemFlow is TestSetup { + using MathLib for uint256; + + uint8 internal constant PRICE_DECIMALS = 18; + + uint64 poolId; + bytes16 trancheId; + uint128 currencyId; + address lPool_; + uint256 investorCurrencyAmount; + + function setUp() public virtual override { + super.setUp(); + lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + poolId = lPool.poolId(); + trancheId = lPool.trancheId(); + currencyId = poolManager.currencyAddressToId(address(lPool.asset())); + + investorCurrencyAmount = 1000 * 10 ** erc20.decimals(); + deal(address(erc20), investor, investorCurrencyAmount); + centrifugeChain.updateMember(poolId, trancheId, investor, uint64(block.timestamp + 1000 days)); + removeDeployerAccess(address(router), address(this)); + } + + function verifyInvestAndRedeemFlow(uint64 poolId_, bytes16 trancheId_, address liquidityPool) public { + uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); + LiquidityPool lPool = LiquidityPool(liquidityPool); + + depositMint(poolId_, trancheId_, price, investorCurrencyAmount, lPool); + uint256 redeemAmount = lPool.balanceOf(investor); + + redeemWithdraw(poolId_, trancheId_, price, redeemAmount, lPool); + } + + function depositMint(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 currencyAmount, LiquidityPool lPool) + public + { + vm.prank(investor); + erc20.approve(address(lPool), currencyAmount); // add allowance + + vm.prank(investor); + lPool.requestDeposit(currencyAmount, investor); + + // ensure funds are locked in escrow + assertEq(erc20.balanceOf(address(escrow)), currencyAmount); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - currencyAmount); + + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectInvest for this user on cent chain + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); + uint128 trancheTokensPayout = currencyAmount.mulDiv( + 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down + ).toUint128(); + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectInvest( + poolId_, trancheId_, investor, _currencyId, uint128(currencyAmount), trancheTokensPayout, 0 + ); + + assertEq(lPool.maxMint(investor), trancheTokensPayout); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - currencyAmount); + + uint256 div = 2; + vm.prank(investor); + lPool.deposit(currencyAmount / div, investor); + + assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); + assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); + + uint256 maxMint = lPool.maxMint(investor); + vm.prank(investor); + lPool.mint(maxMint, investor); + + assertEq(lPool.balanceOf(investor), trancheTokensPayout); + assertLe(lPool.balanceOf(address(escrow)), 1); + assertLe(lPool.maxMint(investor), 1); + } + + function redeemWithdraw(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 tokenAmount, LiquidityPool lPool) + public + { + vm.prank(investor); + lPool.requestRedeem(tokenAmount, investor, investor); + + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectRedeem for this user on cent chain + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + uint128 currencyPayout = tokenAmount.mulDiv( + price, 10 ** (18 - erc20.decimals() + lPool.decimals()), MathLib.Rounding.Down + ).toUint128(); + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectRedeem( + poolId_, trancheId_, investor, _currencyId, currencyPayout, uint128(tokenAmount), 0 + ); + + assertEq(lPool.maxWithdraw(investor), currencyPayout); + assertEq(lPool.maxRedeem(investor), tokenAmount); + assertEq(lPool.balanceOf(address(escrow)), 0); + + uint128 div = 2; + vm.prank(investor); + lPool.redeem(tokenAmount / div, investor, investor); + assertEq(lPool.balanceOf(investor), 0); + assertEq(lPool.balanceOf(address(escrow)), 0); + assertEq(erc20.balanceOf(investor), currencyPayout / div); + assertEq(lPool.maxWithdraw(investor), currencyPayout / div); + assertEq(lPool.maxRedeem(investor), tokenAmount / div); + + uint256 maxWithdraw = lPool.maxWithdraw(investor); + vm.prank(investor); + lPool.withdraw(maxWithdraw, investor, investor); + assertEq(lPool.balanceOf(investor), 0); + assertEq(lPool.balanceOf(address(escrow)), 0); + assertEq(erc20.balanceOf(investor), currencyPayout); + assertEq(lPool.maxWithdraw(investor), 0); + assertEq(lPool.maxRedeem(investor), 0); + } +} diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol new file mode 100644 index 00000000..ba075f03 --- /dev/null +++ b/test/migrations/MigratedAdmin.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import { + MigratedDelayedAdmin, MigratedPauseAdmin, DelayedAdmin, PauseAdmin +} from "./migrationContracts/MigratedAdmin.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +contract MigratedAdmin is InvestRedeemFlow { + function setUp() public override { + super.setUp(); + } + + function testDelayedAdminMigration() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // Deploy new PauseAdmin + MigratedPauseAdmin newPauseAdmin = new MigratedPauseAdmin(address(root)); + + // Deploy new DelayedAdmin + MigratedDelayedAdmin newDelayedAdmin = new MigratedDelayedAdmin(address(root), address(newPauseAdmin)); + + // Rewire contracts + root.rely(address(newDelayedAdmin)); + root.rely(address(newPauseAdmin)); + newPauseAdmin.rely(address(newDelayedAdmin)); + root.deny(address(delayedAdmin)); + root.deny(address(pauseAdmin)); + + // clean up + newPauseAdmin.deny(address(this)); + newDelayedAdmin.deny(address(this)); + root.deny(address(this)); + + // verify permissions + verifyMigratedAdminPermissions(delayedAdmin, newDelayedAdmin, pauseAdmin, newPauseAdmin); + } + + function verifyMigratedAdminPermissions( + DelayedAdmin oldDelayedAdmin, + DelayedAdmin newDelayedAdmin, + PauseAdmin oldPauseAdmin, + PauseAdmin newPauseAdmin + ) public { + // verify permissions + assertTrue(address(oldDelayedAdmin) != address(newDelayedAdmin)); + assertTrue(address(oldPauseAdmin) != address(newPauseAdmin)); + assertEq(root.wards(address(newDelayedAdmin)), 1); + assertEq(root.wards(address(oldDelayedAdmin)), 0); + assertEq(root.wards(address(newPauseAdmin)), 1); + assertEq(root.wards(address(oldPauseAdmin)), 0); + assertEq(newPauseAdmin.wards(address(newDelayedAdmin)), 1); + assertEq(newPauseAdmin.wards(address(oldDelayedAdmin)), 0); + + // verify dependencies + assertEq(address(newDelayedAdmin.pauseAdmin()), address(newPauseAdmin)); + } +} diff --git a/test/migrations/MigratedGateway.t.sol b/test/migrations/MigratedGateway.t.sol new file mode 100644 index 00000000..773c3769 --- /dev/null +++ b/test/migrations/MigratedGateway.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedGateway, Gateway} from "./migrationContracts/MigratedGateway.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +contract MigratedGatewayTest is InvestRedeemFlow { + function setUp() public override { + super.setUp(); + } + + function testGatewayMigration() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // Deploy new Gateway + MigratedGateway newGateway = + new MigratedGateway(address(root), address(investmentManager), address(poolManager), address(router)); + + // Rewire contracts + newGateway.rely(address(root)); + root.relyContract(address(investmentManager), address(this)); + investmentManager.file("gateway", address(newGateway)); + root.relyContract(address(poolManager), address(this)); + poolManager.file("gateway", address(newGateway)); + root.relyContract(address(router), address(this)); + router.file("gateway", address(newGateway)); + + // clean up + newGateway.deny(address(this)); + root.denyContract(address(investmentManager), address(this)); + root.denyContract(address(poolManager), address(this)); + root.denyContract(address(router), address(this)); + root.deny(address(this)); + + // verify permissions + verifyMigratedGatewayPermissions(gateway, newGateway); + + // test that everything is working + gateway = newGateway; + verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); + } + + function verifyMigratedGatewayPermissions(Gateway oldGateway, Gateway newGateway) public { + // verify permissions + assertTrue(address(oldGateway) != address(newGateway)); + assertEq(newGateway.wards(address(root)), 1); + assertEq(address(investmentManager.gateway()), address(newGateway)); + assertEq(address(poolManager.gateway()), address(newGateway)); + assertEq(address(router.gateway()), address(newGateway)); + + // verify dependencies + assertEq(address(oldGateway.investmentManager()), address(newGateway.investmentManager())); + assertEq(address(oldGateway.poolManager()), address(newGateway.poolManager())); + assertEq(address(oldGateway.root()), address(newGateway.root())); + } +} diff --git a/test/migrations/MigratedInvestmentManager.t.sol b/test/migrations/MigratedInvestmentManager.t.sol new file mode 100644 index 00000000..eb662116 --- /dev/null +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedInvestmentManager, InvestmentManager} from "./migrationContracts/MigratedInvestmentManager.sol"; +import {LiquidityPool} from "src/LiquidityPool.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +interface AuthLike { + function rely(address) external; + function deny(address) external; +} + +contract MigratedInvestmentManagerTest is InvestRedeemFlow { + function setUp() public override { + super.setUp(); + } + + function testInvestmentManagerMigration() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // Collect all investors and liquidityPools + // Assume these records are available off-chain + address[] memory investors = new address[](1); + investors[0] = investor; + address[] memory liquidityPools = new address[](1); + liquidityPools[0] = lPool_; + + // Deploy new MigratedInvestmentManager + MigratedInvestmentManager newInvestmentManager = + new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); + + verifyMigratedInvestmentManagerState(investors, liquidityPools, investmentManager, newInvestmentManager); + + // Rewire contracts + root.relyContract(address(gateway), address(this)); + gateway.file("investmentManager", address(newInvestmentManager)); + root.relyContract(address(poolManager), address(this)); + poolManager.file("investmentManager", address(newInvestmentManager)); + newInvestmentManager.rely(address(root)); + newInvestmentManager.rely(address(poolManager)); + root.relyContract(address(escrow), address(this)); + escrow.rely(address(newInvestmentManager)); + escrow.deny(address(investmentManager)); + root.relyContract(address(userEscrow), address(this)); + userEscrow.rely(address(newInvestmentManager)); + userEscrow.deny(address(investmentManager)); + newInvestmentManager.file("poolManager", address(poolManager)); + newInvestmentManager.file("gateway", address(gateway)); + + // file investmentManager on all LiquidityPools + for (uint256 i = 0; i < liquidityPools.length; i++) { + root.relyContract(liquidityPools[i], address(this)); + + LiquidityPool(liquidityPools[i]).file("manager", address(newInvestmentManager)); + LiquidityPool(liquidityPools[i]).rely(address(newInvestmentManager)); + LiquidityPool(liquidityPools[i]).deny(address(investmentManager)); + root.relyContract(address(LiquidityPool(liquidityPools[i]).share()), address(this)); + AuthLike(address(LiquidityPool(liquidityPools[i]).share())).rely(address(newInvestmentManager)); + AuthLike(address(LiquidityPool(liquidityPools[i]).share())).deny(address(investmentManager)); + newInvestmentManager.rely(address(LiquidityPool(liquidityPools[i]))); + escrow.approve(address(LiquidityPool(liquidityPools[i])), address(newInvestmentManager), type(uint256).max); + escrow.approve(address(LiquidityPool(liquidityPools[i])), address(investmentManager), 0); + } + + // clean up + newInvestmentManager.deny(address(this)); + root.denyContract(address(newInvestmentManager), address(this)); + root.denyContract(address(gateway), address(this)); + root.denyContract(address(poolManager), address(this)); + root.denyContract(address(escrow), address(this)); + root.denyContract(address(userEscrow), address(this)); + root.deny(address(this)); + + verifyMigratedInvestmentManagerPermissions(investmentManager, newInvestmentManager); + + investmentManager = newInvestmentManager; + verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); + } + + function verifyMigratedInvestmentManagerPermissions( + InvestmentManager oldInvestmentManager, + InvestmentManager newInvestmentManager + ) public { + // Verify permissions + assertTrue(address(oldInvestmentManager) != address(newInvestmentManager)); + assertEq(address(gateway.investmentManager()), address(newInvestmentManager)); + assertEq(address(poolManager.investmentManager()), address(newInvestmentManager)); + assertEq(newInvestmentManager.wards(address(root)), 1); + assertEq(newInvestmentManager.wards(address(poolManager)), 1); + assertEq(escrow.wards(address(newInvestmentManager)), 1); + assertEq(escrow.wards(address(oldInvestmentManager)), 0); + assertEq(userEscrow.wards(address(newInvestmentManager)), 1); + assertEq(userEscrow.wards(address(oldInvestmentManager)), 0); + + // Verify dependencies + assertEq(address(oldInvestmentManager.gateway()), address(newInvestmentManager.gateway())); + assertEq(address(oldInvestmentManager.poolManager()), address(newInvestmentManager.poolManager())); + assertEq(address(oldInvestmentManager.escrow()), address(newInvestmentManager.escrow())); + assertEq(address(oldInvestmentManager.userEscrow()), address(newInvestmentManager.userEscrow())); + } + + // --- State Verification Helpers --- + + function verifyMigratedInvestmentManagerState( + address[] memory investors, + address[] memory liquidityPools, + InvestmentManager investmentManager, + InvestmentManager newInvestmentManager + ) public { + for (uint256 i = 0; i < investors.length; i++) { + for (uint256 j = 0; j < liquidityPools.length; j++) { + verifyMintDepositWithdraw(investors[i], liquidityPools[j], investmentManager, newInvestmentManager); + verifyRedeemAndRemainingOrders(investors[i], liquidityPools[j], investmentManager, newInvestmentManager); + } + } + } + + function verifyMintDepositWithdraw( + address investor, + address liquidityPool, + InvestmentManager investmentManager, + InvestmentManager newInvestmentManager + ) public { + (uint128 newMaxMint, uint256 newDepositPrice, uint128 newMaxWithdraw,,,,) = + newInvestmentManager.investments(investor, liquidityPool); + (uint128 oldMaxMint, uint256 oldDepositPrice, uint128 oldMaxWithdraw,,,,) = + investmentManager.investments(investor, liquidityPool); + assertEq(newMaxMint, oldMaxMint); + assertEq(newDepositPrice, oldDepositPrice); + assertEq(newMaxWithdraw, oldMaxWithdraw); + } + + function verifyRedeemAndRemainingOrders( + address investor, + address liquidityPool, + InvestmentManager investmentManager, + InvestmentManager newInvestmentManager + ) public { + ( + , + , + , + uint256 newRedeemPrice, + uint128 newRemainingDepositRequest, + uint128 newRemainingRedeemRequest, + bool newExists + ) = newInvestmentManager.investments(investor, liquidityPool); + ( + , + , + , + uint256 oldRedeemPrice, + uint128 oldRemainingDepositRequest, + uint128 oldRemainingRedeemRequest, + bool oldExists + ) = investmentManager.investments(investor, liquidityPool); + assertEq(newRedeemPrice, oldRedeemPrice); + assertEq(newRemainingDepositRequest, oldRemainingDepositRequest); + assertEq(newRemainingRedeemRequest, oldRemainingRedeemRequest); + assertEq(newExists, oldExists); + } +} diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol new file mode 100644 index 00000000..f9493fb9 --- /dev/null +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedLiquidityPool, LiquidityPool} from "./migrationContracts/MigratedLiquidityPool.sol"; +import {MigratedPoolManager, PoolManager} from "./migrationContracts/MigratedPoolManager.sol"; +import {LiquidityPoolFactory, TrancheTokenFactory} from "src/util/Factory.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +interface TrancheTokenLike { + function rely(address usr) external; + function deny(address usr) external; + function restrictionManager() external view returns (address); + function addTrustedForwarder(address forwarder) external; + function removeTrustedForwarder(address forwarder) external; + function trustedForwarders(address) external view returns (bool); + function wards(address) external view returns (uint256); + function allowance(address, address) external view returns (uint256); +} + +interface AuthLike { + function rely(address) external; + function deny(address) external; +} + +contract MigrationsTest is InvestRedeemFlow { + function setUp() public override { + super.setUp(); + } + + function testLiquidityPoolMigration() public { + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + LiquidityPool oldLiquidityPool = LiquidityPool(lPool_); + uint64 poolId = oldLiquidityPool.poolId(); + bytes16 trancheId = oldLiquidityPool.trancheId(); + address currency = address(oldLiquidityPool.asset()); + + LiquidityPoolFactory newLiquidityPoolFactory = new LiquidityPoolFactory(address(root)); + newLiquidityPoolFactory.rely(address(root)); + + // rewire factory contracts + newLiquidityPoolFactory.rely(address(poolManager)); + root.relyContract(address(poolManager), address(this)); + poolManager.file("liquidityPoolFactory", address(newLiquidityPoolFactory)); + + // Remove old liquidity pool + poolManager.removeLiquidityPool(poolId, trancheId, currency); + assertEq(poolManager.getLiquidityPool(poolId, trancheId, currency), address(0)); + + // Deploy new liquidity pool + address newLiquidityPool = poolManager.deployLiquidityPool(poolId, trancheId, currency); + assertEq(poolManager.getLiquidityPool(poolId, trancheId, currency), newLiquidityPool); + + root.denyContract(address(poolManager), address(this)); + root.denyContract(address(newLiquidityPoolFactory), address(this)); + + // verify permissions + verifyLiquidityPoolPermissions(LiquidityPool(lPool_), LiquidityPool(newLiquidityPool)); + + lPool_ = address(newLiquidityPool); + verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); + } + + // --- Permissions & Dependencies Checks --- + + function verifyLiquidityPoolPermissions(LiquidityPool oldLiquidityPool, LiquidityPool newLiquidityPool) public { + // verify permissions + assertTrue(address(oldLiquidityPool) != address(newLiquidityPool)); + address token = poolManager.getTrancheToken(poolId, trancheId); + assertEq(TrancheTokenLike(token).wards(address(oldLiquidityPool)), 0); + assertEq(TrancheTokenLike(token).wards(address(newLiquidityPool)), 1); + assertEq(TrancheTokenLike(token).trustedForwarders(address(oldLiquidityPool)), false); + assertEq(TrancheTokenLike(token).trustedForwarders(address(newLiquidityPool)), true); + assertEq(poolManager.getLiquidityPool(poolId, trancheId, address(erc20)), address(newLiquidityPool)); + assertEq(investmentManager.wards(address(newLiquidityPool)), 1); + assertEq(investmentManager.wards(address(oldLiquidityPool)), 0); + assertEq(newLiquidityPool.wards(address(root)), 1); + assertEq(newLiquidityPool.wards(address(investmentManager)), 1); + assertEq(TrancheTokenLike(token).allowance(address(escrow), address(oldLiquidityPool)), 0); + assertEq(TrancheTokenLike(token).allowance(address(escrow), address(newLiquidityPool)), type(uint256).max); + + // verify dependancies + assertEq(oldLiquidityPool.poolId(), newLiquidityPool.poolId()); + assertEq(oldLiquidityPool.trancheId(), newLiquidityPool.trancheId()); + assertEq(address(oldLiquidityPool.asset()), address(newLiquidityPool.asset())); + assertEq(address(oldLiquidityPool.share()), address(newLiquidityPool.share())); + assertEq(address(newLiquidityPool.share()), token); + assertEq(address(oldLiquidityPool.manager()), address(newLiquidityPool.manager())); + } +} diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol new file mode 100644 index 00000000..63c4f0a1 --- /dev/null +++ b/test/migrations/MigratedPoolManager.t.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedPoolManager, PoolManager} from "./migrationContracts/MigratedPoolManager.sol"; +import {LiquidityPool} from "src/LiquidityPool.sol"; +import {ERC20} from "src/token/ERC20.sol"; +import {TrancheTokenFactory, LiquidityPoolFactory, RestrictionManagerFactory} from "src/util/Factory.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +interface AuthLike { + function rely(address) external; + function deny(address) external; +} + +contract MigrationsTest is InvestRedeemFlow { + function setUp() public override { + super.setUp(); + } + + function testPoolManagerMigrationInvestRedeem() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // deploy new liquidityPoolFactory + LiquidityPoolFactory newLiquidityPoolFactory = new LiquidityPoolFactory(address(root)); + + // rewire factory contracts + newLiquidityPoolFactory.rely(address(root)); + + // Collect all pools, their tranches, allowed currencies and liquidity pool currencies + // assume these records are available off-chain + uint64[] memory poolIds = new uint64[](1); + poolIds[0] = poolId; + bytes16[][] memory trancheIds = new bytes16[][](1); + trancheIds[0] = new bytes16[](1); + trancheIds[0][0] = trancheId; + address[][] memory allowedCurrencies = new address[][](1); + allowedCurrencies[0] = new address[](1); + allowedCurrencies[0][0] = address(erc20); + address[][][] memory liquidityPoolCurrencies = new address[][][](1); + liquidityPoolCurrencies[0] = new address[][](1); + liquidityPoolCurrencies[0][0] = new address[](1); + liquidityPoolCurrencies[0][0][0] = address(erc20); + address[][][] memory liquidityPoolOverrides = new address[][][](0); + + // Deploy new MigratedPoolManager + MigratedPoolManager newPoolManager = new MigratedPoolManager( + address(escrow), + address(newLiquidityPoolFactory), + restrictionManagerFactory, + trancheTokenFactory, + address(poolManager), + poolIds, + trancheIds, + allowedCurrencies, + liquidityPoolCurrencies, + liquidityPoolOverrides + ); + + verifyMigratedPoolManagerState( + poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, poolManager, newPoolManager + ); + + // Rewire contracts + newLiquidityPoolFactory.rely(address(newPoolManager)); + TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); + TrancheTokenFactory(trancheTokenFactory).deny(address(poolManager)); + root.relyContract(address(gateway), address(this)); + gateway.file("poolManager", address(newPoolManager)); + root.relyContract(address(investmentManager), address(this)); + investmentManager.file("poolManager", address(newPoolManager)); + newPoolManager.file("investmentManager", address(investmentManager)); + newPoolManager.file("gateway", address(gateway)); + investmentManager.rely(address(newPoolManager)); + investmentManager.deny(address(poolManager)); + newPoolManager.rely(address(root)); + root.relyContract(address(escrow), address(this)); + escrow.rely(address(newPoolManager)); + escrow.deny(address(poolManager)); + root.relyContract(restrictionManagerFactory, address(this)); + AuthLike(restrictionManagerFactory).rely(address(newPoolManager)); + AuthLike(restrictionManagerFactory).deny(address(poolManager)); + + // clean up + newPoolManager.deny(address(this)); + root.denyContract(address(investmentManager), address(this)); + root.denyContract(address(gateway), address(this)); + root.denyContract(address(newPoolManager), address(this)); + root.denyContract(address(escrow), address(this)); + root.denyContract(restrictionManagerFactory, address(this)); + root.deny(address(this)); + + verifyMigratedPoolManagerPermissions(poolManager, newPoolManager); + + // test that everything is working + poolManager = newPoolManager; + centrifugeChain.addPool(poolId + 1); // add pool + LiquidityPool lPool = LiquidityPool(lPool_); + ERC20 asset = ERC20(lPool.asset()); + centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", asset.decimals(), 2); // add tranche + centrifugeChain.allowInvestmentCurrency(poolId + 1, currencyId); + poolManager.deployTranche(poolId + 1, trancheId); + address _lPool2 = poolManager.deployLiquidityPool(poolId + 1, trancheId, address(erc20)); + centrifugeChain.updateMember(poolId + 1, trancheId, investor, uint64(block.timestamp + 1000 days)); + + verifyInvestAndRedeemFlow(poolId + 1, trancheId, _lPool2); + } + + function verifyMigratedPoolManagerPermissions(PoolManager oldPoolManager, PoolManager newPoolManager) public { + // verify permissions + assertTrue(address(oldPoolManager) != address(newPoolManager)); + assertEq(TrancheTokenFactory(trancheTokenFactory).wards(address(newPoolManager)), 1); + assertEq(TrancheTokenFactory(trancheTokenFactory).wards(address(oldPoolManager)), 0); + assertEq(address(gateway.poolManager()), address(newPoolManager)); + assertEq(address(investmentManager.poolManager()), address(newPoolManager)); + assertEq(address(oldPoolManager.investmentManager()), address(newPoolManager.investmentManager())); + assertEq(address(oldPoolManager.gateway()), address(newPoolManager.gateway())); + assertEq(investmentManager.wards(address(newPoolManager)), 1); + assertEq(investmentManager.wards(address(oldPoolManager)), 0); + assertEq(newPoolManager.wards(address(root)), 1); + assertEq(escrow.wards(address(newPoolManager)), 1); + assertEq(escrow.wards(address(oldPoolManager)), 0); + + // verify dependencies + assertEq(address(oldPoolManager.escrow()), address(newPoolManager.escrow())); + assertFalse(address(oldPoolManager.liquidityPoolFactory()) == address(newPoolManager.liquidityPoolFactory())); + assertEq( + address(oldPoolManager.restrictionManagerFactory()), address(newPoolManager.restrictionManagerFactory()) + ); + assertEq(address(oldPoolManager.trancheTokenFactory()), address(newPoolManager.trancheTokenFactory())); + } + + // --- State Verification Helpers --- + + function verifyMigratedPoolManagerState( + uint64[] memory poolIds, + bytes16[][] memory trancheIds, + address[][] memory allowedCurrencies, + address[][][] memory liquidityPoolCurrencies, + PoolManager poolManager, + PoolManager newPoolManager + ) public { + for (uint256 i = 0; i < poolIds.length; i++) { + (uint256 newCreatedAt) = newPoolManager.pools(poolIds[i]); + (uint256 oldCreatedAt) = poolManager.pools(poolIds[i]); + assertEq(newCreatedAt, oldCreatedAt); + verifyUndeployedTranches(poolIds[i], trancheIds[i], poolManager, newPoolManager); + + for (uint256 j = 0; j < trancheIds[i].length; j++) { + verifyTranche(poolIds[i], trancheIds[i][j], poolManager, newPoolManager); + for (uint256 k = 0; k < liquidityPoolCurrencies[i][j].length; k++) { + verifyLiquidityPoolCurrency( + poolIds[i], trancheIds[i][j], liquidityPoolCurrencies[i][j][k], poolManager, newPoolManager + ); + } + } + + for (uint256 j = 0; j < allowedCurrencies[i].length; j++) { + verifyAllowedCurrency(poolIds[i], allowedCurrencies[i][j], poolManager, newPoolManager); + } + } + } + + function verifyTranche(uint64 poolId, bytes16 trancheId, PoolManager poolManager, PoolManager newPoolManager) + public + { + (address newToken) = newPoolManager.getTrancheToken(poolId, trancheId); + (address oldToken) = poolManager.getTrancheToken(poolId, trancheId); + assertEq(newToken, oldToken); + } + + function verifyUndeployedTranches( + uint64 poolId, + bytes16[] memory trancheIds, + PoolManager poolManager, + PoolManager newPoolManager + ) public { + for (uint256 i = 0; i < trancheIds.length; i++) { + (uint8 oldDecimals, string memory oldTokenName, string memory oldTokenSymbol, uint8 oldRestrictionSet) = + poolManager.undeployedTranches(poolId, trancheIds[i]); + (uint8 newDecimals, string memory newTokenName, string memory newTokenSymbol, uint8 newRestrictionSet) = + newPoolManager.undeployedTranches(poolId, trancheIds[i]); + assertEq(newDecimals, oldDecimals); + assertEq(newTokenName, oldTokenName); + assertEq(newTokenSymbol, oldTokenSymbol); + } + } + + function verifyAllowedCurrency( + uint64 poolId, + address currencyAddress, + PoolManager poolManager, + PoolManager newPoolManager + ) public { + bool newAllowed = newPoolManager.isAllowedAsInvestmentCurrency(poolId, currencyAddress); + bool oldAllowed = poolManager.isAllowedAsInvestmentCurrency(poolId, currencyAddress); + assertEq(newAllowed, oldAllowed); + } + + function verifyLiquidityPoolCurrency( + uint64 poolId, + bytes16 trancheId, + address currencyAddresses, + PoolManager poolManager, + PoolManager newPoolManager + ) public { + address newLiquidityPool = newPoolManager.getLiquidityPool(poolId, trancheId, currencyAddresses); + address oldLiquidityPool = poolManager.getLiquidityPool(poolId, trancheId, currencyAddresses); + assertEq(newLiquidityPool, oldLiquidityPool); + } +} diff --git a/test/migrations/MigratedRestrictionManager.t.sol b/test/migrations/MigratedRestrictionManager.t.sol new file mode 100644 index 00000000..9ac43751 --- /dev/null +++ b/test/migrations/MigratedRestrictionManager.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedRestrictionManager, RestrictionManager} from "./migrationContracts/MigratedRestrictionManager.sol"; +import {RestrictionManagerFactory} from "src/util/Factory.sol"; +import {LiquidityPool} from "src/LiquidityPool.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +interface TrancheTokenLike { + function restrictionManager() external view returns (address); + function file(bytes32, address) external; +} + +contract MigratedRestrictionManagerTest is InvestRedeemFlow { + function setUp() public override { + super.setUp(); + } + + function testRestrictionManagerMigration() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + address[] memory restrictionManagerWards = new address[](1); + restrictionManagerWards[0] = address(poolManager); + address token = address(LiquidityPool(lPool_).share()); + RestrictionManager oldRestrictionManager = RestrictionManager(TrancheTokenLike(token).restrictionManager()); + + // Deploy new RestrictionManagerFactory + RestrictionManagerFactory newRestrictionManagerFactory = new RestrictionManagerFactory(address(root)); + + // rewire factory contracts + root.relyContract(address(poolManager), address(this)); + poolManager.file("restrictionManagerFactory", address(newRestrictionManagerFactory)); + newRestrictionManagerFactory.rely(address(poolManager)); + newRestrictionManagerFactory.rely(address(root)); + + // Collect all tranche tokens + // assume these records are available off-chain + address[] memory trancheTokens = new address[](1); + trancheTokens[0] = token; + + // Deploy new RestrictionManager for each tranche token + for (uint256 i = 0; i < trancheTokens.length; i++) { + MigratedRestrictionManager newRestrictionManager = new MigratedRestrictionManager(token); + + // Rewire contracts + root.relyContract(trancheTokens[i], address(this)); + TrancheTokenLike(trancheTokens[i]).file("restrictionManager", address(newRestrictionManager)); + newRestrictionManager.updateMember(address(escrow), type(uint256).max); + newRestrictionManager.rely(address(root)); + for (uint256 j = 0; j < restrictionManagerWards.length; j++) { + newRestrictionManager.rely(restrictionManagerWards[j]); + } + + // clean up + newRestrictionManager.deny(address(this)); + root.denyContract(trancheTokens[i], address(this)); + root.deny(address(this)); + + // verify permissions + verifyMigratedRestrictionManagerPermissions(oldRestrictionManager, newRestrictionManager); + } + + // TODO: test that everything is working + // restrictionManager = newRestrictionManager; + // verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); + } + + function verifyMigratedRestrictionManagerPermissions( + RestrictionManager oldRestrictionManager, + MigratedRestrictionManager newRestrictionManager + ) internal { + // verify permissions + TrancheTokenLike token = TrancheTokenLike(address(oldRestrictionManager.token())); + assertEq(TrancheTokenLike(token).restrictionManager(), address(newRestrictionManager)); + assertTrue(address(oldRestrictionManager) != address(newRestrictionManager)); + assertTrue(oldRestrictionManager.hasMember(address(escrow)) == newRestrictionManager.hasMember(address(escrow))); + assertTrue(newRestrictionManager.wards(address(root)) == 1); + assertTrue(newRestrictionManager.wards(address(poolManager)) == 1); + + // verify dependancies + assertTrue(oldRestrictionManager.token() == newRestrictionManager.token()); + } +} diff --git a/test/migrations/migrationContracts/MigratedAdmin.sol b/test/migrations/migrationContracts/MigratedAdmin.sol new file mode 100644 index 00000000..b1213fe2 --- /dev/null +++ b/test/migrations/migrationContracts/MigratedAdmin.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "src/admins/DelayedAdmin.sol"; +import "src/admins/PauseAdmin.sol"; + +contract MigratedDelayedAdmin is DelayedAdmin { + constructor(address root_, address pauseAdmin_) DelayedAdmin(root_, pauseAdmin_) {} +} + +contract MigratedPauseAdmin is PauseAdmin { + constructor(address root_) PauseAdmin(root_) {} +} diff --git a/test/migrations/migrationContracts/MigratedGateway.sol b/test/migrations/migrationContracts/MigratedGateway.sol new file mode 100644 index 00000000..d1cf15d2 --- /dev/null +++ b/test/migrations/migrationContracts/MigratedGateway.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "src/gateway/Gateway.sol"; + +contract MigratedGateway is Gateway { + constructor(address _root, address _investmentManager, address poolManager_, address router_) + Gateway(_root, _investmentManager, poolManager_, router_) + { + // TODO: migrate routers + } +} diff --git a/test/migrations/migrationContracts/MigratedInvestmentManager.sol b/test/migrations/migrationContracts/MigratedInvestmentManager.sol new file mode 100644 index 00000000..d637f21e --- /dev/null +++ b/test/migrations/migrationContracts/MigratedInvestmentManager.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "src/InvestmentManager.sol"; + +interface RootLike { + function relyContract(address, address) external; + function denyContract(address, address) external; + function rely(address) external; + function deny(address) external; +} + +contract MigratedInvestmentManager is InvestmentManager { + /// @param investors The investors to migrate. + /// @param liquidityPools The liquidity pools to migrate. + constructor( + address _escrow, + address _userEscrow, + address _oldInvestmentManager, + address[] memory investors, + address[] memory liquidityPools + ) InvestmentManager(_escrow, _userEscrow) { + InvestmentManager oldInvestmentManager = InvestmentManager(_oldInvestmentManager); + gateway = oldInvestmentManager.gateway(); + poolManager = oldInvestmentManager.poolManager(); + + for (uint128 i = 0; i < investors.length; i++) { + address investor = investors[i]; + for (uint128 j = 0; j < liquidityPools.length; j++) { + address liquidityPool = liquidityPools[j]; + ( + uint128 maxMint, + uint256 depositPrice, + uint128 maxWithdraw, + uint256 redeemPrice, + uint128 pendingDepositRequest, + uint128 pendingRedeemRequest, + bool exists + ) = oldInvestmentManager.investments(investor, liquidityPool); + investments[investor][liquidityPool] = InvestmentState({ + maxMint: maxMint, + depositPrice: depositPrice, + maxWithdraw: maxWithdraw, + redeemPrice: redeemPrice, + pendingDepositRequest: pendingDepositRequest, + pendingRedeemRequest: pendingRedeemRequest, + exists: exists + }); + } + } + } +} diff --git a/test/migrations/migrationContracts/MigratedLiquidityPool.sol b/test/migrations/migrationContracts/MigratedLiquidityPool.sol new file mode 100644 index 00000000..40019a70 --- /dev/null +++ b/test/migrations/migrationContracts/MigratedLiquidityPool.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "src/LiquidityPool.sol"; + +contract MigratedLiquidityPool is LiquidityPool { + constructor( + uint64 poolId_, + bytes16 trancheId_, + address asset_, + address share_, + address escrow_, + address investmentManager_ + ) LiquidityPool(poolId_, trancheId_, asset_, share_, escrow_, investmentManager_) {} +} diff --git a/test/migrations/migrationContracts/MigratedPoolManager.sol b/test/migrations/migrationContracts/MigratedPoolManager.sol new file mode 100644 index 00000000..2bea96ce --- /dev/null +++ b/test/migrations/migrationContracts/MigratedPoolManager.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.21; + +import "src/PoolManager.sol"; +import "forge-std/console.sol"; + +contract MigratedPoolManager is PoolManager { + /// @param poolIds The poolIds of the pools to migrate. + /// @param trancheIds The trancheIds of the tranches to migrate, arranged as arrays of trancheIds for each pool. + /// @param allowedCurrencies The allowed currencies of the pools to migrate, arranged as arrays of allowed + /// currencies for each pool. + /// @param liquidityPoolCurrencies The liquidity pool currencies of the tranches to migrate, arranged as arrays of + /// liquidity pool currencies for each tranche of each pool. + constructor( + address escrow_, + address liquidityPoolFactory_, + address restrictionManagerFactory_, + address trancheTokenFactory_, + address oldPoolManager, + uint64[] memory poolIds, + bytes16[][] memory trancheIds, + address[][] memory allowedCurrencies, + address[][][] memory liquidityPoolCurrencies, + address[][][] memory liquidityPoolOverrides + ) PoolManager(escrow_, liquidityPoolFactory_, restrictionManagerFactory_, trancheTokenFactory_) { + // migrate pools + PoolManager oldPoolManager_ = PoolManager(oldPoolManager); + migratePools( + oldPoolManager_, poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, liquidityPoolOverrides + ); + + for (uint256 i = 0; i < allowedCurrencies.length; i++) { + for (uint256 j = 0; j < allowedCurrencies[i].length; j++) { + address currencyAddress = allowedCurrencies[i][j]; + uint128 currencyId = oldPoolManager_.currencyAddressToId(currencyAddress); + currencyAddressToId[currencyAddress] = currencyId; + currencyIdToAddress[currencyId] = currencyAddress; + } + } + } + + function migratePools( + PoolManager oldPoolManager_, + uint64[] memory poolIds, + bytes16[][] memory trancheIds, + address[][] memory allowedCurrencies, + address[][][] memory liquidityPoolCurrencies, + address[][][] memory liquidityPoolOverrides + ) internal { + for (uint256 i = 0; i < poolIds.length; i++) { + (uint256 createdAt) = oldPoolManager_.pools(poolIds[i]); + Pool storage pool = pools[poolIds[i]]; + pool.createdAt = createdAt; + address[][] memory liquidityPoolOverrides_ = + liquidityPoolOverrides.length > 0 ? liquidityPoolOverrides[i] : new address[][](0); + + // migrate tranches + migrateTranches( + poolIds[i], trancheIds[i], liquidityPoolCurrencies[i], oldPoolManager_, liquidityPoolOverrides_ + ); + migrateUndeployedTranches(poolIds[i], trancheIds[i], oldPoolManager_); + + // migrate allowed currencies + for (uint256 j = 0; j < allowedCurrencies[i].length; j++) { + address currencyAddress = allowedCurrencies[i][j]; + pool.allowedCurrencies[currencyAddress] = true; + } + } + } + + function migrateTranches( + uint64 poolId, + bytes16[] memory trancheIds, + address[][] memory liquidityPoolCurrencies, + PoolManager oldPoolManager_, + address[][] memory liquidityPoolOverrides + ) internal { + Pool storage pool = pools[poolId]; + for (uint256 j = 0; j < trancheIds.length; j++) { + address[] memory liquidityPoolOverrides_ = + liquidityPoolOverrides.length > 0 ? liquidityPoolOverrides[j] : new address[](0); + bytes16 trancheId = trancheIds[j]; + pool.tranches[trancheId].token = oldPoolManager_.getTrancheToken(poolId, trancheId); + for (uint256 k = 0; k < liquidityPoolCurrencies[j].length; k++) { + address currencyAddress = liquidityPoolCurrencies[j][k]; + if (liquidityPoolOverrides_.length > 0) { + pool.tranches[trancheId].liquidityPools[currencyAddress] = liquidityPoolOverrides[j][k]; + } else { + pool.tranches[trancheId].liquidityPools[currencyAddress] = + oldPoolManager_.getLiquidityPool(poolId, trancheId, currencyAddress); + } + } + } + } + + function migrateUndeployedTranches(uint64 poolId, bytes16[] memory trancheIds, PoolManager oldPoolManager_) + internal + { + for (uint256 j = 0; j < trancheIds.length; j++) { + bytes16 trancheId = trancheIds[j]; + (uint8 decimals, string memory tokenName, string memory tokenSymbol, uint8 restrictionSet) = + oldPoolManager_.undeployedTranches(poolId, trancheId); + undeployedTranches[poolId][trancheId].decimals = decimals; + undeployedTranches[poolId][trancheId].tokenName = tokenName; + undeployedTranches[poolId][trancheId].tokenSymbol = tokenSymbol; + undeployedTranches[poolId][trancheId].restrictionSet = restrictionSet; + } + } +} diff --git a/test/migrations/migrationContracts/MigratedRestrictionManager.sol b/test/migrations/migrationContracts/MigratedRestrictionManager.sol new file mode 100644 index 00000000..3bfd17e6 --- /dev/null +++ b/test/migrations/migrationContracts/MigratedRestrictionManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "src/token/RestrictionManager.sol"; + +contract MigratedRestrictionManager is RestrictionManager { + constructor(address token_) RestrictionManager(token_) {} +}