From b49b66cb7736942c05d1706f530f6b38e9b64bb0 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Mon, 18 Sep 2023 14:42:23 -0400 Subject: [PATCH 01/51] add migratedInvestmentManager contract --- test/Migrations.t.sol | 193 ++++++++++++++++++ .../MigratedInvestmentmanager.sol | 40 ++++ 2 files changed, 233 insertions(+) create mode 100644 test/Migrations.t.sol create mode 100644 test/migrationContracts/MigratedInvestmentmanager.sol diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol new file mode 100644 index 00000000..6212f174 --- /dev/null +++ b/test/Migrations.t.sol @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "./TestSetup.t.sol"; +import "src/LiquidityPool.sol"; +import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestmentManager.sol"; +import {MathLib} from "src/util/MathLib.sol"; + +// import "forge-std/Test.sol"; + +contract MigrationsTest is TestSetup { + using MathLib for uint128; + + uint8 internal constant PRICE_DECIMALS = 18; + + uint64 poolId; + bytes16 trancheId; + uint128 currencyId; + uint8 trancheTokenDecimals; + address _lPool; + address investor; + uint256 investorCurrencyAmount; + + function setUp() public override { + super.setUp(); + investor = vm.addr(100); + poolId = 1; + trancheId = bytes16(hex"811acd5b3f17c06841c7e41e9e04cb1b"); + currencyId = 1; + trancheTokenDecimals = 18; + _lPool = deployLiquidityPool(poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20)); + + investorCurrencyAmount = 1000 * 10 ** erc20.decimals(); + deal(address(erc20), investor, investorCurrencyAmount); + homePools.updateMember(poolId, trancheId, investor, uint64(block.timestamp + 1000 days)); + } + + function testInvestmentManagerMigration() public { + // Make sure it works first + InvestAndRedeem(poolId, trancheId, _lPool); + // address[] memory investors = new address[](1); + // investors[0] = investor; + // address[] memory liquidityPools = new address[](1); + // liquidityPools[0] = _lPool; + // MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); + + } + + function testLiquidityPoolMigration() public {} + + function testRootMigration() public {} + + function testPoolManagerMigration() public {} + + + // --- Investment and Redeem Flow --- + + function InvestAndRedeem( + uint64 poolId, + bytes16 trancheId, + address _lPool + ) public { + uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); //TODO: fuzz price + LiquidityPool lPool = LiquidityPool(_lPool); + + 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 amount, + LiquidityPool lPool + ) public { + vm.prank(investor); + erc20.approve(address(investmentManager), amount); // add allowance + vm.prank(investor); + lPool.requestDeposit(amount, investor); + + // ensure funds are locked in escrow + assertEq(erc20.balanceOf(address(escrow)), amount); + assertEq(erc20.balanceOf(investor), 0); + + // trigger executed collectInvest + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + + uint128 trancheTokensPayout = _toUint128( + uint128(amount).mulDiv( + 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down + ) + ); + + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectInvest for this user on cent chain + + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectInvest( + poolId, trancheId, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 + ); + + assertEq(lPool.maxMint(investor), trancheTokensPayout); + assertEq(lPool.maxDeposit(investor), amount); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); + assertEq(erc20.balanceOf(investor), 0); + + uint256 div = 2; + vm.prank(investor); + lPool.deposit(amount / div, investor); + + assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); + assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); + assertEq(lPool.maxDeposit(investor), amount - amount / div); // max deposit + + console.log("trancheTokensPayout", trancheTokensPayout); + console.log("lPool.maxDeposit", lPool.maxDeposit(investor)); + console.log("lPool.maxMint", lPool.maxMint(investor)); + console.log("lPool.balanceOf(address(escrow))", lPool.balanceOf(address(escrow))); + console.log("erc20.balanceOf(investor)", erc20.balanceOf(investor)); + // console.log("deposit amount", amount / div); + vm.prank(investor); + lPool.mint(lPool.maxMint(investor), investor); + + assertEq(lPool.balanceOf(investor), trancheTokensPayout); + assertTrue(lPool.balanceOf(address(escrow)) <= 1); + assertTrue(lPool.maxMint(investor) <= 1); + } + + function redeemWithdraw( + uint64 poolId, + bytes16 trancheId, + uint128 price, + uint256 amount, + LiquidityPool lPool + ) public { + vm.expectRevert(bytes("ERC20/insufficient-allowance")); + vm.prank(investor); + lPool.requestRedeem(amount, investor); + vm.prank(investor); + lPool.approve(address(investmentManager), amount); + vm.prank(investor); + lPool.requestRedeem(amount, investor); + + // redeem + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + uint128 currencyPayout = _toUint128( + uint128(amount).mulDiv(price, 10 ** (18 - erc20.decimals() + lPool.decimals()), MathLib.Rounding.Down) + ); + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectRedeem for this user on cent chain + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectRedeem( + poolId, trancheId, investor, _currencyId, currencyPayout, uint128(amount), 0 + ); + + assertEq(lPool.maxWithdraw(investor), currencyPayout); + assertEq(lPool.maxRedeem(investor), amount); + assertEq(lPool.balanceOf(address(escrow)), 0); + + uint128 div = 2; + vm.prank(investor); + lPool.redeem(amount / 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), amount / div); + + vm.prank(investor); + lPool.withdraw(lPool.maxWithdraw(investor), 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); + } + + // --- Helpers --- + + function _toUint128(uint256 _value) internal pure returns (uint128 value) { + if (_value > type(uint128).max) { + revert("InvestmentManager/uint128-overflow"); + } else { + value = uint128(_value); + } + } +} \ No newline at end of file diff --git a/test/migrationContracts/MigratedInvestmentmanager.sol b/test/migrationContracts/MigratedInvestmentmanager.sol new file mode 100644 index 00000000..e1c4d023 --- /dev/null +++ b/test/migrationContracts/MigratedInvestmentmanager.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import "src/InvestmentManager.sol"; + + // struct LPValues { + // uint128 maxDeposit; // denominated in currency + // uint128 maxMint; // denominated in tranche tokens + // uint128 maxWithdraw; // denominated in currency + // uint128 maxRedeem; // denominated in tranche tokens + // uint128 remainingInvestOrder; // denominated in currency + // uint128 remainingRedeemOrder; // denominated in tranche tokens + // } + +contract MigratedInvestmentManager is InvestmentManager { + // mapping(address investor => mapping(address liquidityPool => LPValues)) public orderbook; + + 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(); + + // populate orderBook + 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 maxDeposit, uint128 maxMint, uint128 maxWithdraw, uint128 maxRedeem, uint128 remainingInvestOrder, uint128 remainingRedeemOrder) = oldInvestmentManager.orderbook(investor, liquidityPool); + orderbook[investor][liquidityPool] = LPValues({ + maxDeposit: maxDeposit, + maxMint: maxMint, + maxWithdraw: maxWithdraw, + maxRedeem: maxRedeem, + remainingInvestOrder: remainingInvestOrder, + remainingRedeemOrder: remainingRedeemOrder + }); + } + } + } +} \ No newline at end of file From b439730e8d9a029fe02e74a13a7e97470b06081e Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 20 Sep 2023 12:33:23 -0400 Subject: [PATCH 02/51] wip: working on successfully migrating investmentManager --- src/InvestmentManager.sol | 12 ++- src/LiquidityPool.sol | 6 +- test/Migrations.t.sol | 160 ++++++++++++++++++++++++++------------ 3 files changed, 126 insertions(+), 52 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index 9c0261cb..51a310e3 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -5,6 +5,8 @@ import {Auth} from "./util/Auth.sol"; import {MathLib} from "./util/MathLib.sol"; import {SafeTransferLib} from "./util/SafeTransferLib.sol"; +import "forge-std/Test.sol"; + interface GatewayLike { function increaseInvestOrder(uint64 poolId, bytes16 trancheId, address investor, uint128 currency, uint128 amount) external; @@ -27,6 +29,7 @@ interface ERC20Like { function decimals() external view returns (uint8); function mint(address, uint256) external; function burn(address, uint256) external; + function allowance(address, address) external view returns (uint256); } interface LiquidityPoolLike is ERC20Like { @@ -73,7 +76,7 @@ struct LPValues { /// @title Investment Manager /// @notice This is the main contract LiquidityPools interact with for /// both incoming and outgoing investment transactions. -contract InvestmentManager is Auth { +contract InvestmentManager is Auth, Test { using MathLib for uint256; using MathLib for uint128; @@ -170,6 +173,9 @@ contract InvestmentManager is Auth { // Transfer the currency amount from user to escrow (lock currency in escrow) // Checks actual balance difference to support fee-on-transfer tokens uint256 preBalance = ERC20Like(currency).balanceOf(address(escrow)); + console.log("INVESTMENTMANAGER user", user); + console.log("INVESTMENTMANAGER address(this)", address(this)); + console.log("INVESTMENTMANAGER allowance", ERC20Like(currency).allowance(user, address(this))); SafeTransferLib.safeTransferFrom(currency, user, address(escrow), _currencyAmount); uint256 postBalance = ERC20Like(currency).balanceOf(address(escrow)); uint128 transferredAmount = _toUint128(postBalance - preBalance); @@ -588,6 +594,10 @@ contract InvestmentManager is Auth { returns (uint256 currencyAmount) { uint128 _trancheTokenAmount = _toUint128(trancheTokenAmount); + // console.log("trancheTokenAmount", _trancheTokenAmount); + // console.log("orderbook[owner][liquidityPool].maxMint", orderbook[owner][liquidityPool].maxMint); + // console.log("owner", owner); + // console.log("liquidityPool", liquidityPool); require( (_trancheTokenAmount <= orderbook[owner][liquidityPool].maxMint && _trancheTokenAmount != 0), "InvestmentManager/amount-exceeds-mint-limits" diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index f3e1f47f..866ed2eb 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -6,6 +6,8 @@ import {MathLib} from "./util/MathLib.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import {IERC4626} from "./interfaces/IERC4626.sol"; +import "forge-std/Test.sol"; + interface ERC20PermitLike { function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; @@ -64,7 +66,7 @@ interface InvestmentManagerLike { /// redeem orders are submitted to the pools to be included in the execution of the following epoch. After /// execution users can use the deposit, mint, redeem and withdraw functions to get their shares /// and/or assets from the pools. -contract LiquidityPool is Auth, IERC4626 { +contract LiquidityPool is Auth, IERC4626, Test { using MathLib for uint256; uint64 public immutable poolId; @@ -165,6 +167,8 @@ contract LiquidityPool is Auth, IERC4626 { /// @notice Collect shares for deposited assets after Centrifuge epoch execution. /// maxMint is the max amount of shares that can be collected. function mint(uint256 shares, address receiver) public returns (uint256 assets) { + // console.log("LIQUIDITY POOL msg.sender", msg.sender); + // console.log("LIQUIDITY POOL address(this)", address(this)); assets = investmentManager.processMint(address(this), shares, receiver, msg.sender); emit Deposit(address(this), receiver, assets, shares); } diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 6212f174..cfb3fd1d 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -18,12 +18,10 @@ contract MigrationsTest is TestSetup { uint128 currencyId; uint8 trancheTokenDecimals; address _lPool; - address investor; uint256 investorCurrencyAmount; function setUp() public override { super.setUp(); - investor = vm.addr(100); poolId = 1; trancheId = bytes16(hex"811acd5b3f17c06841c7e41e9e04cb1b"); currencyId = 1; @@ -31,21 +29,69 @@ contract MigrationsTest is TestSetup { _lPool = deployLiquidityPool(poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20)); investorCurrencyAmount = 1000 * 10 ** erc20.decimals(); - deal(address(erc20), investor, investorCurrencyAmount); - homePools.updateMember(poolId, trancheId, investor, uint64(block.timestamp + 1000 days)); + deal(address(erc20), investor, investorCurrencyAmount * 100); + centrifugeChain.updateMember(poolId, trancheId, investor, uint64(block.timestamp + 1000 days)); } function testInvestmentManagerMigration() public { - // Make sure it works first InvestAndRedeem(poolId, trancheId, _lPool); + + // Assume executeScheduledRely() is called on this spell + // address[] memory investors = new address[](1); // investors[0] = investor; // address[] memory liquidityPools = new address[](1); // liquidityPools[0] = _lPool; + // // Deploy new investmentManager // MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); - } + // // Deploy new contracts that take InvestmentManager as constructor argument + // Gateway newGateway = new Gateway(address(root), address(newInvestmentManager), address(poolManager), address(router)); + + // // file investmentManager on all LiquidityPools + // for (uint256 i = 0; i < liquidityPools.length; i++) { + // root.relyContract(address(liquidityPools[i]), address(this)); + // LiquidityPool lPool = LiquidityPool(liquidityPools[i]); + + // lPool.file("investmentManager", address(newInvestmentManager)); + // lPool.rely(address(newInvestmentManager)); + // newInvestmentManager.rely(address(lPool)); + // } + + // // Rewire everything + // newInvestmentManager.file("poolManager", address(poolManager)); + // root.relyContract(address(poolManager), address(this)); + // poolManager.file("investmentManager", address(newInvestmentManager)); + // newInvestmentManager.file("gateway", address(newGateway)); + // poolManager.file("gateway", address(newGateway)); + // newInvestmentManager.rely(address(root)); + // newInvestmentManager.rely(address(poolManager)); + // newGateway.rely(address(root)); + // root.relyContract(address(escrow), address(this)); + // Escrow(address(escrow)).rely(address(newInvestmentManager)); + // root.relyContract(address(userEscrow), address(this)); + // UserEscrow(address(userEscrow)).rely(address(newInvestmentManager)); + + // root.relyContract(address(router), address(this)); + // router.file("gateway", address(newGateway)); + + // // clean up + // root.denyContract(address(newInvestmentManager), address(this)); + // root.denyContract(address(newGateway), address(this)); + // root.denyContract(address(poolManager), address(this)); + // root.denyContract(address(escrow), address(this)); + // root.denyContract(address(userEscrow), address(this)); + // root.deny(address(this)); + + + // // For the sake of these helper functions, set global variables to new contracts + // gateway = newGateway; + // investmentManager = newInvestmentManager; + // // test that everything is working + // InvestAndRedeem(poolId, trancheId, _lPool); + } + function testLiquidityPoolMigration() public {} function testRootMigration() public {} @@ -78,58 +124,71 @@ contract MigrationsTest is TestSetup { uint256 amount, LiquidityPool lPool ) public { + console.log("APPROVAL", erc20.allowance(investor, address(investmentManager))); vm.prank(investor); erc20.approve(address(investmentManager), amount); // add allowance + console.log("address(investor)", address(investor)); + console.log("address(investmentManager)", address(investmentManager)); + console.log("APPROVAL", erc20.allowance(investor, address(investmentManager))); + console.log("requestDeposit amount", amount); + + vm.prank(address(investmentManager)); + erc20.approve(address(poolManager), amount); // add allowance vm.prank(investor); lPool.requestDeposit(amount, investor); // ensure funds are locked in escrow assertEq(erc20.balanceOf(address(escrow)), amount); - assertEq(erc20.balanceOf(investor), 0); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); // trigger executed collectInvest - uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId - - uint128 trancheTokensPayout = _toUint128( - uint128(amount).mulDiv( - 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down - ) - ); - - // Assume an epoch execution happens on cent chain - // Assume a bot calls collectInvest for this user on cent chain - - vm.prank(address(gateway)); - investmentManager.handleExecutedCollectInvest( - poolId, trancheId, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 - ); - - assertEq(lPool.maxMint(investor), trancheTokensPayout); - assertEq(lPool.maxDeposit(investor), amount); - assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); - assertEq(erc20.balanceOf(investor), 0); - - uint256 div = 2; - vm.prank(investor); - lPool.deposit(amount / div, investor); - - assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); - assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); - assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); - assertEq(lPool.maxDeposit(investor), amount - amount / div); // max deposit - - console.log("trancheTokensPayout", trancheTokensPayout); - console.log("lPool.maxDeposit", lPool.maxDeposit(investor)); - console.log("lPool.maxMint", lPool.maxMint(investor)); - console.log("lPool.balanceOf(address(escrow))", lPool.balanceOf(address(escrow))); - console.log("erc20.balanceOf(investor)", erc20.balanceOf(investor)); - // console.log("deposit amount", amount / div); - vm.prank(investor); - lPool.mint(lPool.maxMint(investor), investor); - - assertEq(lPool.balanceOf(investor), trancheTokensPayout); - assertTrue(lPool.balanceOf(address(escrow)) <= 1); - assertTrue(lPool.maxMint(investor) <= 1); + // uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + + // uint128 trancheTokensPayout = _toUint128( + // uint128(amount).mulDiv( + // 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down + // ) + // ); + + // // Assume an epoch execution happens on cent chain + // // Assume a bot calls collectInvest for this user on cent chain + // // console.log("APPROVAL right before collectInvest", erc20.allowance(investor, address(investmentManager))); + // // console.log("amount right before collectInvest", amount); + // vm.prank(address(gateway)); + // investmentManager.handleExecutedCollectInvest( + // poolId, trancheId, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 + // ); + + // assertEq(lPool.maxMint(investor), trancheTokensPayout); + // assertEq(lPool.maxDeposit(investor), amount); + // assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); + // assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); + + // uint256 div = 2; + // vm.prank(investor); + // lPool.deposit(amount / div, investor); + + // assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); + // assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); + // assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); + // assertEq(lPool.maxDeposit(investor), amount - amount / div); // max deposit + + // // console.log("trancheTokensPayout", trancheTokensPayout); + // // console.log("lPool.maxDeposit", lPool.maxDeposit(investor)); + // // console.log("lPool.maxMint", lPool.maxMint(investor)); + // // console.log("investor", investor); + // // console.log("address(this)", address(this)); + // // console.log("lPool.balanceOf(address(escrow))", lPool.balanceOf(address(escrow))); + // // console.log("erc20.balanceOf(investor)", erc20.balanceOf(investor)); + // // console.log("deposit amount", amount / div); + // uint256 maxMint = lPool.maxMint(investor); + // vm.prank(investor); + // lPool.mint(maxMint, investor); + + // assertEq(lPool.balanceOf(investor), trancheTokensPayout); + // assertTrue(lPool.balanceOf(address(escrow)) <= 1); + // assertTrue(lPool.maxMint(investor) <= 1); + // console.log("APPROVAL", erc20.allowance(investor, address(investmentManager))); } function redeemWithdraw( @@ -172,8 +231,9 @@ contract MigrationsTest is TestSetup { assertEq(lPool.maxWithdraw(investor), currencyPayout / div); assertEq(lPool.maxRedeem(investor), amount / div); + uint256 maxWithdraw = lPool.maxWithdraw(investor); vm.prank(investor); - lPool.withdraw(lPool.maxWithdraw(investor), investor, investor); + lPool.withdraw(maxWithdraw, investor, investor); assertEq(lPool.balanceOf(investor), 0); assertEq(lPool.balanceOf(address(escrow)), 0); assertEq(erc20.balanceOf(investor), currencyPayout); From dc9e7b4bc12fa5f8dae65e7ef078b0e3086b7240 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 21 Sep 2023 13:58:46 -0400 Subject: [PATCH 03/51] investmentManager migration working --- src/InvestmentManager.sol | 11 +-- src/LiquidityPool.sol | 6 +- test/Deploy.t.sol | 1 - test/LiquidityPool.t.sol | 21 ++++- test/Migrations.t.sol | 190 +++++++++++++++++--------------------- test/PoolManager.t.sol | 2 - 6 files changed, 107 insertions(+), 124 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index 51a310e3..fded268f 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -5,8 +5,6 @@ import {Auth} from "./util/Auth.sol"; import {MathLib} from "./util/MathLib.sol"; import {SafeTransferLib} from "./util/SafeTransferLib.sol"; -import "forge-std/Test.sol"; - interface GatewayLike { function increaseInvestOrder(uint64 poolId, bytes16 trancheId, address investor, uint128 currency, uint128 amount) external; @@ -76,7 +74,7 @@ struct LPValues { /// @title Investment Manager /// @notice This is the main contract LiquidityPools interact with for /// both incoming and outgoing investment transactions. -contract InvestmentManager is Auth, Test { +contract InvestmentManager is Auth { using MathLib for uint256; using MathLib for uint128; @@ -173,9 +171,6 @@ contract InvestmentManager is Auth, Test { // Transfer the currency amount from user to escrow (lock currency in escrow) // Checks actual balance difference to support fee-on-transfer tokens uint256 preBalance = ERC20Like(currency).balanceOf(address(escrow)); - console.log("INVESTMENTMANAGER user", user); - console.log("INVESTMENTMANAGER address(this)", address(this)); - console.log("INVESTMENTMANAGER allowance", ERC20Like(currency).allowance(user, address(this))); SafeTransferLib.safeTransferFrom(currency, user, address(escrow), _currencyAmount); uint256 postBalance = ERC20Like(currency).balanceOf(address(escrow)); uint128 transferredAmount = _toUint128(postBalance - preBalance); @@ -594,10 +589,6 @@ contract InvestmentManager is Auth, Test { returns (uint256 currencyAmount) { uint128 _trancheTokenAmount = _toUint128(trancheTokenAmount); - // console.log("trancheTokenAmount", _trancheTokenAmount); - // console.log("orderbook[owner][liquidityPool].maxMint", orderbook[owner][liquidityPool].maxMint); - // console.log("owner", owner); - // console.log("liquidityPool", liquidityPool); require( (_trancheTokenAmount <= orderbook[owner][liquidityPool].maxMint && _trancheTokenAmount != 0), "InvestmentManager/amount-exceeds-mint-limits" diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 866ed2eb..f3e1f47f 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -6,8 +6,6 @@ import {MathLib} from "./util/MathLib.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import {IERC4626} from "./interfaces/IERC4626.sol"; -import "forge-std/Test.sol"; - interface ERC20PermitLike { function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; @@ -66,7 +64,7 @@ interface InvestmentManagerLike { /// redeem orders are submitted to the pools to be included in the execution of the following epoch. After /// execution users can use the deposit, mint, redeem and withdraw functions to get their shares /// and/or assets from the pools. -contract LiquidityPool is Auth, IERC4626, Test { +contract LiquidityPool is Auth, IERC4626 { using MathLib for uint256; uint64 public immutable poolId; @@ -167,8 +165,6 @@ contract LiquidityPool is Auth, IERC4626, Test { /// @notice Collect shares for deposited assets after Centrifuge epoch execution. /// maxMint is the max amount of shares that can be collected. function mint(uint256 shares, address receiver) public returns (uint256 assets) { - // console.log("LIQUIDITY POOL msg.sender", msg.sender); - // console.log("LIQUIDITY POOL address(this)", address(this)); assets = investmentManager.processMint(address(this), shares, receiver, msg.sender); emit Deposit(address(this), receiver, assets, shares); } diff --git a/test/Deploy.t.sol b/test/Deploy.t.sol index 6359967d..87469fd9 100644 --- a/test/Deploy.t.sol +++ b/test/Deploy.t.sol @@ -157,7 +157,6 @@ contract DeployTest is Test { vm.expectRevert(bytes("ERC20/insufficient-allowance")); lPool.requestRedeem(amount, self); lPool.approve(address(investmentManager), amount); - console.log(lPool.allowance(self, address(lPool))); lPool.requestRedeem(amount, self); // redeem diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 98ade70a..34ec8d57 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -1012,7 +1012,7 @@ contract LiquidityPoolTest is TestSetup { ) ) ); - + v += 1; lPool.requestDepositWithPermit(amount, investor, block.timestamp, v, r, s); // To avoid stack too deep errors delete v; @@ -1366,4 +1366,23 @@ contract LiquidityPoolTest is TestSetup { function addressAssumption(address user) public returns (bool) { return (user != address(0) && user != address(erc20) && user.code.length == 0); } + + function testThing() public { + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + uint256(0x8e3ec2b1affade3231c6a81c299988176e0343dd8ce9f0986d66ed13e0f178d9), + keccak256( + abi.encodePacked( + "\x19\x01", + erc20.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + erc20.PERMIT_TYPEHASH(), address(0x423420Ae467df6e90291fd0252c0A8a637C1e03f), address(0x0ca994e97156ABAc43e606C2aEd5Af4b417C104d), 0, 0, block.timestamp + ) + ) + ) + ) + ); + assertEq(r, 0); + assertEq(s, 0); + } } diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index cfb3fd1d..9c344107 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -6,8 +6,6 @@ import "src/LiquidityPool.sol"; import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestmentManager.sol"; import {MathLib} from "src/util/MathLib.sol"; -// import "forge-std/Test.sol"; - contract MigrationsTest is TestSetup { using MathLib for uint128; @@ -29,7 +27,7 @@ contract MigrationsTest is TestSetup { _lPool = deployLiquidityPool(poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20)); investorCurrencyAmount = 1000 * 10 ** erc20.decimals(); - deal(address(erc20), investor, investorCurrencyAmount * 100); + deal(address(erc20), investor, investorCurrencyAmount); centrifugeChain.updateMember(poolId, trancheId, investor, uint64(block.timestamp + 1000 days)); } @@ -37,59 +35,59 @@ contract MigrationsTest is TestSetup { InvestAndRedeem(poolId, trancheId, _lPool); // Assume executeScheduledRely() is called on this spell - - // address[] memory investors = new address[](1); - // investors[0] = investor; - // address[] memory liquidityPools = new address[](1); - // liquidityPools[0] = _lPool; - // // Deploy new investmentManager - // MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); + address[] memory investors = new address[](1); + investors[0] = investor; + address[] memory liquidityPools = new address[](1); + liquidityPools[0] = _lPool; + // Deploy new investmentManager + MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); - // // Deploy new contracts that take InvestmentManager as constructor argument - // Gateway newGateway = new Gateway(address(root), address(newInvestmentManager), address(poolManager), address(router)); + // Deploy new contracts that take InvestmentManager as constructor argument + Gateway newGateway = new Gateway(address(root), address(newInvestmentManager), address(poolManager), address(router)); - // // file investmentManager on all LiquidityPools - // for (uint256 i = 0; i < liquidityPools.length; i++) { - // root.relyContract(address(liquidityPools[i]), address(this)); - // LiquidityPool lPool = LiquidityPool(liquidityPools[i]); - - // lPool.file("investmentManager", address(newInvestmentManager)); - // lPool.rely(address(newInvestmentManager)); - // newInvestmentManager.rely(address(lPool)); - // } - - // // Rewire everything - // newInvestmentManager.file("poolManager", address(poolManager)); - // root.relyContract(address(poolManager), address(this)); - // poolManager.file("investmentManager", address(newInvestmentManager)); - // newInvestmentManager.file("gateway", address(newGateway)); - // poolManager.file("gateway", address(newGateway)); - // newInvestmentManager.rely(address(root)); - // newInvestmentManager.rely(address(poolManager)); - // newGateway.rely(address(root)); - // root.relyContract(address(escrow), address(this)); - // Escrow(address(escrow)).rely(address(newInvestmentManager)); - // root.relyContract(address(userEscrow), address(this)); - // UserEscrow(address(userEscrow)).rely(address(newInvestmentManager)); - - // root.relyContract(address(router), address(this)); - // router.file("gateway", address(newGateway)); - - // // clean up - // root.denyContract(address(newInvestmentManager), address(this)); - // root.denyContract(address(newGateway), address(this)); - // root.denyContract(address(poolManager), address(this)); - // root.denyContract(address(escrow), address(this)); - // root.denyContract(address(userEscrow), address(this)); - // root.deny(address(this)); + // file investmentManager on all LiquidityPools + for (uint256 i = 0; i < liquidityPools.length; i++) { + root.relyContract(address(liquidityPools[i]), address(this)); + LiquidityPool lPool = LiquidityPool(liquidityPools[i]); + + lPool.file("investmentManager", address(newInvestmentManager)); + lPool.rely(address(newInvestmentManager)); + newInvestmentManager.rely(address(lPool)); + escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); + } + + // Rewire everything + newInvestmentManager.file("poolManager", address(poolManager)); + root.relyContract(address(poolManager), address(this)); + poolManager.file("investmentManager", address(newInvestmentManager)); + newInvestmentManager.file("gateway", address(newGateway)); + poolManager.file("gateway", address(newGateway)); + newInvestmentManager.rely(address(root)); + newInvestmentManager.rely(address(poolManager)); + newGateway.rely(address(root)); + root.relyContract(address(escrow), address(this)); + Escrow(address(escrow)).rely(address(newInvestmentManager)); + root.relyContract(address(userEscrow), address(this)); + UserEscrow(address(userEscrow)).rely(address(newInvestmentManager)); + + root.relyContract(address(router), address(this)); + router.file("gateway", address(newGateway)); + + // clean up + root.denyContract(address(newInvestmentManager), address(this)); + root.denyContract(address(newGateway), address(this)); + root.denyContract(address(poolManager), address(this)); + root.denyContract(address(escrow), address(this)); + root.denyContract(address(userEscrow), address(this)); + root.deny(address(this)); - // // For the sake of these helper functions, set global variables to new contracts - // gateway = newGateway; - // investmentManager = newInvestmentManager; + // For the sake of these helper functions, set global variables to new contracts + gateway = newGateway; + investmentManager = newInvestmentManager; - // // test that everything is working - // InvestAndRedeem(poolId, trancheId, _lPool); + // test that everything is working + InvestAndRedeem(poolId, trancheId, _lPool); } function testLiquidityPoolMigration() public {} @@ -124,16 +122,9 @@ contract MigrationsTest is TestSetup { uint256 amount, LiquidityPool lPool ) public { - console.log("APPROVAL", erc20.allowance(investor, address(investmentManager))); vm.prank(investor); erc20.approve(address(investmentManager), amount); // add allowance - console.log("address(investor)", address(investor)); - console.log("address(investmentManager)", address(investmentManager)); - console.log("APPROVAL", erc20.allowance(investor, address(investmentManager))); - console.log("requestDeposit amount", amount); - vm.prank(address(investmentManager)); - erc20.approve(address(poolManager), amount); // add allowance vm.prank(investor); lPool.requestDeposit(amount, investor); @@ -142,53 +133,42 @@ contract MigrationsTest is TestSetup { assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); // trigger executed collectInvest - // uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId - - // uint128 trancheTokensPayout = _toUint128( - // uint128(amount).mulDiv( - // 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down - // ) - // ); - - // // Assume an epoch execution happens on cent chain - // // Assume a bot calls collectInvest for this user on cent chain - // // console.log("APPROVAL right before collectInvest", erc20.allowance(investor, address(investmentManager))); - // // console.log("amount right before collectInvest", amount); - // vm.prank(address(gateway)); - // investmentManager.handleExecutedCollectInvest( - // poolId, trancheId, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 - // ); - - // assertEq(lPool.maxMint(investor), trancheTokensPayout); - // assertEq(lPool.maxDeposit(investor), amount); - // assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); - // assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); - - // uint256 div = 2; - // vm.prank(investor); - // lPool.deposit(amount / div, investor); - - // assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); - // assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); - // assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); - // assertEq(lPool.maxDeposit(investor), amount - amount / div); // max deposit - - // // console.log("trancheTokensPayout", trancheTokensPayout); - // // console.log("lPool.maxDeposit", lPool.maxDeposit(investor)); - // // console.log("lPool.maxMint", lPool.maxMint(investor)); - // // console.log("investor", investor); - // // console.log("address(this)", address(this)); - // // console.log("lPool.balanceOf(address(escrow))", lPool.balanceOf(address(escrow))); - // // console.log("erc20.balanceOf(investor)", erc20.balanceOf(investor)); - // // console.log("deposit amount", amount / div); - // uint256 maxMint = lPool.maxMint(investor); - // vm.prank(investor); - // lPool.mint(maxMint, investor); - - // assertEq(lPool.balanceOf(investor), trancheTokensPayout); - // assertTrue(lPool.balanceOf(address(escrow)) <= 1); - // assertTrue(lPool.maxMint(investor) <= 1); - // console.log("APPROVAL", erc20.allowance(investor, address(investmentManager))); + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + + uint128 trancheTokensPayout = _toUint128( + uint128(amount).mulDiv( + 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down + ) + ); + + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectInvest for this user on cent chain + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectInvest( + poolId, trancheId, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 + ); + + assertEq(lPool.maxMint(investor), trancheTokensPayout); + assertEq(lPool.maxDeposit(investor), amount); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); + + uint256 div = 2; + vm.prank(investor); + lPool.deposit(amount / div, investor); + + assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); + assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); + assertEq(lPool.maxDeposit(investor), amount - amount / div); // max deposit + + uint256 maxMint = lPool.maxMint(investor); + vm.prank(investor); + lPool.mint(maxMint, investor); + + assertEq(lPool.balanceOf(investor), trancheTokensPayout); + assertTrue(lPool.balanceOf(address(escrow)) <= 1); + assertTrue(lPool.maxMint(investor) <= 1); } function redeemWithdraw( diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 7520f05b..e1803150 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -287,8 +287,6 @@ contract PoolManagerTest is TestSetup { // Approve and transfer amount from this address to destinationAddress LiquidityPool(lPool_).approve(address(poolManager), amount); - console.logAddress(lPool_); - console.logAddress(LiquidityPool(lPool_).asset()); poolManager.transferTrancheTokensToEVM(poolId, trancheId, uint64(block.chainid), destinationAddress, amount); assertEq(LiquidityPool(lPool_).balanceOf(address(this)), 0); } From 3a6964ea4960545e4d50d6f615e6216a2c743d0c Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 21 Sep 2023 14:03:37 -0400 Subject: [PATCH 04/51] cleanup --- src/InvestmentManager.sol | 1 - test/LiquidityPool.t.sol | 21 +------- test/Migrations.t.sol | 48 +++++++------------ .../MigratedInvestmentmanager.sol | 34 ++++++------- 4 files changed, 36 insertions(+), 68 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index fded268f..9c0261cb 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -27,7 +27,6 @@ interface ERC20Like { function decimals() external view returns (uint8); function mint(address, uint256) external; function burn(address, uint256) external; - function allowance(address, address) external view returns (uint256); } interface LiquidityPoolLike is ERC20Like { diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 34ec8d57..98ade70a 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -1012,7 +1012,7 @@ contract LiquidityPoolTest is TestSetup { ) ) ); - v += 1; + lPool.requestDepositWithPermit(amount, investor, block.timestamp, v, r, s); // To avoid stack too deep errors delete v; @@ -1366,23 +1366,4 @@ contract LiquidityPoolTest is TestSetup { function addressAssumption(address user) public returns (bool) { return (user != address(0) && user != address(erc20) && user.code.length == 0); } - - function testThing() public { - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - uint256(0x8e3ec2b1affade3231c6a81c299988176e0343dd8ce9f0986d66ed13e0f178d9), - keccak256( - abi.encodePacked( - "\x19\x01", - erc20.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - erc20.PERMIT_TYPEHASH(), address(0x423420Ae467df6e90291fd0252c0A8a637C1e03f), address(0x0ca994e97156ABAc43e606C2aEd5Af4b417C104d), 0, 0, block.timestamp - ) - ) - ) - ) - ); - assertEq(r, 0); - assertEq(s, 0); - } } diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 9c344107..b9dd1240 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -24,7 +24,9 @@ contract MigrationsTest is TestSetup { trancheId = bytes16(hex"811acd5b3f17c06841c7e41e9e04cb1b"); currencyId = 1; trancheTokenDecimals = 18; - _lPool = deployLiquidityPool(poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20)); + _lPool = deployLiquidityPool( + poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20) + ); investorCurrencyAmount = 1000 * 10 ** erc20.decimals(); deal(address(erc20), investor, investorCurrencyAmount); @@ -40,11 +42,13 @@ contract MigrationsTest is TestSetup { address[] memory liquidityPools = new address[](1); liquidityPools[0] = _lPool; // Deploy new investmentManager - MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); - + MigratedInvestmentManager newInvestmentManager = + new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); + // Deploy new contracts that take InvestmentManager as constructor argument - Gateway newGateway = new Gateway(address(root), address(newInvestmentManager), address(poolManager), address(router)); - + Gateway newGateway = + new Gateway(address(root), address(newInvestmentManager), address(poolManager), address(router)); + // file investmentManager on all LiquidityPools for (uint256 i = 0; i < liquidityPools.length; i++) { root.relyContract(address(liquidityPools[i]), address(this)); @@ -81,7 +85,6 @@ contract MigrationsTest is TestSetup { root.denyContract(address(userEscrow), address(this)); root.deny(address(this)); - // For the sake of these helper functions, set global variables to new contracts gateway = newGateway; investmentManager = newInvestmentManager; @@ -89,39 +92,26 @@ contract MigrationsTest is TestSetup { // test that everything is working InvestAndRedeem(poolId, trancheId, _lPool); } - + function testLiquidityPoolMigration() public {} function testRootMigration() public {} function testPoolManagerMigration() public {} - // --- Investment and Redeem Flow --- - function InvestAndRedeem( - uint64 poolId, - bytes16 trancheId, - address _lPool - ) public { + function InvestAndRedeem(uint64 poolId, bytes16 trancheId, address _lPool) public { uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); //TODO: fuzz price LiquidityPool lPool = LiquidityPool(_lPool); depositMint(poolId, trancheId, price, investorCurrencyAmount, lPool); uint256 redeemAmount = lPool.balanceOf(investor); - redeemWithdraw( - poolId, trancheId, price, redeemAmount, lPool - ); + redeemWithdraw(poolId, trancheId, price, redeemAmount, lPool); } - function depositMint( - uint64 poolId, - bytes16 trancheId, - uint128 price, - uint256 amount, - LiquidityPool lPool - ) public { + function depositMint(uint64 poolId, bytes16 trancheId, uint128 price, uint256 amount, LiquidityPool lPool) public { vm.prank(investor); erc20.approve(address(investmentManager), amount); // add allowance @@ -171,13 +161,9 @@ contract MigrationsTest is TestSetup { assertTrue(lPool.maxMint(investor) <= 1); } - function redeemWithdraw( - uint64 poolId, - bytes16 trancheId, - uint128 price, - uint256 amount, - LiquidityPool lPool - ) public { + function redeemWithdraw(uint64 poolId, bytes16 trancheId, uint128 price, uint256 amount, LiquidityPool lPool) + public + { vm.expectRevert(bytes("ERC20/insufficient-allowance")); vm.prank(investor); lPool.requestRedeem(amount, investor); @@ -230,4 +216,4 @@ contract MigrationsTest is TestSetup { value = uint128(_value); } } -} \ No newline at end of file +} diff --git a/test/migrationContracts/MigratedInvestmentmanager.sol b/test/migrationContracts/MigratedInvestmentmanager.sol index e1c4d023..9c5a91a9 100644 --- a/test/migrationContracts/MigratedInvestmentmanager.sol +++ b/test/migrationContracts/MigratedInvestmentmanager.sol @@ -3,29 +3,31 @@ pragma solidity 0.8.21; import "src/InvestmentManager.sol"; - // struct LPValues { - // uint128 maxDeposit; // denominated in currency - // uint128 maxMint; // denominated in tranche tokens - // uint128 maxWithdraw; // denominated in currency - // uint128 maxRedeem; // denominated in tranche tokens - // uint128 remainingInvestOrder; // denominated in currency - // uint128 remainingRedeemOrder; // denominated in tranche tokens - // } - contract MigratedInvestmentManager is InvestmentManager { - // mapping(address investor => mapping(address liquidityPool => LPValues)) public orderbook; - - constructor(address _escrow, address _userEscrow, address _oldInvestmentManager, address[] memory investors, address[] memory liquidityPools) InvestmentManager(_escrow, _userEscrow) { + 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(); // populate orderBook - for(uint128 i = 0; i < investors.length; i++) { + for (uint128 i = 0; i < investors.length; i++) { address investor = investors[i]; - for(uint128 j = 0; j < liquidityPools.length; j++) { + for (uint128 j = 0; j < liquidityPools.length; j++) { address liquidityPool = liquidityPools[j]; - (uint128 maxDeposit, uint128 maxMint, uint128 maxWithdraw, uint128 maxRedeem, uint128 remainingInvestOrder, uint128 remainingRedeemOrder) = oldInvestmentManager.orderbook(investor, liquidityPool); + ( + uint128 maxDeposit, + uint128 maxMint, + uint128 maxWithdraw, + uint128 maxRedeem, + uint128 remainingInvestOrder, + uint128 remainingRedeemOrder + ) = oldInvestmentManager.orderbook(investor, liquidityPool); orderbook[investor][liquidityPool] = LPValues({ maxDeposit: maxDeposit, maxMint: maxMint, @@ -37,4 +39,4 @@ contract MigratedInvestmentManager is InvestmentManager { } } } -} \ No newline at end of file +} From b583e1f941c4d02e0f98d54fabaae0df21dda711 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Fri, 22 Sep 2023 14:03:33 -0400 Subject: [PATCH 05/51] wip: add migratedPoolManager.sol --- .../MigratedPoolManager.sol | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/migrationContracts/MigratedPoolManager.sol diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrationContracts/MigratedPoolManager.sol new file mode 100644 index 00000000..1cec6080 --- /dev/null +++ b/test/migrationContracts/MigratedPoolManager.sol @@ -0,0 +1,32 @@ +import "src/PoolManager.sol"; + +// mapping(uint64 poolId => Pool) public pools; + +// /// @dev Chain agnostic currency id -> evm currency address and reverse mapping +// mapping(uint128 currencyId => address) public currencyIdToAddress; +// mapping(address => uint128 currencyId) public currencyAddressToId; + +contract MigratedPoolManager is PoolManager { + uint8 internal constant MAX_DECIMALS = 18; + + constructor( + address escrow_, + address liquidityPoolFactory_, + address restrictionManagerFactory_, + address trancheTokenFactory_, + address oldPoolManager, + uint64[] memory poolIds + ) PoolManager(escrow_, liquidityPoolFactory_, restrictionManagerFactory_, trancheTokenFactory_) { + // migrate pools + for(uint256 i = 0; i < poolIds.length; i++) { + uint64 poolId = poolIds[i]; + PoolManager oldPoolManager_ = PoolManager(oldPoolManager); + Pool memory pool = oldPoolManager_.pools(poolId); + pools[poolId] = pool; + + address currencyAddress = oldPoolManager_.currencyIdToAddress(pool.currencyId); + currencyIdToAddress[pool.currencyId] = currencyAddress; + currencyAddressToId[currencyAddress] = pool.currencyId; + } + } +} From abf3a6d736afeaed72f54ba408b0ef02d6e3b4a3 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 27 Sep 2023 13:43:24 -0400 Subject: [PATCH 06/51] Also verify state has been migrated in addition to full invest/redeem flow --- src/PoolManager.sol | 14 +++++ test/Migrations.t.sol | 50 +++++++++++++++ .../MigratedPoolManager.sol | 61 +++++++++++++++---- 3 files changed, 113 insertions(+), 12 deletions(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index f3bde022..bc4f12a2 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -366,6 +366,20 @@ contract PoolManager is Auth { } // --- Helpers --- + function getTranche(uint64 poolId, bytes16 trancheId) + public + view + returns (address token, uint8 decimals, uint256 createdAt, string memory tokenName, string memory tokenSymbol) + { + Tranche storage tranche = pools[poolId].tranches[trancheId]; + address token = tranche.token; + uint8 decimals = tranche.decimals; + uint256 createdAt = tranche.createdAt; + string memory tokenName = tranche.tokenName; + string memory tokenSymbol = tranche.tokenSymbol; + return (token, decimals, createdAt, tokenName, tokenSymbol); + } + function getTrancheToken(uint64 poolId, bytes16 trancheId) public view returns (address) { Tranche storage tranche = pools[poolId].tranches[trancheId]; return tranche.token; diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index b9dd1240..78458d9e 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -37,10 +37,13 @@ contract MigrationsTest is TestSetup { InvestAndRedeem(poolId, trancheId, _lPool); // Assume executeScheduledRely() is called on this spell + + // 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 investmentManager MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); @@ -85,6 +88,9 @@ contract MigrationsTest is TestSetup { root.denyContract(address(userEscrow), address(this)); root.deny(address(this)); + // very state was migrated successfully + verifyMigratedInvestmentManagerState(investors, liquidityPools, investmentManager, newInvestmentManager); + // For the sake of these helper functions, set global variables to new contracts gateway = newGateway; investmentManager = newInvestmentManager; @@ -93,6 +99,50 @@ contract MigrationsTest is TestSetup { InvestAndRedeem(poolId, trancheId, _lPool); } + 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 newMaxDeposit, uint128 newMaxMint, uint128 newMaxWithdraw,,,) = + newInvestmentManager.orderbook(investor, liquidityPool); + (uint128 oldMaxDeposit, uint128 oldMaxMint, uint128 oldMaxWithdraw,,,) = + investmentManager.orderbook(investor, liquidityPool); + assertEq(newMaxDeposit, oldMaxDeposit); + assertEq(newMaxMint, oldMaxMint); + assertEq(newMaxWithdraw, oldMaxWithdraw); + } + + function verifyRedeemAndRemainingOrders( + address investor, + address liquidityPool, + InvestmentManager investmentManager, + InvestmentManager newInvestmentManager + ) public { + (,,, uint128 newMaxRedeem, uint128 newRemainingInvestOrder, uint128 newRemainingRedeemOrder) = + newInvestmentManager.orderbook(investor, liquidityPool); + (,,, uint128 oldMaxRedeem, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder) = + investmentManager.orderbook(investor, liquidityPool); + assertEq(newMaxRedeem, oldMaxRedeem); + assertEq(newRemainingInvestOrder, oldRemainingInvestOrder); + assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); + } + function testLiquidityPoolMigration() public {} function testRootMigration() public {} diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrationContracts/MigratedPoolManager.sol index 1cec6080..177205dd 100644 --- a/test/migrationContracts/MigratedPoolManager.sol +++ b/test/migrationContracts/MigratedPoolManager.sol @@ -2,31 +2,68 @@ import "src/PoolManager.sol"; // mapping(uint64 poolId => Pool) public pools; -// /// @dev Chain agnostic currency id -> evm currency address and reverse mapping // mapping(uint128 currencyId => address) public currencyIdToAddress; // mapping(address => uint128 currencyId) public currencyAddressToId; contract MigratedPoolManager is PoolManager { - uint8 internal constant MAX_DECIMALS = 18; - + /// @dev Migrating the full state of the PoolManager requires migrated deeply nested mappings, which cannot be passed as an argument. + /// instead we pass 3 arrays. The poolIds, the trancheIds and a uint256 array where the index is the poolId and the value is the number of tranches in that pool. + /// This is used to reconstruct the mapping poolId => trancheId[]. + /// @param escrow_ The address of the escrow contract. + /// @param liquidityPoolFactory_ The address of the liquidityPoolFactory contract. + /// @param restrictionManagerFactory_ The address of the restrictionManagerFactory contract. + /// @param trancheTokenFactory_ The address of the trancheTokenFactory contract. + /// @param oldPoolManager The address of the old poolManager contract. + /// @param poolIds The poolIds of the pools to migrate. + /// @param trancheIds A sequential array of all trancheIds of all pools to migrate. Use the poolIdToTrancheIdMapping array to determine which trancheIds belongs to which poolId. constructor( address escrow_, address liquidityPoolFactory_, address restrictionManagerFactory_, address trancheTokenFactory_, address oldPoolManager, - uint64[] memory poolIds + uint64[] memory poolIds, + bytes16[][] memory trancheIds, + address[][] memory allowedCurrencies ) PoolManager(escrow_, liquidityPoolFactory_, restrictionManagerFactory_, trancheTokenFactory_) { // migrate pools - for(uint256 i = 0; i < poolIds.length; i++) { - uint64 poolId = poolIds[i]; + for (uint256 i = 0; i < poolIds.length; i++) { PoolManager oldPoolManager_ = PoolManager(oldPoolManager); - Pool memory pool = oldPoolManager_.pools(poolId); - pools[poolId] = pool; - - address currencyAddress = oldPoolManager_.currencyIdToAddress(pool.currencyId); - currencyIdToAddress[pool.currencyId] = currencyAddress; - currencyAddressToId[currencyAddress] = pool.currencyId; + (, uint256 createdAt) = oldPoolManager_.pools(poolIds[i]); + + Pool storage pool = pools[poolIds[i]]; + pool.poolId = poolIds[i]; + pool.createdAt = createdAt; + + // migrate tranches + for (uint256 j = 0; j < trancheIds[i].length; j++) { + bytes16 trancheId = trancheIds[i][j]; + + (address token, uint8 decimals, uint256 createdAt, string memory tokenName, string memory tokenSymbol) = + oldPoolManager_.getTranche(poolIds[i], trancheId); + + pool.tranches[trancheId].token = token; + pool.tranches[trancheId].decimals = decimals; + pool.tranches[trancheId].createdAt = createdAt; + pool.tranches[trancheId].tokenName = tokenName; + pool.tranches[trancheId].tokenSymbol = tokenSymbol; + + // (, uint256 maturityDate, uint256 totalSupply, uint256 totalRedeemable) = oldPoolManager.tranches(poolIds[i], trancheId); + // tranche.trancheId = trancheId; + // tranche.maturityDate = maturityDate; + // tranche.totalSupply = totalSupply; + // tranche.totalRedeemable = totalRedeemable; + } + + // migrate allowed currencies + for (uint256 j = 0; j < allowedCurrencies[i].length; j++) { + address currencyAddress = allowedCurrencies[i][j]; + pool.allowedCurrencies[currencyAddress] = true; + } + + // address currencyAddress = oldPoolManager_.currencyIdToAddress(pool.currencyId); + // currencyIdToAddress[pool.currencyId] = currencyAddress; + // currencyAddressToId[currencyAddress] = pool.currencyId; } } } From 0bd8c7b21a2e04c11266f3995fdb721d36950ee0 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 27 Sep 2023 14:25:53 -0400 Subject: [PATCH 07/51] break up migration into separate functions --- .../MigratedPoolManager.sol | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrationContracts/MigratedPoolManager.sol index 177205dd..1cf15768 100644 --- a/test/migrationContracts/MigratedPoolManager.sol +++ b/test/migrationContracts/MigratedPoolManager.sol @@ -24,9 +24,20 @@ contract MigratedPoolManager is PoolManager { address oldPoolManager, uint64[] memory poolIds, bytes16[][] memory trancheIds, - address[][] memory allowedCurrencies + address[][] memory allowedCurrencies, + address[][][] memory liquidityPoolCurrencies ) PoolManager(escrow_, liquidityPoolFactory_, restrictionManagerFactory_, trancheTokenFactory_) { // migrate pools + migratePools(oldPoolManager, poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies); + } + + function migratePools( + address oldPoolManager, + uint64[] memory poolIds, + bytes16[][] memory trancheIds, + address[][] memory allowedCurrencies, + address[][][] memory liquidityPoolCurrencies + ) internal { for (uint256 i = 0; i < poolIds.length; i++) { PoolManager oldPoolManager_ = PoolManager(oldPoolManager); (, uint256 createdAt) = oldPoolManager_.pools(poolIds[i]); @@ -36,34 +47,36 @@ contract MigratedPoolManager is PoolManager { pool.createdAt = createdAt; // migrate tranches - for (uint256 j = 0; j < trancheIds[i].length; j++) { - bytes16 trancheId = trancheIds[i][j]; - - (address token, uint8 decimals, uint256 createdAt, string memory tokenName, string memory tokenSymbol) = - oldPoolManager_.getTranche(poolIds[i], trancheId); - - pool.tranches[trancheId].token = token; - pool.tranches[trancheId].decimals = decimals; - pool.tranches[trancheId].createdAt = createdAt; - pool.tranches[trancheId].tokenName = tokenName; - pool.tranches[trancheId].tokenSymbol = tokenSymbol; - - // (, uint256 maturityDate, uint256 totalSupply, uint256 totalRedeemable) = oldPoolManager.tranches(poolIds[i], trancheId); - // tranche.trancheId = trancheId; - // tranche.maturityDate = maturityDate; - // tranche.totalSupply = totalSupply; - // tranche.totalRedeemable = totalRedeemable; - } + migrateTranches(pool, trancheIds[i], liquidityPoolCurrencies[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(Pool storage pool, bytes16[] memory trancheIds, address[][] memory liquidityPoolCurrencies, PoolManager oldPoolManager_) + internal + { + for (uint256 j = 0; j < trancheIds.length; j++) { + bytes16 trancheId = trancheIds[j]; - // address currencyAddress = oldPoolManager_.currencyIdToAddress(pool.currencyId); - // currencyIdToAddress[pool.currencyId] = currencyAddress; - // currencyAddressToId[currencyAddress] = pool.currencyId; + (address token, uint8 decimals, uint256 createdAt, string memory tokenName, string memory tokenSymbol) = + oldPoolManager_.getTranche(pool.poolId, trancheId); + + pool.tranches[trancheId].token = token; + pool.tranches[trancheId].decimals = decimals; + pool.tranches[trancheId].createdAt = createdAt; + pool.tranches[trancheId].tokenName = tokenName; + pool.tranches[trancheId].tokenSymbol = tokenSymbol; + + for (uint256 k = 0; k < liquidityPoolCurrencies[j].length; k++) { + address currencyAddress = liquidityPoolCurrencies[j][k]; + pool.tranches[trancheId].liquidityPools[currencyAddress] = + oldPoolManager_.getLiquidityPool(pool.poolId, trancheId, currencyAddress); + } } } } From 2fd8b00bb267fd69ffa99d639d86db4f09a5f949 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 27 Sep 2023 16:55:40 -0400 Subject: [PATCH 08/51] poolManager state migrating successfully --- script/Deployer.sol | 10 ++- test/Migrations.t.sol | 75 ++++++++++++++++++- .../MigratedPoolManager.sol | 23 ++++-- 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/script/Deployer.sol b/script/Deployer.sol index 7b8ea567..b39ad6df 100644 --- a/script/Deployer.sol +++ b/script/Deployer.sol @@ -32,6 +32,10 @@ contract Deployer is Script { PauseAdmin public pauseAdmin; DelayedAdmin public delayedAdmin; Gateway public gateway; + address public liquidityPoolFactory; + address public restrictionManagerFactory; + address public trancheTokenFactory; + function deployInvestmentManager() public { escrow = new Escrow(); @@ -40,9 +44,9 @@ contract Deployer is Script { investmentManager = new InvestmentManager(address(escrow), address(userEscrow)); - address liquidityPoolFactory = address(new LiquidityPoolFactory(address(root))); - address restrictionManagerFactory = address(new RestrictionManagerFactory(address(root))); - address trancheTokenFactory = address(new TrancheTokenFactory(address(root))); + liquidityPoolFactory = address(new LiquidityPoolFactory(address(root))); + restrictionManagerFactory = address(new RestrictionManagerFactory(address(root))); + trancheTokenFactory = address(new TrancheTokenFactory(address(root))); investmentManager = new InvestmentManager(address(escrow), address(userEscrow)); poolManager = new PoolManager(address(escrow), liquidityPoolFactory, restrictionManagerFactory, trancheTokenFactory); diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 78458d9e..dca121c3 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.21; import "./TestSetup.t.sol"; import "src/LiquidityPool.sol"; import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestmentManager.sol"; +import {MigratedPoolManager} from "test/migrationContracts/MigratedPoolManager.sol"; import {MathLib} from "src/util/MathLib.sol"; contract MigrationsTest is TestSetup { @@ -143,7 +144,79 @@ contract MigrationsTest is TestSetup { assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); } - function testLiquidityPoolMigration() public {} + function testLiquidityPoolMigration() public { + InvestAndRedeem(poolId, trancheId, _lPool); + + 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); + + + MigratedPoolManager newPoolManager = new MigratedPoolManager( + address(escrow), + liquidityPoolFactory, + restrictionManagerFactory, + trancheTokenFactory, + address(poolManager), + poolIds, + trancheIds, + allowedCurrencies, + liquidityPoolCurrencies + ); + } + + 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); + + 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, uint8 newDecimals, uint256 newCreatedAt, string memory newTokenName, string memory newTokenSymbol) = + newPoolManager.getTranche(poolId, trancheId); + (address oldToken, uint8 oldDecimals, uint256 oldCreatedAt, string memory oldTokenName, string memory oldTokenSymbol) = + poolManager.getTranche(poolId, trancheId); + assertEq(newToken, oldToken); + assertEq(newDecimals, oldDecimals); + assertEq(newCreatedAt, oldCreatedAt); + assertEq(newTokenName, oldTokenName); + assertEq(newTokenSymbol, oldTokenSymbol); + } + + function verifyAllowedCurrency(uint64 poolId, address currencyAddress, PoolManager poolManager, PoolManager newPoolManager) public { + bool newAllowed = newPoolManager.isAllowedAsPoolCurrency(poolId, currencyAddress); + bool oldAllowed = poolManager.isAllowedAsPoolCurrency(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); + } function testRootMigration() public {} diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrationContracts/MigratedPoolManager.sol index 1cf15768..e82ac051 100644 --- a/test/migrationContracts/MigratedPoolManager.sol +++ b/test/migrationContracts/MigratedPoolManager.sol @@ -28,18 +28,26 @@ contract MigratedPoolManager is PoolManager { address[][][] memory liquidityPoolCurrencies ) PoolManager(escrow_, liquidityPoolFactory_, restrictionManagerFactory_, trancheTokenFactory_) { // migrate pools - migratePools(oldPoolManager, poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies); + PoolManager oldPoolManager_ = PoolManager(oldPoolManager); + migratePools(oldPoolManager_, poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies); + 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( - address oldPoolManager, + PoolManager oldPoolManager_, uint64[] memory poolIds, bytes16[][] memory trancheIds, address[][] memory allowedCurrencies, address[][][] memory liquidityPoolCurrencies ) internal { for (uint256 i = 0; i < poolIds.length; i++) { - PoolManager oldPoolManager_ = PoolManager(oldPoolManager); (, uint256 createdAt) = oldPoolManager_.pools(poolIds[i]); Pool storage pool = pools[poolIds[i]]; @@ -57,9 +65,12 @@ contract MigratedPoolManager is PoolManager { } } - function migrateTranches(Pool storage pool, bytes16[] memory trancheIds, address[][] memory liquidityPoolCurrencies, PoolManager oldPoolManager_) - internal - { + function migrateTranches( + Pool storage pool, + bytes16[] memory trancheIds, + address[][] memory liquidityPoolCurrencies, + PoolManager oldPoolManager_ + ) internal { for (uint256 j = 0; j < trancheIds.length; j++) { bytes16 trancheId = trancheIds[j]; From bfa8a83135f231afebf71805035e9321a4eb1970 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 28 Sep 2023 15:26:48 -0400 Subject: [PATCH 09/51] pool manager migration working and clean up --- script/Deployer.sol | 1 - test/Migrations.t.sol | 139 ++++++++++++++++++++++++++++++------------ 2 files changed, 100 insertions(+), 40 deletions(-) diff --git a/script/Deployer.sol b/script/Deployer.sol index b39ad6df..90892197 100644 --- a/script/Deployer.sol +++ b/script/Deployer.sol @@ -36,7 +36,6 @@ contract Deployer is Script { address public restrictionManagerFactory; address public trancheTokenFactory; - function deployInvestmentManager() public { escrow = new Escrow(); userEscrow = new UserEscrow(); diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index dca121c3..7e1d9a48 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -32,12 +32,15 @@ contract MigrationsTest is TestSetup { investorCurrencyAmount = 1000 * 10 ** erc20.decimals(); deal(address(erc20), investor, investorCurrencyAmount); centrifugeChain.updateMember(poolId, trancheId, investor, uint64(block.timestamp + 1000 days)); + removeDeployerAccess(address(router)); } function testInvestmentManagerMigration() public { InvestAndRedeem(poolId, trancheId, _lPool); - // Assume executeScheduledRely() is called on this spell + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); // Assume these records are available off-chain address[] memory investors = new address[](1); @@ -49,9 +52,20 @@ contract MigrationsTest is TestSetup { MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); - // Deploy new contracts that take InvestmentManager as constructor argument - Gateway newGateway = - new Gateway(address(root), address(newInvestmentManager), address(poolManager), address(router)); + // Rewire everything + root.relyContract(address(gateway), address(this)); + gateway.file("investmentManager", address(newInvestmentManager)); + newInvestmentManager.file("poolManager", address(poolManager)); + root.relyContract(address(poolManager), address(this)); + poolManager.file("investmentManager", address(newInvestmentManager)); + newInvestmentManager.file("gateway", address(gateway)); + newInvestmentManager.rely(address(root)); + newInvestmentManager.rely(address(poolManager)); + root.relyContract(address(escrow), address(this)); + escrow.rely(address(newInvestmentManager)); + root.relyContract(address(userEscrow), address(this)); + userEscrow.rely(address(newInvestmentManager)); + root.relyContract(address(router), address(this)); // file investmentManager on all LiquidityPools for (uint256 i = 0; i < liquidityPools.length; i++) { @@ -64,26 +78,9 @@ contract MigrationsTest is TestSetup { escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); } - // Rewire everything - newInvestmentManager.file("poolManager", address(poolManager)); - root.relyContract(address(poolManager), address(this)); - poolManager.file("investmentManager", address(newInvestmentManager)); - newInvestmentManager.file("gateway", address(newGateway)); - poolManager.file("gateway", address(newGateway)); - newInvestmentManager.rely(address(root)); - newInvestmentManager.rely(address(poolManager)); - newGateway.rely(address(root)); - root.relyContract(address(escrow), address(this)); - Escrow(address(escrow)).rely(address(newInvestmentManager)); - root.relyContract(address(userEscrow), address(this)); - UserEscrow(address(userEscrow)).rely(address(newInvestmentManager)); - - root.relyContract(address(router), address(this)); - router.file("gateway", address(newGateway)); - // clean up root.denyContract(address(newInvestmentManager), address(this)); - root.denyContract(address(newGateway), 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)); @@ -93,7 +90,6 @@ contract MigrationsTest is TestSetup { verifyMigratedInvestmentManagerState(investors, liquidityPools, investmentManager, newInvestmentManager); // For the sake of these helper functions, set global variables to new contracts - gateway = newGateway; investmentManager = newInvestmentManager; // test that everything is working @@ -144,9 +140,14 @@ contract MigrationsTest is TestSetup { assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); } - function testLiquidityPoolMigration() public { + function testPoolManagerMigration() public { InvestAndRedeem(poolId, trancheId, _lPool); + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // assume these records are available off-chain uint64[] memory poolIds = new uint64[](1); poolIds[0] = poolId; bytes16[][] memory trancheIds = new bytes16[][](1); @@ -160,7 +161,6 @@ contract MigrationsTest is TestSetup { liquidityPoolCurrencies[0][0] = new address[](1); liquidityPoolCurrencies[0][0][0] = address(erc20); - MigratedPoolManager newPoolManager = new MigratedPoolManager( address(escrow), liquidityPoolFactory, @@ -172,9 +172,50 @@ contract MigrationsTest is TestSetup { allowedCurrencies, liquidityPoolCurrencies ); + + verifyMigratedPoolManagerState( + poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, poolManager, newPoolManager + ); + + LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); + TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); + 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)); + newPoolManager.rely(address(root)); + root.relyContract(address(escrow), address(this)); + escrow.rely(address(newPoolManager)); + + 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.deny(address(this)); + + poolManager = newPoolManager; + + centrifugeChain.addPool(poolId + 1); // add pool + centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", trancheTokenDecimals); // 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)); + + InvestAndRedeem(poolId + 1, trancheId, _lPool2); } - function verifyMigratedPoolManagerState(uint64[] memory poolIds, bytes16[][] memory trancheIds, address[][] memory allowedCurrencies, address[][][] memory liquidityPoolCurrencies, PoolManager poolManager, PoolManager newPoolManager) public { + 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]); @@ -183,22 +224,35 @@ contract MigrationsTest is TestSetup { 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); + 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, uint8 newDecimals, uint256 newCreatedAt, string memory newTokenName, string memory newTokenSymbol) = - newPoolManager.getTranche(poolId, trancheId); - (address oldToken, uint8 oldDecimals, uint256 oldCreatedAt, string memory oldTokenName, string memory oldTokenSymbol) = - poolManager.getTranche(poolId, trancheId); + function verifyTranche(uint64 poolId, bytes16 trancheId, PoolManager poolManager, PoolManager newPoolManager) + public + { + ( + address newToken, + uint8 newDecimals, + uint256 newCreatedAt, + string memory newTokenName, + string memory newTokenSymbol + ) = newPoolManager.getTranche(poolId, trancheId); + ( + address oldToken, + uint8 oldDecimals, + uint256 oldCreatedAt, + string memory oldTokenName, + string memory oldTokenSymbol + ) = poolManager.getTranche(poolId, trancheId); assertEq(newToken, oldToken); assertEq(newDecimals, oldDecimals); assertEq(newCreatedAt, oldCreatedAt); @@ -206,22 +260,29 @@ contract MigrationsTest is TestSetup { assertEq(newTokenSymbol, oldTokenSymbol); } - function verifyAllowedCurrency(uint64 poolId, address currencyAddress, PoolManager poolManager, PoolManager newPoolManager) public { + function verifyAllowedCurrency( + uint64 poolId, + address currencyAddress, + PoolManager poolManager, + PoolManager newPoolManager + ) public { bool newAllowed = newPoolManager.isAllowedAsPoolCurrency(poolId, currencyAddress); bool oldAllowed = poolManager.isAllowedAsPoolCurrency(poolId, currencyAddress); assertEq(newAllowed, oldAllowed); } - function verifyLiquidityPoolCurrency(uint64 poolId, bytes16 trancheId, address currencyAddresses, PoolManager poolManager, PoolManager newPoolManager) public { + 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); } - function testRootMigration() public {} - - function testPoolManagerMigration() public {} - // --- Investment and Redeem Flow --- function InvestAndRedeem(uint64 poolId, bytes16 trancheId, address _lPool) public { From 2beadb1970490cc8db9e5c90d59038cea8e3a4c8 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 28 Sep 2023 16:27:23 -0400 Subject: [PATCH 10/51] Get tests passing again --- test/Migrations.t.sol | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 569d20a1..129540aa 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -7,6 +7,12 @@ import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestm import {MigratedPoolManager} from "test/migrationContracts/MigratedPoolManager.sol"; import {MathLib} from "src/util/MathLib.sol"; +interface AuthLike { + function rely(address) external; + + function deny(address) external; +} + contract MigrationsTest is TestSetup { using MathLib for uint128; @@ -74,6 +80,8 @@ contract MigrationsTest is TestSetup { lPool.file("investmentManager", address(newInvestmentManager)); lPool.rely(address(newInvestmentManager)); + root.relyContract(address(lPool.share()), address(this)); + AuthLike(address(lPool.share())).rely(address(newInvestmentManager)); newInvestmentManager.rely(address(lPool)); escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); } @@ -189,11 +197,14 @@ contract MigrationsTest is TestSetup { newPoolManager.rely(address(root)); root.relyContract(address(escrow), address(this)); escrow.rely(address(newPoolManager)); + root.relyContract(restrictionManagerFactory, address(this)); + AuthLike(restrictionManagerFactory).rely(address(newPoolManager)); 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)); poolManager = newPoolManager; @@ -267,8 +278,8 @@ contract MigrationsTest is TestSetup { PoolManager poolManager, PoolManager newPoolManager ) public { - bool newAllowed = newPoolManager.isAllowedAsPoolCurrency(poolId, currencyAddress); - bool oldAllowed = poolManager.isAllowedAsPoolCurrency(poolId, currencyAddress); + bool newAllowed = newPoolManager.isAllowedAsInvestmentCurrency(poolId, currencyAddress); + bool oldAllowed = poolManager.isAllowedAsInvestmentCurrency(poolId, currencyAddress); assertEq(newAllowed, oldAllowed); } @@ -349,11 +360,6 @@ contract MigrationsTest is TestSetup { function redeemWithdraw(uint64 poolId, bytes16 trancheId, uint128 price, uint256 amount, LiquidityPool lPool) public { - vm.expectRevert(bytes("ERC20/insufficient-allowance")); - vm.prank(investor); - lPool.requestRedeem(amount, investor); - vm.prank(investor); - lPool.approve(address(investmentManager), amount); vm.prank(investor); lPool.requestRedeem(amount, investor); From c896c7b388af33a9b72546c2ecbbcf9ee07d3586 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 28 Sep 2023 16:34:08 -0400 Subject: [PATCH 11/51] update filename --- ...igratedInvestmentmanager.sol => MigratedInvestmentManager.sol} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/migrationContracts/{MigratedInvestmentmanager.sol => MigratedInvestmentManager.sol} (100%) diff --git a/test/migrationContracts/MigratedInvestmentmanager.sol b/test/migrationContracts/MigratedInvestmentManager.sol similarity index 100% rename from test/migrationContracts/MigratedInvestmentmanager.sol rename to test/migrationContracts/MigratedInvestmentManager.sol From 727614de3d15d3c9fe275c1567bf3753f06b63ed Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Fri, 29 Sep 2023 14:36:10 -0400 Subject: [PATCH 12/51] clean up --- test/Migrations.t.sol | 112 ++++++++++-------- .../MigratedInvestmentManager.sol | 3 +- .../MigratedPoolManager.sol | 18 +-- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index faf4f4cd..a2f69b35 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -41,24 +41,30 @@ contract MigrationsTest is TestSetup { removeDeployerAccess(address(router), address(this)); } + // --- Migration Tests --- + function testInvestmentManagerMigration() public { InvestAndRedeem(poolId, trancheId, _lPool); + // 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 investmentManager + // Deploy new MigratedInvestmentManager MigratedInvestmentManager newInvestmentManager = new MigratedInvestmentManager(address(escrow), address(userEscrow), address(investmentManager), investors, liquidityPools); - // Rewire everything + verifyMigratedInvestmentManagerState(investors, liquidityPools, investmentManager, newInvestmentManager); + + // Rewire contracts root.relyContract(address(gateway), address(this)); gateway.file("investmentManager", address(newInvestmentManager)); newInvestmentManager.file("poolManager", address(poolManager)); @@ -94,9 +100,6 @@ contract MigrationsTest is TestSetup { root.denyContract(address(userEscrow), address(this)); root.deny(address(this)); - // very state was migrated successfully - verifyMigratedInvestmentManagerState(investors, liquidityPools, investmentManager, newInvestmentManager); - // For the sake of these helper functions, set global variables to new contracts investmentManager = newInvestmentManager; @@ -104,57 +107,15 @@ contract MigrationsTest is TestSetup { InvestAndRedeem(poolId, trancheId, _lPool); } - 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 newMaxDeposit, uint128 newMaxMint, uint128 newMaxWithdraw,,,) = - newInvestmentManager.orderbook(investor, liquidityPool); - (uint128 oldMaxDeposit, uint128 oldMaxMint, uint128 oldMaxWithdraw,,,) = - investmentManager.orderbook(investor, liquidityPool); - assertEq(newMaxDeposit, oldMaxDeposit); - assertEq(newMaxMint, oldMaxMint); - assertEq(newMaxWithdraw, oldMaxWithdraw); - } - - function verifyRedeemAndRemainingOrders( - address investor, - address liquidityPool, - InvestmentManager investmentManager, - InvestmentManager newInvestmentManager - ) public { - (,,, uint128 newMaxRedeem, uint128 newRemainingInvestOrder, uint128 newRemainingRedeemOrder) = - newInvestmentManager.orderbook(investor, liquidityPool); - (,,, uint128 oldMaxRedeem, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder) = - investmentManager.orderbook(investor, liquidityPool); - assertEq(newMaxRedeem, oldMaxRedeem); - assertEq(newRemainingInvestOrder, oldRemainingInvestOrder); - assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); - } - function testPoolManagerMigration() public { InvestAndRedeem(poolId, trancheId, _lPool); + // Simulate intended upgrade flow centrifugeChain.incomingScheduleUpgrade(address(this)); vm.warp(block.timestamp + 3 days); root.executeScheduledRely(address(this)); + // 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; @@ -169,6 +130,7 @@ contract MigrationsTest is TestSetup { liquidityPoolCurrencies[0][0] = new address[](1); liquidityPoolCurrencies[0][0][0] = address(erc20); + // Deploy new MigratedPoolManager MigratedPoolManager newPoolManager = new MigratedPoolManager( address(escrow), liquidityPoolFactory, @@ -185,6 +147,7 @@ contract MigrationsTest is TestSetup { poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, poolManager, newPoolManager ); + // Rewire contracts LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); root.relyContract(address(gateway), address(this)); @@ -200,6 +163,7 @@ contract MigrationsTest is TestSetup { root.relyContract(restrictionManagerFactory, address(this)); AuthLike(restrictionManagerFactory).rely(address(newPoolManager)); + // clean up root.denyContract(address(investmentManager), address(this)); root.denyContract(address(gateway), address(this)); root.denyContract(address(newPoolManager), address(this)); @@ -207,8 +171,10 @@ contract MigrationsTest is TestSetup { root.denyContract(restrictionManagerFactory, address(this)); root.deny(address(this)); + // For the sake of these helper functions, set global variables to new contracts poolManager = newPoolManager; + // test that everything is working centrifugeChain.addPool(poolId + 1); // add pool centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", trancheTokenDecimals); // add tranche centrifugeChain.allowInvestmentCurrency(poolId + 1, currencyId); @@ -219,6 +185,52 @@ contract MigrationsTest is TestSetup { InvestAndRedeem(poolId + 1, trancheId, _lPool2); } + // --- 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 newMaxDeposit, uint128 newMaxMint, uint128 newMaxWithdraw,,,) = + newInvestmentManager.orderbook(investor, liquidityPool); + (uint128 oldMaxDeposit, uint128 oldMaxMint, uint128 oldMaxWithdraw,,,) = + investmentManager.orderbook(investor, liquidityPool); + assertEq(newMaxDeposit, oldMaxDeposit); + assertEq(newMaxMint, oldMaxMint); + assertEq(newMaxWithdraw, oldMaxWithdraw); + } + + function verifyRedeemAndRemainingOrders( + address investor, + address liquidityPool, + InvestmentManager investmentManager, + InvestmentManager newInvestmentManager + ) public { + (,,, uint128 newMaxRedeem, uint128 newRemainingInvestOrder, uint128 newRemainingRedeemOrder) = + newInvestmentManager.orderbook(investor, liquidityPool); + (,,, uint128 oldMaxRedeem, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder) = + investmentManager.orderbook(investor, liquidityPool); + assertEq(newMaxRedeem, oldMaxRedeem); + assertEq(newRemainingInvestOrder, oldRemainingInvestOrder); + assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); + } + function verifyMigratedPoolManagerState( uint64[] memory poolIds, bytes16[][] memory trancheIds, @@ -398,7 +410,7 @@ contract MigrationsTest is TestSetup { assertEq(lPool.maxRedeem(investor), 0); } - // --- Helpers --- + // --- Utils --- function _toUint128(uint256 _value) internal pure returns (uint128 value) { if (_value > type(uint128).max) { diff --git a/test/migrationContracts/MigratedInvestmentManager.sol b/test/migrationContracts/MigratedInvestmentManager.sol index 9c5a91a9..ff4f51ad 100644 --- a/test/migrationContracts/MigratedInvestmentManager.sol +++ b/test/migrationContracts/MigratedInvestmentManager.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.21; import "src/InvestmentManager.sol"; contract MigratedInvestmentManager is InvestmentManager { + /// @param investors The investors to migrate. + /// @param liquidityPools The liquidity pools to migrate. constructor( address _escrow, address _userEscrow, @@ -15,7 +17,6 @@ contract MigratedInvestmentManager is InvestmentManager { gateway = oldInvestmentManager.gateway(); poolManager = oldInvestmentManager.poolManager(); - // populate orderBook for (uint128 i = 0; i < investors.length; i++) { address investor = investors[i]; for (uint128 j = 0; j < liquidityPools.length; j++) { diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrationContracts/MigratedPoolManager.sol index 9004ffda..02df0f30 100644 --- a/test/migrationContracts/MigratedPoolManager.sol +++ b/test/migrationContracts/MigratedPoolManager.sol @@ -1,23 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.21; import "src/PoolManager.sol"; -// mapping(uint64 poolId => Pool) public pools; - -// mapping(uint128 currencyId => address) public currencyIdToAddress; -// mapping(address => uint128 currencyId) public currencyAddressToId; - contract MigratedPoolManager is PoolManager { - /// @dev Migrating the full state of the PoolManager requires migrated deeply nested mappings, which cannot be passed as an argument. - /// instead we pass 3 arrays. The poolIds, the trancheIds and a uint256 array where the index is the poolId and the value is the number of tranches in that pool. - /// This is used to reconstruct the mapping poolId => trancheId[]. - /// @param escrow_ The address of the escrow contract. - /// @param liquidityPoolFactory_ The address of the liquidityPoolFactory contract. - /// @param restrictionManagerFactory_ The address of the restrictionManagerFactory contract. - /// @param trancheTokenFactory_ The address of the trancheTokenFactory contract. - /// @param oldPoolManager The address of the old poolManager contract. /// @param poolIds The poolIds of the pools to migrate. - /// @param trancheIds A sequential array of all trancheIds of all pools to migrate. Use the poolIdToTrancheIdMapping array to determine which trancheIds belongs to which poolId. + /// @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_, From 440b2906f2fcc3ba62e562e54d19b76ee5c07f18 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 3 Oct 2023 08:56:42 -0400 Subject: [PATCH 13/51] remove helper function --- src/PoolManager.sol | 12 ------------ test/Migrations.t.sol | 4 ++-- test/migrationContracts/MigratedPoolManager.sol | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 949aa9d7..2c3fff9c 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -397,18 +397,6 @@ contract PoolManager is Auth { } // --- Helpers --- - function getUndeployedTranche(uint64 poolId, bytes16 trancheId) - public - view - returns (uint8 decimals, string memory tokenName, string memory tokenSymbol) - { - UndeployedTranche storage tranche = undeployedTranches[poolId][trancheId]; - uint8 decimals = tranche.decimals; - string memory tokenName = tranche.tokenName; - string memory tokenSymbol = tranche.tokenSymbol; - return (decimals, tokenName, tokenSymbol); - } - function getTrancheToken(uint64 poolId, bytes16 trancheId) public view returns (address) { Tranche storage tranche = pools[poolId].tranches[trancheId]; return tranche.token; diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index a2f69b35..4c2beb63 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -275,9 +275,9 @@ contract MigrationsTest is TestSetup { ) public { for (uint256 i = 0; i < trancheIds.length; i++) { (uint8 oldDecimals, string memory oldTokenName, string memory oldTokenSymbol) = - poolManager.getUndeployedTranche(poolId, trancheIds[i]); + poolManager.undeployedTranches(poolId, trancheIds[i]); (uint8 newDecimals, string memory newTokenName, string memory newTokenSymbol) = - newPoolManager.getUndeployedTranche(poolId, trancheIds[i]); + newPoolManager.undeployedTranches(poolId, trancheIds[i]); assertEq(newDecimals, oldDecimals); assertEq(newTokenName, oldTokenName); assertEq(newTokenSymbol, oldTokenSymbol); diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrationContracts/MigratedPoolManager.sol index 02df0f30..1342313f 100644 --- a/test/migrationContracts/MigratedPoolManager.sol +++ b/test/migrationContracts/MigratedPoolManager.sol @@ -86,7 +86,7 @@ contract MigratedPoolManager is PoolManager { bytes16 trancheId = trancheIds[j]; (uint8 decimals, string memory tokenName, string memory tokenSymbol) = - oldPoolManager_.getUndeployedTranche(poolId, trancheId); + oldPoolManager_.undeployedTranches(poolId, trancheId); undeployedTranches[poolId][trancheId].decimals = decimals; undeployedTranches[poolId][trancheId].tokenName = tokenName; From 089b8e8207d5ef73d00433deea84f9bb8f055490 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 3 Oct 2023 14:09:13 -0400 Subject: [PATCH 14/51] add explicit permission checks --- test/Migrations.t.sol | 54 ++++++++++++++----- .../MigratedInvestmentManager.sol | 7 +++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 8ffe2df7..fdc69254 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -44,8 +44,6 @@ contract MigrationsTest is TestSetup { // --- Migration Tests --- function testInvestmentManagerMigration() public { - InvestAndRedeem(poolId, trancheId, _lPool); - // Simulate intended upgrade flow centrifugeChain.incomingScheduleUpgrade(address(this)); vm.warp(block.timestamp + 3 days); @@ -77,7 +75,6 @@ contract MigrationsTest is TestSetup { escrow.rely(address(newInvestmentManager)); root.relyContract(address(userEscrow), address(this)); userEscrow.rely(address(newInvestmentManager)); - root.relyContract(address(router), address(this)); // file investmentManager on all LiquidityPools for (uint256 i = 0; i < liquidityPools.length; i++) { @@ -100,15 +97,14 @@ contract MigrationsTest is TestSetup { root.denyContract(address(userEscrow), address(this)); root.deny(address(this)); - // For the sake of these helper functions, set global variables to new contracts - investmentManager = newInvestmentManager; + verifyMigratedInvestmentManagerPermissions(investmentManager, newInvestmentManager); - // test that everything is working - InvestAndRedeem(poolId, trancheId, _lPool); + investmentManager = newInvestmentManager; + VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } - function testPoolManagerMigration() public { - InvestAndRedeem(poolId, trancheId, _lPool); + function testPoolManagerMigrationInvestRedeem() public { + VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); // Simulate intended upgrade flow centrifugeChain.incomingScheduleUpgrade(address(this)); @@ -171,10 +167,10 @@ contract MigrationsTest is TestSetup { root.denyContract(restrictionManagerFactory, address(this)); root.deny(address(this)); - // For the sake of these helper functions, set global variables to new contracts - poolManager = newPoolManager; + verifyMigratedPoolManagerPermissions(poolManager, newPoolManager); // test that everything is working + poolManager = newPoolManager; centrifugeChain.addPool(poolId + 1); // add pool centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", trancheTokenDecimals); // add tranche centrifugeChain.allowInvestmentCurrency(poolId + 1, currencyId); @@ -182,7 +178,39 @@ contract MigrationsTest is TestSetup { address _lPool2 = poolManager.deployLiquidityPool(poolId + 1, trancheId, address(erc20)); centrifugeChain.updateMember(poolId + 1, trancheId, investor, uint64(block.timestamp + 1000 days)); - InvestAndRedeem(poolId + 1, trancheId, _lPool2); + VerifyInvestAndRedeemFlow(poolId + 1, trancheId, _lPool2); + } + + // --- Permissions & Dependencies Checks --- + + function verifyMigratedInvestmentManagerPermissions(InvestmentManager oldInvestmentManager, InvestmentManager newInvestmentManager) public { + assertTrue(address(oldInvestmentManager) != address(newInvestmentManager)); + 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())); + 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(investmentManager)), 1); + assertEq(userEscrow.wards(address(investmentManager)), 1); + } + + function verifyMigratedPoolManagerPermissions(PoolManager oldPoolManager, PoolManager newPoolManager) public { + assertTrue(address(oldPoolManager) != address(newPoolManager)); + assertEq(address(oldPoolManager.escrow()), address(newPoolManager.escrow())); + assertEq(address(oldPoolManager.liquidityPoolFactory()), address(newPoolManager.liquidityPoolFactory())); + assertEq(address(oldPoolManager.restrictionManagerFactory()), address(newPoolManager.restrictionManagerFactory())); + assertEq(address(oldPoolManager.trancheTokenFactory()), address(newPoolManager.trancheTokenFactory())); + assertEq(address(oldPoolManager.investmentManager()), address(newPoolManager.investmentManager())); + assertEq(address(oldPoolManager.gateway()), address(newPoolManager.gateway())); + assertEq(address(gateway.poolManager()), address(newPoolManager)); + assertEq(address(investmentManager.poolManager()), address(newPoolManager)); + assertEq(investmentManager.wards(address(poolManager)), 1); + assertEq(poolManager.wards(address(root)), 1); + assertEq(escrow.wards(address(poolManager)), 1); + assertEq(investmentManager.wards(address(poolManager)), 1); } // --- State Verification Helpers --- @@ -309,7 +337,7 @@ contract MigrationsTest is TestSetup { // --- Investment and Redeem Flow --- - function InvestAndRedeem(uint64 poolId, bytes16 trancheId, address _lPool) public { + function VerifyInvestAndRedeemFlow(uint64 poolId, bytes16 trancheId, address _lPool) public { uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); //TODO: fuzz price LiquidityPool lPool = LiquidityPool(_lPool); diff --git a/test/migrationContracts/MigratedInvestmentManager.sol b/test/migrationContracts/MigratedInvestmentManager.sol index a4d65848..871e7bcd 100644 --- a/test/migrationContracts/MigratedInvestmentManager.sol +++ b/test/migrationContracts/MigratedInvestmentManager.sol @@ -3,6 +3,13 @@ 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. From bb03c863b34a0dd63281f42f1964262561129178 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 3 Oct 2023 14:57:17 -0400 Subject: [PATCH 15/51] Add migratedGateway and add tests --- test/Migrations.t.sol | 45 ++++++++++++++++++++- test/migrationContracts/MigratedGateway.sol | 12 ++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 test/migrationContracts/MigratedGateway.sol diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index fdc69254..9f4dabee 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -5,6 +5,7 @@ import "./TestSetup.t.sol"; import {LiquidityPool} from "src/LiquidityPool.sol"; import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestmentManager.sol"; import {MigratedPoolManager} from "test/migrationContracts/MigratedPoolManager.sol"; +import {MigratedGateway} from "test/migrationContracts/MigratedGateway.sol"; import {MathLib} from "src/util/MathLib.sol"; interface AuthLike { @@ -104,8 +105,6 @@ contract MigrationsTest is TestSetup { } function testPoolManagerMigrationInvestRedeem() public { - VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); - // Simulate intended upgrade flow centrifugeChain.incomingScheduleUpgrade(address(this)); vm.warp(block.timestamp + 3 days); @@ -181,6 +180,37 @@ contract MigrationsTest is TestSetup { VerifyInvestAndRedeemFlow(poolId + 1, trancheId, _lPool2); } + 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 + root.denyContract(address(investmentManager), address(this)); + root.denyContract(address(poolManager), address(this)); + root.denyContract(address(router), address(this)); + + // verify permissions + verifyMigratedGatewayPermissions(gateway, newGateway); + + // test that everything is working + gateway = newGateway; + VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + } + // --- Permissions & Dependencies Checks --- function verifyMigratedInvestmentManagerPermissions(InvestmentManager oldInvestmentManager, InvestmentManager newInvestmentManager) public { @@ -213,6 +243,17 @@ contract MigrationsTest is TestSetup { assertEq(investmentManager.wards(address(poolManager)), 1); } + function verifyMigratedGatewayPermissions(Gateway oldGateway, Gateway newGateway) public { + assertTrue(address(oldGateway) != address(newGateway)); + assertEq(address(oldGateway.investmentManager()), address(newGateway.investmentManager())); + assertEq(address(oldGateway.poolManager()), address(newGateway.poolManager())); + assertEq(address(oldGateway.root()), address(newGateway.root())); + assertEq(address(investmentManager.gateway()), address(newGateway)); + assertEq(address(poolManager.gateway()), address(newGateway)); + assertEq(address(router.gateway()), address(newGateway)); + assertEq(newGateway.wards(address(root)), 1); + } + // --- State Verification Helpers --- function verifyMigratedInvestmentManagerState( diff --git a/test/migrationContracts/MigratedGateway.sol b/test/migrationContracts/MigratedGateway.sol new file mode 100644 index 00000000..d1cf15d2 --- /dev/null +++ b/test/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 + } +} From a879f5d7fd325b1cd663747994f4412668a91c6e Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 3 Oct 2023 16:35:26 -0400 Subject: [PATCH 16/51] add MigratedLiquidityPool --- test/Migrations.t.sol | 53 ++++++++++++++++++- .../MigratedLiquidityPool.sol | 12 +++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 test/migrationContracts/MigratedLiquidityPool.sol diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 9f4dabee..00ecce4b 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -6,6 +6,7 @@ import {LiquidityPool} from "src/LiquidityPool.sol"; import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestmentManager.sol"; import {MigratedPoolManager} from "test/migrationContracts/MigratedPoolManager.sol"; import {MigratedGateway} from "test/migrationContracts/MigratedGateway.sol"; +import {MigratedLiquidityPool} from "test/migrationContracts/MigratedLiquidityPool.sol"; import {MathLib} from "src/util/MathLib.sol"; interface AuthLike { @@ -91,6 +92,7 @@ contract MigrationsTest is TestSetup { } // clean up + newInvestmentManager.deny(address(this)); root.denyContract(address(newInvestmentManager), address(this)); root.denyContract(address(gateway), address(this)); root.denyContract(address(poolManager), address(this)); @@ -159,6 +161,7 @@ contract MigrationsTest is TestSetup { AuthLike(restrictionManagerFactory).rely(address(newPoolManager)); // clean up + newPoolManager.deny(address(this)); root.denyContract(address(investmentManager), address(this)); root.denyContract(address(gateway), address(this)); root.denyContract(address(newPoolManager), address(this)); @@ -199,9 +202,11 @@ contract MigrationsTest is TestSetup { 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); @@ -211,6 +216,38 @@ contract MigrationsTest is TestSetup { VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } + function testLiquidityPoolMigration() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // Deploy new LiquidityPool + MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( + poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(investmentManager) + ); + + // Rewire contracts + newLiquidityPool.rely(address(root)); + newLiquidityPool.rely(address(investmentManager)); + root.relyContract(address(investmentManager), address(this)); + investmentManager.rely(address(newLiquidityPool)); + root.relyContract(address(escrow), address(this)); + escrow.approve(address(newLiquidityPool), address(investmentManager), type(uint256).max); + + // clean up + escrow.approve(_lPool, address(investmentManager), 0); + newLiquidityPool.deny(address(this)); + root.deny(address(this)); + + // verify permissions + verifyLiquidityPoolPermissions(LiquidityPool(_lPool), newLiquidityPool); + + // test that everything is working + // _lPool = address(newLiquidityPool); + // VerifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); + } + // --- Permissions & Dependencies Checks --- function verifyMigratedInvestmentManagerPermissions(InvestmentManager oldInvestmentManager, InvestmentManager newInvestmentManager) public { @@ -254,6 +291,20 @@ contract MigrationsTest is TestSetup { assertEq(newGateway.wards(address(root)), 1); } + function verifyLiquidityPoolPermissions(LiquidityPool oldLiquidityPool, LiquidityPool newLiquidityPool) public { + assertTrue(address(oldLiquidityPool) != address(newLiquidityPool)); + assertEq(oldLiquidityPool.poolId(), newLiquidityPool.poolId()); + assertEq(oldLiquidityPool.trancheId(), newLiquidityPool.trancheId()); + assertEq(address(oldLiquidityPool.asset()), address(newLiquidityPool.asset())); + assertEq(address(oldLiquidityPool.share()), address(newLiquidityPool.share())); + address token = poolManager.getTrancheToken(poolId, trancheId); + assertEq(address(newLiquidityPool.share()), token); + assertEq(address(oldLiquidityPool.investmentManager()), address(newLiquidityPool.investmentManager())); + assertEq(newLiquidityPool.wards(address(root)), 1); + assertEq(newLiquidityPool.wards(address(investmentManager)), 1); + assertEq(investmentManager.wards(address(newLiquidityPool)), 1); + } + // --- State Verification Helpers --- function verifyMigratedInvestmentManagerState( @@ -416,7 +467,6 @@ contract MigrationsTest is TestSetup { ); assertEq(lPool.maxMint(investor), trancheTokensPayout); - assertEq(lPool.maxDeposit(investor), amount); assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); @@ -427,7 +477,6 @@ contract MigrationsTest is TestSetup { assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); assertEq(lPool.maxMint(investor), trancheTokensPayout - trancheTokensPayout / div); - assertEq(lPool.maxDeposit(investor), amount - amount / div); // max deposit uint256 maxMint = lPool.maxMint(investor); vm.prank(investor); diff --git a/test/migrationContracts/MigratedLiquidityPool.sol b/test/migrationContracts/MigratedLiquidityPool.sol new file mode 100644 index 00000000..9e0509a1 --- /dev/null +++ b/test/migrationContracts/MigratedLiquidityPool.sol @@ -0,0 +1,12 @@ +// 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 investmentManager_) + LiquidityPool(poolId_, trancheId_, asset_, share_, investmentManager_) + { + // TODO: migrate state, revoke approvals + } +} From a41cb2b2c6a85318051c9c3407acd7575a0c257a Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 5 Oct 2023 12:31:54 -0400 Subject: [PATCH 17/51] add lpValues.exists --- test/Migrations.t.sol | 8 ++++---- test/migrationContracts/MigratedInvestmentManager.sol | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol index 00ecce4b..555a44e6 100644 --- a/test/Migrations.t.sol +++ b/test/Migrations.t.sol @@ -327,9 +327,9 @@ contract MigrationsTest is TestSetup { InvestmentManager investmentManager, InvestmentManager newInvestmentManager ) public { - (uint128 newMaxMint, uint256 newDepositPrice, uint128 newMaxWithdraw,,,) = + (uint128 newMaxMint, uint256 newDepositPrice, uint128 newMaxWithdraw,,,,) = newInvestmentManager.orderbook(investor, liquidityPool); - (uint128 oldMaxMint, uint256 oldDepositPrice, uint128 oldMaxWithdraw,,,) = + (uint128 oldMaxMint, uint256 oldDepositPrice, uint128 oldMaxWithdraw,,,,) = investmentManager.orderbook(investor, liquidityPool); assertEq(newMaxMint, oldMaxMint); assertEq(newDepositPrice, oldDepositPrice); @@ -342,9 +342,9 @@ contract MigrationsTest is TestSetup { InvestmentManager investmentManager, InvestmentManager newInvestmentManager ) public { - (,,, uint256 newRedeemPrice, uint128 newRemainingInvestOrder, uint128 newRemainingRedeemOrder) = + (,,, uint256 newRedeemPrice, uint128 newRemainingInvestOrder, uint128 newRemainingRedeemOrder, bool newExists) = newInvestmentManager.orderbook(investor, liquidityPool); - (,,, uint256 oldRedeemPrice, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder) = + (,,, uint256 oldRedeemPrice, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder, bool oldExists) = investmentManager.orderbook(investor, liquidityPool); assertEq(newRedeemPrice, oldRedeemPrice); assertEq(newRemainingInvestOrder, oldRemainingInvestOrder); diff --git a/test/migrationContracts/MigratedInvestmentManager.sol b/test/migrationContracts/MigratedInvestmentManager.sol index 871e7bcd..b771a445 100644 --- a/test/migrationContracts/MigratedInvestmentManager.sol +++ b/test/migrationContracts/MigratedInvestmentManager.sol @@ -34,7 +34,8 @@ contract MigratedInvestmentManager is InvestmentManager { uint128 maxWithdraw, uint256 redeemPrice, uint128 remainingInvestOrder, - uint128 remainingRedeemOrder + uint128 remainingRedeemOrder, + bool exists ) = oldInvestmentManager.orderbook(investor, liquidityPool); orderbook[investor][liquidityPool] = LPValues({ maxMint: maxMint, @@ -42,7 +43,8 @@ contract MigratedInvestmentManager is InvestmentManager { maxWithdraw: maxWithdraw, redeemPrice: redeemPrice, remainingInvestOrder: remainingInvestOrder, - remainingRedeemOrder: remainingRedeemOrder + remainingRedeemOrder: remainingRedeemOrder, + exists: exists }); } } From e3f58e60d2119cfa969bef750d59d32b39caa606 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 5 Oct 2023 13:37:58 -0400 Subject: [PATCH 18/51] reorganize migration tests into separate files --- test/Migrations.t.sol | 540 ------------------ test/migrations/InvestRedeemFlow.t.sol | 151 +++++ test/migrations/MigratedGateway.t.sol | 61 ++ .../MigratedInvestmentManager.t.sol | 142 +++++ test/migrations/MigratedLiquidityPool.t.sol | 68 +++ test/migrations/MigratedPoolManager.t.sol | 191 +++++++ .../migrationContracts/MigratedGateway.sol | 0 .../MigratedInvestmentManager.sol | 0 .../MigratedLiquidityPool.sol | 0 .../MigratedPoolManager.sol | 1 - 10 files changed, 613 insertions(+), 541 deletions(-) delete mode 100644 test/Migrations.t.sol create mode 100644 test/migrations/InvestRedeemFlow.t.sol create mode 100644 test/migrations/MigratedGateway.t.sol create mode 100644 test/migrations/MigratedInvestmentManager.t.sol create mode 100644 test/migrations/MigratedLiquidityPool.t.sol create mode 100644 test/migrations/MigratedPoolManager.t.sol rename test/{ => migrations}/migrationContracts/MigratedGateway.sol (100%) rename test/{ => migrations}/migrationContracts/MigratedInvestmentManager.sol (100%) rename test/{ => migrations}/migrationContracts/MigratedLiquidityPool.sol (100%) rename test/{ => migrations}/migrationContracts/MigratedPoolManager.sol (98%) diff --git a/test/Migrations.t.sol b/test/Migrations.t.sol deleted file mode 100644 index 555a44e6..00000000 --- a/test/Migrations.t.sol +++ /dev/null @@ -1,540 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.21; - -import "./TestSetup.t.sol"; -import {LiquidityPool} from "src/LiquidityPool.sol"; -import {MigratedInvestmentManager} from "test/migrationContracts/MigratedInvestmentManager.sol"; -import {MigratedPoolManager} from "test/migrationContracts/MigratedPoolManager.sol"; -import {MigratedGateway} from "test/migrationContracts/MigratedGateway.sol"; -import {MigratedLiquidityPool} from "test/migrationContracts/MigratedLiquidityPool.sol"; -import {MathLib} from "src/util/MathLib.sol"; - -interface AuthLike { - function rely(address) external; - - function deny(address) external; -} - -contract MigrationsTest is TestSetup { - using MathLib for uint128; - - uint8 internal constant PRICE_DECIMALS = 18; - - uint64 poolId; - bytes16 trancheId; - uint128 currencyId; - uint8 trancheTokenDecimals; - address _lPool; - uint256 investorCurrencyAmount; - - function setUp() public override { - super.setUp(); - poolId = 1; - trancheId = bytes16(hex"811acd5b3f17c06841c7e41e9e04cb1b"); - currencyId = 1; - trancheTokenDecimals = 18; - _lPool = deployLiquidityPool( - poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20) - ); - - 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)); - } - - // --- Migration Tests --- - - 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)); - newInvestmentManager.file("poolManager", address(poolManager)); - root.relyContract(address(poolManager), address(this)); - poolManager.file("investmentManager", address(newInvestmentManager)); - newInvestmentManager.file("gateway", address(gateway)); - newInvestmentManager.rely(address(root)); - newInvestmentManager.rely(address(poolManager)); - root.relyContract(address(escrow), address(this)); - escrow.rely(address(newInvestmentManager)); - root.relyContract(address(userEscrow), address(this)); - userEscrow.rely(address(newInvestmentManager)); - - // file investmentManager on all LiquidityPools - for (uint256 i = 0; i < liquidityPools.length; i++) { - root.relyContract(address(liquidityPools[i]), address(this)); - LiquidityPool lPool = LiquidityPool(liquidityPools[i]); - - lPool.file("investmentManager", address(newInvestmentManager)); - lPool.rely(address(newInvestmentManager)); - root.relyContract(address(lPool.share()), address(this)); - AuthLike(address(lPool.share())).rely(address(newInvestmentManager)); - newInvestmentManager.rely(address(lPool)); - escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); - } - - // 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 testPoolManagerMigrationInvestRedeem() public { - // Simulate intended upgrade flow - centrifugeChain.incomingScheduleUpgrade(address(this)); - vm.warp(block.timestamp + 3 days); - root.executeScheduledRely(address(this)); - - // 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); - - // Deploy new MigratedPoolManager - MigratedPoolManager newPoolManager = new MigratedPoolManager( - address(escrow), - liquidityPoolFactory, - restrictionManagerFactory, - trancheTokenFactory, - address(poolManager), - poolIds, - trancheIds, - allowedCurrencies, - liquidityPoolCurrencies - ); - - verifyMigratedPoolManagerState( - poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, poolManager, newPoolManager - ); - - // Rewire contracts - LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); - TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); - 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)); - newPoolManager.rely(address(root)); - root.relyContract(address(escrow), address(this)); - escrow.rely(address(newPoolManager)); - root.relyContract(restrictionManagerFactory, address(this)); - AuthLike(restrictionManagerFactory).rely(address(newPoolManager)); - - // 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 - centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", trancheTokenDecimals); // 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 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 testLiquidityPoolMigration() public { - // Simulate intended upgrade flow - centrifugeChain.incomingScheduleUpgrade(address(this)); - vm.warp(block.timestamp + 3 days); - root.executeScheduledRely(address(this)); - - // Deploy new LiquidityPool - MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( - poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(investmentManager) - ); - - // Rewire contracts - newLiquidityPool.rely(address(root)); - newLiquidityPool.rely(address(investmentManager)); - root.relyContract(address(investmentManager), address(this)); - investmentManager.rely(address(newLiquidityPool)); - root.relyContract(address(escrow), address(this)); - escrow.approve(address(newLiquidityPool), address(investmentManager), type(uint256).max); - - // clean up - escrow.approve(_lPool, address(investmentManager), 0); - newLiquidityPool.deny(address(this)); - root.deny(address(this)); - - // verify permissions - verifyLiquidityPoolPermissions(LiquidityPool(_lPool), newLiquidityPool); - - // test that everything is working - // _lPool = address(newLiquidityPool); - // VerifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); - } - - // --- Permissions & Dependencies Checks --- - - function verifyMigratedInvestmentManagerPermissions(InvestmentManager oldInvestmentManager, InvestmentManager newInvestmentManager) public { - assertTrue(address(oldInvestmentManager) != address(newInvestmentManager)); - 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())); - 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(investmentManager)), 1); - assertEq(userEscrow.wards(address(investmentManager)), 1); - } - - function verifyMigratedPoolManagerPermissions(PoolManager oldPoolManager, PoolManager newPoolManager) public { - assertTrue(address(oldPoolManager) != address(newPoolManager)); - assertEq(address(oldPoolManager.escrow()), address(newPoolManager.escrow())); - assertEq(address(oldPoolManager.liquidityPoolFactory()), address(newPoolManager.liquidityPoolFactory())); - assertEq(address(oldPoolManager.restrictionManagerFactory()), address(newPoolManager.restrictionManagerFactory())); - assertEq(address(oldPoolManager.trancheTokenFactory()), address(newPoolManager.trancheTokenFactory())); - assertEq(address(oldPoolManager.investmentManager()), address(newPoolManager.investmentManager())); - assertEq(address(oldPoolManager.gateway()), address(newPoolManager.gateway())); - assertEq(address(gateway.poolManager()), address(newPoolManager)); - assertEq(address(investmentManager.poolManager()), address(newPoolManager)); - assertEq(investmentManager.wards(address(poolManager)), 1); - assertEq(poolManager.wards(address(root)), 1); - assertEq(escrow.wards(address(poolManager)), 1); - assertEq(investmentManager.wards(address(poolManager)), 1); - } - - function verifyMigratedGatewayPermissions(Gateway oldGateway, Gateway newGateway) public { - assertTrue(address(oldGateway) != address(newGateway)); - assertEq(address(oldGateway.investmentManager()), address(newGateway.investmentManager())); - assertEq(address(oldGateway.poolManager()), address(newGateway.poolManager())); - assertEq(address(oldGateway.root()), address(newGateway.root())); - assertEq(address(investmentManager.gateway()), address(newGateway)); - assertEq(address(poolManager.gateway()), address(newGateway)); - assertEq(address(router.gateway()), address(newGateway)); - assertEq(newGateway.wards(address(root)), 1); - } - - function verifyLiquidityPoolPermissions(LiquidityPool oldLiquidityPool, LiquidityPool newLiquidityPool) public { - assertTrue(address(oldLiquidityPool) != address(newLiquidityPool)); - assertEq(oldLiquidityPool.poolId(), newLiquidityPool.poolId()); - assertEq(oldLiquidityPool.trancheId(), newLiquidityPool.trancheId()); - assertEq(address(oldLiquidityPool.asset()), address(newLiquidityPool.asset())); - assertEq(address(oldLiquidityPool.share()), address(newLiquidityPool.share())); - address token = poolManager.getTrancheToken(poolId, trancheId); - assertEq(address(newLiquidityPool.share()), token); - assertEq(address(oldLiquidityPool.investmentManager()), address(newLiquidityPool.investmentManager())); - assertEq(newLiquidityPool.wards(address(root)), 1); - assertEq(newLiquidityPool.wards(address(investmentManager)), 1); - assertEq(investmentManager.wards(address(newLiquidityPool)), 1); - } - - // --- 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.orderbook(investor, liquidityPool); - (uint128 oldMaxMint, uint256 oldDepositPrice, uint128 oldMaxWithdraw,,,,) = - investmentManager.orderbook(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 newRemainingInvestOrder, uint128 newRemainingRedeemOrder, bool newExists) = - newInvestmentManager.orderbook(investor, liquidityPool); - (,,, uint256 oldRedeemPrice, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder, bool oldExists) = - investmentManager.orderbook(investor, liquidityPool); - assertEq(newRedeemPrice, oldRedeemPrice); - assertEq(newRemainingInvestOrder, oldRemainingInvestOrder); - assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); - } - - 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); - - 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) = - poolManager.undeployedTranches(poolId, trancheIds[i]); - (uint8 newDecimals, string memory newTokenName, string memory newTokenSymbol) = - 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); - } - - // --- Investment and Redeem Flow --- - - function VerifyInvestAndRedeemFlow(uint64 poolId, bytes16 trancheId, address _lPool) public { - uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); //TODO: fuzz price - LiquidityPool lPool = LiquidityPool(_lPool); - - 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 amount, LiquidityPool lPool) public { - vm.prank(investor); - erc20.approve(address(investmentManager), amount); // add allowance - - vm.prank(investor); - lPool.requestDeposit(amount); - - // ensure funds are locked in escrow - assertEq(erc20.balanceOf(address(escrow)), amount); - assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); - - // trigger executed collectInvest - uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId - - uint128 trancheTokensPayout = _toUint128( - uint128(amount).mulDiv( - 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down - ) - ); - - // Assume an epoch execution happens on cent chain - // Assume a bot calls collectInvest for this user on cent chain - vm.prank(address(gateway)); - investmentManager.handleExecutedCollectInvest( - poolId, trancheId, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 - ); - - assertEq(lPool.maxMint(investor), trancheTokensPayout); - assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); - assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); - - uint256 div = 2; - vm.prank(investor); - lPool.deposit(amount / 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); - assertTrue(lPool.balanceOf(address(escrow)) <= 1); - assertTrue(lPool.maxMint(investor) <= 1); - } - - function redeemWithdraw(uint64 poolId, bytes16 trancheId, uint128 price, uint256 amount, LiquidityPool lPool) - public - { - vm.prank(investor); - lPool.requestRedeem(amount); - - // redeem - uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId - uint128 currencyPayout = _toUint128( - uint128(amount).mulDiv(price, 10 ** (18 - erc20.decimals() + lPool.decimals()), MathLib.Rounding.Down) - ); - // Assume an epoch execution happens on cent chain - // Assume a bot calls collectRedeem for this user on cent chain - vm.prank(address(gateway)); - investmentManager.handleExecutedCollectRedeem( - poolId, trancheId, investor, _currencyId, currencyPayout, uint128(amount), 0 - ); - - assertEq(lPool.maxWithdraw(investor), currencyPayout); - assertEq(lPool.maxRedeem(investor), amount); - assertEq(lPool.balanceOf(address(escrow)), 0); - - uint128 div = 2; - vm.prank(investor); - lPool.redeem(amount / 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), amount / 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); - } - - // --- Utils --- - - function _toUint128(uint256 _value) internal pure returns (uint128 value) { - if (_value > type(uint128).max) { - revert("InvestmentManager/uint128-overflow"); - } else { - value = uint128(_value); - } - } -} diff --git a/test/migrations/InvestRedeemFlow.t.sol b/test/migrations/InvestRedeemFlow.t.sol new file mode 100644 index 00000000..08299b67 --- /dev/null +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -0,0 +1,151 @@ +// 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"; + +interface AuthLike { + function rely(address) external; + function deny(address) external; +} + +contract InvestRedeemFlow is TestSetup { + using MathLib for uint128; + + uint8 internal constant PRICE_DECIMALS = 18; + + uint64 poolId; + bytes16 trancheId; + uint128 currencyId; + uint8 trancheTokenDecimals; + address _lPool; + uint256 investorCurrencyAmount; + + function setUp() public virtual override { + super.setUp(); + poolId = 1; + trancheId = bytes16(hex"811acd5b3f17c06841c7e41e9e04cb1b"); + currencyId = 1; + trancheTokenDecimals = 18; + _lPool = deployLiquidityPool( + poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20) + ); + + 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); //TODO: fuzz price + 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 amount, LiquidityPool lPool) + public + { + vm.prank(investor); + erc20.approve(address(investmentManager), amount); // add allowance + + vm.prank(investor); + lPool.requestDeposit(amount); + + // ensure funds are locked in escrow + assertEq(erc20.balanceOf(address(escrow)), amount); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); + + // trigger executed collectInvest + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + + uint128 trancheTokensPayout = _toUint128( + uint128(amount).mulDiv( + 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down + ) + ); + + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectInvest for this user on cent chain + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectInvest( + poolId_, trancheId_, investor, _currencyId, uint128(amount), trancheTokensPayout, 0 + ); + + assertEq(lPool.maxMint(investor), trancheTokensPayout); + assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); + + uint256 div = 2; + vm.prank(investor); + lPool.deposit(amount / 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); + assertTrue(lPool.balanceOf(address(escrow)) <= 1); + assertTrue(lPool.maxMint(investor) <= 1); + } + + function redeemWithdraw(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 amount, LiquidityPool lPool) + public + { + vm.prank(investor); + lPool.requestRedeem(amount); + + // redeem + uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId + uint128 currencyPayout = _toUint128( + uint128(amount).mulDiv(price, 10 ** (18 - erc20.decimals() + lPool.decimals()), MathLib.Rounding.Down) + ); + // Assume an epoch execution happens on cent chain + // Assume a bot calls collectRedeem for this user on cent chain + vm.prank(address(gateway)); + investmentManager.handleExecutedCollectRedeem( + poolId_, trancheId_, investor, _currencyId, currencyPayout, uint128(amount), 0 + ); + + assertEq(lPool.maxWithdraw(investor), currencyPayout); + assertEq(lPool.maxRedeem(investor), amount); + assertEq(lPool.balanceOf(address(escrow)), 0); + + uint128 div = 2; + vm.prank(investor); + lPool.redeem(amount / 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), amount / 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); + } + + // --- Utils --- + + function _toUint128(uint256 _value) internal pure returns (uint128 value) { + if (_value > type(uint128).max) { + revert("InvestmentManager/uint128-overflow"); + } else { + value = uint128(_value); + } + } +} diff --git a/test/migrations/MigratedGateway.t.sol b/test/migrations/MigratedGateway.t.sol new file mode 100644 index 00000000..34935efd --- /dev/null +++ b/test/migrations/MigratedGateway.t.sol @@ -0,0 +1,61 @@ +// 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"; + +interface AuthLike { + function rely(address) external; + function deny(address) external; +} + +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 { + assertTrue(address(oldGateway) != address(newGateway)); + assertEq(address(oldGateway.investmentManager()), address(newGateway.investmentManager())); + assertEq(address(oldGateway.poolManager()), address(newGateway.poolManager())); + assertEq(address(oldGateway.root()), address(newGateway.root())); + assertEq(address(investmentManager.gateway()), address(newGateway)); + assertEq(address(poolManager.gateway()), address(newGateway)); + assertEq(address(router.gateway()), address(newGateway)); + assertEq(newGateway.wards(address(root)), 1); + } +} diff --git a/test/migrations/MigratedInvestmentManager.t.sol b/test/migrations/MigratedInvestmentManager.t.sol new file mode 100644 index 00000000..211eba74 --- /dev/null +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -0,0 +1,142 @@ +// 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)); + newInvestmentManager.file("poolManager", address(poolManager)); + root.relyContract(address(poolManager), address(this)); + poolManager.file("investmentManager", address(newInvestmentManager)); + newInvestmentManager.file("gateway", address(gateway)); + newInvestmentManager.rely(address(root)); + newInvestmentManager.rely(address(poolManager)); + root.relyContract(address(escrow), address(this)); + escrow.rely(address(newInvestmentManager)); + root.relyContract(address(userEscrow), address(this)); + userEscrow.rely(address(newInvestmentManager)); + + // file investmentManager on all LiquidityPools + for (uint256 i = 0; i < liquidityPools.length; i++) { + root.relyContract(address(liquidityPools[i]), address(this)); + LiquidityPool lPool = LiquidityPool(liquidityPools[i]); + + lPool.file("investmentManager", address(newInvestmentManager)); + lPool.rely(address(newInvestmentManager)); + root.relyContract(address(lPool.share()), address(this)); + AuthLike(address(lPool.share())).rely(address(newInvestmentManager)); + newInvestmentManager.rely(address(lPool)); + escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); + } + + // 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 { + assertTrue(address(oldInvestmentManager) != address(newInvestmentManager)); + 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())); + 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(investmentManager)), 1); + assertEq(userEscrow.wards(address(investmentManager)), 1); + } + + // --- 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.orderbook(investor, liquidityPool); + (uint128 oldMaxMint, uint256 oldDepositPrice, uint128 oldMaxWithdraw,,,,) = + investmentManager.orderbook(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 newRemainingInvestOrder, uint128 newRemainingRedeemOrder, bool newExists) = + newInvestmentManager.orderbook(investor, liquidityPool); + (,,, uint256 oldRedeemPrice, uint128 oldRemainingInvestOrder, uint128 oldRemainingRedeemOrder, bool oldExists) = + investmentManager.orderbook(investor, liquidityPool); + assertEq(newRedeemPrice, oldRedeemPrice); + assertEq(newRemainingInvestOrder, oldRemainingInvestOrder); + assertEq(newRemainingRedeemOrder, oldRemainingRedeemOrder); + assertEq(newExists, oldExists); + } +} diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol new file mode 100644 index 00000000..76ba71be --- /dev/null +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedInvestmentManager, InvestmentManager} from "./migrationContracts/MigratedInvestmentManager.sol"; +import {MigratedPoolManager, PoolManager} from "./migrationContracts/MigratedPoolManager.sol"; +import {MigratedGateway, Gateway} from "./migrationContracts/MigratedGateway.sol"; +import {MigratedLiquidityPool, LiquidityPool} from "./migrationContracts/MigratedLiquidityPool.sol"; +import {LiquidityPoolFactory, TrancheTokenFactory} 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 testLiquidityPoolMigration() public { + // Simulate intended upgrade flow + centrifugeChain.incomingScheduleUpgrade(address(this)); + vm.warp(block.timestamp + 3 days); + root.executeScheduledRely(address(this)); + + // Deploy new LiquidityPool + MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( + poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(investmentManager) + ); + + // Rewire contracts + newLiquidityPool.rely(address(root)); + newLiquidityPool.rely(address(investmentManager)); + root.relyContract(address(investmentManager), address(this)); + investmentManager.rely(address(newLiquidityPool)); + root.relyContract(address(escrow), address(this)); + escrow.approve(address(newLiquidityPool), address(investmentManager), type(uint256).max); + + // clean up + escrow.approve(_lPool, address(investmentManager), 0); + newLiquidityPool.deny(address(this)); + root.deny(address(this)); + + // verify permissions + verifyLiquidityPoolPermissions(LiquidityPool(_lPool), newLiquidityPool); + + // test that everything is working + // _lPool = address(newLiquidityPool); + // VerifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); + } + + // --- Permissions & Dependencies Checks --- + + function verifyLiquidityPoolPermissions(LiquidityPool oldLiquidityPool, LiquidityPool newLiquidityPool) public { + assertTrue(address(oldLiquidityPool) != address(newLiquidityPool)); + assertEq(oldLiquidityPool.poolId(), newLiquidityPool.poolId()); + assertEq(oldLiquidityPool.trancheId(), newLiquidityPool.trancheId()); + assertEq(address(oldLiquidityPool.asset()), address(newLiquidityPool.asset())); + assertEq(address(oldLiquidityPool.share()), address(newLiquidityPool.share())); + address token = poolManager.getTrancheToken(poolId, trancheId); + assertEq(address(newLiquidityPool.share()), token); + assertEq(address(oldLiquidityPool.investmentManager()), address(newLiquidityPool.investmentManager())); + assertEq(newLiquidityPool.wards(address(root)), 1); + assertEq(newLiquidityPool.wards(address(investmentManager)), 1); + assertEq(investmentManager.wards(address(newLiquidityPool)), 1); + } +} diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol new file mode 100644 index 00000000..42e42a8c --- /dev/null +++ b/test/migrations/MigratedPoolManager.t.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedPoolManager, PoolManager} from "./migrationContracts/MigratedPoolManager.sol"; +import {LiquidityPoolFactory, TrancheTokenFactory} 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)); + + // 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); + + // Deploy new MigratedPoolManager + MigratedPoolManager newPoolManager = new MigratedPoolManager( + address(escrow), + liquidityPoolFactory, + restrictionManagerFactory, + trancheTokenFactory, + address(poolManager), + poolIds, + trancheIds, + allowedCurrencies, + liquidityPoolCurrencies + ); + + verifyMigratedPoolManagerState( + poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, poolManager, newPoolManager + ); + + // Rewire contracts + LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); + TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); + 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)); + newPoolManager.rely(address(root)); + root.relyContract(address(escrow), address(this)); + escrow.rely(address(newPoolManager)); + root.relyContract(restrictionManagerFactory, address(this)); + AuthLike(restrictionManagerFactory).rely(address(newPoolManager)); + + // 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 + centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", trancheTokenDecimals); // 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 { + assertTrue(address(oldPoolManager) != address(newPoolManager)); + assertEq(address(oldPoolManager.escrow()), address(newPoolManager.escrow())); + assertEq(address(oldPoolManager.liquidityPoolFactory()), address(newPoolManager.liquidityPoolFactory())); + assertEq( + address(oldPoolManager.restrictionManagerFactory()), address(newPoolManager.restrictionManagerFactory()) + ); + assertEq(address(oldPoolManager.trancheTokenFactory()), address(newPoolManager.trancheTokenFactory())); + assertEq(address(oldPoolManager.investmentManager()), address(newPoolManager.investmentManager())); + assertEq(address(oldPoolManager.gateway()), address(newPoolManager.gateway())); + assertEq(address(gateway.poolManager()), address(newPoolManager)); + assertEq(address(investmentManager.poolManager()), address(newPoolManager)); + assertEq(investmentManager.wards(address(poolManager)), 1); + assertEq(poolManager.wards(address(root)), 1); + assertEq(escrow.wards(address(poolManager)), 1); + assertEq(investmentManager.wards(address(poolManager)), 1); + } + + // --- 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) = + poolManager.undeployedTranches(poolId, trancheIds[i]); + (uint8 newDecimals, string memory newTokenName, string memory newTokenSymbol) = + 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/migrationContracts/MigratedGateway.sol b/test/migrations/migrationContracts/MigratedGateway.sol similarity index 100% rename from test/migrationContracts/MigratedGateway.sol rename to test/migrations/migrationContracts/MigratedGateway.sol diff --git a/test/migrationContracts/MigratedInvestmentManager.sol b/test/migrations/migrationContracts/MigratedInvestmentManager.sol similarity index 100% rename from test/migrationContracts/MigratedInvestmentManager.sol rename to test/migrations/migrationContracts/MigratedInvestmentManager.sol diff --git a/test/migrationContracts/MigratedLiquidityPool.sol b/test/migrations/migrationContracts/MigratedLiquidityPool.sol similarity index 100% rename from test/migrationContracts/MigratedLiquidityPool.sol rename to test/migrations/migrationContracts/MigratedLiquidityPool.sol diff --git a/test/migrationContracts/MigratedPoolManager.sol b/test/migrations/migrationContracts/MigratedPoolManager.sol similarity index 98% rename from test/migrationContracts/MigratedPoolManager.sol rename to test/migrations/migrationContracts/MigratedPoolManager.sol index dcd7c1ef..fb561fdd 100644 --- a/test/migrationContracts/MigratedPoolManager.sol +++ b/test/migrations/migrationContracts/MigratedPoolManager.sol @@ -83,7 +83,6 @@ contract MigratedPoolManager is PoolManager { function migrateUndeployedTranches(uint64 poolId, bytes16[] memory trancheIds, PoolManager oldPoolManager_) internal { - Pool storage pool = pools[poolId]; for (uint256 j = 0; j < trancheIds.length; j++) { bytes16 trancheId = trancheIds[j]; From d2008169328af008db647318aad516d2ed2b66e6 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 5 Oct 2023 15:01:45 -0400 Subject: [PATCH 19/51] add delayedAdmin migration --- test/migrations/MigratedAdmin.t.sol | 53 +++++++++++++++++++ .../migrationContracts/MigratedAdmin.sol | 13 +++++ 2 files changed, 66 insertions(+) create mode 100644 test/migrations/MigratedAdmin.t.sol create mode 100644 test/migrations/migrationContracts/MigratedAdmin.sol diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol new file mode 100644 index 00000000..743399ee --- /dev/null +++ b/test/migrations/MigratedAdmin.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedDelayedAdmin, 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 + PauseAdmin newPauseAdmin = new PauseAdmin(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 + verifyMigratedDelayedAdminPermissions(delayedAdmin, newDelayedAdmin, pauseAdmin, newPauseAdmin); + + // TODO: test admin functionality still works + } + + function verifyMigratedDelayedAdminPermissions(DelayedAdmin oldDelayedAdmin, DelayedAdmin newDelayedAdmin, PauseAdmin oldPauseAdmin, PauseAdmin newPauseAdmin) public { + assertTrue(address(oldDelayedAdmin) != address(newDelayedAdmin)); + assertTrue(address(oldPauseAdmin) != address(newPauseAdmin)); + assertEq(address(newDelayedAdmin.pauseAdmin()), 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); + } +} diff --git a/test/migrations/migrationContracts/MigratedAdmin.sol b/test/migrations/migrationContracts/MigratedAdmin.sol new file mode 100644 index 00000000..ea03f9a2 --- /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_) {} +} \ No newline at end of file From 400a7e2a2d26d0ac45345f8be6f4c812d79bb4ba Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 5 Oct 2023 15:07:04 -0400 Subject: [PATCH 20/51] use migratedPauseAdmin --- test/migrations/MigratedAdmin.t.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol index 743399ee..fb928e5f 100644 --- a/test/migrations/MigratedAdmin.t.sol +++ b/test/migrations/MigratedAdmin.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.21; -import {MigratedDelayedAdmin, DelayedAdmin, PauseAdmin} from "./migrationContracts/MigratedAdmin.sol"; +import {MigratedDelayedAdmin, MigratedPauseAdmin, DelayedAdmin, PauseAdmin} from "./migrationContracts/MigratedAdmin.sol"; import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; contract MigratedAdmin is InvestRedeemFlow { @@ -16,7 +16,7 @@ contract MigratedAdmin is InvestRedeemFlow { root.executeScheduledRely(address(this)); // Deploy new PauseAdmin - PauseAdmin newPauseAdmin = new PauseAdmin(address(root)); + MigratedPauseAdmin newPauseAdmin = new MigratedPauseAdmin(address(root)); // Deploy new DelayedAdmin MigratedDelayedAdmin newDelayedAdmin = new MigratedDelayedAdmin(address(root), address(newPauseAdmin)); From a1a4f00e1b97f87249e7c760baefeed645874b59 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 5 Oct 2023 15:09:11 -0400 Subject: [PATCH 21/51] forge fmt --- test/migrations/MigratedAdmin.t.sol | 11 +++++++++-- test/migrations/migrationContracts/MigratedAdmin.sol | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol index fb928e5f..a0bd9923 100644 --- a/test/migrations/MigratedAdmin.t.sol +++ b/test/migrations/MigratedAdmin.t.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.21; -import {MigratedDelayedAdmin, MigratedPauseAdmin, DelayedAdmin, PauseAdmin} from "./migrationContracts/MigratedAdmin.sol"; +import { + MigratedDelayedAdmin, MigratedPauseAdmin, DelayedAdmin, PauseAdmin +} from "./migrationContracts/MigratedAdmin.sol"; import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; contract MigratedAdmin is InvestRedeemFlow { @@ -39,7 +41,12 @@ contract MigratedAdmin is InvestRedeemFlow { // TODO: test admin functionality still works } - function verifyMigratedDelayedAdminPermissions(DelayedAdmin oldDelayedAdmin, DelayedAdmin newDelayedAdmin, PauseAdmin oldPauseAdmin, PauseAdmin newPauseAdmin) public { + function verifyMigratedDelayedAdminPermissions( + DelayedAdmin oldDelayedAdmin, + DelayedAdmin newDelayedAdmin, + PauseAdmin oldPauseAdmin, + PauseAdmin newPauseAdmin + ) public { assertTrue(address(oldDelayedAdmin) != address(newDelayedAdmin)); assertTrue(address(oldPauseAdmin) != address(newPauseAdmin)); assertEq(address(newDelayedAdmin.pauseAdmin()), address(newPauseAdmin)); diff --git a/test/migrations/migrationContracts/MigratedAdmin.sol b/test/migrations/migrationContracts/MigratedAdmin.sol index ea03f9a2..b1213fe2 100644 --- a/test/migrations/migrationContracts/MigratedAdmin.sol +++ b/test/migrations/migrationContracts/MigratedAdmin.sol @@ -10,4 +10,4 @@ contract MigratedDelayedAdmin is DelayedAdmin { contract MigratedPauseAdmin is PauseAdmin { constructor(address root_) PauseAdmin(root_) {} -} \ No newline at end of file +} From 5fdeb5317d9c55853019a79f7d8cc0264372e219 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 5 Oct 2023 15:30:46 -0400 Subject: [PATCH 22/51] deny old contracts and verify they're denied in permissions checks --- test/migrations/MigratedInvestmentManager.t.sol | 11 +++++++++-- test/migrations/MigratedLiquidityPool.t.sol | 8 ++++++-- test/migrations/MigratedPoolManager.t.sol | 14 ++++++++++---- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/test/migrations/MigratedInvestmentManager.t.sol b/test/migrations/MigratedInvestmentManager.t.sol index 211eba74..90203119 100644 --- a/test/migrations/MigratedInvestmentManager.t.sol +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -45,8 +45,10 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { 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)); // file investmentManager on all LiquidityPools for (uint256 i = 0; i < liquidityPools.length; i++) { @@ -55,10 +57,13 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { lPool.file("investmentManager", address(newInvestmentManager)); lPool.rely(address(newInvestmentManager)); + lPool.deny(address(investmentManager)); root.relyContract(address(lPool.share()), address(this)); AuthLike(address(lPool.share())).rely(address(newInvestmentManager)); + AuthLike(address(lPool.share())).deny(address(investmentManager)); newInvestmentManager.rely(address(lPool)); escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); + escrow.approve(address(lPool), address(investmentManager), 0); } // clean up @@ -89,8 +94,10 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { assertEq(address(poolManager.investmentManager()), address(newInvestmentManager)); assertEq(newInvestmentManager.wards(address(root)), 1); assertEq(newInvestmentManager.wards(address(poolManager)), 1); - assertEq(escrow.wards(address(investmentManager)), 1); - assertEq(userEscrow.wards(address(investmentManager)), 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); } // --- State Verification Helpers --- diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index 76ba71be..dda92b82 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -34,18 +34,21 @@ contract MigrationsTest is InvestRedeemFlow { newLiquidityPool.rely(address(investmentManager)); root.relyContract(address(investmentManager), address(this)); investmentManager.rely(address(newLiquidityPool)); + investmentManager.deny(_lPool); root.relyContract(address(escrow), address(this)); escrow.approve(address(newLiquidityPool), address(investmentManager), type(uint256).max); + escrow.approve(_lPool, address(investmentManager), 0); // clean up - escrow.approve(_lPool, address(investmentManager), 0); newLiquidityPool.deny(address(this)); + root.denyContract(address(investmentManager), address(this)); + root.denyContract(address(escrow), address(this)); root.deny(address(this)); // verify permissions verifyLiquidityPoolPermissions(LiquidityPool(_lPool), newLiquidityPool); - // test that everything is working + // TODO: test that everything is working // _lPool = address(newLiquidityPool); // VerifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); } @@ -64,5 +67,6 @@ contract MigrationsTest is InvestRedeemFlow { assertEq(newLiquidityPool.wards(address(root)), 1); assertEq(newLiquidityPool.wards(address(investmentManager)), 1); assertEq(investmentManager.wards(address(newLiquidityPool)), 1); + assertEq(investmentManager.wards(address(oldLiquidityPool)), 0); } } diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol index 42e42a8c..05257d7a 100644 --- a/test/migrations/MigratedPoolManager.t.sol +++ b/test/migrations/MigratedPoolManager.t.sol @@ -63,11 +63,14 @@ contract MigrationsTest is InvestRedeemFlow { 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)); @@ -104,10 +107,13 @@ contract MigrationsTest is InvestRedeemFlow { assertEq(address(oldPoolManager.gateway()), address(newPoolManager.gateway())); assertEq(address(gateway.poolManager()), address(newPoolManager)); assertEq(address(investmentManager.poolManager()), address(newPoolManager)); - assertEq(investmentManager.wards(address(poolManager)), 1); - assertEq(poolManager.wards(address(root)), 1); - assertEq(escrow.wards(address(poolManager)), 1); - assertEq(investmentManager.wards(address(poolManager)), 1); + assertEq(newPoolManager.wards(address(root)), 1); + assertEq(investmentManager.wards(address(newPoolManager)), 1); + assertEq(investmentManager.wards(address(oldPoolManager)), 0); + assertEq(escrow.wards(address(newPoolManager)), 1); + assertEq(escrow.wards(address(oldPoolManager)), 0); + assertEq(investmentManager.wards(address(newPoolManager)), 1); + assertEq(investmentManager.wards(address(oldPoolManager)), 0); } // --- State Verification Helpers --- From b6330a9fa4486917737b18a4af8bd201ee313d91 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Fri, 6 Oct 2023 13:17:33 -0400 Subject: [PATCH 23/51] Add restrictionManager migration --- src/LiquidityPool.sol | 2 + test/migrations/InvestRedeemFlow.t.sol | 5 -- test/migrations/MigratedGateway.t.sol | 5 -- test/migrations/MigratedLiquidityPool.t.sol | 9 --- .../MigratedRestrictionManager.t.sol | 62 +++++++++++++++++++ .../MigratedRestrictionManager.sol | 8 +++ 6 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 test/migrations/MigratedRestrictionManager.t.sol create mode 100644 test/migrations/migrationContracts/MigratedRestrictionManager.sol diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index e39b787c..96baf568 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -14,6 +14,8 @@ interface ERC20PermitLike { } interface TrancheTokenLike is IERC20, ERC20PermitLike { + function restrictionManager() external view returns (address); + function file(bytes32 what, address data) external; function mint(address user, uint256 value) external; function burn(address user, uint256 value) external; } diff --git a/test/migrations/InvestRedeemFlow.t.sol b/test/migrations/InvestRedeemFlow.t.sol index 08299b67..af8d75b9 100644 --- a/test/migrations/InvestRedeemFlow.t.sol +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -5,11 +5,6 @@ import "../TestSetup.t.sol"; import {LiquidityPool} from "src/LiquidityPool.sol"; import {MathLib} from "src/util/MathLib.sol"; -interface AuthLike { - function rely(address) external; - function deny(address) external; -} - contract InvestRedeemFlow is TestSetup { using MathLib for uint128; diff --git a/test/migrations/MigratedGateway.t.sol b/test/migrations/MigratedGateway.t.sol index 34935efd..9f6e3e0c 100644 --- a/test/migrations/MigratedGateway.t.sol +++ b/test/migrations/MigratedGateway.t.sol @@ -4,11 +4,6 @@ pragma solidity 0.8.21; import {MigratedGateway, Gateway} from "./migrationContracts/MigratedGateway.sol"; import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; -interface AuthLike { - function rely(address) external; - function deny(address) external; -} - contract MigratedGatewayTest is InvestRedeemFlow { function setUp() public override { super.setUp(); diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index dda92b82..1b4feed6 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -1,18 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.21; -import {MigratedInvestmentManager, InvestmentManager} from "./migrationContracts/MigratedInvestmentManager.sol"; -import {MigratedPoolManager, PoolManager} from "./migrationContracts/MigratedPoolManager.sol"; -import {MigratedGateway, Gateway} from "./migrationContracts/MigratedGateway.sol"; import {MigratedLiquidityPool, LiquidityPool} from "./migrationContracts/MigratedLiquidityPool.sol"; -import {LiquidityPoolFactory, TrancheTokenFactory} 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(); diff --git a/test/migrations/MigratedRestrictionManager.t.sol b/test/migrations/MigratedRestrictionManager.t.sol new file mode 100644 index 00000000..9db30a8a --- /dev/null +++ b/test/migrations/MigratedRestrictionManager.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedRestrictionManager, RestrictionManager} from "./migrationContracts/MigratedRestrictionManager.sol"; +// import {TrancheTokenLike} from "src/token/Tranche.sol"; +import {LiquidityPool} from "src/LiquidityPool.sol"; +import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; + +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); + + // Deploy new Gateway + MigratedRestrictionManager newRestrictionManager = + new MigratedRestrictionManager(address(LiquidityPool(_lPool).share())); + + RestrictionManager oldRestrictionManager = + RestrictionManager(LiquidityPool(_lPool).share().restrictionManager()); + + // Rewire contracts + root.relyContract(address(LiquidityPool(_lPool).share()), address(this)); + LiquidityPool(_lPool).share().file("restrictionManager", address(newRestrictionManager)); + newRestrictionManager.updateMember(address(escrow), type(uint256).max); + newRestrictionManager.rely(address(root)); + for (uint256 i = 0; i < restrictionManagerWards.length; i++) { + newRestrictionManager.rely(restrictionManagerWards[i]); + } + + // clean up + newRestrictionManager.deny(address(this)); + root.denyContract(address(LiquidityPool(_lPool).share()), 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, + RestrictionManager newRestrictionManager + ) internal { + assertTrue(address(oldRestrictionManager) != address(newRestrictionManager)); + assertTrue(oldRestrictionManager.token() == newRestrictionManager.token()); + assertTrue(oldRestrictionManager.hasMember(address(escrow)) == newRestrictionManager.hasMember(address(escrow))); + assertTrue(newRestrictionManager.wards(address(root)) == 1); + assertTrue(newRestrictionManager.wards(address(poolManager)) == 1); + } +} 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_) {} +} From 94ee8ffd27f843072dd040fb06363748e9866a1b Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 10 Oct 2023 14:42:31 -0400 Subject: [PATCH 24/51] Address comments --- src/LiquidityPool.sol | 5 +- test/migrations/InvestRedeemFlow.t.sol | 68 +++++++------------ test/migrations/MigratedAdmin.t.sol | 4 +- test/migrations/MigratedGateway.t.sol | 2 +- .../MigratedInvestmentManager.t.sol | 24 +++++-- test/migrations/MigratedLiquidityPool.t.sol | 27 +++++++- test/migrations/MigratedPoolManager.t.sol | 2 +- .../MigratedRestrictionManager.t.sol | 21 +++--- 8 files changed, 87 insertions(+), 66 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index a3897eac..aa3a3e5b 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -11,10 +11,7 @@ interface ERC20PermitLike { external; } -interface TrancheTokenLike is IERC20, ERC20PermitLike { - function restrictionManager() external view returns (address); - function file(bytes32 what, address data) external; -} +interface TrancheTokenLike is IERC20, ERC20PermitLike {} interface ManagerLike { function deposit(address lp, uint256 assets, address receiver, address owner) external returns (uint256); diff --git a/test/migrations/InvestRedeemFlow.t.sol b/test/migrations/InvestRedeemFlow.t.sol index af8d75b9..db1da884 100644 --- a/test/migrations/InvestRedeemFlow.t.sol +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -6,7 +6,7 @@ import {LiquidityPool} from "src/LiquidityPool.sol"; import {MathLib} from "src/util/MathLib.sol"; contract InvestRedeemFlow is TestSetup { - using MathLib for uint128; + using MathLib for uint256; uint8 internal constant PRICE_DECIMALS = 18; @@ -33,7 +33,7 @@ contract InvestRedeemFlow is TestSetup { removeDeployerAccess(address(router), address(this)); } - function VerifyInvestAndRedeemFlow(uint64 poolId_, bytes16 trancheId_, address liquidityPool) public { + function verifyInvestAndRedeemFlow(uint64 poolId_, bytes16 trancheId_, address liquidityPool) public { uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); //TODO: fuzz price LiquidityPool lPool = LiquidityPool(liquidityPool); @@ -43,42 +43,37 @@ contract InvestRedeemFlow is TestSetup { redeemWithdraw(poolId_, trancheId_, price, redeemAmount, lPool); } - function depositMint(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 amount, LiquidityPool lPool) + function depositMint(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 currencyAmount, LiquidityPool lPool) public { vm.prank(investor); - erc20.approve(address(investmentManager), amount); // add allowance + erc20.approve(address(investmentManager), currencyAmount); // add allowance vm.prank(investor); - lPool.requestDeposit(amount); + lPool.requestDeposit(currencyAmount); // ensure funds are locked in escrow - assertEq(erc20.balanceOf(address(escrow)), amount); - assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); - - // trigger executed collectInvest - uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId - - uint128 trancheTokensPayout = _toUint128( - uint128(amount).mulDiv( - 10 ** (PRICE_DECIMALS - erc20.decimals() + lPool.decimals()), price, MathLib.Rounding.Down - ) - ); + 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(amount), trancheTokensPayout, 0 + poolId_, trancheId_, investor, _currencyId, uint128(currencyAmount), trancheTokensPayout, 0 ); assertEq(lPool.maxMint(investor), trancheTokensPayout); assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); - assertEq(erc20.balanceOf(investor), investorCurrencyAmount - amount); + assertEq(erc20.balanceOf(investor), investorCurrencyAmount - currencyAmount); uint256 div = 2; vm.prank(investor); - lPool.deposit(amount / div, investor); + lPool.deposit(currencyAmount / div, investor); assertEq(lPool.balanceOf(investor), trancheTokensPayout / div); assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout - trancheTokensPayout / div); @@ -89,40 +84,39 @@ contract InvestRedeemFlow is TestSetup { lPool.mint(maxMint, investor); assertEq(lPool.balanceOf(investor), trancheTokensPayout); - assertTrue(lPool.balanceOf(address(escrow)) <= 1); - assertTrue(lPool.maxMint(investor) <= 1); + assertLe(lPool.balanceOf(address(escrow)), 1); + assertLe(lPool.maxMint(investor), 1); } - function redeemWithdraw(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 amount, LiquidityPool lPool) + function redeemWithdraw(uint64 poolId_, bytes16 trancheId_, uint128 price, uint256 tokenAmount, LiquidityPool lPool) public { vm.prank(investor); - lPool.requestRedeem(amount); + lPool.requestRedeem(tokenAmount); - // redeem - uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId - uint128 currencyPayout = _toUint128( - uint128(amount).mulDiv(price, 10 ** (18 - erc20.decimals() + lPool.decimals()), MathLib.Rounding.Down) - ); // 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(amount), 0 + poolId_, trancheId_, investor, _currencyId, currencyPayout, uint128(tokenAmount), 0 ); assertEq(lPool.maxWithdraw(investor), currencyPayout); - assertEq(lPool.maxRedeem(investor), amount); + assertEq(lPool.maxRedeem(investor), tokenAmount); assertEq(lPool.balanceOf(address(escrow)), 0); uint128 div = 2; vm.prank(investor); - lPool.redeem(amount / div, investor, 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), amount / div); + assertEq(lPool.maxRedeem(investor), tokenAmount / div); uint256 maxWithdraw = lPool.maxWithdraw(investor); vm.prank(investor); @@ -133,14 +127,4 @@ contract InvestRedeemFlow is TestSetup { assertEq(lPool.maxWithdraw(investor), 0); assertEq(lPool.maxRedeem(investor), 0); } - - // --- Utils --- - - function _toUint128(uint256 _value) internal pure returns (uint128 value) { - if (_value > type(uint128).max) { - revert("InvestmentManager/uint128-overflow"); - } else { - value = uint128(_value); - } - } } diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol index a0bd9923..58c764e2 100644 --- a/test/migrations/MigratedAdmin.t.sol +++ b/test/migrations/MigratedAdmin.t.sol @@ -36,12 +36,12 @@ contract MigratedAdmin is InvestRedeemFlow { root.deny(address(this)); // verify permissions - verifyMigratedDelayedAdminPermissions(delayedAdmin, newDelayedAdmin, pauseAdmin, newPauseAdmin); + verifyMigratedAdminPermissions(delayedAdmin, newDelayedAdmin, pauseAdmin, newPauseAdmin); // TODO: test admin functionality still works } - function verifyMigratedDelayedAdminPermissions( + function verifyMigratedAdminPermissions( DelayedAdmin oldDelayedAdmin, DelayedAdmin newDelayedAdmin, PauseAdmin oldPauseAdmin, diff --git a/test/migrations/MigratedGateway.t.sol b/test/migrations/MigratedGateway.t.sol index 9f6e3e0c..4cb15a42 100644 --- a/test/migrations/MigratedGateway.t.sol +++ b/test/migrations/MigratedGateway.t.sol @@ -40,7 +40,7 @@ contract MigratedGatewayTest is InvestRedeemFlow { // test that everything is working gateway = newGateway; - VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } function verifyMigratedGatewayPermissions(Gateway oldGateway, Gateway newGateway) public { diff --git a/test/migrations/MigratedInvestmentManager.t.sol b/test/migrations/MigratedInvestmentManager.t.sol index e2c3bfd8..c2b0c7d4 100644 --- a/test/migrations/MigratedInvestmentManager.t.sol +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -78,7 +78,7 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { verifyMigratedInvestmentManagerPermissions(investmentManager, newInvestmentManager); investmentManager = newInvestmentManager; - VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } function verifyMigratedInvestmentManagerPermissions( @@ -137,10 +137,24 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { 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); + ( + , + , + , + 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); diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index fa13a855..a1ac94fd 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -4,6 +4,16 @@ pragma solidity 0.8.21; import {MigratedLiquidityPool, LiquidityPool} from "./migrationContracts/MigratedLiquidityPool.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); +} + contract MigrationsTest is InvestRedeemFlow { function setUp() public override { super.setUp(); @@ -21,6 +31,10 @@ contract MigrationsTest is InvestRedeemFlow { ); // Rewire contracts + TrancheTokenLike token = TrancheTokenLike(poolManager.getTrancheToken(poolId, trancheId)); + root.relyContract(address(token), address(this)); + token.rely(address(newLiquidityPool)); + token.addTrustedForwarder(address(newLiquidityPool)); newLiquidityPool.rely(address(root)); newLiquidityPool.rely(address(investmentManager)); root.relyContract(address(investmentManager), address(this)); @@ -28,10 +42,15 @@ contract MigrationsTest is InvestRedeemFlow { investmentManager.deny(_lPool); root.relyContract(address(escrow), address(this)); escrow.approve(address(newLiquidityPool), address(investmentManager), type(uint256).max); - escrow.approve(_lPool, address(investmentManager), 0); + escrow.approve(address(token), address(newLiquidityPool), type(uint256).max); // clean up + escrow.approve(_lPool, address(investmentManager), 0); + escrow.approve(address(token), _lPool, 0); newLiquidityPool.deny(address(this)); + token.deny(_lPool); + token.removeTrustedForwarder(_lPool); + root.denyContract(address(token), address(this)); root.denyContract(address(investmentManager), address(this)); root.denyContract(address(escrow), address(this)); root.deny(address(this)); @@ -41,7 +60,7 @@ contract MigrationsTest is InvestRedeemFlow { // TODO: test that everything is working // _lPool = address(newLiquidityPool); - // VerifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); + // verifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); } // --- Permissions & Dependencies Checks --- @@ -54,6 +73,10 @@ contract MigrationsTest is InvestRedeemFlow { assertEq(address(oldLiquidityPool.share()), address(newLiquidityPool.share())); address token = poolManager.getTrancheToken(poolId, trancheId); assertEq(address(newLiquidityPool.share()), token); + assertEq(TrancheTokenLike(token).trustedForwarders(address(oldLiquidityPool)), false); + assertEq(TrancheTokenLike(token).trustedForwarders(address(newLiquidityPool)), true); + assertEq(TrancheTokenLike(token).wards(address(oldLiquidityPool)), 0); + assertEq(TrancheTokenLike(token).wards(address(newLiquidityPool)), 1); assertEq(address(oldLiquidityPool.manager()), address(newLiquidityPool.manager())); assertEq(newLiquidityPool.wards(address(root)), 1); assertEq(newLiquidityPool.wards(address(investmentManager)), 1); diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol index 05257d7a..abbff00b 100644 --- a/test/migrations/MigratedPoolManager.t.sol +++ b/test/migrations/MigratedPoolManager.t.sol @@ -92,7 +92,7 @@ contract MigrationsTest is InvestRedeemFlow { 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); + verifyInvestAndRedeemFlow(poolId + 1, trancheId, _lPool2); } function verifyMigratedPoolManagerPermissions(PoolManager oldPoolManager, PoolManager newPoolManager) public { diff --git a/test/migrations/MigratedRestrictionManager.t.sol b/test/migrations/MigratedRestrictionManager.t.sol index 9db30a8a..59a03e27 100644 --- a/test/migrations/MigratedRestrictionManager.t.sol +++ b/test/migrations/MigratedRestrictionManager.t.sol @@ -2,10 +2,14 @@ pragma solidity 0.8.21; import {MigratedRestrictionManager, RestrictionManager} from "./migrationContracts/MigratedRestrictionManager.sol"; -// import {TrancheTokenLike} from "src/token/Tranche.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(); @@ -19,17 +23,16 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { address[] memory restrictionManagerWards = new address[](1); restrictionManagerWards[0] = address(poolManager); + address token = address(LiquidityPool(_lPool).share()); // Deploy new Gateway - MigratedRestrictionManager newRestrictionManager = - new MigratedRestrictionManager(address(LiquidityPool(_lPool).share())); + MigratedRestrictionManager newRestrictionManager = new MigratedRestrictionManager(token); - RestrictionManager oldRestrictionManager = - RestrictionManager(LiquidityPool(_lPool).share().restrictionManager()); + RestrictionManager oldRestrictionManager = RestrictionManager(TrancheTokenLike(token).restrictionManager()); // Rewire contracts - root.relyContract(address(LiquidityPool(_lPool).share()), address(this)); - LiquidityPool(_lPool).share().file("restrictionManager", address(newRestrictionManager)); + root.relyContract(token, address(this)); + TrancheTokenLike(token).file("restrictionManager", address(newRestrictionManager)); newRestrictionManager.updateMember(address(escrow), type(uint256).max); newRestrictionManager.rely(address(root)); for (uint256 i = 0; i < restrictionManagerWards.length; i++) { @@ -38,7 +41,7 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { // clean up newRestrictionManager.deny(address(this)); - root.denyContract(address(LiquidityPool(_lPool).share()), address(this)); + root.denyContract(token, address(this)); root.deny(address(this)); // verify permissions @@ -46,7 +49,7 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { // TODO: test that everything is working // restrictionManager = newRestrictionManager; - // VerifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + // verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } function verifyMigratedRestrictionManagerPermissions( From 9a289df57fbb3967e84f7921fe231b23e1f7d5cf Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 11 Oct 2023 15:29:06 -0400 Subject: [PATCH 25/51] add invest/redeem flow run to liquidity pool migration --- src/PoolManager.sol | 17 ++++++++++++++++- test/migrations/MigratedLiquidityPool.t.sol | 21 +++++++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index b4507a90..1db7bcda 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -396,7 +396,7 @@ contract PoolManager is Auth { // in the escrow to transfer to the user on deposit or mint escrow.approve(tranche.token, address(investmentManager), type(uint256).max); - // Give investment manager infinite approval for tranche tokens + // Give liquidity pool infinite approval for tranche tokens // in the escrow to burn on executed redemptions escrow.approve(tranche.token, liquidityPool, type(uint256).max); @@ -405,6 +405,21 @@ contract PoolManager is Auth { } // --- Helpers --- + function updateLiquidityPoolAddress(uint64 poolId, bytes16 trancheId, address currency, address liquidityPool) + external + auth + { + Tranche storage tranche = pools[poolId].tranches[trancheId]; + require(tranche.token != address(0), "PoolManager/tranche-does-not-exist"); + require(isAllowedAsInvestmentCurrency(poolId, currency), "PoolManager/currency-not-supported"); + + address oldLiquidityPool = tranche.liquidityPools[currency]; + require(oldLiquidityPool != address(0), "PoolManager/liquidity-pool-not-deployed"); + + tranche.liquidityPools[currency] = liquidityPool; + emit DeployLiquidityPool(poolId, trancheId, liquidityPool); + } + function getTrancheToken(uint64 poolId, bytes16 trancheId) public view returns (address) { Tranche storage tranche = pools[poolId].tranches[trancheId]; return tranche.token; diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index a1ac94fd..80465ed3 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -12,6 +12,7 @@ interface TrancheTokenLike { 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); } contract MigrationsTest is InvestRedeemFlow { @@ -31,21 +32,22 @@ contract MigrationsTest is InvestRedeemFlow { ); // Rewire contracts - TrancheTokenLike token = TrancheTokenLike(poolManager.getTrancheToken(poolId, trancheId)); + TrancheTokenLike token = TrancheTokenLike(address(LiquidityPool(_lPool).share())); root.relyContract(address(token), address(this)); token.rely(address(newLiquidityPool)); token.addTrustedForwarder(address(newLiquidityPool)); - newLiquidityPool.rely(address(root)); - newLiquidityPool.rely(address(investmentManager)); root.relyContract(address(investmentManager), address(this)); investmentManager.rely(address(newLiquidityPool)); - investmentManager.deny(_lPool); + newLiquidityPool.rely(address(root)); + newLiquidityPool.rely(address(investmentManager)); root.relyContract(address(escrow), address(this)); - escrow.approve(address(newLiquidityPool), address(investmentManager), type(uint256).max); + escrow.approve(address(token), address(investmentManager), type(uint256).max); escrow.approve(address(token), address(newLiquidityPool), type(uint256).max); + root.relyContract(address(poolManager), address(this)); + poolManager.updateLiquidityPoolAddress(poolId, trancheId, address(erc20), address(newLiquidityPool)); // clean up - escrow.approve(_lPool, address(investmentManager), 0); + investmentManager.deny(_lPool); escrow.approve(address(token), _lPool, 0); newLiquidityPool.deny(address(this)); token.deny(_lPool); @@ -53,14 +55,15 @@ contract MigrationsTest is InvestRedeemFlow { root.denyContract(address(token), address(this)); root.denyContract(address(investmentManager), address(this)); root.denyContract(address(escrow), address(this)); + root.denyContract(address(poolManager), address(this)); root.deny(address(this)); // verify permissions verifyLiquidityPoolPermissions(LiquidityPool(_lPool), newLiquidityPool); // TODO: test that everything is working - // _lPool = address(newLiquidityPool); - // verifyInvestAndRedeemFlow(poolId, trancheId, address(_lPool)); + _lPool = address(newLiquidityPool); + verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } // --- Permissions & Dependencies Checks --- @@ -82,5 +85,7 @@ contract MigrationsTest is InvestRedeemFlow { assertEq(newLiquidityPool.wards(address(investmentManager)), 1); assertEq(investmentManager.wards(address(newLiquidityPool)), 1); assertEq(investmentManager.wards(address(oldLiquidityPool)), 0); + assertEq(TrancheTokenLike(token).allowance(address(escrow), address(oldLiquidityPool)), 0); + assertEq(TrancheTokenLike(token).allowance(address(escrow), address(newLiquidityPool)), type(uint256).max); } } From e587e030602a1a2e4d9740e933286c302bf7502f Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 12 Oct 2023 22:51:39 -0400 Subject: [PATCH 26/51] Migrate poolManager along with liquidityPools --- test/migrations/MigratedLiquidityPool.t.sol | 75 +++++++++++++++++-- test/migrations/MigratedPoolManager.t.sol | 4 +- .../MigratedPoolManager.sol | 35 ++++++--- 3 files changed, 93 insertions(+), 21 deletions(-) diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index 80465ed3..3292c5ac 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -2,6 +2,8 @@ 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 { @@ -15,6 +17,11 @@ interface TrancheTokenLike { 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(); @@ -26,27 +33,79 @@ contract MigrationsTest is InvestRedeemFlow { vm.warp(block.timestamp + 3 days); root.executeScheduledRely(address(this)); - // Deploy new LiquidityPool + // Deploy new liquidity pool MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(investmentManager) ); - // Rewire contracts + // set MigratedPoolManager input parameters + 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[][][](1); + liquidityPoolOverrides[0] = new address[][](1); + liquidityPoolOverrides[0][0] = new address[](1); + liquidityPoolOverrides[0][0][0] = address(newLiquidityPool); + + // Deploy new MigratedPoolManager + MigratedPoolManager newPoolManager = new MigratedPoolManager( + address(escrow), + liquidityPoolFactory, + restrictionManagerFactory, + trancheTokenFactory, + address(poolManager), + poolIds, + trancheIds, + allowedCurrencies, + liquidityPoolCurrencies, + liquidityPoolOverrides + ); + + // rewire migrated pool manager + LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); + TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); + 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 migrated pool manager + newPoolManager.deny(address(this)); + root.denyContract(address(gateway), address(this)); + root.denyContract(restrictionManagerFactory, address(this)); + + // Rewire new liquidity pool TrancheTokenLike token = TrancheTokenLike(address(LiquidityPool(_lPool).share())); root.relyContract(address(token), address(this)); token.rely(address(newLiquidityPool)); token.addTrustedForwarder(address(newLiquidityPool)); - root.relyContract(address(investmentManager), address(this)); investmentManager.rely(address(newLiquidityPool)); newLiquidityPool.rely(address(root)); newLiquidityPool.rely(address(investmentManager)); - root.relyContract(address(escrow), address(this)); escrow.approve(address(token), address(investmentManager), type(uint256).max); escrow.approve(address(token), address(newLiquidityPool), type(uint256).max); - root.relyContract(address(poolManager), address(this)); - poolManager.updateLiquidityPoolAddress(poolId, trancheId, address(erc20), address(newLiquidityPool)); - // clean up + // clean up new liquidity pool investmentManager.deny(_lPool); escrow.approve(address(token), _lPool, 0); newLiquidityPool.deny(address(this)); @@ -55,7 +114,6 @@ contract MigrationsTest is InvestRedeemFlow { root.denyContract(address(token), address(this)); root.denyContract(address(investmentManager), address(this)); root.denyContract(address(escrow), address(this)); - root.denyContract(address(poolManager), address(this)); root.deny(address(this)); // verify permissions @@ -63,6 +121,7 @@ contract MigrationsTest is InvestRedeemFlow { // TODO: test that everything is working _lPool = address(newLiquidityPool); + poolManager = newPoolManager; verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol index abbff00b..c265b0a0 100644 --- a/test/migrations/MigratedPoolManager.t.sol +++ b/test/migrations/MigratedPoolManager.t.sol @@ -35,6 +35,7 @@ contract MigrationsTest is InvestRedeemFlow { 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( @@ -46,7 +47,8 @@ contract MigrationsTest is InvestRedeemFlow { poolIds, trancheIds, allowedCurrencies, - liquidityPoolCurrencies + liquidityPoolCurrencies, + liquidityPoolOverrides ); verifyMigratedPoolManagerState( diff --git a/test/migrations/migrationContracts/MigratedPoolManager.sol b/test/migrations/migrationContracts/MigratedPoolManager.sol index fb561fdd..2bd194bf 100644 --- a/test/migrations/migrationContracts/MigratedPoolManager.sol +++ b/test/migrations/migrationContracts/MigratedPoolManager.sol @@ -2,6 +2,7 @@ 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. @@ -19,11 +20,14 @@ contract MigratedPoolManager is PoolManager { uint64[] memory poolIds, bytes16[][] memory trancheIds, address[][] memory allowedCurrencies, - address[][][] memory liquidityPoolCurrencies + address[][][] memory liquidityPoolCurrencies, + address[][][] memory liquidityPoolOverrides ) PoolManager(escrow_, liquidityPoolFactory_, restrictionManagerFactory_, trancheTokenFactory_) { // migrate pools PoolManager oldPoolManager_ = PoolManager(oldPoolManager); - migratePools(oldPoolManager_, poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies); + migratePools( + oldPoolManager_, poolIds, trancheIds, allowedCurrencies, liquidityPoolCurrencies, liquidityPoolOverrides + ); for (uint256 i = 0; i < allowedCurrencies.length; i++) { for (uint256 j = 0; j < allowedCurrencies[i].length; j++) { @@ -40,16 +44,20 @@ contract MigratedPoolManager is PoolManager { uint64[] memory poolIds, bytes16[][] memory trancheIds, address[][] memory allowedCurrencies, - address[][][] memory liquidityPoolCurrencies + 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_); + migrateTranches( + poolIds[i], trancheIds[i], liquidityPoolCurrencies[i], oldPoolManager_, liquidityPoolOverrides_ + ); migrateUndeployedTranches(poolIds[i], trancheIds[i], oldPoolManager_); // migrate allowed currencies @@ -64,18 +72,23 @@ contract MigratedPoolManager is PoolManager { uint64 poolId, bytes16[] memory trancheIds, address[][] memory liquidityPoolCurrencies, - PoolManager oldPoolManager_ + 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]; - pool.tranches[trancheId].liquidityPools[currencyAddress] = - oldPoolManager_.getLiquidityPool(poolId, trancheId, currencyAddress); + if (liquidityPoolOverrides_.length > 0) { + pool.tranches[trancheId].liquidityPools[currencyAddress] = liquidityPoolOverrides[j][k]; + } else { + pool.tranches[trancheId].liquidityPools[currencyAddress] = + oldPoolManager_.getLiquidityPool(poolId, trancheId, currencyAddress); + } } } } @@ -85,10 +98,8 @@ contract MigratedPoolManager is PoolManager { { for (uint256 j = 0; j < trancheIds.length; j++) { bytes16 trancheId = trancheIds[j]; - (uint8 decimals, string memory tokenName, string memory tokenSymbol) = oldPoolManager_.undeployedTranches(poolId, trancheId); - undeployedTranches[poolId][trancheId].decimals = decimals; undeployedTranches[poolId][trancheId].tokenName = tokenName; undeployedTranches[poolId][trancheId].tokenSymbol = tokenSymbol; From fb9b6232d5fb27f55b571938c50eab604b526521 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Fri, 13 Oct 2023 16:22:31 -0400 Subject: [PATCH 27/51] deploy new liquidityPoolFactory --- test/migrations/MigratedPoolManager.t.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol index c265b0a0..0c1c94fc 100644 --- a/test/migrations/MigratedPoolManager.t.sol +++ b/test/migrations/MigratedPoolManager.t.sol @@ -21,6 +21,12 @@ contract MigrationsTest is InvestRedeemFlow { 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); @@ -40,7 +46,7 @@ contract MigrationsTest is InvestRedeemFlow { // Deploy new MigratedPoolManager MigratedPoolManager newPoolManager = new MigratedPoolManager( address(escrow), - liquidityPoolFactory, + address(newLiquidityPoolFactory), restrictionManagerFactory, trancheTokenFactory, address(poolManager), @@ -56,7 +62,7 @@ contract MigrationsTest is InvestRedeemFlow { ); // Rewire contracts - LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); + newLiquidityPoolFactory.rely(address(newPoolManager)); TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); root.relyContract(address(gateway), address(this)); gateway.file("poolManager", address(newPoolManager)); @@ -100,7 +106,7 @@ contract MigrationsTest is InvestRedeemFlow { function verifyMigratedPoolManagerPermissions(PoolManager oldPoolManager, PoolManager newPoolManager) public { assertTrue(address(oldPoolManager) != address(newPoolManager)); assertEq(address(oldPoolManager.escrow()), address(newPoolManager.escrow())); - assertEq(address(oldPoolManager.liquidityPoolFactory()), address(newPoolManager.liquidityPoolFactory())); + assertFalse(address(oldPoolManager.liquidityPoolFactory()) == address(newPoolManager.liquidityPoolFactory())); assertEq( address(oldPoolManager.restrictionManagerFactory()), address(newPoolManager.restrictionManagerFactory()) ); From 1207dc9cb9c752f67337b5b7f4042574a1640cc0 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Mon, 16 Oct 2023 10:29:52 -0400 Subject: [PATCH 28/51] remove unused helper --- src/PoolManager.sol | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 1db7bcda..d21c936e 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -405,20 +405,6 @@ contract PoolManager is Auth { } // --- Helpers --- - function updateLiquidityPoolAddress(uint64 poolId, bytes16 trancheId, address currency, address liquidityPool) - external - auth - { - Tranche storage tranche = pools[poolId].tranches[trancheId]; - require(tranche.token != address(0), "PoolManager/tranche-does-not-exist"); - require(isAllowedAsInvestmentCurrency(poolId, currency), "PoolManager/currency-not-supported"); - - address oldLiquidityPool = tranche.liquidityPools[currency]; - require(oldLiquidityPool != address(0), "PoolManager/liquidity-pool-not-deployed"); - - tranche.liquidityPools[currency] = liquidityPool; - emit DeployLiquidityPool(poolId, trancheId, liquidityPool); - } function getTrancheToken(uint64 poolId, bytes16 trancheId) public view returns (address) { Tranche storage tranche = pools[poolId].tranches[trancheId]; From 10494500b333dcd9303381fb573c0eea4491f19a Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Mon, 16 Oct 2023 12:18:54 -0400 Subject: [PATCH 29/51] remove new line --- src/PoolManager.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index d21c936e..b265c8e5 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -405,7 +405,6 @@ contract PoolManager is Auth { } // --- Helpers --- - function getTrancheToken(uint64 poolId, bytes16 trancheId) public view returns (address) { Tranche storage tranche = pools[poolId].tranches[trancheId]; return tranche.token; From a52fb6d077f5b9c156c501d0387538a020cfe406 Mon Sep 17 00:00:00 2001 From: Alina Sinelnikova Date: Tue, 17 Oct 2023 06:56:16 +0100 Subject: [PATCH 30/51] fix: add tranche (#180) --- src/PoolManager.sol | 1 + test/PoolManager.t.sol | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index b4507a90..192f83ec 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -243,6 +243,7 @@ contract PoolManager is Auth { UndeployedTranche storage undeployedTranche = undeployedTranches[poolId][trancheId]; require(undeployedTranche.decimals == 0, "PoolManager/tranche-already-exists"); + require(getTrancheToken(poolId, trancheId) == address(0), "PoolManager/tranche-already-deployed"); undeployedTranche.decimals = decimals; undeployedTranche.tokenName = tokenName; diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 9d844082..cbc2e954 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -568,6 +568,9 @@ contract PoolManagerTest is TestSetup { _bytes32ToString(_stringToBytes32(tokenSymbol)), _bytes32ToString(_stringToBytes32(trancheToken.symbol())) ); assertEq(decimals, trancheToken.decimals()); + + vm.expectRevert(bytes("PoolManager/tranche-already-deployed")); + centrifugeChain.addTranche(poolId, trancheId, tokenName, tokenSymbol, decimals); } function testAddingTrancheMultipleTimesFails( From 69aa8b5bf34d7cb692b2773f310607f069a9cd31 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:38:02 +0200 Subject: [PATCH 31/51] Align approval flow and interface with EIP draft (#178) * Explore new approval flow * Fix permit logic * Allowance checks * Remove fee-on-transfer support * Fix revert message * Fix tests * Remove unused function from interface * Fix comment * Naming * Remove console * Remove duplicate transfer from function * Fix balance check * Add operator param * Align naming of view functions * Address pr comments --- src/InvestmentManager.sol | 67 ++++++++++++------- src/LiquidityPool.sol | 132 +++++++++++++++++++++----------------- src/PoolManager.sol | 11 +--- src/util/Factory.sol | 5 +- test/Deploy.t.sol | 6 +- test/LiquidityPool.t.sol | 131 ++++++++++++++++++------------------- 6 files changed, 190 insertions(+), 162 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index e5555ff0..e3902425 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -153,7 +153,11 @@ contract InvestmentManager is Auth { /// proceed with tranche token payouts in case their orders got fulfilled. /// @dev The user currency amount required to fulfill the deposit request have to be locked, /// even though the tranche token payout can only happen after epoch execution. - function requestDeposit(address liquidityPool, uint256 currencyAmount, address user) public auth { + function requestDeposit(address liquidityPool, uint256 currencyAmount, address sender, address user) + public + auth + returns (bool) + { LiquidityPoolLike lPool = LiquidityPoolLike(liquidityPool); uint128 _currencyAmount = currencyAmount.toUint128(); require(_currencyAmount != 0, "InvestmentManager/zero-amount-not-allowed"); @@ -161,25 +165,24 @@ contract InvestmentManager is Auth { uint64 poolId = lPool.poolId(); address currency = lPool.asset(); require(poolManager.isAllowedAsInvestmentCurrency(poolId, currency), "InvestmentManager/currency-not-allowed"); + + require( + _checkTransferRestriction(liquidityPool, address(0), sender, 0), "InvestmentManager/sender-is-restricted" + ); require( _checkTransferRestriction(liquidityPool, address(0), user, convertToShares(liquidityPool, currencyAmount)), "InvestmentManager/transfer-not-allowed" ); - // Transfer the currency amount from user to escrow (lock currency in escrow) - // Checks actual balance difference to support fee-on-transfer tokens - uint256 preBalance = ERC20Like(currency).balanceOf(address(escrow)); - SafeTransferLib.safeTransferFrom(currency, user, address(escrow), _currencyAmount); - uint256 postBalance = ERC20Like(currency).balanceOf(address(escrow)); - uint128 transferredAmount = (postBalance - preBalance).toUint128(); - InvestmentState storage state = investments[liquidityPool][user]; - state.remainingDepositRequest = state.remainingDepositRequest + transferredAmount; + state.remainingDepositRequest = state.remainingDepositRequest + _currencyAmount; state.exists = true; gateway.increaseInvestOrder( - poolId, lPool.trancheId(), user, poolManager.currencyAddressToId(currency), transferredAmount + poolId, lPool.trancheId(), user, poolManager.currencyAddressToId(currency), _currencyAmount ); + + return true; } /// @notice Request tranche token redemption. Liquidity pools have to request redemptions @@ -189,7 +192,11 @@ contract InvestmentManager is Auth { /// in case their orders got fulfilled. /// @dev The user tranche tokens required to fulfill the redemption request have to be locked, /// even though the currency payout can only happen after epoch execution. - function requestRedeem(address liquidityPool, uint256 trancheTokenAmount, address user) public auth { + function requestRedeem(address liquidityPool, uint256 trancheTokenAmount, address user) + public + auth + returns (bool) + { uint128 _trancheTokenAmount = trancheTokenAmount.toUint128(); require(_trancheTokenAmount != 0, "InvestmentManager/zero-amount-not-allowed"); LiquidityPoolLike lPool = LiquidityPoolLike(liquidityPool); @@ -200,24 +207,23 @@ contract InvestmentManager is Auth { "InvestmentManager/currency-not-allowed" ); - _processIncreaseRedeemRequest(liquidityPool, _trancheTokenAmount, user); + return _processRedeemRequest(liquidityPool, _trancheTokenAmount, user); } - function _processIncreaseRedeemRequest(address liquidityPool, uint128 trancheTokenAmount, address user) internal { + function _processRedeemRequest(address liquidityPool, uint128 trancheTokenAmount, address user) + internal + returns (bool) + { LiquidityPoolLike lPool = LiquidityPoolLike(liquidityPool); InvestmentState storage state = investments[liquidityPool][user]; state.remainingRedeemRequest = state.remainingRedeemRequest + trancheTokenAmount; state.exists = true; - // Transfer the tranche token amount from user to escrow (lock tranche tokens in escrow) - require( - AuthTransferLike(address(lPool.share())).authTransferFrom(user, address(escrow), trancheTokenAmount), - "InvestmentManager/transfer-failed" - ); - gateway.increaseRedeemOrder( lPool.poolId(), lPool.trancheId(), user, poolManager.currencyAddressToId(lPool.asset()), trancheTokenAmount ); + + return true; } function decreaseDepositRequest(address liquidityPool, uint256 _currencyAmount, address user) public auth { @@ -259,7 +265,7 @@ contract InvestmentManager is Auth { function cancelRedeemRequest(address liquidityPool, address user) public auth { LiquidityPoolLike _liquidityPool = LiquidityPoolLike(liquidityPool); - uint256 approximateTrancheTokensPayout = userRedeemRequest(liquidityPool, user); + uint256 approximateTrancheTokensPayout = pendingRedeemRequest(liquidityPool, user); require( _checkTransferRestriction(liquidityPool, address(0), user, approximateTrancheTokensPayout), "InvestmentManager/transfer-not-allowed" @@ -410,7 +416,18 @@ contract InvestmentManager is Auth { uint128 trancheTokenAmount ) public onlyGateway { address liquidityPool = poolManager.getLiquidityPool(poolId, trancheId, currencyId); - _processIncreaseRedeemRequest(liquidityPool, trancheTokenAmount, user); + require( + _processRedeemRequest(liquidityPool, trancheTokenAmount, user), "InvestmentManager/failed-redeem-request" + ); + + // Transfer the tranche token amount from user to escrow (lock tranche tokens in escrow) + require( + AuthTransferLike(address(LiquidityPoolLike(liquidityPool).share())).authTransferFrom( + user, address(escrow), trancheTokenAmount + ), + "InvestmentManager/transfer-failed" + ); + emit TriggerIncreaseRedeemOrder(poolId, trancheId, user, currencyId, trancheTokenAmount); } @@ -489,11 +506,15 @@ contract InvestmentManager is Auth { currencyAmount = uint256(_calculateCurrencyAmount(trancheTokenAmount, liquidityPool, state.redeemPrice)); } - function userDepositRequest(address liquidityPool, address user) public view returns (uint256 currencyAmount) { + function pendingDepositRequest(address liquidityPool, address user) public view returns (uint256 currencyAmount) { currencyAmount = uint256(investments[liquidityPool][user].remainingDepositRequest); } - function userRedeemRequest(address liquidityPool, address user) public view returns (uint256 trancheTokenAmount) { + function pendingRedeemRequest(address liquidityPool, address user) + public + view + returns (uint256 trancheTokenAmount) + { trancheTokenAmount = uint256(investments[liquidityPool][user].remainingRedeemRequest); } diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index aa3a3e5b..9c97ef4b 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.21; import {Auth} from "./util/Auth.sol"; import {MathLib} from "./util/MathLib.sol"; +import {SafeTransferLib} from "./util/SafeTransferLib.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import {IERC4626} from "./interfaces/IERC4626.sol"; @@ -18,24 +19,24 @@ interface ManagerLike { function mint(address lp, uint256 shares, address receiver, address owner) external returns (uint256); function withdraw(address lp, uint256 assets, address receiver, address owner) external returns (uint256); function redeem(address lp, uint256 shares, address receiver, address owner) external returns (uint256); - function maxDeposit(address lp, address user) external view returns (uint256); - function maxMint(address lp, address user) external view returns (uint256); - function maxWithdraw(address lp, address user) external view returns (uint256); - function maxRedeem(address lp, address user) external view returns (uint256); + function maxDeposit(address lp, address receiver) external view returns (uint256); + function maxMint(address lp, address receiver) external view returns (uint256); + function maxWithdraw(address lp, address receiver) external view returns (uint256); + function maxRedeem(address lp, address receiver) external view returns (uint256); function convertToShares(address lp, uint256 assets) external view returns (uint256); function convertToAssets(address lp, uint256 shares) external view returns (uint256); - function previewDeposit(address lp, address user, uint256 assets) external view returns (uint256); - function previewMint(address lp, address user, uint256 shares) external view returns (uint256); - function previewWithdraw(address lp, address user, uint256 assets) external view returns (uint256); - function previewRedeem(address lp, address user, uint256 shares) external view returns (uint256); - function requestRedeem(address lp, uint256 shares, address receiver) external; - function requestDeposit(address lp, uint256 assets, address receiver) external; - function decreaseDepositRequest(address lp, uint256 assets, address receiver) external; - function decreaseRedeemRequest(address lp, uint256 shares, address receiver) external; - function cancelDepositRequest(address lp, address receiver) external; - function cancelRedeemRequest(address lp, address receiver) external; - function userDepositRequest(address lp, address user) external view returns (uint256); - function userRedeemRequest(address lp, address user) external view returns (uint256); + function previewDeposit(address lp, address operator, uint256 assets) external view returns (uint256); + function previewMint(address lp, address operator, uint256 shares) external view returns (uint256); + function previewWithdraw(address lp, address operator, uint256 assets) external view returns (uint256); + function previewRedeem(address lp, address operator, uint256 shares) external view returns (uint256); + function requestDeposit(address lp, uint256 assets, address sender, address operator) external returns (bool); + function requestRedeem(address lp, uint256 shares, address operator) external returns (bool); + function decreaseDepositRequest(address lp, uint256 assets, address operator) external; + function decreaseRedeemRequest(address lp, uint256 shares, address operator) external; + function cancelDepositRequest(address lp, address operator) external; + function cancelRedeemRequest(address lp, address operator) external; + function pendingDepositRequest(address lp, address operator) external view returns (uint256); + function pendingRedeemRequest(address lp, address operator) external view returns (uint256); } /// @title Liquidity Pool @@ -67,6 +68,9 @@ contract LiquidityPool is Auth, IERC4626 { /// @dev Also known as tranche tokens. TrancheTokenLike public immutable share; + /// @notice Escrow contract for tokens + address public immutable escrow; + /// @notice Liquidity Pool business logic implementation contract ManagerLike public manager; @@ -78,31 +82,26 @@ contract LiquidityPool is Auth, IERC4626 { // --- Events --- event File(bytes32 indexed what, address data); - event DepositRequest(address indexed owner, uint256 assets); - event RedeemRequest(address indexed owner, uint256 shares); - event DecreaseDepositRequest(address indexed owner, uint256 assets); - event DecreaseRedeemRequest(address indexed owner, uint256 shares); - event CancelDepositRequest(address indexed owner); - event CancelRedeemRequest(address indexed owner); + event DepositRequest(address indexed sender, address indexed operator, uint256 assets); + event RedeemRequest(address indexed sender, address indexed operator, address indexed owner, uint256 shares); + event DecreaseDepositRequest(address indexed sender, uint256 assets); + event DecreaseRedeemRequest(address indexed sender, uint256 shares); + event CancelDepositRequest(address indexed sender); + event CancelRedeemRequest(address indexed sender); event PriceUpdate(uint256 price); - constructor(uint64 poolId_, bytes16 trancheId_, address asset_, address share_, address manager_) { + constructor(uint64 poolId_, bytes16 trancheId_, address asset_, address share_, address escrow_, address manager_) { poolId = poolId_; trancheId = trancheId_; asset = asset_; share = TrancheTokenLike(share_); + escrow = escrow_; manager = ManagerLike(manager_); wards[msg.sender] = 1; emit Rely(msg.sender); } - /// @dev Owner needs to be the msg.sender - modifier withApproval(address owner) { - require((msg.sender == owner), "LiquidityPool/no-approval"); - _; - } - // --- Administration --- function file(bytes32 what, address data) public auth { if (what == "manager") manager = ManagerLike(data); @@ -178,12 +177,10 @@ contract LiquidityPool is Auth, IERC4626 { /// @notice Withdraw assets after successful epoch execution. Receiver will receive an exact amount of assets for /// a certain amount of shares that has been redeemed from Owner during epoch execution. + /// DOES NOT support owner != msg.sender since shares are already transferred on requestRedeem /// @return shares that have been redeemed for the exact assets amount - function withdraw(uint256 assets, address receiver, address owner) - public - withApproval(owner) - returns (uint256 shares) - { + function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares) { + require((msg.sender == owner), "LiquidityPool/not-the-owner"); shares = manager.withdraw(address(this), assets, receiver, owner); emit Withdraw(address(this), receiver, owner, assets, shares); } @@ -201,12 +198,10 @@ contract LiquidityPool is Auth, IERC4626 { /// @notice Redeem shares after successful epoch execution. Receiver will receive assets for /// @notice Redeem shares can only be called by the Owner or an authorized admin. /// the exact amount of redeemed shares from Owner after epoch execution. + /// DOES NOT support owner != msg.sender since shares are already transferred on requestRedeem /// @return assets payout for the exact amount of redeemed shares - function redeem(uint256 shares, address receiver, address owner) - public - withApproval(owner) - returns (uint256 assets) - { + function redeem(uint256 shares, address receiver, address owner) public returns (uint256 assets) { + require((msg.sender == owner), "LiquidityPool/not-the-owner"); assets = manager.redeem(address(this), shares, receiver, owner); emit Withdraw(address(this), receiver, owner, assets, shares); } @@ -215,23 +210,30 @@ contract LiquidityPool is Auth, IERC4626 { /// @notice Request asset deposit for a receiver to be included in the next epoch execution. /// @notice Request can only be called by the owner of the assets /// Asset is locked in the escrow on request submission - function requestDeposit(uint256 assets) public { - manager.requestDeposit(address(this), assets, msg.sender); - emit DepositRequest(msg.sender, assets); + function requestDeposit(uint256 assets, address operator) public { + require(IERC20(asset).balanceOf(msg.sender) >= assets, "LiquidityPool/insufficient-balance"); + require( + manager.requestDeposit(address(this), assets, msg.sender, operator), "LiquidityPool/request-deposit-failed" + ); + SafeTransferLib.safeTransferFrom(asset, msg.sender, address(escrow), assets); + emit DepositRequest(msg.sender, operator, assets); } /// @notice Similar to requestDeposit, but with a permit option function requestDepositWithPermit(uint256 assets, address owner, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { - _withPermit(asset, owner, address(manager), assets, deadline, v, r, s); - manager.requestDeposit(address(this), assets, owner); - emit DepositRequest(owner, assets); + _withPermit(asset, owner, address(this), assets, deadline, v, r, s); + require(manager.requestDeposit(address(this), assets, owner, owner), "LiquidityPool/request-deposit-failed"); + SafeTransferLib.safeTransferFrom(asset, owner, address(escrow), assets); + emit DepositRequest(owner, owner, assets); } - /// @notice View the total amount the user has requested to deposit but isn't able to deposit or mint yet - function userDepositRequest(address user) external view returns (uint256 assets) { - assets = manager.userDepositRequest(address(this), user); + /// @notice View the total amount the operator has requested to deposit but isn't able to deposit or mint yet + /// @dev Due to the asynchronous nature, this value might be outdated, and should only + /// be used for informational purposes. + function pendingDepositRequest(address operator) external view returns (uint256 assets) { + assets = manager.pendingDepositRequest(address(this), operator); } /// @notice Request decreasing the outstanding deposit orders. Will return the assets once the order @@ -249,11 +251,17 @@ contract LiquidityPool is Auth, IERC4626 { } /// @notice Request share redemption for a receiver to be included in the next epoch execution. - /// @notice Request can only be called by the owner of the shares + /// DOES support flow where owner != msg.sender but has allowance to spend its shares /// Shares are locked in the escrow on request submission - function requestRedeem(uint256 shares) public { - manager.requestRedeem(address(this), shares, msg.sender); - emit RedeemRequest(msg.sender, shares); + function requestRedeem(uint256 shares, address operator, address owner) public { + require(share.balanceOf(owner) >= shares, "LiquidityPool/insufficient-balance"); + require(manager.requestRedeem(address(this), shares, operator), "LiquidityPool/request-redeem-failed"); + + // This is possible because of the trusted forwarder pattern -> msg.sender is forwarded + // and the call can only be executed, if msg.sender has owner's approval to spend tokens + require(transferFrom(owner, address(escrow), shares), "LiquidityPool/transfer-failed"); + + emit RedeemRequest(msg.sender, operator, owner, shares); } /// @notice Request decreasing the outstanding redemption orders. Will return the shares once the order @@ -270,9 +278,11 @@ contract LiquidityPool is Auth, IERC4626 { emit CancelRedeemRequest(msg.sender); } - /// @notice View the total amount the user has requested to redeem but isn't able to withdraw or redeem yet - function userRedeemRequest(address user) external view returns (uint256 shares) { - shares = manager.userRedeemRequest(address(this), user); + /// @notice View the total amount the operator has requested to redeem but isn't able to withdraw or redeem yet + /// @dev Due to the asynchronous nature, this value might be outdated, and should only + /// be used for informational purposes. + function pendingRedeemRequest(address operator) external view returns (uint256 shares) { + shares = manager.pendingRedeemRequest(address(this), operator); } // --- ERC20 overrides --- @@ -300,19 +310,23 @@ contract LiquidityPool is Auth, IERC4626 { return share.allowance(owner, spender); } - function transferFrom(address, address, uint256) public returns (bool) { - (bool success, bytes memory data) = address(share).call(bytes.concat(msg.data, bytes20(msg.sender))); + function transferFrom(address from, address to, uint256 value) public returns (bool) { + (bool success, bytes memory data) = address(share).call( + bytes.concat( + abi.encodeWithSignature("transferFrom(address,address,uint256)", from, to, value), bytes20(msg.sender) + ) + ); _successCheck(success); return abi.decode(data, (bool)); } - function transfer(address, uint256) public returns (bool) { + function transfer(address, uint256) external returns (bool) { (bool success, bytes memory data) = address(share).call(bytes.concat(msg.data, bytes20(msg.sender))); _successCheck(success); return abi.decode(data, (bool)); } - function approve(address, uint256) public returns (bool) { + function approve(address, uint256) external returns (bool) { (bool success, bytes memory data) = address(share).call(bytes.concat(msg.data, bytes20(msg.sender))); _successCheck(success); return abi.decode(data, (bool)); diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 192f83ec..1c245135 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -146,15 +146,10 @@ contract PoolManager is Auth { uint128 currency = currencyAddressToId[currencyAddress]; require(currency != 0, "PoolManager/unknown-currency"); - // Transfer the currency amount from user to escrow (lock currency in escrow) - // Checks actual balance difference to support fee-on-transfer tokens - uint256 preBalance = IERC20(currencyAddress).balanceOf(address(escrow)); SafeTransferLib.safeTransferFrom(currencyAddress, msg.sender, address(escrow), amount); - uint256 postBalance = IERC20(currencyAddress).balanceOf(address(escrow)); - uint128 transferredAmount = (postBalance - preBalance).toUint128(); - gateway.transfer(currency, msg.sender, recipient, transferredAmount); - emit TransferCurrency(currencyAddress, recipient, transferredAmount); + gateway.transfer(currency, msg.sender, recipient, amount); + emit TransferCurrency(currencyAddress, recipient, amount); } function transferTrancheTokensToCentrifuge( @@ -382,7 +377,7 @@ contract PoolManager is Auth { // Deploy liquidity pool liquidityPool = liquidityPoolFactory.newLiquidityPool( - poolId, trancheId, currency, tranche.token, address(investmentManager), liquidityPoolWards + poolId, trancheId, currency, tranche.token, address(escrow), address(investmentManager), liquidityPoolWards ); tranche.liquidityPools[currency] = liquidityPool; diff --git a/src/util/Factory.sol b/src/util/Factory.sol index cb4773b5..09ceb6fc 100644 --- a/src/util/Factory.sol +++ b/src/util/Factory.sol @@ -16,6 +16,7 @@ interface LiquidityPoolFactoryLike { bytes16 trancheId, address currency, address trancheToken, + address escrow, address investmentManager, address[] calldata wards_ ) external returns (address); @@ -38,10 +39,12 @@ contract LiquidityPoolFactory is Auth { bytes16 trancheId, address currency, address trancheToken, + address escrow, address investmentManager, address[] calldata wards_ ) public auth returns (address) { - LiquidityPool liquidityPool = new LiquidityPool(poolId, trancheId, currency, trancheToken, investmentManager); + LiquidityPool liquidityPool = + new LiquidityPool(poolId, trancheId, currency, trancheToken, escrow, investmentManager); liquidityPool.rely(root); for (uint256 i = 0; i < wards_.length; i++) { diff --git a/test/Deploy.t.sol b/test/Deploy.t.sol index b4a02cd2..760a4725 100644 --- a/test/Deploy.t.sol +++ b/test/Deploy.t.sol @@ -84,8 +84,8 @@ contract DeployTest is Test, Deployer { } function depositMint(uint64 poolId, bytes16 trancheId, uint128 price, uint256 amount, LiquidityPool lPool) public { - erc20.approve(address(investmentManager), amount); // add allowance - lPool.requestDeposit(amount); + erc20.approve(address(lPool), amount); // add allowance + lPool.requestDeposit(amount, self); // ensure funds are locked in escrow assertEq(erc20.balanceOf(address(escrow)), amount); @@ -131,7 +131,7 @@ contract DeployTest is Test, Deployer { function redeemWithdraw(uint64 poolId, bytes16 trancheId, uint128 price, uint256 amount, LiquidityPool lPool) public { - lPool.requestRedeem(amount); + lPool.requestRedeem(amount, address(this), address(this)); // redeem uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 346f0f19..00d00280 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -84,11 +84,16 @@ contract LiquidityPoolTest is TestSetup { vm.expectRevert(bytes("MathLib/uint128-overflow")); lPool.redeem(amount, random_, self); + erc20.mint(address(this), amount); vm.expectRevert(bytes("MathLib/uint128-overflow")); - lPool.requestDeposit(amount); + lPool.requestDeposit(amount, self); + TrancheTokenLike trancheToken = TrancheTokenLike(address(lPool.share())); + centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, type(uint64).max); + root.relyContract(address(trancheToken), self); + trancheToken.mint(address(this), amount); vm.expectRevert(bytes("MathLib/uint128-overflow")); - lPool.requestRedeem(amount); + lPool.requestRedeem(amount, address(this), address(this)); vm.expectRevert(bytes("MathLib/uint128-overflow")); lPool.decreaseDepositRequest(amount); @@ -110,14 +115,7 @@ contract LiquidityPoolTest is TestSetup { // investor can requestRedeem vm.prank(investor); - lPool.requestRedeem(amount); - - // fail: ward can not requestRedeem if investment manager has no auth on the tranche token - root.denyContract(address(lPool.share()), address(investmentManager)); - vm.prank(investor); - vm.expectRevert(bytes("Auth/not-authorized")); - lPool.requestRedeem(amount); - root.relyContract(address(lPool.share()), address(investmentManager)); + lPool.requestRedeem(amount, investor, investor); uint128 tokenAmount = uint128(lPool.balanceOf(address(escrow))); centrifugeChain.isExecutedCollectRedeem( @@ -136,16 +134,16 @@ contract LiquidityPoolTest is TestSetup { // test for both scenarios redeem & withdraw // fail: self cannot redeem for investor - vm.expectRevert(bytes("LiquidityPool/no-approval")); + vm.expectRevert(bytes("LiquidityPool/not-the-owner")); lPool.redeem(redemption1, investor, investor); - vm.expectRevert(bytes("LiquidityPool/no-approval")); + vm.expectRevert(bytes("LiquidityPool/not-the-owner")); lPool.withdraw(redemption1, investor, investor); // fail: ward can not make requests on behalf of investor root.relyContract(lPool_, self); - vm.expectRevert(bytes("LiquidityPool/no-approval")); + vm.expectRevert(bytes("LiquidityPool/not-the-owner")); lPool.redeem(redemption1, investor, investor); - vm.expectRevert(bytes("LiquidityPool/no-approval")); + vm.expectRevert(bytes("LiquidityPool/not-the-owner")); lPool.withdraw(redemption1, investor, investor); // investor redeems rest for himself @@ -355,9 +353,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000; // 100 * 10**6 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); // trigger executed collectInvest of the first 50% at a price of 1.2 uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId @@ -393,7 +391,7 @@ contract LiquidityPoolTest is TestSetup { assertEq(lPool.balanceOf(self), firstTrancheTokenPayout + secondTrancheTokenPayout); // redeem - lPool.requestRedeem(firstTrancheTokenPayout + secondTrancheTokenPayout); + lPool.requestRedeem(firstTrancheTokenPayout + secondTrancheTokenPayout, address(this), address(this)); // trigger executed collectRedeem at a price of 1.5 // 50% invested at 1.2 and 50% invested at 1.4 leads to ~77 tranche tokens @@ -438,9 +436,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000000000000000; // 100 * 10**18 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); // trigger executed collectInvest of the first 50% at a price of 1.2 uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId @@ -476,7 +474,7 @@ contract LiquidityPoolTest is TestSetup { assertEq(lPool.balanceOf(self), firstTrancheTokenPayout + secondTrancheTokenPayout); // redeem - lPool.requestRedeem(firstTrancheTokenPayout + secondTrancheTokenPayout); + lPool.requestRedeem(firstTrancheTokenPayout + secondTrancheTokenPayout, address(this), address(this)); // trigger executed collectRedeem at a price of 1.5 // 50% invested at 1.2 and 50% invested at 1.4 leads to ~77 tranche tokens @@ -523,9 +521,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000; // 100 * 10**6 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); // trigger executed collectInvest at a tranche token price of 1.2 uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId @@ -573,9 +571,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000000000000000; // 100 * 10**18 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); // trigger executed collectInvest at a tranche token price of 1.2 uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId @@ -615,9 +613,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000; // 100 * 10**6 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); // trigger executed collectInvest at a price of 1.0 uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId @@ -658,9 +656,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000000000000000; // 100 * 10**18 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); // trigger executed collectInvest at a price of 1.0 uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId @@ -694,10 +692,10 @@ contract LiquidityPoolTest is TestSetup { LiquidityPool lPool = LiquidityPool(lPool_); centrifugeChain.updateTrancheTokenPrice(lPool.poolId(), lPool.trancheId(), defaultCurrencyId, price); erc20.mint(self, amount); - erc20.approve(address(investmentManager), amount); // add allowance + erc20.approve(lPool_, amount); // add allowance centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, type(uint64).max); - lPool.requestDeposit(amount); + lPool.requestDeposit(amount, self); assertEq(erc20.balanceOf(address(escrow)), amount); assertEq(erc20.balanceOf(address(self)), 0); @@ -733,26 +731,27 @@ contract LiquidityPoolTest is TestSetup { erc20.mint(self, amount); // will fail - user not member: can not receive trancheToken - vm.expectRevert(bytes("InvestmentManager/transfer-not-allowed")); - lPool.requestDeposit(amount); + vm.expectRevert(bytes("InvestmentManager/sender-is-restricted")); + lPool.requestDeposit(amount, self); + centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, type(uint64).max); // add user as member - // // will fail - user did not give currency allowance to investmentManager + // will fail - user did not give currency allowance to liquidity pool vm.expectRevert(bytes("SafeTransferLib/safe-transfer-from-failed")); - lPool.requestDeposit(amount); + lPool.requestDeposit(amount, self); // success - erc20.approve(address(investmentManager), amount); // add allowance - lPool.requestDeposit(amount); + erc20.approve(lPool_, amount); + lPool.requestDeposit(amount, self); // fail: no currency left - vm.expectRevert(bytes("SafeTransferLib/safe-transfer-from-failed")); - lPool.requestDeposit(amount); + vm.expectRevert(bytes("LiquidityPool/insufficient-balance")); + lPool.requestDeposit(amount, self); // ensure funds are locked in escrow assertEq(erc20.balanceOf(address(escrow)), amount); assertEq(erc20.balanceOf(self), 0); - assertEq(lPool.userDepositRequest(self), amount); + assertEq(lPool.pendingDepositRequest(self), amount); // trigger executed collectInvest uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId @@ -771,7 +770,7 @@ contract LiquidityPoolTest is TestSetup { // assert deposit & mint values adjusted assertEq(lPool.maxMint(self), trancheTokensPayout); assertApproxEqAbs(lPool.maxDeposit(self), amount, 1); - assertEq(lPool.userDepositRequest(self), 0); + assertEq(lPool.pendingDepositRequest(self), 0); // assert tranche tokens minted assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); // assert conversions @@ -811,8 +810,8 @@ contract LiquidityPoolTest is TestSetup { // fund user & request deposit centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, uint64(block.timestamp)); erc20.mint(self, totalAmount); - erc20.approve(address(investmentManager), totalAmount); - lPool.requestDeposit(totalAmount); + erc20.approve(address(lPool), totalAmount); + lPool.requestDeposit(totalAmount, self); // Ensure funds were locked in escrow assertEq(erc20.balanceOf(address(escrow)), totalAmount); @@ -868,8 +867,8 @@ contract LiquidityPoolTest is TestSetup { // fund user & request deposit centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, uint64(block.timestamp)); erc20.mint(self, totalAmount); - erc20.approve(address(investmentManager), totalAmount); - lPool.requestDeposit(totalAmount); + erc20.approve(address(lPool), totalAmount); + lPool.requestDeposit(totalAmount, self); // Ensure funds were locked in escrow assertEq(erc20.balanceOf(address(escrow)), totalAmount); @@ -916,8 +915,8 @@ contract LiquidityPoolTest is TestSetup { erc20.mint(self, amount); centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, type(uint64).max); // add user as member - erc20.approve(address(investmentManager), amount); // add allowance - lPool.requestDeposit(amount); + erc20.approve(lPool_, amount); // add allowance + lPool.requestDeposit(amount, self); // trigger executed collectInvest uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId @@ -983,7 +982,7 @@ contract LiquidityPoolTest is TestSetup { erc20.DOMAIN_SEPARATOR(), keccak256( abi.encode( - erc20.PERMIT_TYPEHASH(), investor, address(investmentManager), amount, 0, block.timestamp + erc20.PERMIT_TYPEHASH(), investor, lPool_, amount, 0, block.timestamp ) ) ) @@ -991,7 +990,7 @@ contract LiquidityPoolTest is TestSetup { ); vm.prank(random_); // random fr permit - erc20.permit(investor, address(investmentManager), amount, block.timestamp, v, r, s); + erc20.permit(investor, lPool_, amount, block.timestamp, v, r, s); // investor still able to requestDepositWithPermit lPool.requestDepositWithPermit(amount, investor, block.timestamp, v, r, s); @@ -1022,7 +1021,7 @@ contract LiquidityPoolTest is TestSetup { erc20.DOMAIN_SEPARATOR(), keccak256( abi.encode( - erc20.PERMIT_TYPEHASH(), investor, address(investmentManager), amount, 0, block.timestamp + erc20.PERMIT_TYPEHASH(), investor, lPool_, amount, 0, block.timestamp ) ) ) @@ -1067,13 +1066,13 @@ contract LiquidityPoolTest is TestSetup { centrifugeChain.updateTrancheTokenPrice(lPool.poolId(), lPool.trancheId(), defaultCurrencyId, defaultPrice); // success - lPool.requestRedeem(amount); + lPool.requestRedeem(amount, address(this), address(this)); assertEq(lPool.balanceOf(address(escrow)), amount); - assertEq(lPool.userRedeemRequest(self), amount); + assertEq(lPool.pendingRedeemRequest(self), amount); // fail: no tokens left - vm.expectRevert(bytes("ERC20/insufficient-balance")); - lPool.requestRedeem(amount); + vm.expectRevert(bytes("LiquidityPool/insufficient-balance")); + lPool.requestRedeem(amount, address(this), address(this)); // trigger executed collectRedeem uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId @@ -1085,7 +1084,7 @@ contract LiquidityPoolTest is TestSetup { // assert withdraw & redeem values adjusted assertEq(lPool.maxWithdraw(self), currencyPayout); // max deposit assertEq(lPool.maxRedeem(self), amount); // max deposit - assertEq(lPool.userRedeemRequest(self), 0); + assertEq(lPool.pendingRedeemRequest(self), 0); assertEq(lPool.balanceOf(address(escrow)), 0); assertEq(erc20.balanceOf(address(userEscrow)), currencyPayout); // assert conversions @@ -1125,7 +1124,7 @@ contract LiquidityPoolTest is TestSetup { LiquidityPool lPool = LiquidityPool(lPool_); deposit(lPool_, self, amount); // deposit funds first - lPool.requestRedeem(amount); + lPool.requestRedeem(amount, address(this), address(this)); assertEq(lPool.balanceOf(address(escrow)), amount); assertEq(lPool.balanceOf(self), 0); @@ -1155,14 +1154,10 @@ contract LiquidityPoolTest is TestSetup { deposit(lPool_, self, amount); // deposit funds first centrifugeChain.updateTrancheTokenPrice(lPool.poolId(), lPool.trancheId(), defaultCurrencyId, defaultPrice); - // will fail - user did not give tranche token allowance to investmentManager - vm.expectRevert(bytes("SafeTransferLib/safe-transfer-from-failed")); - lPool.requestDeposit(amount); - - lPool.requestRedeem(amount); + lPool.requestRedeem(amount, address(this), address(this)); assertEq(lPool.balanceOf(address(escrow)), amount); assertEq(erc20.balanceOf(address(userEscrow)), 0); - assertGt(lPool.userRedeemRequest(self), 0); + assertGt(lPool.pendingRedeemRequest(self), 0); // trigger executed collectRedeem uint128 _currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId @@ -1211,8 +1206,8 @@ contract LiquidityPoolTest is TestSetup { erc20.mint(self, amount); centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), self, type(uint64).max); // add user as member - erc20.approve(address(investmentManager), amount); // add allowance - lPool.requestDeposit(amount); + erc20.approve(lPool_, amount); // add allowance + lPool.requestDeposit(amount, self); assertEq(erc20.balanceOf(address(escrow)), amount); assertEq(erc20.balanceOf(self), 0); @@ -1238,7 +1233,7 @@ contract LiquidityPoolTest is TestSetup { LiquidityPool lPool = LiquidityPool(lPool_); centrifugeChain.updateTrancheTokenPrice(lPool.poolId(), lPool.trancheId(), defaultCurrencyId, defaultPrice); deposit(lPool_, self, amount); - lPool.requestRedeem(amount); + lPool.requestRedeem(amount, address(this), address(this)); assertEq(lPool.balanceOf(address(escrow)), amount); assertEq(lPool.balanceOf(self), 0); @@ -1304,9 +1299,9 @@ contract LiquidityPoolTest is TestSetup { // invest uint256 investmentAmount = 100000000; // 100 * 10**6 centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); - currency.approve(address(investmentManager), investmentAmount); + currency.approve(lPool_, investmentAmount); currency.mint(self, investmentAmount); - lPool.requestDeposit(investmentAmount); + lPool.requestDeposit(investmentAmount, self); uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId // first trigger executed collectInvest of the first 50% at a price of 1.4 @@ -1349,10 +1344,10 @@ contract LiquidityPoolTest is TestSetup { erc20.mint(investor, amount); centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), investor, type(uint64).max); // add user as member vm.prank(investor); - erc20.approve(address(investmentManager), amount); // add allowance + erc20.approve(_lPool, amount); // add allowance vm.prank(investor); - lPool.requestDeposit(amount); + lPool.requestDeposit(amount, investor); // trigger executed collectInvest uint128 currencyId = poolManager.currencyAddressToId(address(erc20)); // retrieve currencyId centrifugeChain.isExecutedCollectInvest( From ac440b05de1ba41c99d4459eff5b8721ce495cb4 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 17 Oct 2023 07:43:40 -0400 Subject: [PATCH 32/51] disallow freezing the zero address (#181) --- src/token/RestrictionManager.sol | 1 + test/token/RestrictionManager.t.sol | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/token/RestrictionManager.sol b/src/token/RestrictionManager.sol index bc8b6b49..0da5511e 100644 --- a/src/token/RestrictionManager.sol +++ b/src/token/RestrictionManager.sol @@ -71,6 +71,7 @@ contract RestrictionManager is Auth { // --- Handling freezes --- function freeze(address user) public auth { + require(user != address(0), "RestrictionManager/cannot-freeze-zero-address"); frozen[user] = 1; emit Freeze(user); } diff --git a/test/token/RestrictionManager.t.sol b/test/token/RestrictionManager.t.sol index aece12da..704eb205 100644 --- a/test/token/RestrictionManager.t.sol +++ b/test/token/RestrictionManager.t.sol @@ -27,4 +27,15 @@ contract RestrictionManagerTest is Test { restrictionManager.updateMember(address(this), validUntil); assert(restrictionManager.hasMember(address(this))); } + + function testFreeze() public { + restrictionManager.freeze(address(this)); + assertEq(restrictionManager.frozen(address(this)), 1); + } + + function testFreezingZeroAddress() public { + vm.expectRevert("RestrictionManager/cannot-freeze-zero-address"); + restrictionManager.freeze(address(0)); + assertEq(restrictionManager.frozen(address(0)), 0); + } } From ade49527ff48373a9fa845987d73cb26fb8c2d25 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 17 Oct 2023 09:25:19 -0400 Subject: [PATCH 33/51] Make restrictionManagerFactory mutable on PoolManager --- src/PoolManager.sol | 3 ++- test/PoolManager.t.sol | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 1c245135..6181cff8 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -78,8 +78,8 @@ contract PoolManager is Auth { EscrowLike public immutable escrow; LiquidityPoolFactoryLike public immutable liquidityPoolFactory; - RestrictionManagerFactoryLike public immutable restrictionManagerFactory; TrancheTokenFactoryLike public immutable trancheTokenFactory; + RestrictionManagerFactoryLike public restrictionManagerFactory; GatewayLike public gateway; InvestmentManagerLike public investmentManager; @@ -137,6 +137,7 @@ contract PoolManager is Auth { function file(bytes32 what, address data) external auth { if (what == "gateway") gateway = GatewayLike(data); else if (what == "investmentManager") investmentManager = InvestmentManagerLike(data); + else if (what == "restrictionManagerFactory") restrictionManagerFactory = RestrictionManagerFactoryLike(data); else revert("PoolManager/file-unrecognized-param"); emit File(what, data); } diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index cbc2e954..58ccfcde 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -4,6 +4,25 @@ pragma solidity 0.8.21; import "./TestSetup.t.sol"; contract PoolManagerTest is TestSetup { + + function testFile() public { + address newGateway = makeAddr("newGateway"); + poolManager.file("gateway", newGateway); + assertEq(address(poolManager.gateway()), newGateway); + + address newInvestmentManager = makeAddr("newInvestmentManager"); + poolManager.file("investmentManager", newInvestmentManager); + assertEq(address(poolManager.investmentManager()), newInvestmentManager); + + address newRestrictionManagerFactory = makeAddr("newRestrictionManagerFactory"); + poolManager.file("restrictionManagerFactory", newRestrictionManagerFactory); + assertEq(address(poolManager.restrictionManagerFactory()), newRestrictionManagerFactory); + + address newEscrow = makeAddr("newEscrow"); + vm.expectRevert("PoolManager/file-unrecognized-param"); + poolManager.file("escrow", newEscrow); + } + // Deployment function testDeployment() public { // values set correctly From 100d4a9c29fc9f9b85dfdaa5f24a11704c7bb5c6 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 17 Oct 2023 09:29:11 -0400 Subject: [PATCH 34/51] group RestrictionManagerFactory with other public vars --- src/PoolManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 6181cff8..6e222f61 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -79,8 +79,8 @@ contract PoolManager is Auth { EscrowLike public immutable escrow; LiquidityPoolFactoryLike public immutable liquidityPoolFactory; TrancheTokenFactoryLike public immutable trancheTokenFactory; - RestrictionManagerFactoryLike public restrictionManagerFactory; + RestrictionManagerFactoryLike public restrictionManagerFactory; GatewayLike public gateway; InvestmentManagerLike public investmentManager; From fa2ce2ab7e386e12214f27a73c2074a9c03920f1 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 17 Oct 2023 11:42:59 -0400 Subject: [PATCH 35/51] fix broken tests after merge --- test/migrations/InvestRedeemFlow.t.sol | 6 +++--- test/migrations/MigratedLiquidityPool.t.sol | 2 +- .../migrationContracts/MigratedLiquidityPool.sol | 11 ++++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/test/migrations/InvestRedeemFlow.t.sol b/test/migrations/InvestRedeemFlow.t.sol index db1da884..cb68b67e 100644 --- a/test/migrations/InvestRedeemFlow.t.sol +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -47,10 +47,10 @@ contract InvestRedeemFlow is TestSetup { public { vm.prank(investor); - erc20.approve(address(investmentManager), currencyAmount); // add allowance + erc20.approve(address(lPool), currencyAmount); // add allowance vm.prank(investor); - lPool.requestDeposit(currencyAmount); + lPool.requestDeposit(currencyAmount, investor); // ensure funds are locked in escrow assertEq(erc20.balanceOf(address(escrow)), currencyAmount); @@ -92,7 +92,7 @@ contract InvestRedeemFlow is TestSetup { public { vm.prank(investor); - lPool.requestRedeem(tokenAmount); + lPool.requestRedeem(tokenAmount, investor, investor); // Assume an epoch execution happens on cent chain // Assume a bot calls collectRedeem for this user on cent chain diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index 3292c5ac..a3b4b187 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -35,7 +35,7 @@ contract MigrationsTest is InvestRedeemFlow { // Deploy new liquidity pool MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( - poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(investmentManager) + poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(escrow), address(investmentManager) ); // set MigratedPoolManager input parameters diff --git a/test/migrations/migrationContracts/MigratedLiquidityPool.sol b/test/migrations/migrationContracts/MigratedLiquidityPool.sol index 9e0509a1..28aa0ee4 100644 --- a/test/migrations/migrationContracts/MigratedLiquidityPool.sol +++ b/test/migrations/migrationContracts/MigratedLiquidityPool.sol @@ -4,9 +4,14 @@ pragma solidity 0.8.21; import "src/LiquidityPool.sol"; contract MigratedLiquidityPool is LiquidityPool { - constructor(uint64 poolId_, bytes16 trancheId_, address asset_, address share_, address investmentManager_) - LiquidityPool(poolId_, trancheId_, asset_, share_, investmentManager_) - { + constructor( + uint64 poolId_, + bytes16 trancheId_, + address asset_, + address share_, + address escrow_, + address investmentManager_ + ) LiquidityPool(poolId_, trancheId_, asset_, share_, escrow_, investmentManager_) { // TODO: migrate state, revoke approvals } } From b4b11d22e23c2497388c6f9cddfb4a0d71b0ccfa Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 17 Oct 2023 12:48:34 -0400 Subject: [PATCH 36/51] add migratedFactory contract --- .../migrationContracts/MigratedFactory.sol | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/migrations/migrationContracts/MigratedFactory.sol diff --git a/test/migrations/migrationContracts/MigratedFactory.sol b/test/migrations/migrationContracts/MigratedFactory.sol new file mode 100644 index 00000000..77e1a354 --- /dev/null +++ b/test/migrations/migrationContracts/MigratedFactory.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {MigratedLiquidityPool} from "./MigratedLiquidityPool.sol"; +import {MigratedRestrictionManager} from "./MigratedRestrictionManager.sol"; +import {Auth} from "./Auth.sol"; + +interface RootLike { + function escrow() external view returns (address); +} + +contract MigratedLiquidityPoolFactory is Auth { + address public immutable root; + + constructor(address _root) { + root = _root; + + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + function newLiquidityPool( + uint64 poolId, + bytes16 trancheId, + address currency, + address trancheToken, + address escrow, + address investmentManager, + address[] calldata wards_ + ) public auth returns (address) { + LiquidityPool liquidityPool = + new MigratedLiquidityPool(poolId, trancheId, currency, trancheToken, escrow, investmentManager); + + liquidityPool.rely(root); + for (uint256 i = 0; i < wards_.length; i++) { + liquidityPool.rely(wards_[i]); + } + liquidityPool.deny(address(this)); + return address(liquidityPool); + } +} + +contract MigratedRestrictionManagerFactory is Auth { + address immutable root; + + constructor(address _root) { + root = _root; + + wards[msg.sender] = 1; + emit Rely(msg.sender); + } + + function newRestrictionManager(address token, address[] calldata restrictionManagerWards) + public + auth + returns (address) + { + RestrictionManager restrictionManager = new MigratedRestrictionManager(token); + + restrictionManager.updateMember(RootLike(root).escrow(), type(uint256).max); + + restrictionManager.rely(root); + for (uint256 i = 0; i < restrictionManagerWards.length; i++) { + restrictionManager.rely(restrictionManagerWards[i]); + } + restrictionManager.deny(address(this)); + + return (address(restrictionManager)); + } +} From d79708346395fa70a082c2fe77120d3cf48481b8 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Tue, 17 Oct 2023 15:44:17 -0400 Subject: [PATCH 37/51] clean up, make tests easier to review --- test/migrations/MigratedAdmin.t.sol | 5 +- test/migrations/MigratedGateway.t.sol | 11 +-- .../MigratedInvestmentManager.t.sol | 35 +++++----- test/migrations/MigratedLiquidityPool.t.sol | 42 ++++++----- test/migrations/MigratedPoolManager.t.sol | 28 ++++---- .../MigratedRestrictionManager.t.sol | 59 +++++++++++----- .../migrationContracts/MigratedFactory.sol | 70 ------------------- 7 files changed, 112 insertions(+), 138 deletions(-) delete mode 100644 test/migrations/migrationContracts/MigratedFactory.sol diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol index 58c764e2..160bcdea 100644 --- a/test/migrations/MigratedAdmin.t.sol +++ b/test/migrations/MigratedAdmin.t.sol @@ -47,14 +47,17 @@ contract MigratedAdmin is InvestRedeemFlow { PauseAdmin oldPauseAdmin, PauseAdmin newPauseAdmin ) public { + // verify permissions assertTrue(address(oldDelayedAdmin) != address(newDelayedAdmin)); assertTrue(address(oldPauseAdmin) != address(newPauseAdmin)); - assertEq(address(newDelayedAdmin.pauseAdmin()), 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 index 4cb15a42..66f48e06 100644 --- a/test/migrations/MigratedGateway.t.sol +++ b/test/migrations/MigratedGateway.t.sol @@ -44,13 +44,16 @@ contract MigratedGatewayTest is InvestRedeemFlow { } function verifyMigratedGatewayPermissions(Gateway oldGateway, Gateway newGateway) public { + // verify permissions assertTrue(address(oldGateway) != address(newGateway)); - assertEq(address(oldGateway.investmentManager()), address(newGateway.investmentManager())); - assertEq(address(oldGateway.poolManager()), address(newGateway.poolManager())); - assertEq(address(oldGateway.root()), address(newGateway.root())); + assertEq(newGateway.wards(address(root)), 1); assertEq(address(investmentManager.gateway()), address(newGateway)); assertEq(address(poolManager.gateway()), address(newGateway)); assertEq(address(router.gateway()), address(newGateway)); - assertEq(newGateway.wards(address(root)), 1); + + // 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 index c2b0c7d4..7b0fb923 100644 --- a/test/migrations/MigratedInvestmentManager.t.sol +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -37,10 +37,8 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { // Rewire contracts root.relyContract(address(gateway), address(this)); gateway.file("investmentManager", address(newInvestmentManager)); - newInvestmentManager.file("poolManager", address(poolManager)); root.relyContract(address(poolManager), address(this)); poolManager.file("investmentManager", address(newInvestmentManager)); - newInvestmentManager.file("gateway", address(gateway)); newInvestmentManager.rely(address(root)); newInvestmentManager.rely(address(poolManager)); root.relyContract(address(escrow), address(this)); @@ -49,21 +47,23 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { 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(address(liquidityPools[i]), address(this)); + root.relyContract(liquidityPools[i], address(this)); LiquidityPool lPool = LiquidityPool(liquidityPools[i]); - lPool.file("manager", address(newInvestmentManager)); - lPool.rely(address(newInvestmentManager)); - lPool.deny(address(investmentManager)); - root.relyContract(address(lPool.share()), address(this)); - AuthLike(address(lPool.share())).rely(address(newInvestmentManager)); - AuthLike(address(lPool.share())).deny(address(investmentManager)); - newInvestmentManager.rely(address(lPool)); - escrow.approve(address(lPool), address(newInvestmentManager), type(uint256).max); - escrow.approve(address(lPool), address(investmentManager), 0); + 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 @@ -85,11 +85,8 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { InvestmentManager oldInvestmentManager, InvestmentManager newInvestmentManager ) public { + // Verify permissions assertTrue(address(oldInvestmentManager) != address(newInvestmentManager)); - 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())); assertEq(address(gateway.investmentManager()), address(newInvestmentManager)); assertEq(address(poolManager.investmentManager()), address(newInvestmentManager)); assertEq(newInvestmentManager.wards(address(root)), 1); @@ -98,6 +95,12 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { 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 --- diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index a3b4b187..3a550cc2 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -33,6 +33,12 @@ contract MigrationsTest is InvestRedeemFlow { 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)); + // Deploy new liquidity pool MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(escrow), address(investmentManager) @@ -59,7 +65,7 @@ contract MigrationsTest is InvestRedeemFlow { // Deploy new MigratedPoolManager MigratedPoolManager newPoolManager = new MigratedPoolManager( address(escrow), - liquidityPoolFactory, + address(newLiquidityPoolFactory), restrictionManagerFactory, trancheTokenFactory, address(poolManager), @@ -71,6 +77,7 @@ contract MigrationsTest is InvestRedeemFlow { ); // rewire migrated pool manager + newLiquidityPoolFactory.rely(address(newPoolManager)); LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); root.relyContract(address(gateway), address(this)); @@ -98,19 +105,19 @@ contract MigrationsTest is InvestRedeemFlow { TrancheTokenLike token = TrancheTokenLike(address(LiquidityPool(_lPool).share())); root.relyContract(address(token), address(this)); token.rely(address(newLiquidityPool)); + token.deny(_lPool); token.addTrustedForwarder(address(newLiquidityPool)); + token.removeTrustedForwarder(_lPool); investmentManager.rely(address(newLiquidityPool)); + investmentManager.deny(_lPool); newLiquidityPool.rely(address(root)); newLiquidityPool.rely(address(investmentManager)); - escrow.approve(address(token), address(investmentManager), type(uint256).max); + // escrow.approve(address(token), address(investmentManager), type(uint256).max); escrow.approve(address(token), address(newLiquidityPool), type(uint256).max); + escrow.approve(address(token), _lPool, 0); // clean up new liquidity pool - investmentManager.deny(_lPool); - escrow.approve(address(token), _lPool, 0); newLiquidityPool.deny(address(this)); - token.deny(_lPool); - token.removeTrustedForwarder(_lPool); root.denyContract(address(token), address(this)); root.denyContract(address(investmentManager), address(this)); root.denyContract(address(escrow), address(this)); @@ -128,23 +135,26 @@ contract MigrationsTest is InvestRedeemFlow { // --- Permissions & Dependencies Checks --- function verifyLiquidityPoolPermissions(LiquidityPool oldLiquidityPool, LiquidityPool newLiquidityPool) public { + // verify permissions assertTrue(address(oldLiquidityPool) != address(newLiquidityPool)); - assertEq(oldLiquidityPool.poolId(), newLiquidityPool.poolId()); - assertEq(oldLiquidityPool.trancheId(), newLiquidityPool.trancheId()); - assertEq(address(oldLiquidityPool.asset()), address(newLiquidityPool.asset())); - assertEq(address(oldLiquidityPool.share()), address(newLiquidityPool.share())); address token = poolManager.getTrancheToken(poolId, trancheId); - assertEq(address(newLiquidityPool.share()), token); - assertEq(TrancheTokenLike(token).trustedForwarders(address(oldLiquidityPool)), false); - assertEq(TrancheTokenLike(token).trustedForwarders(address(newLiquidityPool)), true); assertEq(TrancheTokenLike(token).wards(address(oldLiquidityPool)), 0); assertEq(TrancheTokenLike(token).wards(address(newLiquidityPool)), 1); - assertEq(address(oldLiquidityPool.manager()), address(newLiquidityPool.manager())); - assertEq(newLiquidityPool.wards(address(root)), 1); - assertEq(newLiquidityPool.wards(address(investmentManager)), 1); + assertEq(TrancheTokenLike(token).trustedForwarders(address(oldLiquidityPool)), false); + assertEq(TrancheTokenLike(token).trustedForwarders(address(newLiquidityPool)), true); 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 index 0c1c94fc..d0b665de 100644 --- a/test/migrations/MigratedPoolManager.t.sol +++ b/test/migrations/MigratedPoolManager.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.21; import {MigratedPoolManager, PoolManager} from "./migrationContracts/MigratedPoolManager.sol"; -import {LiquidityPoolFactory, TrancheTokenFactory} from "src/util/Factory.sol"; +import {TrancheTokenFactory, LiquidityPoolFactory, RestrictionManagerFactory} from "src/util/Factory.sol"; import {InvestRedeemFlow} from "./InvestRedeemFlow.t.sol"; interface AuthLike { @@ -64,6 +64,7 @@ contract MigrationsTest is InvestRedeemFlow { // 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)); @@ -104,24 +105,27 @@ contract MigrationsTest is InvestRedeemFlow { } function verifyMigratedPoolManagerPermissions(PoolManager oldPoolManager, PoolManager newPoolManager) public { + // verify permissions assertTrue(address(oldPoolManager) != address(newPoolManager)); - 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())); - assertEq(address(oldPoolManager.investmentManager()), address(newPoolManager.investmentManager())); - assertEq(address(oldPoolManager.gateway()), address(newPoolManager.gateway())); + 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(newPoolManager.wards(address(root)), 1); + 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); - assertEq(investmentManager.wards(address(newPoolManager)), 1); - assertEq(investmentManager.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 --- diff --git a/test/migrations/MigratedRestrictionManager.t.sol b/test/migrations/MigratedRestrictionManager.t.sol index 59a03e27..3cdafb18 100644 --- a/test/migrations/MigratedRestrictionManager.t.sol +++ b/test/migrations/MigratedRestrictionManager.t.sol @@ -2,6 +2,7 @@ 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"; @@ -24,28 +25,43 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { address[] memory restrictionManagerWards = new address[](1); restrictionManagerWards[0] = address(poolManager); address token = address(LiquidityPool(_lPool).share()); + RestrictionManager oldRestrictionManager = RestrictionManager(TrancheTokenLike(token).restrictionManager()); - // Deploy new Gateway - MigratedRestrictionManager newRestrictionManager = new MigratedRestrictionManager(token); + // Deploy new RestrictionManagerFactory + RestrictionManagerFactory newRestrictionManagerFactory = new RestrictionManagerFactory(address(root)); - RestrictionManager oldRestrictionManager = RestrictionManager(TrancheTokenLike(token).restrictionManager()); + // rewire factory contracts + root.relyContract(address(poolManager), address(this)); + poolManager.file("restrictionManagerFactory", address(newRestrictionManagerFactory)); + newRestrictionManagerFactory.rely(address(poolManager)); + newRestrictionManagerFactory.rely(address(root)); - // Rewire contracts - root.relyContract(token, address(this)); - TrancheTokenLike(token).file("restrictionManager", address(newRestrictionManager)); - newRestrictionManager.updateMember(address(escrow), type(uint256).max); - newRestrictionManager.rely(address(root)); - for (uint256 i = 0; i < restrictionManagerWards.length; i++) { - newRestrictionManager.rely(restrictionManagerWards[i]); - } + // Collect all tranche tokens + // assume these records are available off-chain + address[] memory trancheTokens = new address[](1); + trancheTokens[0] = token; - // clean up - newRestrictionManager.deny(address(this)); - root.denyContract(token, address(this)); - root.deny(address(this)); + // Deploy new RestrictionManager for each tranche token + for (uint256 i = 0; i < trancheTokens.length; i++) { + MigratedRestrictionManager newRestrictionManager = new MigratedRestrictionManager(token); - // verify permissions - verifyMigratedRestrictionManagerPermissions(oldRestrictionManager, newRestrictionManager); + // 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; @@ -54,12 +70,17 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { function verifyMigratedRestrictionManagerPermissions( RestrictionManager oldRestrictionManager, - RestrictionManager newRestrictionManager + MigratedRestrictionManager newRestrictionManager ) internal { + // verify permissions + TrancheTokenLike token = TrancheTokenLike(address(oldRestrictionManager.token())); + assertEq(TrancheTokenLike(token).restrictionManager(), address(newRestrictionManager)); assertTrue(address(oldRestrictionManager) != address(newRestrictionManager)); - assertTrue(oldRestrictionManager.token() == newRestrictionManager.token()); 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/MigratedFactory.sol b/test/migrations/migrationContracts/MigratedFactory.sol deleted file mode 100644 index 77e1a354..00000000 --- a/test/migrations/migrationContracts/MigratedFactory.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.21; - -import {MigratedLiquidityPool} from "./MigratedLiquidityPool.sol"; -import {MigratedRestrictionManager} from "./MigratedRestrictionManager.sol"; -import {Auth} from "./Auth.sol"; - -interface RootLike { - function escrow() external view returns (address); -} - -contract MigratedLiquidityPoolFactory is Auth { - address public immutable root; - - constructor(address _root) { - root = _root; - - wards[msg.sender] = 1; - emit Rely(msg.sender); - } - - function newLiquidityPool( - uint64 poolId, - bytes16 trancheId, - address currency, - address trancheToken, - address escrow, - address investmentManager, - address[] calldata wards_ - ) public auth returns (address) { - LiquidityPool liquidityPool = - new MigratedLiquidityPool(poolId, trancheId, currency, trancheToken, escrow, investmentManager); - - liquidityPool.rely(root); - for (uint256 i = 0; i < wards_.length; i++) { - liquidityPool.rely(wards_[i]); - } - liquidityPool.deny(address(this)); - return address(liquidityPool); - } -} - -contract MigratedRestrictionManagerFactory is Auth { - address immutable root; - - constructor(address _root) { - root = _root; - - wards[msg.sender] = 1; - emit Rely(msg.sender); - } - - function newRestrictionManager(address token, address[] calldata restrictionManagerWards) - public - auth - returns (address) - { - RestrictionManager restrictionManager = new MigratedRestrictionManager(token); - - restrictionManager.updateMember(RootLike(root).escrow(), type(uint256).max); - - restrictionManager.rely(root); - for (uint256 i = 0; i < restrictionManagerWards.length; i++) { - restrictionManager.rely(restrictionManagerWards[i]); - } - restrictionManager.deny(address(this)); - - return (address(restrictionManager)); - } -} From 6af024f4e2753bc1b7675152ba96bebc3da58bff Mon Sep 17 00:00:00 2001 From: Alina Sinelnikova Date: Wed, 18 Oct 2023 12:00:37 +0100 Subject: [PATCH 38/51] Fix permit fr with lower asset amounts (#183) * fix frontrunning with smaller asset amounts with permits * add test condition Signed-off-by: ilin --------- Signed-off-by: ilin --- src/LiquidityPool.sol | 2 +- test/LiquidityPool.t.sol | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 9c97ef4b..6fe92f41 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -353,7 +353,7 @@ contract LiquidityPool is Auth, IERC4626 { try ERC20PermitLike(token).permit(owner, spender, value, deadline, v, r, s) { return; } catch { - if (IERC20(token).allowance(owner, spender) >= value) { + if (IERC20(token).allowance(owner, spender) == value) { return; } } diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 00d00280..b2fc05d2 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -1005,7 +1005,8 @@ contract LiquidityPoolTest is TestSetup { // Use a wallet with a known private key so we can sign the permit message address investor = vm.addr(0xABCD); - vm.prank(vm.addr(0xABCD)); + address randomUser = makeAddr("randomUser"); + vm.prank(investor); address lPool_ = deploySimplePool(); LiquidityPool lPool = LiquidityPool(lPool_); @@ -1026,7 +1027,14 @@ contract LiquidityPoolTest is TestSetup { ) ) ) - ); + ); + + // frontrunnign with lower amount should not be possible + vm.startPrank(randomUser); + ERC20(address(lPool.asset())).permit(investor, lPool_, amount, block.timestamp, v, r, s); + vm.expectRevert(bytes("LiquidityPool/permit-failure")); + lPool.requestDepositWithPermit((amount - 1), investor, block.timestamp, v, r, s); + vm.stopPrank(); lPool.requestDepositWithPermit(amount, investor, block.timestamp, v, r, s); // To avoid stack too deep errors From 6bfdcea3c99649bd965284ca22841f5194b7815f Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 18 Oct 2023 08:21:22 -0400 Subject: [PATCH 39/51] Make restrictionManagerFactory mutable on PoolManager (#182) * Make restrictionManagerFactory mutable on PoolManager * group RestrictionManagerFactory with other public vars --- src/PoolManager.sol | 3 ++- test/PoolManager.t.sol | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 1c245135..6e222f61 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -78,9 +78,9 @@ contract PoolManager is Auth { EscrowLike public immutable escrow; LiquidityPoolFactoryLike public immutable liquidityPoolFactory; - RestrictionManagerFactoryLike public immutable restrictionManagerFactory; TrancheTokenFactoryLike public immutable trancheTokenFactory; + RestrictionManagerFactoryLike public restrictionManagerFactory; GatewayLike public gateway; InvestmentManagerLike public investmentManager; @@ -137,6 +137,7 @@ contract PoolManager is Auth { function file(bytes32 what, address data) external auth { if (what == "gateway") gateway = GatewayLike(data); else if (what == "investmentManager") investmentManager = InvestmentManagerLike(data); + else if (what == "restrictionManagerFactory") restrictionManagerFactory = RestrictionManagerFactoryLike(data); else revert("PoolManager/file-unrecognized-param"); emit File(what, data); } diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index cbc2e954..58ccfcde 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -4,6 +4,25 @@ pragma solidity 0.8.21; import "./TestSetup.t.sol"; contract PoolManagerTest is TestSetup { + + function testFile() public { + address newGateway = makeAddr("newGateway"); + poolManager.file("gateway", newGateway); + assertEq(address(poolManager.gateway()), newGateway); + + address newInvestmentManager = makeAddr("newInvestmentManager"); + poolManager.file("investmentManager", newInvestmentManager); + assertEq(address(poolManager.investmentManager()), newInvestmentManager); + + address newRestrictionManagerFactory = makeAddr("newRestrictionManagerFactory"); + poolManager.file("restrictionManagerFactory", newRestrictionManagerFactory); + assertEq(address(poolManager.restrictionManagerFactory()), newRestrictionManagerFactory); + + address newEscrow = makeAddr("newEscrow"); + vm.expectRevert("PoolManager/file-unrecognized-param"); + poolManager.file("escrow", newEscrow); + } + // Deployment function testDeployment() public { // values set correctly From bda5ba6e3fc94419899a5df3ecc8218f3bdb5a7f Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:29:29 +0200 Subject: [PATCH 40/51] Preview methods should revert (#186) * Remove preview methods from investment mgr * Add comment * Format * Update src/InvestmentManager.sol Co-authored-by: Adam Stox * Update src/InvestmentManager.sol Co-authored-by: Adam Stox * Update src/InvestmentManager.sol Co-authored-by: Adam Stox * Update src/InvestmentManager.sol Co-authored-by: Adam Stox * Update src/InvestmentManager.sol Co-authored-by: Adam Stox --------- Co-authored-by: Adam Stox --- src/InvestmentManager.sol | 66 +++++++++++---------------------------- src/LiquidityPool.sol | 44 +++++++++++--------------- test/LiquidityPool.t.sol | 41 ++++++++++++------------ 3 files changed, 56 insertions(+), 95 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index e3902425..6667cc1c 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -466,46 +466,6 @@ contract InvestmentManager is Auth { return uint256(_calculateTrancheTokenAmount(state.maxWithdraw, liquidityPool, state.redeemPrice)); } - function previewDeposit(address liquidityPool, address user, uint256 _currencyAmount) - public - view - returns (uint256 trancheTokenAmount) - { - uint128 currencyAmount = _currencyAmount.toUint128(); - InvestmentState memory state = investments[liquidityPool][user]; - trancheTokenAmount = uint256(_calculateTrancheTokenAmount(currencyAmount, liquidityPool, state.depositPrice)); - } - - function previewMint(address liquidityPool, address user, uint256 _trancheTokenAmount) - public - view - returns (uint256 currencyAmount) - { - uint128 trancheTokenAmount = _trancheTokenAmount.toUint128(); - InvestmentState memory state = investments[liquidityPool][user]; - currencyAmount = uint256(_calculateCurrencyAmount(trancheTokenAmount, liquidityPool, state.depositPrice)); - } - - function previewWithdraw(address liquidityPool, address user, uint256 _currencyAmount) - public - view - returns (uint256 trancheTokenAmount) - { - uint128 currencyAmount = _currencyAmount.toUint128(); - InvestmentState memory state = investments[liquidityPool][user]; - trancheTokenAmount = uint256(_calculateTrancheTokenAmount(currencyAmount, liquidityPool, state.redeemPrice)); - } - - function previewRedeem(address liquidityPool, address user, uint256 _trancheTokenAmount) - public - view - returns (uint256 currencyAmount) - { - uint128 trancheTokenAmount = _trancheTokenAmount.toUint128(); - InvestmentState memory state = investments[liquidityPool][user]; - currencyAmount = uint256(_calculateCurrencyAmount(trancheTokenAmount, liquidityPool, state.redeemPrice)); - } - function pendingDepositRequest(address liquidityPool, address user) public view returns (uint256 currencyAmount) { currencyAmount = uint256(investments[liquidityPool][user].remainingDepositRequest); } @@ -527,8 +487,11 @@ contract InvestmentManager is Auth { auth returns (uint256 trancheTokenAmount) { - trancheTokenAmount = previewDeposit(liquidityPool, owner, currencyAmount); - _processDeposit(investments[liquidityPool][owner], trancheTokenAmount.toUint128(), liquidityPool, receiver); + InvestmentState storage state = investments[liquidityPool][owner]; + uint128 trancheTokenAmount_ = + _calculateTrancheTokenAmount(currencyAmount.toUint128(), liquidityPool, state.depositPrice); + _processDeposit(state, trancheTokenAmount_, liquidityPool, receiver); + trancheTokenAmount = uint256(trancheTokenAmount_); } /// @notice Processes owner's currency deposit / investment after the epoch has been executed on Centrifuge. @@ -539,8 +502,10 @@ contract InvestmentManager is Auth { auth returns (uint256 currencyAmount) { - currencyAmount = previewMint(liquidityPool, owner, trancheTokenAmount); - _processDeposit(investments[liquidityPool][owner], trancheTokenAmount.toUint128(), liquidityPool, receiver); + InvestmentState storage state = investments[liquidityPool][owner]; + _processDeposit(state, trancheTokenAmount.toUint128(), liquidityPool, receiver); + currencyAmount = + uint256(_calculateCurrencyAmount(trancheTokenAmount.toUint128(), liquidityPool, state.depositPrice)); } function _processDeposit( @@ -567,8 +532,11 @@ contract InvestmentManager is Auth { auth returns (uint256 currencyAmount) { - currencyAmount = previewRedeem(liquidityPool, owner, trancheTokenAmount); - _processRedeem(investments[liquidityPool][owner], currencyAmount.toUint128(), liquidityPool, receiver, owner); + InvestmentState storage state = investments[liquidityPool][owner]; + uint128 currencyAmount_ = + _calculateCurrencyAmount(trancheTokenAmount.toUint128(), liquidityPool, state.redeemPrice); + _processRedeem(state, currencyAmount_, liquidityPool, receiver, owner); + currencyAmount = uint256(currencyAmount_); } /// @dev Processes owner's tranche token redemption after the epoch has been executed on Centrifuge. @@ -579,8 +547,10 @@ contract InvestmentManager is Auth { auth returns (uint256 trancheTokenAmount) { - trancheTokenAmount = previewWithdraw(liquidityPool, owner, currencyAmount); - _processRedeem(investments[liquidityPool][owner], currencyAmount.toUint128(), liquidityPool, receiver, owner); + InvestmentState storage state = investments[liquidityPool][owner]; + _processRedeem(state, currencyAmount.toUint128(), liquidityPool, receiver, owner); + trancheTokenAmount = + uint256(_calculateTrancheTokenAmount(currencyAmount.toUint128(), liquidityPool, state.redeemPrice)); } function _processRedeem( diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 6fe92f41..3742b6c9 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -25,10 +25,6 @@ interface ManagerLike { function maxRedeem(address lp, address receiver) external view returns (uint256); function convertToShares(address lp, uint256 assets) external view returns (uint256); function convertToAssets(address lp, uint256 shares) external view returns (uint256); - function previewDeposit(address lp, address operator, uint256 assets) external view returns (uint256); - function previewMint(address lp, address operator, uint256 shares) external view returns (uint256); - function previewWithdraw(address lp, address operator, uint256 assets) external view returns (uint256); - function previewRedeem(address lp, address operator, uint256 shares) external view returns (uint256); function requestDeposit(address lp, uint256 assets, address sender, address operator) external returns (bool); function requestRedeem(address lp, uint256 shares, address operator) external returns (bool); function decreaseDepositRequest(address lp, uint256 assets, address operator) external; @@ -134,14 +130,9 @@ contract LiquidityPool is Auth, IERC4626 { function maxDeposit(address receiver) public view returns (uint256 maxAssets) { maxAssets = manager.maxDeposit(address(this), receiver); } - - /// @return shares that any user would get for an amount of assets provided - function previewDeposit(uint256 assets) public view returns (uint256 shares) { - shares = manager.previewDeposit(address(this), msg.sender, assets); - } - /// @notice Collect shares for deposited assets after Centrifuge epoch execution. /// maxDeposit is the max amount of assets that can be deposited. + function deposit(uint256 assets, address receiver) public returns (uint256 shares) { shares = manager.deposit(address(this), assets, receiver, msg.sender); emit Deposit(address(this), receiver, assets, shares); @@ -159,22 +150,11 @@ contract LiquidityPool is Auth, IERC4626 { maxShares = manager.maxMint(address(this), receiver); } - /// @return assets that any user would get for an amount of shares provided -> convertToAssets - function previewMint(uint256 shares) external view returns (uint256 assets) { - assets = manager.previewMint(address(this), msg.sender, shares); - } - /// @return maxAssets that the receiver can withdraw function maxWithdraw(address receiver) public view returns (uint256 maxAssets) { maxAssets = manager.maxWithdraw(address(this), receiver); } - /// @return shares that a user would need to redeem in order to receive - /// the given amount of assets -> convertToAssets - function previewWithdraw(uint256 assets) public view returns (uint256 shares) { - shares = manager.previewWithdraw(address(this), msg.sender, assets); - } - /// @notice Withdraw assets after successful epoch execution. Receiver will receive an exact amount of assets for /// a certain amount of shares that has been redeemed from Owner during epoch execution. /// DOES NOT support owner != msg.sender since shares are already transferred on requestRedeem @@ -190,11 +170,6 @@ contract LiquidityPool is Auth, IERC4626 { maxShares = manager.maxRedeem(address(this), owner); } - /// @return assets that any user could redeem for a given amount of shares - function previewRedeem(uint256 shares) public view returns (uint256 assets) { - assets = manager.previewRedeem(address(this), msg.sender, shares); - } - /// @notice Redeem shares after successful epoch execution. Receiver will receive assets for /// @notice Redeem shares can only be called by the Owner or an authorized admin. /// the exact amount of redeemed shares from Owner after epoch execution. @@ -285,6 +260,23 @@ contract LiquidityPool is Auth, IERC4626 { shares = manager.pendingRedeemRequest(address(this), operator); } + /// @dev Preview functions for async 4626 vaults revert + function previewDeposit(uint256) external pure returns (uint256) { + revert(); + } + + function previewMint(uint256) external pure returns (uint256) { + revert(); + } + + function previewWithdraw(uint256) external pure returns (uint256) { + revert(); + } + + function previewRedeem(uint256) external pure returns (uint256) { + revert(); + } + // --- ERC20 overrides --- function name() public view returns (string memory) { return share.name(); diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index b2fc05d2..2d27bb4b 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -60,18 +60,6 @@ contract LiquidityPoolTest is TestSetup { vm.expectRevert(bytes("MathLib/uint128-overflow")); lPool.convertToAssets(amount); - vm.expectRevert(bytes("MathLib/uint128-overflow")); - lPool.previewDeposit(amount); - - vm.expectRevert(bytes("MathLib/uint128-overflow")); - lPool.previewRedeem(amount); - - vm.expectRevert(bytes("MathLib/uint128-overflow")); - lPool.previewMint(amount); - - vm.expectRevert(bytes("MathLib/uint128-overflow")); - lPool.previewWithdraw(amount); - vm.expectRevert(bytes("MathLib/uint128-overflow")); lPool.deposit(amount, random_); @@ -102,6 +90,26 @@ contract LiquidityPoolTest is TestSetup { lPool.decreaseRedeemRequest(amount); } + // --- preview checks --- + function testPreviewReverts(uint256 amount, address random_) public { + vm.assume(amount > MAX_UINT128); // amount has to overflow UINT128 + vm.assume(random_.code.length == 0); + address lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + + vm.expectRevert(bytes("")); + lPool.previewDeposit(amount); + + vm.expectRevert(bytes("")); + lPool.previewRedeem(amount); + + vm.expectRevert(bytes("")); + lPool.previewMint(amount); + + vm.expectRevert(bytes("")); + lPool.previewWithdraw(amount); + } + function testRedeemWithApproval(uint256 redemption1, uint256 redemption2) public { redemption1 = uint128(bound(redemption1, 2, MAX_UINT128 / 4)); redemption2 = uint128(bound(redemption2, 2, MAX_UINT128 / 4)); @@ -773,9 +781,6 @@ contract LiquidityPoolTest is TestSetup { assertEq(lPool.pendingDepositRequest(self), 0); // assert tranche tokens minted assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); - // assert conversions - assertEq(lPool.previewDeposit(amount), trancheTokensPayout); - assertApproxEqAbs(lPool.previewMint(trancheTokensPayout), amount, 1); // deposit 50% of the amount lPool.deposit(amount / 2, self); // mint half the amount @@ -937,9 +942,6 @@ contract LiquidityPoolTest is TestSetup { assertEq(lPool.maxDeposit(self), amount); // max deposit // assert tranche tokens minted assertEq(lPool.balanceOf(address(escrow)), trancheTokensPayout); - // assert conversions - assertEq(lPool.previewDeposit(amount), trancheTokensPayout); - assertApproxEqAbs(lPool.previewMint(trancheTokensPayout), amount, 1); // deposit 1/2 funds to receiver vm.expectRevert(bytes("RestrictionManager/destination-not-a-member")); @@ -1095,9 +1097,6 @@ contract LiquidityPoolTest is TestSetup { assertEq(lPool.pendingRedeemRequest(self), 0); assertEq(lPool.balanceOf(address(escrow)), 0); assertEq(erc20.balanceOf(address(userEscrow)), currencyPayout); - // assert conversions - assertEq(lPool.previewWithdraw(currencyPayout), amount); - assertEq(lPool.previewRedeem(amount), currencyPayout); // success lPool.redeem(amount / 2, self, self); // redeem half the amount to own wallet From 7c0815858370a163c710aee74f250e27d287c390 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Wed, 18 Oct 2023 14:27:56 -0400 Subject: [PATCH 41/51] remove unused local variable --- test/migrations/MigratedInvestmentManager.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/migrations/MigratedInvestmentManager.t.sol b/test/migrations/MigratedInvestmentManager.t.sol index 7b0fb923..a8ec1d12 100644 --- a/test/migrations/MigratedInvestmentManager.t.sol +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -53,7 +53,6 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { // file investmentManager on all LiquidityPools for (uint256 i = 0; i < liquidityPools.length; i++) { root.relyContract(liquidityPools[i], address(this)); - LiquidityPool lPool = LiquidityPool(liquidityPools[i]); LiquidityPool(liquidityPools[i]).file("manager", address(newInvestmentManager)); LiquidityPool(liquidityPools[i]).rely(address(newInvestmentManager)); From 45f374593ec186d61db7e751c3a73a78fa22f5c9 Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:12:32 +0200 Subject: [PATCH 42/51] Add IERC7540 (#190) * Add IERC7540 * Update interface * Update ERC-20 interfaces * Fix comment * Skip slither step --- .github/workflows/ci.yml | 58 +++++++++++----------- src/LiquidityPool.sol | 94 +++++++++++++++++------------------- src/PoolManager.sol | 6 +-- src/interfaces/IERC20.sol | 95 +++++++++++++++++++++++++++++++++++++ src/interfaces/IERC4626.sol | 4 +- src/interfaces/IERC7540.sol | 49 +++++++++++++++++++ 6 files changed, 221 insertions(+), 85 deletions(-) create mode 100644 src/interfaces/IERC7540.sol diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b36a6b7b..cacc1de7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,33 +113,33 @@ jobs: coverage-files: ./lcov.info minimum-coverage: 60 # Set coverage threshold. - slither-analyze: - runs-on: "ubuntu-latest" - permissions: - actions: "read" - contents: "read" - security-events: "write" - steps: - - name: "Check out the repo" - uses: "actions/checkout@v3" - with: - submodules: "recursive" - - - name: "Run Slither analysis" - uses: "crytic/slither-action@v0.3.0" - id: "slither" - with: - fail-on: "none" - sarif: "results.sarif" - solc-version: "0.8.21" - target: "src/" - - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: ${{ steps.slither.outputs.sarif }} + # slither-analyze: + # runs-on: "ubuntu-latest" + # permissions: + # actions: "read" + # contents: "read" + # security-events: "write" + # steps: + # - name: "Check out the repo" + # uses: "actions/checkout@v3" + # with: + # submodules: "recursive" + + # - name: "Run Slither analysis" + # uses: "crytic/slither-action@v0.3.0" + # id: "slither" + # with: + # fail-on: "none" + # sarif: "results.sarif" + # solc-version: "0.8.21" + # target: "src/" + + # - name: Upload SARIF file + # uses: github/codeql-action/upload-sarif@v2 + # with: + # sarif_file: ${{ steps.slither.outputs.sarif }} - - name: "Add Slither summary" - run: | - echo "## Slither result" >> $GITHUB_STEP_SUMMARY - echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + # - name: "Add Slither summary" + # run: | + # echo "## Slither result" >> $GITHUB_STEP_SUMMARY + # echo "✅ Uploaded to GitHub code scanning" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 3742b6c9..96fda35d 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -4,15 +4,8 @@ pragma solidity 0.8.21; import {Auth} from "./util/Auth.sol"; import {MathLib} from "./util/MathLib.sol"; import {SafeTransferLib} from "./util/SafeTransferLib.sol"; -import {IERC20} from "./interfaces/IERC20.sol"; -import {IERC4626} from "./interfaces/IERC4626.sol"; - -interface ERC20PermitLike { - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external; -} - -interface TrancheTokenLike is IERC20, ERC20PermitLike {} +import {IERC20, IERC20Metadata, IERC20Permit} from "./interfaces/IERC20.sol"; +import {IERC7540} from "./interfaces/IERC7540.sol"; interface ManagerLike { function deposit(address lp, uint256 assets, address receiver, address owner) external returns (uint256); @@ -37,16 +30,16 @@ interface ManagerLike { /// @title Liquidity Pool /// @notice Liquidity Pool implementation for Centrifuge pools -/// following the EIP4626 standard, with asynchronous extension methods. +/// following the ERC-7540 Asynchronous Tokenized Vault standard /// -/// @dev Each Liquidity Pool is a tokenized vault issuing shares of Centrifuge tranches as restricted ERC20 tokens +/// @dev Each Liquidity Pool is a tokenized vault issuing shares of Centrifuge tranches as restricted ERC-20 tokens /// against currency deposits based on the current share price. /// -/// This is extending the EIP4626 standard by 'requestDeposit' & 'requestRedeem' functions, where deposit and -/// redeem orders are submitted to the pools to be included in the execution of the following epoch. After -/// execution users can use the deposit, mint, redeem and withdraw functions to get their shares +/// ERC-7540 is an extension of the ERC-4626 standard by 'requestDeposit' & 'requestRedeem' methods, where +/// deposit and redeem orders are submitted to the pools to be included in the execution of the following epoch. +/// After execution users can use the deposit, mint, redeem and withdraw functions to get their shares /// and/or assets from the pools. -contract LiquidityPool is Auth, IERC4626 { +contract LiquidityPool is Auth, IERC7540 { using MathLib for uint256; uint64 public immutable poolId; @@ -62,7 +55,7 @@ contract LiquidityPool is Auth, IERC4626 { /// @notice The restricted ERC-20 Liquidity Pool token. Has a ratio (token price) of underlying assets /// exchanged on deposit/withdraw/redeem. /// @dev Also known as tranche tokens. - TrancheTokenLike public immutable share; + IERC20Metadata public immutable share; /// @notice Escrow contract for tokens address public immutable escrow; @@ -78,8 +71,6 @@ contract LiquidityPool is Auth, IERC4626 { // --- Events --- event File(bytes32 indexed what, address data); - event DepositRequest(address indexed sender, address indexed operator, uint256 assets); - event RedeemRequest(address indexed sender, address indexed operator, address indexed owner, uint256 shares); event DecreaseDepositRequest(address indexed sender, uint256 assets); event DecreaseRedeemRequest(address indexed sender, uint256 shares); event CancelDepositRequest(address indexed sender); @@ -90,7 +81,7 @@ contract LiquidityPool is Auth, IERC4626 { poolId = poolId_; trancheId = trancheId_; asset = asset_; - share = TrancheTokenLike(share_); + share = IERC20Metadata(share_); escrow = escrow_; manager = ManagerLike(manager_); @@ -105,7 +96,7 @@ contract LiquidityPool is Auth, IERC4626 { emit File(what, data); } - // --- ERC4626 functions --- + // --- ERC-4626 methods --- /// @return Total value of the shares, denominated in the asset of this Liquidity Pool function totalAssets() public view returns (uint256) { return convertToAssets(totalSupply()); @@ -181,7 +172,7 @@ contract LiquidityPool is Auth, IERC4626 { emit Withdraw(address(this), receiver, owner, assets, shares); } - // --- Asynchronous 4626 functions --- + // --- ERC-7540 methods --- /// @notice Request asset deposit for a receiver to be included in the next epoch execution. /// @notice Request can only be called by the owner of the assets /// Asset is locked in the escrow on request submission @@ -211,20 +202,6 @@ contract LiquidityPool is Auth, IERC4626 { assets = manager.pendingDepositRequest(address(this), operator); } - /// @notice Request decreasing the outstanding deposit orders. Will return the assets once the order - /// on Centrifuge is successfully decreased. - function decreaseDepositRequest(uint256 assets) public { - manager.decreaseDepositRequest(address(this), assets, msg.sender); - emit DecreaseDepositRequest(msg.sender, assets); - } - - /// @notice Request cancelling the outstanding deposit orders. Will return the assets once the order - /// on Centrifuge is successfully cancelled. - function cancelDepositRequest() public { - manager.cancelDepositRequest(address(this), msg.sender); - emit CancelDepositRequest(msg.sender); - } - /// @notice Request share redemption for a receiver to be included in the next epoch execution. /// DOES support flow where owner != msg.sender but has allowance to spend its shares /// Shares are locked in the escrow on request submission @@ -239,20 +216,6 @@ contract LiquidityPool is Auth, IERC4626 { emit RedeemRequest(msg.sender, operator, owner, shares); } - /// @notice Request decreasing the outstanding redemption orders. Will return the shares once the order - /// on Centrifuge is successfully decreased. - function decreaseRedeemRequest(uint256 shares) public { - manager.decreaseRedeemRequest(address(this), shares, msg.sender); - emit DecreaseRedeemRequest(msg.sender, shares); - } - - /// @notice Request cancelling the outstanding redemption orders. Will return the shares once the order - /// on Centrifuge is successfully cancelled. - function cancelRedeemRequest() public { - manager.cancelRedeemRequest(address(this), msg.sender); - emit CancelRedeemRequest(msg.sender); - } - /// @notice View the total amount the operator has requested to redeem but isn't able to withdraw or redeem yet /// @dev Due to the asynchronous nature, this value might be outdated, and should only /// be used for informational purposes. @@ -277,7 +240,36 @@ contract LiquidityPool is Auth, IERC4626 { revert(); } - // --- ERC20 overrides --- + // --- Misc asynchronous vault methods --- + /// @notice Request decreasing the outstanding deposit orders. Will return the assets once the order + /// on Centrifuge is successfully decreased. + function decreaseDepositRequest(uint256 assets) public { + manager.decreaseDepositRequest(address(this), assets, msg.sender); + emit DecreaseDepositRequest(msg.sender, assets); + } + + /// @notice Request cancelling the outstanding deposit orders. Will return the assets once the order + /// on Centrifuge is successfully cancelled. + function cancelDepositRequest() public { + manager.cancelDepositRequest(address(this), msg.sender); + emit CancelDepositRequest(msg.sender); + } + + /// @notice Request decreasing the outstanding redemption orders. Will return the shares once the order + /// on Centrifuge is successfully decreased. + function decreaseRedeemRequest(uint256 shares) public { + manager.decreaseRedeemRequest(address(this), shares, msg.sender); + emit DecreaseRedeemRequest(msg.sender, shares); + } + + /// @notice Request cancelling the outstanding redemption orders. Will return the shares once the order + /// on Centrifuge is successfully cancelled. + function cancelRedeemRequest() public { + manager.cancelRedeemRequest(address(this), msg.sender); + emit CancelRedeemRequest(msg.sender); + } + + // --- ERC-20 overrides --- function name() public view returns (string memory) { return share.name(); } @@ -342,7 +334,7 @@ contract LiquidityPool is Auth, IERC4626 { bytes32 r, bytes32 s ) internal { - try ERC20PermitLike(token).permit(owner, spender, value, deadline, v, r, s) { + try IERC20Permit(token).permit(owner, spender, value, deadline, v, r, s) { return; } catch { if (IERC20(token).allowance(owner, spender) == value) { diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 6e222f61..976c9eb5 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.21; import {TrancheTokenFactoryLike, RestrictionManagerFactoryLike, LiquidityPoolFactoryLike} from "./util/Factory.sol"; import {TrancheTokenLike} from "./token/Tranche.sol"; import {RestrictionManagerLike} from "./token/RestrictionManager.sol"; -import {IERC20} from "./interfaces/IERC20.sol"; +import {IERC20Metadata} from "./interfaces/IERC20.sol"; import {Auth} from "./util/Auth.sol"; import {SafeTransferLib} from "./util/SafeTransferLib.sol"; import {MathLib} from "./util/MathLib.sol"; @@ -299,7 +299,7 @@ contract PoolManager is Auth { require(currencyIdToAddress[currency] == address(0), "PoolManager/currency-id-in-use"); require(currencyAddressToId[currencyAddress] == 0, "PoolManager/currency-address-in-use"); - uint8 currencyDecimals = IERC20(currencyAddress).decimals(); + uint8 currencyDecimals = IERC20Metadata(currencyAddress).decimals(); require(currencyDecimals >= MIN_DECIMALS, "PoolManager/too-few-currency-decimals"); require(currencyDecimals <= MAX_DECIMALS, "PoolManager/too-many-currency-decimals"); @@ -393,7 +393,7 @@ contract PoolManager is Auth { // in the escrow to transfer to the user on deposit or mint escrow.approve(tranche.token, address(investmentManager), type(uint256).max); - // Give investment manager infinite approval for tranche tokens + // Give liquidity pool infinite approval for tranche tokens // in the escrow to burn on executed redemptions escrow.approve(tranche.token, liquidityPool, type(uint256).max); diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol index 69b4f97b..03721db6 100644 --- a/src/interfaces/IERC20.sol +++ b/src/interfaces/IERC20.sol @@ -75,8 +75,103 @@ interface IERC20 { * Emits a {Transfer} event. */ function transferFrom(address from, address to, uint256 value) external returns (bool); +} +/** + * @dev Interface for the optional metadata functions from the ERC20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ function decimals() external view returns (uint8); } + +/** + * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in + * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by + * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't + * need to send a transaction, and thus is not required to hold Ether at all. + * + * ==== Security Considerations + * + * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature + * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be + * considered as an intention to spend the allowance in any specific way. The second is that because permits have + * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should + * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be + * generally recommended is: + * + * ```solidity + * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { + * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} + * doThing(..., value); + * } + * + * function doThing(..., uint256 value) public { + * token.safeTransferFrom(msg.sender, address(this), value); + * ... + * } + * ``` + * + * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of + * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also + * {SafeERC20-safeTransferFrom}). + * + * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so + * contracts should have entry points that don't rely on permit. + */ +interface IERC20Permit { + /** + * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, + * given ``owner``'s signed approval. + * + * IMPORTANT: The same issues {IERC20-approve} has related to transaction + * ordering also apply here. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` + * over the EIP712-formatted function arguments. + * - the signature must use ``owner``'s current nonce (see {nonces}). + * + * For more information on the signature format, see the + * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP + * section]. + * + * CAUTION: See Security Considerations above. + */ + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external; + + /** + * @dev Returns the current nonce for `owner`. This value must be + * included whenever a signature is generated for {permit}. + * + * Every successful call to {permit} increases ``owner``'s nonce by one. This + * prevents a signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/src/interfaces/IERC4626.sol b/src/interfaces/IERC4626.sol index c220eb34..fcf0f4ed 100644 --- a/src/interfaces/IERC4626.sol +++ b/src/interfaces/IERC4626.sol @@ -1,13 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.21; -import {IERC20} from "./IERC20.sol"; +import {IERC20Metadata} from "./IERC20.sol"; /// @title IERC4626 /// @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in /// https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. /// @author Modified from OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4626.sol) -interface IERC4626 is IERC20 { +interface IERC4626 is IERC20Metadata { event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); event Withdraw( diff --git a/src/interfaces/IERC7540.sol b/src/interfaces/IERC7540.sol new file mode 100644 index 00000000..c07f3d1f --- /dev/null +++ b/src/interfaces/IERC7540.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.21; + +import {IERC4626} from "./IERC4626.sol"; + +/// @title IERC7540 +/// @dev Interface of the ERC7540 "Asynchronous Tokenized Vault Standard", as defined in +/// https://github.com/ethereum/EIPs/blob/2e63f2096b0c7d8388458bb0a03a7ce0eb3422a4/EIPS/eip-7540.md[ERC-7540]. +interface IERC7540 is IERC4626 { + event DepositRequest(address indexed sender, address indexed operator, uint256 assets); + event RedeemRequest(address indexed sender, address indexed operator, address indexed owner, uint256 shares); + + /** + * @dev Transfers assets from msg.sender into the Vault and submits a Request for asynchronous deposit/mint. + * + * - MUST support ERC-20 approve / transferFrom on asset as a deposit Request flow. + * - MUST revert if all of assets cannot be requested for deposit/mint. + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault's underlying asset token. + */ + function requestDeposit(uint256 assets, address operator) external; + + /** + * @dev Returns the amount of requested assets in Pending state for the operator to deposit or mint. + * + * - MUST NOT include any assets in Claimable state for deposit or mint. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function pendingDepositRequest(address operator) external view returns (uint256 assets); + + /** + * @dev Assumes control of shares from owner and submits a Request for asynchronous redeem/withdraw. + * + * - MUST support a redeem Request flow where the control of shares is taken from owner directly + * where msg.sender has ERC-20 approval over the shares of owner. + * - MUST revert if all of shares cannot be requested for redeem / withdraw. + */ + function requestRedeem(uint256 shares, address operator, address owner) external; + + /** + * @dev Returns the amount of requested shares in Pending state for the operator to redeem or withdraw. + * + * - MUST NOT include any shares in Claimable state for redeem or withdraw. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT revert unless due to integer overflow caused by an unreasonably large input. + */ + function pendingRedeemRequest(address operator) external view returns (uint256 shares); +} From 411494ce96102ef6af69344d8fd95f7defcbef1c Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:25:04 +0200 Subject: [PATCH 43/51] Fix price calculation issue on decrease executions (#184) * Add test for failing price conversion on decrease * Attempt to fix conversions --- src/InvestmentManager.sol | 39 +++++++++++++++++++--------- test/LiquidityPool.t.sol | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index 6667cc1c..9478aa76 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -368,11 +368,14 @@ contract InvestmentManager is Auth { // Calculating the price with both payouts as currencyPayout // leads to an effective redeem price of 1.0 and thus the user actually receiving // exactly currencyPayout on both deposit() and mint() + (uint8 currencyDecimals, uint8 trancheTokenDecimals) = _getPoolDecimals(liquidityPool); + uint256 currencyPayoutInPriceDecimals = _toPriceDecimals(currencyPayout, currencyDecimals); state.redeemPrice = _calculatePrice( - liquidityPool, - state.maxWithdraw + currencyPayout, - ((maxRedeem(liquidityPool, user)) + currencyPayout).toUint128() + _toPriceDecimals(state.maxWithdraw, currencyDecimals) + currencyPayoutInPriceDecimals, + _toPriceDecimals(maxRedeem(liquidityPool, user).toUint128(), trancheTokenDecimals) + + currencyPayoutInPriceDecimals ); + state.maxWithdraw = state.maxWithdraw + currencyPayout; state.remainingDepositRequest = remainingInvestOrder; @@ -394,14 +397,19 @@ contract InvestmentManager is Auth { uint128 remainingRedeemOrder ) public onlyGateway { address liquidityPool = poolManager.getLiquidityPool(poolId, trancheId, currencyId); + InvestmentState storage state = investments[liquidityPool][user]; // Calculating the price with both payouts as trancheTokenPayout // leads to an effective redeem price of 1.0 and thus the user actually receiving // exactly trancheTokenPayout on both deposit() and mint() - InvestmentState storage state = investments[liquidityPool][user]; + (uint8 currencyDecimals, uint8 trancheTokenDecimals) = _getPoolDecimals(liquidityPool); + uint256 trancheTokenPayoutInPriceDecimals = _toPriceDecimals(trancheTokenPayout, trancheTokenDecimals); state.depositPrice = _calculatePrice( - liquidityPool, _maxDeposit(liquidityPool, user) + trancheTokenPayout, state.maxMint + trancheTokenPayout + _toPriceDecimals(_maxDeposit(liquidityPool, user), currencyDecimals).toUint128() + + trancheTokenPayoutInPriceDecimals, + _toPriceDecimals(state.maxMint, trancheTokenDecimals) + trancheTokenPayoutInPriceDecimals ); + state.maxMint = state.maxMint + trancheTokenPayout; state.remainingRedeemRequest = remainingRedeemOrder; @@ -605,18 +613,25 @@ contract InvestmentManager is Auth { } function _calculatePrice(address liquidityPool, uint128 currencyAmount, uint128 trancheTokenAmount) - public + internal view returns (uint256 price) { - if (currencyAmount == 0 || trancheTokenAmount == 0) { - return 0; - } - (uint8 currencyDecimals, uint8 trancheTokenDecimals) = _getPoolDecimals(liquidityPool); + price = _calculatePrice( + _toPriceDecimals(currencyAmount, currencyDecimals), + _toPriceDecimals(trancheTokenAmount, trancheTokenDecimals) + ); + } - uint256 currencyAmountInPriceDecimals = _toPriceDecimals(currencyAmount, currencyDecimals); - uint256 trancheTokenAmountInPriceDecimals = _toPriceDecimals(trancheTokenAmount, trancheTokenDecimals); + function _calculatePrice(uint256 currencyAmountInPriceDecimals, uint256 trancheTokenAmountInPriceDecimals) + internal + view + returns (uint256 price) + { + if (currencyAmountInPriceDecimals == 0 || trancheTokenAmountInPriceDecimals == 0) { + return 0; + } price = currencyAmountInPriceDecimals.mulDiv( 10 ** PRICE_DECIMALS, trancheTokenAmountInPriceDecimals, MathLib.Rounding.Down diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 2d27bb4b..90d9a6cc 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -606,6 +606,60 @@ contract LiquidityPoolTest is TestSetup { assertEq(depositPrice, 1200000000000000000); } + function testDecreaseDepositPrecision(uint64 poolId, bytes16 trancheId, uint128 currencyId) public { + vm.assume(currencyId > 0); + + uint8 TRANCHE_TOKEN_DECIMALS = 18; // Like DAI + uint8 INVESTMENT_CURRENCY_DECIMALS = 6; // 6, like USDC + + ERC20 currency = _newErc20("Currency", "CR", INVESTMENT_CURRENCY_DECIMALS); + address lPool_ = + deployLiquidityPool(poolId, TRANCHE_TOKEN_DECIMALS, "", "", trancheId, currencyId, address(currency)); + LiquidityPool lPool = LiquidityPool(lPool_); + centrifugeChain.updateTrancheTokenPrice(poolId, trancheId, currencyId, 1000000000000000000); + + // invest + uint256 investmentAmount = 100000000; // 100 * 10**6 + centrifugeChain.updateMember(poolId, trancheId, self, type(uint64).max); + currency.approve(lPool_, investmentAmount); + currency.mint(self, investmentAmount); + lPool.requestDeposit(investmentAmount, self); + + // trigger executed collectInvest of the first 50% at a price of 1.2 + uint128 _currencyId = poolManager.currencyAddressToId(address(currency)); // retrieve currencyId + uint128 currencyPayout = 50000000; // 50 * 10**6 + uint128 firstTrancheTokenPayout = 41666666666666666666; // 50 * 10**18 / 1.2, rounded down + centrifugeChain.isExecutedCollectInvest( + poolId, + trancheId, + bytes32(bytes20(self)), + _currencyId, + currencyPayout, + firstTrancheTokenPayout, + uint128(investmentAmount) - currencyPayout + ); + + // assert deposit & mint values adjusted + assertApproxEqAbs(lPool.maxDeposit(self), currencyPayout, 1); + assertEq(lPool.maxMint(self), firstTrancheTokenPayout); + + // decrease the remaining 50% + uint256 decreaseAmount = 50000000; + lPool.decreaseDepositRequest(decreaseAmount); + centrifugeChain.isExecutedDecreaseInvestOrder( + poolId, + trancheId, bytes32(bytes20(self)), _currencyId, uint128(decreaseAmount), 0 + ); + + + // deposit price should be ~1.2*10**18, redeem price should be 1.0*10**18 + (, uint256 depositPrice,, uint256 redeemPrice,,,) = investmentManager.investments(address(lPool), self); + assertEq(depositPrice, 1200000000000000000); + assertEq(redeemPrice, 1000000000000000000); + assertEq(lPool.maxWithdraw(self), 50000000); + assertEq(lPool.maxRedeem(self), 50000000000000000000); + } + function testAssetShareConversion(uint64 poolId, bytes16 trancheId, uint128 currencyId) public { vm.assume(currencyId > 0); From 899a30488f8e30766e2da71255d8a84bb7586b1b Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:51:41 +0200 Subject: [PATCH 44/51] Automatically claim deposits on trigger request redeem (#187) * Automatically claim deposits on trigger request redeem * trigger redemptions when user still has tokens in escrow (#189) * trigger redemptions when user still has tokens in escrow Signed-off-by: ilin * fix comments Signed-off-by: ilin * add fixes from review * add fixes from review * add fixes from review --------- Signed-off-by: ilin --------- Signed-off-by: ilin Co-authored-by: Alina Sinelnikova --- src/InvestmentManager.sol | 33 +++++++--- test/LiquidityPool.t.sol | 125 +++++++++++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 24 deletions(-) diff --git a/src/InvestmentManager.sol b/src/InvestmentManager.sol index 9478aa76..5704097d 100644 --- a/src/InvestmentManager.sol +++ b/src/InvestmentManager.sol @@ -423,19 +423,36 @@ contract InvestmentManager is Auth { uint128 currencyId, uint128 trancheTokenAmount ) public onlyGateway { + require(trancheTokenAmount != 0, "InvestmentManager/tranche-token-amount-is-zero"); address liquidityPool = poolManager.getLiquidityPool(poolId, trancheId, currencyId); - require( - _processRedeemRequest(liquidityPool, trancheTokenAmount, user), "InvestmentManager/failed-redeem-request" - ); - // Transfer the tranche token amount from user to escrow (lock tranche tokens in escrow) + // If there's any unclaimed deposits, claim those first + InvestmentState storage state = investments[liquidityPool][user]; + uint128 tokensToTransfer = trancheTokenAmount; + if (state.maxMint >= trancheTokenAmount) { + // The full redeem request is covered by the claimable amount + tokensToTransfer = 0; + state.maxMint = state.maxMint - trancheTokenAmount; + } else if (state.maxMint > 0) { + // The redeem request is only partially covered by the claimable amount + tokensToTransfer = trancheTokenAmount - state.maxMint; + state.maxMint = 0; + } + require( - AuthTransferLike(address(LiquidityPoolLike(liquidityPool).share())).authTransferFrom( - user, address(escrow), trancheTokenAmount - ), - "InvestmentManager/transfer-failed" + _processRedeemRequest(liquidityPool, trancheTokenAmount, user), "InvestmentManager/failed-redeem-request" ); + // Transfer the tranche token amount that was not covered by tokens still in escrow for claims, + // from user to escrow (lock tranche tokens in escrow) + if (tokensToTransfer > 0) { + require( + AuthTransferLike(address(LiquidityPoolLike(liquidityPool).share())).authTransferFrom( + user, address(escrow), tokensToTransfer + ), + "InvestmentManager/transfer-failed" + ); + } emit TriggerIncreaseRedeemOrder(poolId, trancheId, user, currencyId, trancheTokenAmount); } diff --git a/test/LiquidityPool.t.sol b/test/LiquidityPool.t.sol index 90d9a6cc..dc070003 100644 --- a/test/LiquidityPool.t.sol +++ b/test/LiquidityPool.t.sol @@ -1311,38 +1311,125 @@ contract LiquidityPoolTest is TestSetup { assertEq(lPool.maxMint(self), decreaseAmount); } - function testTriggerIncreaseRedeemOrder(uint256 amount) public { - amount = uint128(bound(amount, 2, MAX_UINT128)); + + function testTriggerIncreaseRedeemOrderTokens(uint128 amount) public { + amount = uint128(bound(amount, 2, (MAX_UINT128 - 1))); address lPool_ = deploySimplePool(); LiquidityPool lPool = LiquidityPool(lPool_); - deposit(lPool_, investor, amount); // deposit funds first + deposit(lPool_, investor, amount, false); // request and execute deposit, but don't claim uint256 investorBalanceBefore = erc20.balanceOf(investor); - // Trigger request redeem of half the amount + assertEq(lPool.maxMint(investor), amount); + uint64 poolId = lPool.poolId(); + bytes16 trancheId = lPool.trancheId(); + + vm.prank(investor); + lPool.mint(amount / 2, investor); // investor mints half of the amount + + assertApproxEqAbs(lPool.balanceOf(investor), amount / 2, 1); + assertApproxEqAbs(lPool.balanceOf(address(escrow)), amount / 2, 1); + assertApproxEqAbs(lPool.maxMint(investor), amount / 2, 1); + + + // Fail - Redeem amount too big + vm.expectRevert(bytes("ERC20/insufficient-balance")); centrifugeChain.triggerIncreaseRedeemOrder( - lPool.poolId(), lPool.trancheId(), investor, defaultCurrencyId, uint128(amount / 2) + poolId, trancheId, investor, defaultCurrencyId, uint128(amount + 1) ); - assertApproxEqAbs(lPool.balanceOf(address(escrow)), amount / 2, 1); - assertApproxEqAbs(lPool.balanceOf(investor), amount / 2, 1); + //Fail - Tranche token amount zero + vm.expectRevert(bytes("InvestmentManager/tranche-token-amount-is-zero")); + centrifugeChain.triggerIncreaseRedeemOrder( + poolId, trancheId, investor, defaultCurrencyId, 0 + ); + + // should work even if investor is frozen + centrifugeChain.freeze(poolId, trancheId, investor); // freeze investor + assertTrue(!TrancheToken(address(lPool.share())).checkTransferRestriction(investor, address(escrow), amount)); + + + // half of the amount will be trabsferred from the investor's wallet & half of the amount will be taken from escrow + centrifugeChain.triggerIncreaseRedeemOrder( + poolId, trancheId, investor, defaultCurrencyId, amount + ); + + assertApproxEqAbs(lPool.balanceOf(investor), 0, 1); + assertApproxEqAbs(lPool.balanceOf(address(escrow)), amount, 1); + assertEq(lPool.maxMint(investor), 0); centrifugeChain.isExecutedCollectRedeem( lPool.poolId(), lPool.trancheId(), bytes32(bytes20(investor)), defaultCurrencyId, - uint128(amount / 2), - uint128(amount / 2), - uint128(amount / 2) + uint128(amount), + uint128(amount), + uint128(amount) ); assertApproxEqAbs(lPool.balanceOf(address(escrow)), 0, 1); - assertApproxEqAbs(erc20.balanceOf(address(userEscrow)), amount / 2, 1); + assertApproxEqAbs(erc20.balanceOf(address(userEscrow)), amount, 1); + vm.prank(investor); + lPool.redeem(amount, investor, investor); + assertApproxEqAbs(erc20.balanceOf(investor), investorBalanceBefore + amount, 1); + } + function testTriggerIncreaseRedeemOrderTokensUnmitedTokensInEscrow(uint128 amount) public { + amount = uint128(bound(amount, 2, (MAX_UINT128 - 1))); + + address lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + deposit(lPool_, investor, amount, false); // request and execute deposit, but don't claim + uint256 investorBalanceBefore = erc20.balanceOf(investor); + assertEq(lPool.maxMint(investor), amount); + uint64 poolId = lPool.poolId(); + bytes16 trancheId = lPool.trancheId(); + + // Fail - Redeem amount too big + vm.expectRevert(bytes("ERC20/insufficient-balance")); + centrifugeChain.triggerIncreaseRedeemOrder( + poolId, trancheId, investor, defaultCurrencyId, uint128(amount + 1) + ); + + // should work even if investor is frozen + centrifugeChain.freeze(poolId, trancheId, investor); // freeze investor + assertTrue(!TrancheToken(address(lPool.share())).checkTransferRestriction(investor, address(escrow), amount)); + + // Test trigger partial redeem (maxMint > redeemAmount), where investor did not mint their tokens - user tokens are still locked in escrow + uint128 redeemAmount = uint128(amount / 2); + centrifugeChain.triggerIncreaseRedeemOrder( + poolId, trancheId, investor, defaultCurrencyId, redeemAmount + ); + assertApproxEqAbs(lPool.balanceOf(address(escrow)), amount, 1); + assertEq(lPool.balanceOf(investor), 0); + assertEq(lPool.maxMint(investor), (amount - redeemAmount)); + + // Test trigger full redeem (maxMint = redeemAmount), where investor did not mint their tokens - user tokens are still locked in escrow + redeemAmount = uint128(lPool.maxMint(investor)); + centrifugeChain.triggerIncreaseRedeemOrder( + poolId, trancheId, investor, defaultCurrencyId, redeemAmount + ); + assertApproxEqAbs(lPool.balanceOf(address(escrow)), amount, 1); + assertEq(lPool.balanceOf(investor), 0); + assertEq(lPool.maxMint(investor), 0); + + + centrifugeChain.isExecutedCollectRedeem( + lPool.poolId(), + lPool.trancheId(), + bytes32(bytes20(investor)), + defaultCurrencyId, + uint128(amount), + uint128(amount), + uint128(amount) + ); + + assertApproxEqAbs(lPool.balanceOf(address(escrow)), 0, 1); + assertApproxEqAbs(erc20.balanceOf(address(userEscrow)), amount, 1); vm.prank(investor); - lPool.redeem(amount / 2, investor, investor); + lPool.redeem(amount, investor, investor); - assertApproxEqAbs(erc20.balanceOf(investor), investorBalanceBefore + amount / 2, 1); + assertApproxEqAbs(erc20.balanceOf(investor), investorBalanceBefore + amount, 1); } function testPartialExecutions(uint64 poolId, bytes16 trancheId, uint128 currencyId) public { @@ -1400,7 +1487,7 @@ contract LiquidityPoolTest is TestSetup { } // helpers - function deposit(address _lPool, address investor, uint256 amount) public { + function deposit(address _lPool, address investor, uint256 amount, bool claimDeposit) public { LiquidityPool lPool = LiquidityPool(_lPool); erc20.mint(investor, amount); centrifugeChain.updateMember(lPool.poolId(), lPool.trancheId(), investor, type(uint64).max); // add user as member @@ -1421,8 +1508,14 @@ contract LiquidityPoolTest is TestSetup { 0 ); - vm.prank(investor); - lPool.deposit(amount, investor); // withdraw the amount + if (claimDeposit) { + vm.prank(investor); + lPool.deposit(amount, investor); // withdraw the amount + } + } + + function deposit(address _lPool, address investor, uint256 amount) public { + deposit(_lPool, investor, amount, true); } function amountAssumption(uint256 amount) public pure returns (bool) { From 17f202ab70287be34b04f905dab2cad9358374bc Mon Sep 17 00:00:00 2001 From: Jeroen <1748621+hieronx@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:10:14 +0200 Subject: [PATCH 45/51] Fix event callers (#191) --- src/LiquidityPool.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/LiquidityPool.sol b/src/LiquidityPool.sol index 96fda35d..7f5febfa 100644 --- a/src/LiquidityPool.sol +++ b/src/LiquidityPool.sol @@ -126,14 +126,14 @@ contract LiquidityPool is Auth, IERC7540 { function deposit(uint256 assets, address receiver) public returns (uint256 shares) { shares = manager.deposit(address(this), assets, receiver, msg.sender); - emit Deposit(address(this), receiver, assets, shares); + emit Deposit(msg.sender, receiver, assets, shares); } /// @notice Collect shares for deposited assets after Centrifuge epoch execution. /// maxMint is the max amount of shares that can be minted. function mint(uint256 shares, address receiver) public returns (uint256 assets) { assets = manager.mint(address(this), shares, receiver, msg.sender); - emit Deposit(address(this), receiver, assets, shares); + emit Deposit(msg.sender, receiver, assets, shares); } /// @notice maxShares that can be claimed by the receiver after the epoch has been executed on the Centrifuge side. @@ -153,7 +153,7 @@ contract LiquidityPool is Auth, IERC7540 { function withdraw(uint256 assets, address receiver, address owner) public returns (uint256 shares) { require((msg.sender == owner), "LiquidityPool/not-the-owner"); shares = manager.withdraw(address(this), assets, receiver, owner); - emit Withdraw(address(this), receiver, owner, assets, shares); + emit Withdraw(msg.sender, receiver, owner, assets, shares); } /// @notice maxShares that can be redeemed by the owner after redemption was requested @@ -169,7 +169,7 @@ contract LiquidityPool is Auth, IERC7540 { function redeem(uint256 shares, address receiver, address owner) public returns (uint256 assets) { require((msg.sender == owner), "LiquidityPool/not-the-owner"); assets = manager.redeem(address(this), shares, receiver, owner); - emit Withdraw(address(this), receiver, owner, assets, shares); + emit Withdraw(msg.sender, receiver, owner, assets, shares); } // --- ERC-7540 methods --- @@ -192,7 +192,7 @@ contract LiquidityPool is Auth, IERC7540 { _withPermit(asset, owner, address(this), assets, deadline, v, r, s); require(manager.requestDeposit(address(this), assets, owner, owner), "LiquidityPool/request-deposit-failed"); SafeTransferLib.safeTransferFrom(asset, owner, address(escrow), assets); - emit DepositRequest(owner, owner, assets); + emit DepositRequest(msg.sender, owner, assets); } /// @notice View the total amount the operator has requested to deposit but isn't able to deposit or mint yet From 46fe0284d7a75df08dd11bdb428720436f5bcb9b Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Thu, 9 Nov 2023 16:16:20 -0500 Subject: [PATCH 46/51] update for new main branch --- script/Deployer.sol | 6 +++--- src/PoolManager.sol | 1 - .../migrationContracts/MigratedInvestmentManager.sol | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/script/Deployer.sol b/script/Deployer.sol index eb0bbd89..2f1680c8 100644 --- a/script/Deployer.sol +++ b/script/Deployer.sol @@ -46,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/src/PoolManager.sol b/src/PoolManager.sol index 7a3d669f..dedb23fb 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -87,7 +87,6 @@ contract PoolManager is Auth { LiquidityPoolFactoryLike public immutable liquidityPoolFactory; TrancheTokenFactoryLike public immutable trancheTokenFactory; - RestrictionManagerFactoryLike public restrictionManagerFactory; GatewayLike public gateway; InvestmentManagerLike public investmentManager; RestrictionManagerFactoryLike public restrictionManagerFactory; diff --git a/test/migrations/migrationContracts/MigratedInvestmentManager.sol b/test/migrations/migrationContracts/MigratedInvestmentManager.sol index 8bb0eafd..d637f21e 100644 --- a/test/migrations/migrationContracts/MigratedInvestmentManager.sol +++ b/test/migrations/migrationContracts/MigratedInvestmentManager.sol @@ -33,8 +33,8 @@ contract MigratedInvestmentManager is InvestmentManager { uint256 depositPrice, uint128 maxWithdraw, uint256 redeemPrice, - uint128 remainingDepositRequest, - uint128 remainingRedeemRequest, + uint128 pendingDepositRequest, + uint128 pendingRedeemRequest, bool exists ) = oldInvestmentManager.investments(investor, liquidityPool); investments[investor][liquidityPool] = InvestmentState({ @@ -42,8 +42,8 @@ contract MigratedInvestmentManager is InvestmentManager { depositPrice: depositPrice, maxWithdraw: maxWithdraw, redeemPrice: redeemPrice, - remainingDepositRequest: remainingDepositRequest, - remainingRedeemRequest: remainingRedeemRequest, + pendingDepositRequest: pendingDepositRequest, + pendingRedeemRequest: pendingRedeemRequest, exists: exists }); } From 05f5cf6e525f498a34a7b3ffe3905f6b7101f603 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Sat, 11 Nov 2023 15:59:25 -0500 Subject: [PATCH 47/51] add updateLiquidityPool function to poolManager --- src/PoolManager.sol | 7 +++ test/migrations/MigratedLiquidityPool.t.sol | 63 ++----------------- .../MigratedLiquidityPool.sol | 1 - 3 files changed, 13 insertions(+), 58 deletions(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index dedb23fb..80bb5cdf 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -430,6 +430,13 @@ contract PoolManager is Auth { return liquidityPool; } + function updateLiquidityPool(uint64 poolId, bytes16 trancheId, address currency, address liquidityPool) public auth { + require(pools[poolId].createdAt != 0, "PoolManager/pool-does-not-exist"); + Tranche storage tranche = pools[poolId].tranches[trancheId]; + require(tranche.token != address(0), "PoolManager/tranche-does-not-exist"); + tranche.liquidityPools[currency] = liquidityPool; + } + // --- Helpers --- function getTrancheToken(uint64 poolId, bytes16 trancheId) public view returns (address) { Tranche storage tranche = pools[poolId].tranches[trancheId]; diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index 3a550cc2..6396a2e7 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -44,62 +44,8 @@ contract MigrationsTest is InvestRedeemFlow { poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(escrow), address(investmentManager) ); - // set MigratedPoolManager input parameters - 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[][][](1); - liquidityPoolOverrides[0] = new address[][](1); - liquidityPoolOverrides[0][0] = new address[](1); - liquidityPoolOverrides[0][0][0] = address(newLiquidityPool); - - // Deploy new MigratedPoolManager - MigratedPoolManager newPoolManager = new MigratedPoolManager( - address(escrow), - address(newLiquidityPoolFactory), - restrictionManagerFactory, - trancheTokenFactory, - address(poolManager), - poolIds, - trancheIds, - allowedCurrencies, - liquidityPoolCurrencies, - liquidityPoolOverrides - ); - - // rewire migrated pool manager - newLiquidityPoolFactory.rely(address(newPoolManager)); - LiquidityPoolFactory(liquidityPoolFactory).rely(address(newPoolManager)); - TrancheTokenFactory(trancheTokenFactory).rely(address(newPoolManager)); - 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 migrated pool manager - newPoolManager.deny(address(this)); - root.denyContract(address(gateway), address(this)); - root.denyContract(restrictionManagerFactory, address(this)); + root.relyContract(address(poolManager), address(this)); + poolManager.updateLiquidityPool(poolId, trancheId, address(erc20), address(newLiquidityPool)); // Rewire new liquidity pool TrancheTokenLike token = TrancheTokenLike(address(LiquidityPool(_lPool).share())); @@ -108,11 +54,13 @@ contract MigrationsTest is InvestRedeemFlow { token.deny(_lPool); token.addTrustedForwarder(address(newLiquidityPool)); token.removeTrustedForwarder(_lPool); + root.relyContract(address(investmentManager), address(this)); investmentManager.rely(address(newLiquidityPool)); investmentManager.deny(_lPool); newLiquidityPool.rely(address(root)); newLiquidityPool.rely(address(investmentManager)); // escrow.approve(address(token), address(investmentManager), type(uint256).max); + root.relyContract(address(escrow), address(this)); escrow.approve(address(token), address(newLiquidityPool), type(uint256).max); escrow.approve(address(token), _lPool, 0); @@ -128,7 +76,7 @@ contract MigrationsTest is InvestRedeemFlow { // TODO: test that everything is working _lPool = address(newLiquidityPool); - poolManager = newPoolManager; + // poolManager = newPoolManager; verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); } @@ -142,6 +90,7 @@ contract MigrationsTest is InvestRedeemFlow { 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); diff --git a/test/migrations/migrationContracts/MigratedLiquidityPool.sol b/test/migrations/migrationContracts/MigratedLiquidityPool.sol index 28aa0ee4..bf3b8646 100644 --- a/test/migrations/migrationContracts/MigratedLiquidityPool.sol +++ b/test/migrations/migrationContracts/MigratedLiquidityPool.sol @@ -12,6 +12,5 @@ contract MigratedLiquidityPool is LiquidityPool { address escrow_, address investmentManager_ ) LiquidityPool(poolId_, trancheId_, asset_, share_, escrow_, investmentManager_) { - // TODO: migrate state, revoke approvals } } From aa7e514465d44328ce85e05b68c1656a76c3ff9a Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Sat, 11 Nov 2023 16:24:45 -0500 Subject: [PATCH 48/51] add unit tests --- src/PoolManager.sol | 6 ++- test/PoolManager.t.sol | 43 +++++++++++++++++++ .../MigratedLiquidityPool.sol | 3 +- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/PoolManager.sol b/src/PoolManager.sol index 80bb5cdf..8d87ff23 100644 --- a/src/PoolManager.sol +++ b/src/PoolManager.sol @@ -430,8 +430,12 @@ contract PoolManager is Auth { return liquidityPool; } - function updateLiquidityPool(uint64 poolId, bytes16 trancheId, address currency, address liquidityPool) public auth { + function updateLiquidityPool(uint64 poolId, bytes16 trancheId, address currency, address liquidityPool) + public + auth + { require(pools[poolId].createdAt != 0, "PoolManager/pool-does-not-exist"); + require(isAllowedAsInvestmentCurrency(poolId, currency), "PoolManager/currency-not-supported"); Tranche storage tranche = pools[poolId].tranches[trancheId]; require(tranche.token != address(0), "PoolManager/tranche-does-not-exist"); tranche.liquidityPools[currency] = liquidityPool; diff --git a/test/PoolManager.t.sol b/test/PoolManager.t.sol index 96bde22d..f1ad5605 100644 --- a/test/PoolManager.t.sol +++ b/test/PoolManager.t.sol @@ -862,6 +862,49 @@ contract PoolManagerTest is TestSetup { vm.expectRevert(bytes("PoolManager/tranche-does-not-exist")); centrifugeChain.updateTrancheTokenPrice(poolId, trancheId, currencyId, price, uint64(block.timestamp)); } + + function testUpdateLiquidityPool() public { + address lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + address newLiquidityPool = makeAddr("newLiquidityPool"); + poolManager.updateLiquidityPool(lPool.poolId(), lPool.trancheId(), address(erc20), newLiquidityPool); + assertEq(poolManager.getLiquidityPool(lPool.poolId(), lPool.trancheId(), address(erc20)), newLiquidityPool); + } + + function testUpdateLiquidityPoolFailsForNonExistentPool() public { + address lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + address newLiquidityPool = makeAddr("newLiquidityPool"); + uint64 poolId = lPool.poolId(); + bytes16 trancheId = lPool.trancheId(); + address currency = lPool.asset(); + vm.expectRevert(bytes("PoolManager/pool-does-not-exist")); + poolManager.updateLiquidityPool(poolId+1, trancheId, currency, address(newLiquidityPool)); + } + + function testUpdateLiquidityPoolFailsForNonExistentTranche() public { + address lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + address newLiquidityPool = makeAddr("newLiquidityPool"); + uint64 poolId = lPool.poolId(); + bytes16 trancheId = lPool.trancheId(); + address currency = lPool.asset(); + vm.expectRevert(bytes("PoolManager/tranche-does-not-exist")); + poolManager.updateLiquidityPool(poolId, bytes16(0), currency, address(newLiquidityPool)); + } + + function testUpdateLiquidityPoolFailsForNonExistentCurrency() public { + address lPool_ = deploySimplePool(); + LiquidityPool lPool = LiquidityPool(lPool_); + address newLiquidityPool = makeAddr("newLiquidityPool"); + uint64 poolId = lPool.poolId(); + bytes16 trancheId = lPool.trancheId(); + address currency = lPool.asset(); + vm.expectRevert(bytes("PoolManager/currency-not-supported")); + poolManager.updateLiquidityPool(poolId, trancheId, makeAddr("wrong currency"), address(newLiquidityPool)); + } + + // helpers function hasDuplicates(bytes16[4] calldata array) internal pure returns (bool) { uint256 length = array.length; diff --git a/test/migrations/migrationContracts/MigratedLiquidityPool.sol b/test/migrations/migrationContracts/MigratedLiquidityPool.sol index bf3b8646..40019a70 100644 --- a/test/migrations/migrationContracts/MigratedLiquidityPool.sol +++ b/test/migrations/migrationContracts/MigratedLiquidityPool.sol @@ -11,6 +11,5 @@ contract MigratedLiquidityPool is LiquidityPool { address share_, address escrow_, address investmentManager_ - ) LiquidityPool(poolId_, trancheId_, asset_, share_, escrow_, investmentManager_) { - } + ) LiquidityPool(poolId_, trancheId_, asset_, share_, escrow_, investmentManager_) {} } From 183e80b3b30e3f0ddcf9fc1cd1280017b35e3be9 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Sat, 11 Nov 2023 16:26:40 -0500 Subject: [PATCH 49/51] remove solt generate json --- solc-input-tranche.json | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 solc-input-tranche.json diff --git a/solc-input-tranche.json b/solc-input-tranche.json deleted file mode 100644 index 4ba724b8..00000000 --- a/solc-input-tranche.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "language": "Solidity", - "sources": { - "./src/token/Tranche.sol": { - "content": "// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity 0.8.21;\n\nimport {ERC20} from \"./ERC20.sol\";\nimport {IERC20} from \"../interfaces/IERC20.sol\";\n\ninterface TrancheTokenLike is IERC20 {\n function mint(address user, uint256 value) external;\n function burn(address user, uint256 value) external;\n function file(bytes32 what, string memory data) external;\n function file(bytes32 what, address data) external;\n function restrictionManager() external view returns (address);\n function addTrustedForwarder(address forwarder) external;\n function checkTransferRestriction(address from, address to, uint256 value) external view returns (bool);\n}\n\ninterface ERC1404Like {\n function detectTransferRestriction(address from, address to, uint256 value) external view returns (uint8);\n function messageForTransferRestriction(uint8 restrictionCode) external view returns (string memory);\n function SUCCESS_CODE() external view returns (uint8);\n}\n\n/// @title Tranche Token\n/// @notice Extension of ERC20 + ERC1404 for tranche tokens,\n/// which manages the trusted forwarders for the ERC20 token, and ensures\n/// the transfer restrictions as defined in the RestrictionManager.\ncontract TrancheToken is ERC20 {\n ERC1404Like public restrictionManager;\n\n mapping(address => bool) public trustedForwarders;\n\n // --- Events ---\n event File(bytes32 indexed what, address data);\n event AddTrustedForwarder(address indexed trustedForwarder);\n event RemoveTrustedForwarder(address indexed trustedForwarder);\n\n constructor(uint8 decimals_) ERC20(decimals_) {}\n\n modifier restricted(address from, address to, uint256 value) {\n uint8 restrictionCode = detectTransferRestriction(from, to, value);\n require(restrictionCode == SUCCESS_CODE(), messageForTransferRestriction(restrictionCode));\n _;\n }\n\n // --- Administration ---\n function file(bytes32 what, address data) public auth {\n if (what == \"restrictionManager\") restrictionManager = ERC1404Like(data);\n else revert(\"TrancheToken/file-unrecognized-param\");\n emit File(what, data);\n }\n\n function addTrustedForwarder(address trustedForwarder) public auth {\n trustedForwarders[trustedForwarder] = true;\n emit AddTrustedForwarder(trustedForwarder);\n }\n\n function removeTrustedForwarder(address trustedForwarder) public auth {\n trustedForwarders[trustedForwarder] = false;\n emit RemoveTrustedForwarder(trustedForwarder);\n }\n\n // --- ERC20 overrides with restrictions ---\n function transfer(address to, uint256 value) public override restricted(_msgSender(), to, value) returns (bool) {\n return super.transfer(to, value);\n }\n\n function transferFrom(address from, address to, uint256 value)\n public\n override\n restricted(from, to, value)\n returns (bool)\n {\n return super.transferFrom(from, to, value);\n }\n\n function mint(address to, uint256 value) public override restricted(_msgSender(), to, value) {\n return super.mint(to, value);\n }\n\n // --- ERC1404 implementation ---\n function detectTransferRestriction(address from, address to, uint256 value) public view returns (uint8) {\n return restrictionManager.detectTransferRestriction(from, to, value);\n }\n\n function checkTransferRestriction(address from, address to, uint256 value) public view returns (bool) {\n return restrictionManager.detectTransferRestriction(from, to, value) == SUCCESS_CODE();\n }\n\n function messageForTransferRestriction(uint8 restrictionCode) public view returns (string memory) {\n return restrictionManager.messageForTransferRestriction(restrictionCode);\n }\n\n function SUCCESS_CODE() public view returns (uint8) {\n return restrictionManager.SUCCESS_CODE();\n }\n\n // --- ERC2771Context ---\n /// @dev Trusted forwarders can forward custom msg.sender and\n /// msg.data to the underlying ERC20 contract\n function isTrustedForwarder(address forwarder) public view returns (bool) {\n return trustedForwarders[forwarder];\n }\n\n /// @dev Override for `msg.sender`. Defaults to the original `msg.sender` whenever\n /// a call is not performed by the trusted forwarder or the calldata length is less than\n /// 20 bytes (an address length).\n function _msgSender() internal view virtual override returns (address sender) {\n if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) {\n // The assembly code is more direct than the Solidity version using `abi.decode`.\n /// @solidity memory-safe-assembly\n assembly {\n sender := shr(96, calldataload(sub(calldatasize(), 20)))\n }\n } else {\n return super._msgSender();\n }\n }\n}\n" - }, - "./src/token/ERC20.sol": { - "content": "// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity 0.8.21;\n\ninterface IERC1271 {\n function isValidSignature(bytes32, bytes memory) external view returns (bytes4);\n}\n\n/// @title ERC20\n/// @notice Standard ERC20 implementation, with mint/burn functionality and permit logic.\n/// Includes ERC1271 context support to allow multiple trusted forwarders\n/// @author Modified from https://github.com/makerdao/xdomain-dss/blob/master/src/Dai.sol\ncontract ERC20 {\n mapping(address => uint256) public wards;\n\n string public name;\n string public symbol;\n uint8 public immutable decimals;\n uint256 public totalSupply;\n\n mapping(address => uint256) public balanceOf;\n mapping(address => mapping(address => uint256)) public allowance;\n mapping(address => uint256) public nonces;\n\n // --- EIP712 ---\n bytes32 private immutable nameHash;\n bytes32 private immutable versionHash;\n uint256 public immutable deploymentChainId;\n bytes32 private immutable _DOMAIN_SEPARATOR;\n bytes32 public constant PERMIT_TYPEHASH =\n keccak256(\"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\");\n\n // --- Events ---\n event Rely(address indexed user);\n event Deny(address indexed user);\n event File(bytes32 indexed what, string data);\n event Approval(address indexed owner, address indexed spender, uint256 value);\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n constructor(uint8 decimals_) {\n decimals = decimals_;\n wards[_msgSender()] = 1;\n emit Rely(_msgSender());\n\n nameHash = keccak256(bytes(\"Centrifuge\"));\n versionHash = keccak256(bytes(\"1\"));\n deploymentChainId = block.chainid;\n _DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid);\n }\n\n modifier auth() {\n // Custom auth modifier that uses _msgSender()\n require(wards[_msgSender()] == 1, \"Auth/not-authorized\");\n _;\n }\n\n function rely(address user) external auth {\n wards[user] = 1;\n emit Rely(user);\n }\n\n function deny(address user) external auth {\n wards[user] = 0;\n emit Deny(user);\n }\n\n function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) {\n return keccak256(\n abi.encode(\n // keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')\n 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f,\n nameHash,\n versionHash,\n chainId,\n address(this)\n )\n );\n }\n\n function DOMAIN_SEPARATOR() external view returns (bytes32) {\n return block.chainid == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid);\n }\n\n function file(bytes32 what, string memory data) external auth {\n if (what == \"name\") name = data;\n else if (what == \"symbol\") symbol = data;\n else revert(\"ERC20/file-unrecognized-param\");\n emit File(what, data);\n }\n\n // --- ERC20 Mutations ---\n function transfer(address to, uint256 value) public virtual returns (bool) {\n require(to != address(0) && to != address(this), \"ERC20/invalid-address\");\n uint256 balance = balanceOf[_msgSender()];\n require(balance >= value, \"ERC20/insufficient-balance\");\n\n unchecked {\n balanceOf[_msgSender()] = balance - value;\n balanceOf[to] += value;\n }\n\n emit Transfer(_msgSender(), to, value);\n\n return true;\n }\n\n function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {\n require(to != address(0) && to != address(this), \"ERC20/invalid-address\");\n uint256 balance = balanceOf[from];\n require(balance >= value, \"ERC20/insufficient-balance\");\n\n if (from != _msgSender()) {\n uint256 allowed = allowance[from][_msgSender()];\n if (allowed != type(uint256).max) {\n require(allowed >= value, \"ERC20/insufficient-allowance\");\n unchecked {\n allowance[from][_msgSender()] = allowed - value;\n }\n }\n }\n\n unchecked {\n balanceOf[from] = balance - value;\n balanceOf[to] += value;\n }\n\n emit Transfer(from, to, value);\n\n return true;\n }\n\n function approve(address spender, uint256 value) external returns (bool) {\n allowance[_msgSender()][spender] = value;\n\n emit Approval(_msgSender(), spender, value);\n\n return true;\n }\n\n // --- Mint/Burn ---\n function mint(address to, uint256 value) public virtual auth {\n require(to != address(0) && to != address(this), \"ERC20/invalid-address\");\n unchecked {\n // We don't need an overflow check here b/c balanceOf[to] <= totalSupply\n // and there is an overflow check below\n balanceOf[to] = balanceOf[to] + value;\n }\n totalSupply = totalSupply + value;\n\n emit Transfer(address(0), to, value);\n }\n\n function burn(address from, uint256 value) external auth {\n uint256 balance = balanceOf[from];\n require(balance >= value, \"ERC20/insufficient-balance\");\n\n if (from != _msgSender()) {\n uint256 allowed = allowance[from][_msgSender()];\n if (allowed != type(uint256).max) {\n require(allowed >= value, \"ERC20/insufficient-allowance\");\n\n unchecked {\n allowance[from][_msgSender()] = allowed - value;\n }\n }\n }\n\n unchecked {\n // We don't need overflow checks b/c require(balance >= value) and balance <= totalSupply\n balanceOf[from] = balance - value;\n totalSupply = totalSupply - value;\n }\n\n emit Transfer(from, address(0), value);\n }\n\n // --- Approve by signature ---\n function _isValidSignature(address signer, bytes32 digest, bytes memory signature) internal view returns (bool) {\n if (signature.length == 65) {\n bytes32 r;\n bytes32 s;\n uint8 v;\n assembly {\n r := mload(add(signature, 0x20))\n s := mload(add(signature, 0x40))\n v := byte(0, mload(add(signature, 0x60)))\n }\n if (signer == ecrecover(digest, v, r, s)) {\n return true;\n }\n }\n\n (bool success, bytes memory result) =\n signer.staticcall(abi.encodeWithSelector(IERC1271.isValidSignature.selector, digest, signature));\n return (success && result.length == 32 && abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector);\n }\n\n function permit(address owner, address spender, uint256 value, uint256 deadline, bytes memory signature) public {\n require(block.timestamp <= deadline, \"ERC20/permit-expired\");\n require(owner != address(0), \"ERC20/invalid-owner\");\n\n uint256 nonce;\n unchecked {\n nonce = nonces[owner]++;\n }\n\n bytes32 digest = keccak256(\n abi.encodePacked(\n \"\\x19\\x01\",\n block.chainid == deploymentChainId ? _DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid),\n keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline))\n )\n );\n\n require(_isValidSignature(owner, digest, signature), \"ERC20/invalid-permit\");\n\n allowance[owner][spender] = value;\n emit Approval(owner, spender, value);\n }\n\n function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)\n external\n {\n permit(owner, spender, value, deadline, abi.encodePacked(r, s, v));\n }\n\n // --- Fail-safe ---\n function authTransferFrom(address from, address to, uint256 value) public auth returns (bool) {\n require(to != address(0) && to != address(this), \"ERC20/invalid-address\");\n uint256 balance = balanceOf[from];\n require(balance >= value, \"ERC20/insufficient-balance\");\n\n unchecked {\n balanceOf[from] = balance - value;\n balanceOf[to] += value;\n }\n\n emit Transfer(from, to, value);\n\n return true;\n }\n\n // --- ERC1271 context ---\n function _msgSender() internal view virtual returns (address) {\n return msg.sender;\n }\n}\n" - }, - "./src/interfaces/IERC20.sol": { - "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.21;\n\n/// @title IERC20\n/// @dev Interface of the ERC20 standard as defined in the EIP.\n/// @author Modified from OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol)\ninterface IERC20 {\n /**\n * @dev Emitted when `value` tokens are moved from one account (`from`) to\n * another (`to`).\n *\n * Note that `value` may be zero.\n */\n event Transfer(address indexed from, address indexed to, uint256 value);\n\n /**\n * @dev Emitted when the allowance of a `spender` for an `owner` is set by\n * a call to {approve}. `value` is the new allowance.\n */\n event Approval(address indexed owner, address indexed spender, uint256 value);\n\n /**\n * @dev Returns the value of tokens in existence.\n */\n function totalSupply() external view returns (uint256);\n\n /**\n * @dev Returns the value of tokens owned by `account`.\n */\n function balanceOf(address account) external view returns (uint256);\n\n /**\n * @dev Moves a `value` amount of tokens from the caller's account to `to`.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transfer(address to, uint256 value) external returns (bool);\n\n /**\n * @dev Returns the remaining number of tokens that `spender` will be\n * allowed to spend on behalf of `owner` through {transferFrom}. This is\n * zero by default.\n *\n * This value changes when {approve} or {transferFrom} are called.\n */\n function allowance(address owner, address spender) external view returns (uint256);\n\n /**\n * @dev Sets a `value` amount of tokens as the allowance of `spender` over the\n * caller's tokens.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * IMPORTANT: Beware that changing an allowance with this method brings the risk\n * that someone may use both the old and the new allowance by unfortunate\n * transaction ordering. One possible solution to mitigate this race\n * condition is to first reduce the spender's allowance to 0 and set the\n * desired value afterwards:\n * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729\n *\n * Emits an {Approval} event.\n */\n function approve(address spender, uint256 value) external returns (bool);\n\n /**\n * @dev Moves a `value` amount of tokens from `from` to `to` using the\n * allowance mechanism. `value` is then deducted from the caller's\n * allowance.\n *\n * Returns a boolean value indicating whether the operation succeeded.\n *\n * Emits a {Transfer} event.\n */\n function transferFrom(address from, address to, uint256 value) external returns (bool);\n}\n\n/**\n * @dev Interface for the optional metadata functions from the ERC20 standard.\n */\ninterface IERC20Metadata is IERC20 {\n /**\n * @dev Returns the name of the token.\n */\n function name() external view returns (string memory);\n\n /**\n * @dev Returns the symbol of the token.\n */\n function symbol() external view returns (string memory);\n\n /**\n * @dev Returns the decimals places of the token.\n */\n function decimals() external view returns (uint8);\n}\n\n/**\n * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in\n * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612].\n *\n * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by\n * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't\n * need to send a transaction, and thus is not required to hold Ether at all.\n *\n * ==== Security Considerations\n *\n * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature\n * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be\n * considered as an intention to spend the allowance in any specific way. The second is that because permits have\n * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should\n * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be\n * generally recommended is:\n *\n * ```solidity\n * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public {\n * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {}\n * doThing(..., value);\n * }\n *\n * function doThing(..., uint256 value) public {\n * token.safeTransferFrom(msg.sender, address(this), value);\n * ...\n * }\n * ```\n *\n * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of\n * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also\n * {SafeERC20-safeTransferFrom}).\n *\n * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so\n * contracts should have entry points that don't rely on permit.\n */\ninterface IERC20Permit {\n /**\n * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens,\n * given ``owner``'s signed approval.\n *\n * IMPORTANT: The same issues {IERC20-approve} has related to transaction\n * ordering also apply here.\n *\n * Emits an {Approval} event.\n *\n * Requirements:\n *\n * - `spender` cannot be the zero address.\n * - `deadline` must be a timestamp in the future.\n * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`\n * over the EIP712-formatted function arguments.\n * - the signature must use ``owner``'s current nonce (see {nonces}).\n *\n * For more information on the signature format, see the\n * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP\n * section].\n *\n * CAUTION: See Security Considerations above.\n */\n function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)\n external;\n\n /**\n * @dev Returns the current nonce for `owner`. This value must be\n * included whenever a signature is generated for {permit}.\n *\n * Every successful call to {permit} increases ``owner``'s nonce by one. This\n * prevents a signature from being used multiple times.\n */\n function nonces(address owner) external view returns (uint256);\n\n /**\n * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}.\n */\n // solhint-disable-next-line func-name-mixedcase\n function DOMAIN_SEPARATOR() external view returns (bytes32);\n}\n" - } - }, - "settings": { - "metadata": { - "useLiteralContent": true - }, - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "id", - "ast" - ] - } - } - } -} \ No newline at end of file From b3ba524403e36345192f31e3a4cbc50ad431d3db Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Sat, 11 Nov 2023 16:28:48 -0500 Subject: [PATCH 50/51] remove todos --- test/migrations/InvestRedeemFlow.t.sol | 2 +- test/migrations/MigratedAdmin.t.sol | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/migrations/InvestRedeemFlow.t.sol b/test/migrations/InvestRedeemFlow.t.sol index cb68b67e..068a9d02 100644 --- a/test/migrations/InvestRedeemFlow.t.sol +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -34,7 +34,7 @@ contract InvestRedeemFlow is TestSetup { } function verifyInvestAndRedeemFlow(uint64 poolId_, bytes16 trancheId_, address liquidityPool) public { - uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); //TODO: fuzz price + uint128 price = uint128(2 * 10 ** PRICE_DECIMALS); LiquidityPool lPool = LiquidityPool(liquidityPool); depositMint(poolId_, trancheId_, price, investorCurrencyAmount, lPool); diff --git a/test/migrations/MigratedAdmin.t.sol b/test/migrations/MigratedAdmin.t.sol index 160bcdea..ba075f03 100644 --- a/test/migrations/MigratedAdmin.t.sol +++ b/test/migrations/MigratedAdmin.t.sol @@ -37,8 +37,6 @@ contract MigratedAdmin is InvestRedeemFlow { // verify permissions verifyMigratedAdminPermissions(delayedAdmin, newDelayedAdmin, pauseAdmin, newPauseAdmin); - - // TODO: test admin functionality still works } function verifyMigratedAdminPermissions( From 750430c6a715c5bec2e26c17737083e7d156b526 Mon Sep 17 00:00:00 2001 From: Adam Stox Date: Mon, 20 Nov 2023 15:25:34 -0500 Subject: [PATCH 51/51] fix tests --- test/PoolManager.t.sol | 23 -------- test/migrations/InvestRedeemFlow.t.sol | 15 ++--- test/migrations/MigratedGateway.t.sol | 2 +- .../MigratedInvestmentManager.t.sol | 4 +- test/migrations/MigratedLiquidityPool.t.sol | 57 +++++++------------ test/migrations/MigratedPoolManager.t.sol | 10 +++- .../MigratedRestrictionManager.t.sol | 4 +- .../MigratedPoolManager.sol | 3 +- 8 files changed, 40 insertions(+), 78 deletions(-) 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 index 068a9d02..d3c7de6e 100644 --- a/test/migrations/InvestRedeemFlow.t.sol +++ b/test/migrations/InvestRedeemFlow.t.sol @@ -13,19 +13,16 @@ contract InvestRedeemFlow is TestSetup { uint64 poolId; bytes16 trancheId; uint128 currencyId; - uint8 trancheTokenDecimals; - address _lPool; + address lPool_; uint256 investorCurrencyAmount; function setUp() public virtual override { super.setUp(); - poolId = 1; - trancheId = bytes16(hex"811acd5b3f17c06841c7e41e9e04cb1b"); - currencyId = 1; - trancheTokenDecimals = 18; - _lPool = deployLiquidityPool( - poolId, trancheTokenDecimals, erc20.name(), erc20.symbol(), trancheId, currencyId, address(erc20) - ); + 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); diff --git a/test/migrations/MigratedGateway.t.sol b/test/migrations/MigratedGateway.t.sol index 66f48e06..773c3769 100644 --- a/test/migrations/MigratedGateway.t.sol +++ b/test/migrations/MigratedGateway.t.sol @@ -40,7 +40,7 @@ contract MigratedGatewayTest is InvestRedeemFlow { // test that everything is working gateway = newGateway; - verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); } function verifyMigratedGatewayPermissions(Gateway oldGateway, Gateway newGateway) public { diff --git a/test/migrations/MigratedInvestmentManager.t.sol b/test/migrations/MigratedInvestmentManager.t.sol index a8ec1d12..eb662116 100644 --- a/test/migrations/MigratedInvestmentManager.t.sol +++ b/test/migrations/MigratedInvestmentManager.t.sol @@ -26,7 +26,7 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { address[] memory investors = new address[](1); investors[0] = investor; address[] memory liquidityPools = new address[](1); - liquidityPools[0] = _lPool; + liquidityPools[0] = lPool_; // Deploy new MigratedInvestmentManager MigratedInvestmentManager newInvestmentManager = @@ -77,7 +77,7 @@ contract MigratedInvestmentManagerTest is InvestRedeemFlow { verifyMigratedInvestmentManagerPermissions(investmentManager, newInvestmentManager); investmentManager = newInvestmentManager; - verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); } function verifyMigratedInvestmentManagerPermissions( diff --git a/test/migrations/MigratedLiquidityPool.t.sol b/test/migrations/MigratedLiquidityPool.t.sol index 6396a2e7..f9493fb9 100644 --- a/test/migrations/MigratedLiquidityPool.t.sol +++ b/test/migrations/MigratedLiquidityPool.t.sol @@ -28,56 +28,39 @@ contract MigrationsTest is InvestRedeemFlow { } function testLiquidityPoolMigration() public { - // Simulate intended upgrade flow centrifugeChain.incomingScheduleUpgrade(address(this)); vm.warp(block.timestamp + 3 days); root.executeScheduledRely(address(this)); - // deploy new liquidityPoolFactory + 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(root)); + 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 - MigratedLiquidityPool newLiquidityPool = new MigratedLiquidityPool( - poolId, trancheId, address(erc20), address(LiquidityPool(_lPool).share()), address(escrow), address(investmentManager) - ); + address newLiquidityPool = poolManager.deployLiquidityPool(poolId, trancheId, currency); + assertEq(poolManager.getLiquidityPool(poolId, trancheId, currency), newLiquidityPool); - root.relyContract(address(poolManager), address(this)); - poolManager.updateLiquidityPool(poolId, trancheId, address(erc20), address(newLiquidityPool)); - - // Rewire new liquidity pool - TrancheTokenLike token = TrancheTokenLike(address(LiquidityPool(_lPool).share())); - root.relyContract(address(token), address(this)); - token.rely(address(newLiquidityPool)); - token.deny(_lPool); - token.addTrustedForwarder(address(newLiquidityPool)); - token.removeTrustedForwarder(_lPool); - root.relyContract(address(investmentManager), address(this)); - investmentManager.rely(address(newLiquidityPool)); - investmentManager.deny(_lPool); - newLiquidityPool.rely(address(root)); - newLiquidityPool.rely(address(investmentManager)); - // escrow.approve(address(token), address(investmentManager), type(uint256).max); - root.relyContract(address(escrow), address(this)); - escrow.approve(address(token), address(newLiquidityPool), type(uint256).max); - escrow.approve(address(token), _lPool, 0); - - // clean up new liquidity pool - newLiquidityPool.deny(address(this)); - root.denyContract(address(token), address(this)); - root.denyContract(address(investmentManager), address(this)); - root.denyContract(address(escrow), address(this)); - root.deny(address(this)); + root.denyContract(address(poolManager), address(this)); + root.denyContract(address(newLiquidityPoolFactory), address(this)); // verify permissions - verifyLiquidityPoolPermissions(LiquidityPool(_lPool), newLiquidityPool); + verifyLiquidityPoolPermissions(LiquidityPool(lPool_), LiquidityPool(newLiquidityPool)); - // TODO: test that everything is working - _lPool = address(newLiquidityPool); - // poolManager = newPoolManager; - verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + lPool_ = address(newLiquidityPool); + verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); } // --- Permissions & Dependencies Checks --- diff --git a/test/migrations/MigratedPoolManager.t.sol b/test/migrations/MigratedPoolManager.t.sol index d0b665de..63c4f0a1 100644 --- a/test/migrations/MigratedPoolManager.t.sol +++ b/test/migrations/MigratedPoolManager.t.sol @@ -2,6 +2,8 @@ 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"; @@ -95,7 +97,9 @@ contract MigrationsTest is InvestRedeemFlow { // test that everything is working poolManager = newPoolManager; centrifugeChain.addPool(poolId + 1); // add pool - centrifugeChain.addTranche(poolId + 1, trancheId, "Test Token 2", "TT2", trancheTokenDecimals); // add tranche + 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)); @@ -174,9 +178,9 @@ contract MigrationsTest is InvestRedeemFlow { PoolManager newPoolManager ) public { for (uint256 i = 0; i < trancheIds.length; i++) { - (uint8 oldDecimals, string memory oldTokenName, string memory oldTokenSymbol) = + (uint8 oldDecimals, string memory oldTokenName, string memory oldTokenSymbol, uint8 oldRestrictionSet) = poolManager.undeployedTranches(poolId, trancheIds[i]); - (uint8 newDecimals, string memory newTokenName, string memory newTokenSymbol) = + (uint8 newDecimals, string memory newTokenName, string memory newTokenSymbol, uint8 newRestrictionSet) = newPoolManager.undeployedTranches(poolId, trancheIds[i]); assertEq(newDecimals, oldDecimals); assertEq(newTokenName, oldTokenName); diff --git a/test/migrations/MigratedRestrictionManager.t.sol b/test/migrations/MigratedRestrictionManager.t.sol index 3cdafb18..9ac43751 100644 --- a/test/migrations/MigratedRestrictionManager.t.sol +++ b/test/migrations/MigratedRestrictionManager.t.sol @@ -24,7 +24,7 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { address[] memory restrictionManagerWards = new address[](1); restrictionManagerWards[0] = address(poolManager); - address token = address(LiquidityPool(_lPool).share()); + address token = address(LiquidityPool(lPool_).share()); RestrictionManager oldRestrictionManager = RestrictionManager(TrancheTokenLike(token).restrictionManager()); // Deploy new RestrictionManagerFactory @@ -65,7 +65,7 @@ contract MigratedRestrictionManagerTest is InvestRedeemFlow { // TODO: test that everything is working // restrictionManager = newRestrictionManager; - // verifyInvestAndRedeemFlow(poolId, trancheId, _lPool); + // verifyInvestAndRedeemFlow(poolId, trancheId, lPool_); } function verifyMigratedRestrictionManagerPermissions( diff --git a/test/migrations/migrationContracts/MigratedPoolManager.sol b/test/migrations/migrationContracts/MigratedPoolManager.sol index 2bd194bf..2bea96ce 100644 --- a/test/migrations/migrationContracts/MigratedPoolManager.sol +++ b/test/migrations/migrationContracts/MigratedPoolManager.sol @@ -98,11 +98,12 @@ contract MigratedPoolManager is PoolManager { { for (uint256 j = 0; j < trancheIds.length; j++) { bytes16 trancheId = trancheIds[j]; - (uint8 decimals, string memory tokenName, string memory tokenSymbol) = + (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; } } }