diff --git a/Makefile b/Makefile index 3bc53220d..26c8f9105 100644 --- a/Makefile +++ b/Makefile @@ -121,7 +121,7 @@ build-sizes: ## Builds the project and shows sizes .PHONY: test-vvv test-vvv: ## Runs tests with verbose output - forge test --match-test test_crossChainRebalance_updateSuperformData_allErrors --evm-version cancun -vvv + forge test --match-test test_rebalanceMultiPositions_tokenRefunds_interimDust_allowanceNot0 --evm-version cancun -vvvvv .PHONY: ftest ftest: ## Runs tests with cancun evm version diff --git a/src/interfaces/ISuperformRouterPlus.sol b/src/interfaces/ISuperformRouterPlus.sol index 97f417059..005304367 100644 --- a/src/interfaces/ISuperformRouterPlus.sol +++ b/src/interfaces/ISuperformRouterPlus.sol @@ -49,9 +49,15 @@ interface ISuperformRouterPlus is IBaseSuperformRouterPlus { /// @notice thrown if the amount of assets received is lower than the slippage error ASSETS_RECEIVED_OUT_OF_SLIPPAGE(); + /// @notice thrown if the slippage is invalid + error INVALID_GLOBAL_SLIPPAGE(); + /// @notice thrown if the tolerance is exceeded during shares redemption error TOLERANCE_EXCEEDED(); + /// @notice thrown if the amountIn is not equal or lower than the balance available + error AMOUNT_IN_NOT_EQUAL_OR_LOWER_THAN_BALANCE(); + ////////////////////////////////////////////////////////////// // EVENTS // ////////////////////////////////////////////////////////////// @@ -230,4 +236,9 @@ interface ISuperformRouterPlus is IBaseSuperformRouterPlus { /// @dev Forwards dust to Paymaster /// @param token_ the token to forward function forwardDustToPaymaster(address token_) external; + + /// @dev only callable by Emergency Admin + /// @notice sets the global slippage for all rebalances + /// @param slippage_ The slippage tolerance for same chain rebalances + function setGlobalSlippage(uint256 slippage_) external; } diff --git a/src/router-plus/BaseSuperformRouterPlus.sol b/src/router-plus/BaseSuperformRouterPlus.sol index eea99f427..294c1e18c 100644 --- a/src/router-plus/BaseSuperformRouterPlus.sol +++ b/src/router-plus/BaseSuperformRouterPlus.sol @@ -6,6 +6,7 @@ import { IERC1155Receiver } from "openzeppelin-contracts/contracts/token/ERC1155 import { IERC165 } from "openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; import { IERC20 } from "openzeppelin-contracts/contracts/interfaces/IERC20.sol"; import { SafeERC20 } from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import { ISuperRBAC } from "src/interfaces/ISuperRBAC.sol"; import { ISuperRegistry } from "src/interfaces/ISuperRegistry.sol"; import { IBaseSuperformRouterPlus } from "src/interfaces/IBaseSuperformRouterPlus.sol"; @@ -143,4 +144,12 @@ abstract contract BaseSuperformRouterPlus is IBaseSuperformRouterPlus, IERC1155R function _getAddress(bytes32 id_) internal view returns (address) { return superRegistry.getAddress(id_); } + + /// @dev returns if an address has a specific role + /// @param id_ the role id + /// @param addressToCheck_ the address to check + /// @return true if the address has the role, false otherwise + function _hasRole(bytes32 id_, address addressToCheck_) internal view returns (bool) { + return ISuperRBAC(superRegistry.getAddress(keccak256("SUPER_RBAC"))).hasRole(id_, addressToCheck_); + } } diff --git a/src/router-plus/SuperformRouterPlus.sol b/src/router-plus/SuperformRouterPlus.sol index 3f3cedfac..a3d379e45 100644 --- a/src/router-plus/SuperformRouterPlus.sol +++ b/src/router-plus/SuperformRouterPlus.sol @@ -18,6 +18,8 @@ import { import { IBaseRouter } from "src/interfaces/IBaseRouter.sol"; import { ISuperformRouterPlus, IERC20 } from "src/interfaces/ISuperformRouterPlus.sol"; import { ISuperformRouterPlusAsync } from "src/interfaces/ISuperformRouterPlusAsync.sol"; +import { LiqRequest } from "src/types/DataTypes.sol"; +import { IBridgeValidator } from "src/interfaces/IBridgeValidator.sol"; /// @title SuperformRouterPlus /// @dev Performs rebalances and deposits on the Superform platform @@ -25,6 +27,7 @@ import { ISuperformRouterPlusAsync } from "src/interfaces/ISuperformRouterPlusAs contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { using SafeERC20 for IERC20; + uint256 public GLOBAL_SLIPPAGE; uint256 public ROUTER_PLUS_PAYLOAD_ID; /// @dev Tolerance constant to account for tokens with rounding issues on transfer @@ -34,7 +37,10 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { // CONSTRUCTOR // ////////////////////////////////////////////////////////////// - constructor(address superRegistry_) BaseSuperformRouterPlus(superRegistry_) { } + constructor(address superRegistry_) BaseSuperformRouterPlus(superRegistry_) { + /// @dev default to 0.1% slippage as a start + GLOBAL_SLIPPAGE = 10; + } ////////////////////////////////////////////////////////////// // EXTERNAL WRITE FUNCTIONS // @@ -357,9 +363,8 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { /// @inheritdoc ISuperformRouterPlus function deposit4626(address[] calldata vaults_, Deposit4626Args[] calldata args) external payable { - uint256 length = vaults_.length; - + if (length != args.length) { revert Error.ARRAY_LENGTH_MISMATCH(); } @@ -390,6 +395,19 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { } } + /// @inheritdoc ISuperformRouterPlus + function setGlobalSlippage(uint256 slippage_) external { + if (!_hasRole(keccak256("EMERGENCY_ADMIN_ROLE"), msg.sender)) { + revert Error.NOT_PRIVILEGED_CALLER(keccak256("EMERGENCY_ADMIN_ROLE")); + } + + if (slippage_ > ENTIRE_SLIPPAGE || slippage_ == 0) { + revert INVALID_GLOBAL_SLIPPAGE(); + } + + GLOBAL_SLIPPAGE = slippage_; + } + ////////////////////////////////////////////////////////////// // INTERNAL FUNCTIONS // ////////////////////////////////////////////////////////////// @@ -439,6 +457,7 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { if (req.superformData.liqRequests[i].liqDstChainId != CHAIN_ID) { revert REBALANCE_MULTI_POSITIONS_DIFFERENT_CHAIN(); } + if (req.superformData.amounts[i] != args.sharesToRedeem[i]) { revert REBALANCE_MULTI_POSITIONS_DIFFERENT_AMOUNTS(); } @@ -450,23 +469,29 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { /// @dev send SPs to router _callSuperformRouter(router_, callData, args.rebalanceFromMsgValue); - uint256 amountToDeposit = interimAsset.balanceOf(address(this)) - args.balanceBefore; + uint256 availableBalanceToDeposit = interimAsset.balanceOf(address(this)) - args.balanceBefore; - if (amountToDeposit == 0) revert Error.ZERO_AMOUNT(); + if (availableBalanceToDeposit == 0) revert Error.ZERO_AMOUNT(); if ( - ENTIRE_SLIPPAGE * amountToDeposit + ENTIRE_SLIPPAGE * availableBalanceToDeposit < ((args.expectedAmountToReceivePostRebalanceFrom * (ENTIRE_SLIPPAGE - args.slippage))) ) { revert Error.VAULT_IMPLEMENTATION_FAILED(); } - /// @dev step 3: rebalance into a new superform with rebalanceCallData - if (!whitelistedSelectors[Actions.DEPOSIT][_parseSelectorMem(rebalanceToCallData)]) { - revert INVALID_DEPOSIT_SELECTOR(); - } + uint256 amountIn = _validateAndGetAmountIn(rebalanceToCallData, availableBalanceToDeposit); + + _deposit(router_, interimAsset, amountIn, args.rebalanceToMsgValue, rebalanceToCallData); + } - _deposit(router_, interimAsset, amountToDeposit, args.rebalanceToMsgValue, rebalanceToCallData); + function _takeAmountIn(LiqRequest memory liqReq, uint256 sfDataAmount) internal view returns (uint256 amountIn) { + bytes memory txData = liqReq.txData; + if (txData.length == 0) { + amountIn = sfDataAmount; + } else { + amountIn = IBridgeValidator(superRegistry.getBridgeValidator(liqReq.bridgeId)).decodeAmountIn(txData, false); + } } function _transferSuperPositions( @@ -523,9 +548,7 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { if (assets < TOLERANCE_CONSTANT || balanceDifference < assets - TOLERANCE_CONSTANT) revert TOLERANCE_EXCEEDED(); /// @dev validate the slippage - if ( - (ENTIRE_SLIPPAGE * assets < ((expectedOutputAmount_ * (ENTIRE_SLIPPAGE - maxSlippage_)))) - ) { + if ((ENTIRE_SLIPPAGE * assets < ((expectedOutputAmount_ * (ENTIRE_SLIPPAGE - maxSlippage_))))) { revert ASSETS_RECEIVED_OUT_OF_SLIPPAGE(); } } @@ -604,7 +627,7 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { /// @notice deposits ERC4626 vault shares into superform /// @param vault_ The ERC4626 vault to redeem from /// @param args Rest of the arguments to deposit 4626 - function _deposit4626(address vault_, Deposit4626Args calldata args, uint256 arrayLength) internal { + function _deposit4626(address vault_, Deposit4626Args calldata args, uint256 arrayLength) internal { _transferERC20In(IERC20(vault_), args.receiverAddressSP, args.amount); IERC4626 vault = IERC4626(vault_); address assetAdr = vault.asset(); @@ -614,12 +637,88 @@ contract SuperformRouterPlus is ISuperformRouterPlus, BaseSuperformRouterPlus { uint256 amountRedeemed = _redeemShare(vault, assetAdr, args.amount, args.expectedOutputAmount, args.maxSlippage); + uint256 amountIn = _validateAndGetAmountIn(args.depositCallData, amountRedeemed); + uint256 msgValue = msg.value / arrayLength; address router = _getAddress(keccak256("SUPERFORM_ROUTER")); - _deposit(router, asset, amountRedeemed, msgValue, args.depositCallData); + + _deposit(router, asset, amountIn, msgValue, args.depositCallData); _tokenRefunds(router, assetAdr, args.receiverAddressSP, balanceBefore); emit Deposit4626Completed(args.receiverAddressSP, vault_); } + + function _validateAndGetAmountIn( + bytes calldata rebalanceToCallData, + uint256 availableBalanceToDeposit + ) + internal + view + returns (uint256 amountIn) + { + bytes4 rebalanceToSelector = _parseSelectorMem(rebalanceToCallData); + + if (!whitelistedSelectors[Actions.DEPOSIT][rebalanceToSelector]) { + revert INVALID_DEPOSIT_SELECTOR(); + } + + uint256 amountInTemp; + + if (rebalanceToSelector == IBaseRouter.singleDirectSingleVaultDeposit.selector) { + SingleVaultSFData memory sfData = + abi.decode(_parseCallData(rebalanceToCallData), (SingleDirectSingleVaultStateReq)).superformData; + amountIn = _takeAmountIn(sfData.liqRequest, sfData.amount); + } else if (rebalanceToSelector == IBaseRouter.singleXChainSingleVaultDeposit.selector) { + SingleVaultSFData memory sfData = + abi.decode(_parseCallData(rebalanceToCallData), (SingleXChainSingleVaultStateReq)).superformData; + amountIn = _takeAmountIn(sfData.liqRequest, sfData.amount); + } else if (rebalanceToSelector == IBaseRouter.singleDirectMultiVaultDeposit.selector) { + MultiVaultSFData memory sfData = + abi.decode(_parseCallData(rebalanceToCallData), (SingleDirectMultiVaultStateReq)).superformData; + uint256 len = sfData.liqRequests.length; + + for (uint256 i; i < len; ++i) { + amountInTemp = _takeAmountIn(sfData.liqRequests[i], sfData.amounts[i]); + amountIn += amountInTemp; + } + } else if (rebalanceToSelector == IBaseRouter.singleXChainMultiVaultDeposit.selector) { + MultiVaultSFData memory sfData = + abi.decode(_parseCallData(rebalanceToCallData), (SingleXChainMultiVaultStateReq)).superformsData; + uint256 len = sfData.liqRequests.length; + for (uint256 i; i < len; ++i) { + amountInTemp = _takeAmountIn(sfData.liqRequests[i], sfData.amounts[i]); + amountIn += amountInTemp; + } + } else if (rebalanceToSelector == IBaseRouter.multiDstSingleVaultDeposit.selector) { + SingleVaultSFData[] memory sfData = + abi.decode(_parseCallData(rebalanceToCallData), (MultiDstSingleVaultStateReq)).superformsData; + uint256 lenDst = sfData.length; + for (uint256 i; i < lenDst; ++i) { + amountInTemp = _takeAmountIn(sfData[i].liqRequest, sfData[i].amount); + amountIn += amountInTemp; + } + } else if (rebalanceToSelector == IBaseRouter.multiDstMultiVaultDeposit.selector) { + MultiVaultSFData[] memory sfData = + abi.decode(_parseCallData(rebalanceToCallData), (MultiDstMultiVaultStateReq)).superformsData; + uint256 lenDst = sfData.length; + for (uint256 i; i < lenDst; ++i) { + uint256 len = sfData[i].liqRequests.length; + for (uint256 j; j < len; ++j) { + amountInTemp = _takeAmountIn(sfData[i].liqRequests[j], sfData[i].amounts[j]); + amountIn += amountInTemp; + } + } + } + + /// @dev amountIn must be artificially off-chain reduced to be less than availableBalanceToDeposit otherwise the + /// @dev approval to transfer tokens to SuperformRouter won't work + if (amountIn > availableBalanceToDeposit) revert AMOUNT_IN_NOT_EQUAL_OR_LOWER_THAN_BALANCE(); + + /// @dev check amountIn against availableBalanceToDeposit (available balance) via a GLOBAL_SLIPPAGE to prevent a + /// @dev malicious keeper from sending a low amountIn + if (ENTIRE_SLIPPAGE * amountIn < ((availableBalanceToDeposit * (ENTIRE_SLIPPAGE - GLOBAL_SLIPPAGE)))) { + revert ASSETS_RECEIVED_OUT_OF_SLIPPAGE(); + } + } } diff --git a/src/router-plus/SuperformRouterPlusAsync.sol b/src/router-plus/SuperformRouterPlusAsync.sol index 2290bfd12..09968d473 100644 --- a/src/router-plus/SuperformRouterPlusAsync.sol +++ b/src/router-plus/SuperformRouterPlusAsync.sol @@ -17,7 +17,8 @@ import { } from "src/router-plus/BaseSuperformRouterPlus.sol"; import { IBaseRouter } from "src/interfaces/IBaseRouter.sol"; import { ISuperformRouterPlusAsync } from "src/interfaces/ISuperformRouterPlusAsync.sol"; -import { ISuperRBAC } from "src/interfaces/ISuperRBAC.sol"; +import { SuperformFactory } from "src/SuperformFactory.sol"; + /// @title SuperformRouterPlusAsync /// @dev Completes the async step of cross chain rebalances and separates the balance from SuperformRouterPlus @@ -527,12 +528,6 @@ contract SuperformRouterPlusAsync is ISuperformRouterPlusAsync, BaseSuperformRou return sfData; } - /// @dev returns if an address has a specific role - - function _hasRole(bytes32 id_, address addressToCheck_) internal view returns (bool) { - return ISuperRBAC(superRegistry.getAddress(keccak256("SUPER_RBAC"))).hasRole(id_, addressToCheck_); - } - function _castToMultiVaultData(SingleVaultSFData memory data_) internal pure diff --git a/test/fuzz/crosschain-data/adapters/HyperlaneImplementation.t.sol b/test/fuzz/crosschain-data/adapters/HyperlaneImplementation.t.sol index 9d5eb1436..d919b8a6e 100644 --- a/test/fuzz/crosschain-data/adapters/HyperlaneImplementation.t.sol +++ b/test/fuzz/crosschain-data/adapters/HyperlaneImplementation.t.sol @@ -107,19 +107,17 @@ contract HyperlaneImplementationTest is CommonProtocolActions { vm.startPrank(deployer); uint256 userIndex = userSeed_ % users.length; - - vm.assume(malice_ != getContract(ETH, "CoreStateRegistry")); - vm.assume(malice_ != getContract(ETH, "TimelockStateRegistry")); - vm.assume(malice_ != getContract(ETH, "BroadcastRegistry")); - vm.assume(malice_ != getContract(ETH, "AsyncStateRegistry")); - AMBMessage memory ambMessage; BroadCastAMBExtraData memory ambExtraData; address coreStateRegistry; (ambMessage, ambExtraData, coreStateRegistry) = setupBroadcastPayloadAMBData(users[userIndex], address(hyperlaneImplementation)); - vm.stopPrank(); + + vm.assume(malice_ != getContract(ETH, "CoreStateRegistry")); + vm.assume(malice_ != getContract(ETH, "TimelockStateRegistry")); + vm.assume(malice_ != getContract(ETH, "BroadcastRegistry")); + vm.assume(malice_ != getContract(ETH, "AsyncStateRegistry")); vm.deal(malice_, 100 ether); vm.prank(malice_); diff --git a/test/mainnet/SmokeTest.Staging.t.sol b/test/mainnet/SmokeTest.Staging.t.sol index 46ea05041..be3605b83 100644 --- a/test/mainnet/SmokeTest.Staging.t.sol +++ b/test/mainnet/SmokeTest.Staging.t.sol @@ -568,8 +568,6 @@ contract SmokeTestStaging is MainnetBaseSetup { } function test_asyncStateRegistry() public { - AsyncStateRegistry asyncStateRegistry; - for (uint256 i; i < TARGET_DEPLOYMENT_CHAINS.length; ++i) { uint64 chainId = TARGET_DEPLOYMENT_CHAINS[i]; vm.selectFork(FORKS[chainId]); diff --git a/test/unit/router-plus/SuperformRouterPlus.t.sol b/test/unit/router-plus/SuperformRouterPlus.t.sol index 8e029af84..4c085e773 100644 --- a/test/unit/router-plus/SuperformRouterPlus.t.sol +++ b/test/unit/router-plus/SuperformRouterPlus.t.sol @@ -8,6 +8,14 @@ import { ISuperformRouterPlusAsync } from "src/interfaces/ISuperformRouterPlusAs import { IBaseSuperformRouterPlus } from "src/interfaces/IBaseSuperformRouterPlus.sol"; import { IBaseRouter } from "src/interfaces/IBaseRouter.sol"; import { ERC20 } from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { + MultiVaultSFData, + MultiDstMultiVaultStateReq, + MultiDstSingleVaultStateReq, + SingleXChainMultiVaultStateReq +} from "src/types/DataTypes.sol"; +import "forge-std/console2.sol"; + import { IERC4626 } from "openzeppelin-contracts/contracts/interfaces/IERC4626.sol"; contract RejectEther { @@ -489,7 +497,7 @@ contract SuperformRouterPlusTest is ProtocolActions { // Prepare deposit4626 args ISuperformRouterPlus.Deposit4626Args[] memory argsArray = new ISuperformRouterPlus.Deposit4626Args[](1); - + argsArray[0] = ISuperformRouterPlus.Deposit4626Args({ amount: vaultTokenAmount, expectedOutputAmount: daiAmount, @@ -537,29 +545,29 @@ contract SuperformRouterPlusTest is ProtocolActions { // Approve and deposit DAI into the first mock vault MockERC20(getContract(SOURCE_CHAIN, "DAI")).approve(address(mockVault1), daiAmount); - uint256 vaultTokenAmount1 = mockVault1.deposit(daiAmount/2, deployer); + uint256 vaultTokenAmount1 = mockVault1.deposit(daiAmount / 2, deployer); // Approve and deposit DAI into the secondmock vault MockERC20(getContract(SOURCE_CHAIN, "DAI")).approve(address(mockVault2), daiAmount); - uint256 vaultTokenAmount2 = mockVault2.deposit(daiAmount/2, deployer); + uint256 vaultTokenAmount2 = mockVault2.deposit(daiAmount / 2, deployer); // Prepare deposit4626 args for both deposits ISuperformRouterPlus.Deposit4626Args[] memory argsArray = new ISuperformRouterPlus.Deposit4626Args[](2); argsArray[0] = ISuperformRouterPlus.Deposit4626Args({ amount: vaultTokenAmount1, - expectedOutputAmount: daiAmount/2, + expectedOutputAmount: daiAmount / 2, maxSlippage: 100, // 1% receiverAddressSP: deployer, - depositCallData: _buildDepositCallData(superformId1, daiAmount/2) + depositCallData: _buildDepositCallData(superformId1, daiAmount / 2) }); argsArray[1] = ISuperformRouterPlus.Deposit4626Args({ amount: vaultTokenAmount2, - expectedOutputAmount: daiAmount/2, - maxSlippage: 100, // 1% - receiverAddressSP: deployer, - depositCallData: _buildDepositCallData(superformId1, daiAmount/2) + expectedOutputAmount: daiAmount / 2, + maxSlippage: 100, // 1% + receiverAddressSP: deployer, + depositCallData: _buildDepositCallData(superformId1, daiAmount / 2) }); // Approve RouterPlus to spend vault tokens @@ -635,12 +643,491 @@ contract SuperformRouterPlusTest is ProtocolActions { vm.stopPrank(); } + function test_rebalanceSinglePosition_invalidDepositSelector() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToOneVaultArgs(deployer); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + + args.rebalanceToCallData = abi.encodeWithSelector(bytes4(keccak256("invalidSelector()"))); + + vm.expectRevert(ISuperformRouterPlus.INVALID_DEPOSIT_SELECTOR.selector); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + + vm.stopPrank(); + } + + function test_rebalanceSinglePosition_noSwapData() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToOneVaultArgs(deployer); + SingleVaultSFData memory sfData = + abi.decode(_parseCallData(args.rebalanceToCallData), (SingleDirectSingleVaultStateReq)).superformData; + bytes memory emptyData; + sfData.liqRequest.txData = emptyData; + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), 0); + + assertGt(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2), 0); + } + + function test_rebalanceSinglePosition_0Amount() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToOneVaultArgs(deployer); + + bytes memory emptyData; + + SingleVaultSFData memory sfDataRebalanceFrom = + abi.decode(_parseCallData(args.callData), (SingleDirectSingleVaultStateReq)).superformData; + sfDataRebalanceFrom.liqRequest.txData = emptyData; + + args.callData = abi.encodeCall( + IBaseRouter.singleDirectSingleVaultWithdraw, SingleDirectSingleVaultStateReq(sfDataRebalanceFrom) + ); + + SingleVaultSFData memory sfDataRebalanceTo = + abi.decode(_parseCallData(args.rebalanceToCallData), (SingleDirectSingleVaultStateReq)).superformData; + sfDataRebalanceTo.liqRequest.txData = emptyData; + + args.rebalanceToCallData = abi.encodeCall( + IBaseRouter.singleDirectSingleVaultDeposit, SingleDirectSingleVaultStateReq(sfDataRebalanceTo) + ); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + // mock interim asset to have a balance of 0 on router plus source + vm.mockCall( + args.interimAsset, + abi.encodeWithSelector(IERC20.balanceOf.selector, address(ROUTER_PLUS_SOURCE)), + abi.encode(0) + ); + vm.expectRevert(Error.ZERO_AMOUNT.selector); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + } + + function test_rebalanceSinglePosition_singleDirectSingleVaultDepositSelector() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToOneVaultArgs(deployer); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + + vm.stopPrank(); + + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), 0); + } + + function test_revert_AMOUNT_IN_NOT_EQUAL_OR_LOWER_THAN_BALANCE() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToOneVaultArgs(deployer); + + SingleVaultSFData memory sfDataRebalanceTo = + abi.decode(_parseCallData(args.rebalanceToCallData), (SingleDirectSingleVaultStateReq)).superformData; + sfDataRebalanceTo.amount = 1e30; + + args.rebalanceToCallData = abi.encodeCall( + IBaseRouter.singleDirectSingleVaultDeposit, SingleDirectSingleVaultStateReq(sfDataRebalanceTo) + ); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + + vm.expectRevert(ISuperformRouterPlus.AMOUNT_IN_NOT_EQUAL_OR_LOWER_THAN_BALANCE.selector); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + } + + function test_revert_ASSETS_RECEIVED_OUT_OF_SLIPPAGE() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e6); + + (ISuperformRouterPlus.RebalanceMultiPositionsSyncArgs memory args, uint256 totalAmountToDeposit) = + _buildRebalanceTwoPositionsToOneVaultXChainArgs(); + + SingleVaultSFData memory sfDataRebalanceTo = + abi.decode(_parseCallData(args.rebalanceToCallData), (SingleXChainSingleVaultStateReq)).superformData; + + /// @dev keeper attempting to rug the user by reducing amount in + sfDataRebalanceTo.liqRequest.txData = _buildLiqBridgeTxData( + LiqBridgeTxDataArgs( + 1, + args.interimAsset, + getContract(OP, "DAI"), + getContract(OP, "DAI"), + getContract(SOURCE_CHAIN, "SuperformRouter"), + SOURCE_CHAIN, + OP, + OP, + false, + getContract(OP, "CoreStateRegistry"), + uint256(OP), + totalAmountToDeposit - 5e5, + false, + 0, + 1, + 1, + 1, + address(0) + ), + false + ); + + args.rebalanceToCallData = abi.encodeCall( + IBaseRouter.singleXChainSingleVaultDeposit, SingleXChainSingleVaultStateReq(AMBs, OP, sfDataRebalanceTo) + ); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance( + ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem[0] + ); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance( + ROUTER_PLUS_SOURCE, superformId2, args.sharesToRedeem[1] + ); + vm.expectRevert(ISuperformRouterPlus.ASSETS_RECEIVED_OUT_OF_SLIPPAGE.selector); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceMultiPositions{ value: 2 ether }(args); + } + + function test_rebalanceMultiPositions_tokenRefunds_interimDust() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e6); + + (ISuperformRouterPlus.RebalanceMultiPositionsSyncArgs memory args, uint256 totalAmountToDeposit) = + _buildRebalanceTwoPositionsToOneVaultXChainArgs(); + + SingleVaultSFData memory sfDataRebalanceTo = + abi.decode(_parseCallData(args.rebalanceToCallData), (SingleXChainSingleVaultStateReq)).superformData; + + /// @dev keeper attempting to rug the user by reducing amount in + sfDataRebalanceTo.liqRequest.txData = _buildLiqBridgeTxData( + LiqBridgeTxDataArgs( + 1, + args.interimAsset, + getContract(OP, "DAI"), + getContract(OP, "DAI"), + getContract(SOURCE_CHAIN, "SuperformRouter"), + SOURCE_CHAIN, + OP, + OP, + false, + getContract(OP, "CoreStateRegistry"), + uint256(OP), + totalAmountToDeposit - 1e4, + false, + 0, + 1, + 1, + 1, + address(0) + ), + false + ); + + args.rebalanceToCallData = abi.encodeCall( + IBaseRouter.singleXChainSingleVaultDeposit, SingleXChainSingleVaultStateReq(AMBs, OP, sfDataRebalanceTo) + ); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance( + ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem[0] + ); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance( + ROUTER_PLUS_SOURCE, superformId2, args.sharesToRedeem[1] + ); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceMultiPositions{ value: 2 ether }(args); + } + + function test_rebalanceSinglePosition_singleXChainSingleVaultDepositSelector() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToOneVaultArgs(deployer); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), 0); + + assertGt(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2), 0); + } + + function test_rebalanceSinglePosition_singleXChainMultiVaultDeposit() public { + vm.startPrank(deployer); + + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e18); + + ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = + _buildRebalanceSinglePositionToTwoVaultsXChainArgs(); + + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, args.sharesToRedeem); + + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceSinglePosition{ value: 2 ether }(args); + + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), 0); + + assertGt(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2), 0); + } + + function test_rebalanceMultiPositions_multiDstSingleVaultDepositSelector() public { + vm.startPrank(deployer); + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e6); + vm.stopPrank(); + + uint256[] memory superformIds = new uint256[](2); + superformIds[0] = superformId1; + superformIds[1] = superformId2; + + SingleVaultSFData[] memory superformsData = new SingleVaultSFData[](2); + + LiqBridgeTxDataArgs memory liqBridgeTxDataArgs1 = LiqBridgeTxDataArgs( + 1, + getContract(SOURCE_CHAIN, "DAI"), + getContract(SOURCE_CHAIN, "USDC"), + getContract(SOURCE_CHAIN, "USDC"), + superform2, + SOURCE_CHAIN, + SOURCE_CHAIN, + SOURCE_CHAIN, + false, + superform2, + uint256(SOURCE_CHAIN), + 1e18, // This should be updated with the actual amount if available + false, + 0, + 1, + 1, + 1, + address(0) + ); + address interimAsset = getContract(SOURCE_CHAIN, "DAI"); + + superformsData[0] = SingleVaultSFData({ + superformId: superformId1, + amount: 1e18, + outputAmount: 1e18, + maxSlippage: 100, + liqRequest: LiqRequest("", interimAsset, address(0), 1, SOURCE_CHAIN, 0), + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: deployer, + receiverAddressSP: deployer, + extraFormData: "" + }); + + superformsData[1] = SingleVaultSFData({ + superformId: superformId2, + amount: 1e6, + outputAmount: 1e6, + maxSlippage: 100, + liqRequest: LiqRequest( + _buildLiqBridgeTxData(liqBridgeTxDataArgs1, true), interimAsset, address(0), 1, SOURCE_CHAIN, 0 + ), + permit2data: "", + hasDstSwap: false, + retain4626: false, + receiverAddress: deployer, + receiverAddressSP: deployer, + extraFormData: "" + }); + + uint8[][] memory ambIds = new uint8[][](2); + ambIds[0] = AMBs; + ambIds[1] = AMBs; + + uint64[] memory dstChainIds = new uint64[](2); + dstChainIds[0] = SOURCE_CHAIN; + dstChainIds[1] = SOURCE_CHAIN; + + MultiDstSingleVaultStateReq memory multiDstSingleVaultStateReq = + MultiDstSingleVaultStateReq({ ambIds: ambIds, dstChainIds: dstChainIds, superformsData: superformsData }); + + uint256[] memory sharesToRedeem = new uint256[](2); + sharesToRedeem[0] = 1e18; + sharesToRedeem[1] = 1e6; + + ISuperformRouterPlus.RebalanceMultiPositionsSyncArgs memory args = ISuperformRouterPlus + .RebalanceMultiPositionsSyncArgs( + superformIds, + sharesToRedeem, + 1e18, + 1 ether, + 1 ether, + interimAsset, + 100, + deployer, + _callDataRebalanceFromTwoVaults(interimAsset), + abi.encodeCall(IBaseRouter.multiDstSingleVaultDeposit, multiDstSingleVaultStateReq) + ); + + vm.startPrank(deployer); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, 1e18); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId2, 1e18); + + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceMultiPositions{ value: 2 ether }(args); + vm.stopPrank(); + + /// @dev assert positions preserved to be the same + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), 1e18); + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2), 1e6); + } + + function test_rebalanceMultiPositions_multiDstMultiVaultDepositSelector() public { + vm.startPrank(deployer); + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e6); + vm.stopPrank(); + + uint256[] memory superformIds = new uint256[](2); + superformIds[0] = superformId1; + superformIds[1] = superformId2; + + address interimAsset = getContract(SOURCE_CHAIN, "DAI"); + uint256[] memory sfIds = new uint256[](1); + sfIds[0] = superformId1; + + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1e18; + + uint256[] memory slippages = new uint256[](1); + slippages[0] = 100; + + bool[] memory empty = new bool[](1); + + LiqRequest[] memory liqRequests = new LiqRequest[](1); + liqRequests[0] = LiqRequest("", interimAsset, address(0), 1, SOURCE_CHAIN, 0); + + MultiVaultSFData[] memory superformsData = new MultiVaultSFData[](2); + + superformsData[0] = MultiVaultSFData({ + superformIds: sfIds, + amounts: amounts, + outputAmounts: amounts, + maxSlippages: slippages, + liqRequests: liqRequests, + permit2data: "", + hasDstSwaps: empty, + retain4626s: empty, + receiverAddress: deployer, + receiverAddressSP: deployer, + extraFormData: "" + }); + + sfIds = new uint256[](1); + sfIds[0] = superformId2; + + amounts = new uint256[](1); + amounts[0] = 1e6; + + LiqBridgeTxDataArgs memory liqBridgeTxDataArgs1 = LiqBridgeTxDataArgs( + 1, + getContract(SOURCE_CHAIN, "DAI"), + getContract(SOURCE_CHAIN, "USDC"), + getContract(SOURCE_CHAIN, "USDC"), + superform2, + SOURCE_CHAIN, + SOURCE_CHAIN, + SOURCE_CHAIN, + false, + superform2, + uint256(SOURCE_CHAIN), + 1e18, // This should be updated with the actual amount if available + false, + 0, + 1, + 1, + 1, + address(0) + ); + + liqRequests = new LiqRequest[](1); + liqRequests[0] = + LiqRequest(_buildLiqBridgeTxData(liqBridgeTxDataArgs1, true), interimAsset, address(0), 1, SOURCE_CHAIN, 0); + + superformsData[1] = MultiVaultSFData({ + superformIds: sfIds, + amounts: amounts, + outputAmounts: amounts, + maxSlippages: slippages, + liqRequests: liqRequests, + permit2data: "", + hasDstSwaps: empty, + retain4626s: empty, + receiverAddress: deployer, + receiverAddressSP: deployer, + extraFormData: "" + }); + + uint8[][] memory ambIds = new uint8[][](2); + ambIds[0] = AMBs; + ambIds[1] = AMBs; + + uint64[] memory dstChainIds = new uint64[](2); + dstChainIds[0] = SOURCE_CHAIN; + dstChainIds[1] = SOURCE_CHAIN; + + MultiDstMultiVaultStateReq memory multiDstMultiVaultStateReq = + MultiDstMultiVaultStateReq({ ambIds: ambIds, dstChainIds: dstChainIds, superformsData: superformsData }); + + uint256[] memory sharesToRedeem = new uint256[](2); + sharesToRedeem[0] = 1e18; + sharesToRedeem[1] = 1e6; + + ISuperformRouterPlus.RebalanceMultiPositionsSyncArgs memory args = ISuperformRouterPlus + .RebalanceMultiPositionsSyncArgs( + superformIds, + sharesToRedeem, + 1e18, + 1 ether, + 1 ether, + interimAsset, + 100, + deployer, + _callDataRebalanceFromTwoVaults(interimAsset), + abi.encodeCall(IBaseRouter.multiDstMultiVaultDeposit, multiDstMultiVaultStateReq) + ); + + vm.startPrank(deployer); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId1, 1e18); + SuperPositions(SUPER_POSITIONS_SOURCE).increaseAllowance(ROUTER_PLUS_SOURCE, superformId2, 1e18); + + SuperformRouterPlus(ROUTER_PLUS_SOURCE).rebalanceMultiPositions{ value: 2 ether }(args); + vm.stopPrank(); + + /// @dev assert positions preserved to be the same + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), 1e18); + assertEq(SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2), 1e6); + } + function test_refundUnusedAndResetApprovals_failedToSendNative() public { address rejectEther = address(new RejectEther()); deal(rejectEther, 3 ether); vm.startPrank(deployer); - _directDeposit(superformId1); + _directDeposit(superformId1, 1e18); SuperPositions(SUPER_POSITIONS_SOURCE).safeTransferFrom(deployer, rejectEther, superformId1, 1e18, abi.encode()); @@ -842,7 +1329,7 @@ contract SuperformRouterPlusTest is ProtocolActions { function test_rebalanceSinglePosition_errors() public { vm.startPrank(deployer); - _directDeposit(superformId1); + _directDeposit(superformId1, 1e18); ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = _buildRebalanceSinglePositionToOneVaultArgs(deployer); @@ -889,8 +1376,8 @@ contract SuperformRouterPlusTest is ProtocolActions { function test_rebalanceMultiPositions_errors() public { vm.startPrank(deployer); - _directDeposit(superformId1); - _directDeposit(superformId2); + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e6); (ISuperformRouterPlus.RebalanceMultiPositionsSyncArgs memory args,) = _buildRebalanceTwoPositionsToOneVaultXChainArgs(); @@ -1460,8 +1947,8 @@ contract SuperformRouterPlusTest is ProtocolActions { vm.selectFork(FORKS[SOURCE_CHAIN]); // Setup: Create two destination superforms on the same chain - uint256 superformId1 = superformId1; - uint256 superformId2 = superformId1; + uint256 superformId1t = superformId1; + uint256 superformId2t = superformId1; MultiVaultSFData memory sfData = MultiVaultSFData({ superformIds: new uint256[](2), @@ -1477,8 +1964,8 @@ contract SuperformRouterPlusTest is ProtocolActions { extraFormData: "" }); - sfData.superformIds[0] = superformId1; - sfData.superformIds[1] = superformId2; + sfData.superformIds[0] = superformId1t; + sfData.superformIds[1] = superformId2t; sfData.amounts[0] = 5e17; // 0.5 ether sfData.amounts[1] = 5e17; // 0.5 ether sfData.outputAmounts[0] = 5e17; @@ -1546,12 +2033,12 @@ contract SuperformRouterPlusTest is ProtocolActions { // Verify the results assertGt( - SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1), + SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1t), 0, "Destination superform 1 balance should be greater than 0" ); assertGt( - SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2), + SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId2t), 0, "Destination superform 2 balance should be greater than 0" ); @@ -1720,10 +2207,9 @@ contract SuperformRouterPlusTest is ProtocolActions { function test_crossChainRebalance_multiDstMultiVaultDeposit() public { vm.selectFork(FORKS[SOURCE_CHAIN]); - // Setup: Create two destination superforms on the same chain - uint256 superformId1 = superformId1; - uint256 superformId2 = superformId1; + uint256 superformId1t = superformId1; + uint256 superformId2t = superformId1; MultiVaultSFData memory sfData = MultiVaultSFData({ superformIds: new uint256[](2), @@ -1739,8 +2225,8 @@ contract SuperformRouterPlusTest is ProtocolActions { extraFormData: "" }); - sfData.superformIds[0] = superformId1; - sfData.superformIds[1] = superformId2; + sfData.superformIds[0] = superformId1t; + sfData.superformIds[1] = superformId2t; sfData.amounts[0] = 5e17; // 0.5 ether sfData.amounts[1] = 5e17; // 0.5 ether sfData.outputAmounts[0] = 5e17; @@ -2251,7 +2737,7 @@ contract SuperformRouterPlusTest is ProtocolActions { function test_rebalanceFromSinglePosition_toOneVault() public { vm.startPrank(deployer); - _directDeposit(superformId1); + _directDeposit(superformId1, 1e18); ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = _buildRebalanceSinglePositionToOneVaultArgs(deployer); @@ -2268,7 +2754,7 @@ contract SuperformRouterPlusTest is ProtocolActions { function test_rebalanceFromSinglePosition_toTwoVaults() public { vm.startPrank(deployer); - _directDeposit(superformId1); + _directDeposit(superformId1, 1e18); ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args = _buildRebalanceSinglePositionToTwoVaultsArgs(); @@ -2285,8 +2771,8 @@ contract SuperformRouterPlusTest is ProtocolActions { function test_rebalanceFromTwoPositions_toOneXChainVault() public { vm.startPrank(deployer); - _directDeposit(superformId1); - _directDeposit(superformId2); + _directDeposit(superformId1, 1e18); + _directDeposit(superformId2, 1e6); (ISuperformRouterPlus.RebalanceMultiPositionsSyncArgs memory args, uint256 totalAmountToDeposit) = _buildRebalanceTwoPositionsToOneVaultXChainArgs(); @@ -2825,23 +3311,24 @@ contract SuperformRouterPlusTest is ProtocolActions { args.callData = _callDataRebalanceFromTwoVaults(args.interimAsset); uint256 decimal1 = MockERC20(getContract(SOURCE_CHAIN, "DAI")).decimals(); - uint256 decimal2 = MockERC20(args.interimAsset).decimals(); + uint256 decimal2 = MockERC20(getContract(SOURCE_CHAIN, "USDC")).decimals(); + uint256 decimalInterim = MockERC20(args.interimAsset).decimals(); uint256 previewRedeemAmount1 = IBaseForm(superform1).previewRedeemFrom(args.sharesToRedeem[0]); uint256 expectedAmountToReceivePostRebalanceFrom1; - if (decimal1 > decimal2) { - expectedAmountToReceivePostRebalanceFrom1 = previewRedeemAmount1 / (10 ** (decimal1 - decimal2)); + if (decimal1 > decimalInterim) { + expectedAmountToReceivePostRebalanceFrom1 = previewRedeemAmount1 / (10 ** (decimal1 - decimalInterim)); } else { - expectedAmountToReceivePostRebalanceFrom1 = previewRedeemAmount1 * 10 ** (decimal2 - decimal1); + expectedAmountToReceivePostRebalanceFrom1 = previewRedeemAmount1 * 10 ** (decimalInterim - decimal1); } uint256 previewRedeemAmount2 = IBaseForm(superform2).previewRedeemFrom(args.sharesToRedeem[1]); uint256 expectedAmountToReceivePostRebalanceFrom2; - if (decimal1 > decimal2) { - expectedAmountToReceivePostRebalanceFrom2 = previewRedeemAmount2 / (10 ** (decimal1 - decimal2)); + if (decimal2 > decimalInterim) { + expectedAmountToReceivePostRebalanceFrom2 = previewRedeemAmount2 / (10 ** (decimal2 - decimalInterim)); } else { - expectedAmountToReceivePostRebalanceFrom2 = previewRedeemAmount2 * 10 ** (decimal2 - decimal1); + expectedAmountToReceivePostRebalanceFrom2 = previewRedeemAmount2 * 10 ** (decimalInterim - decimal2); } totalAmountToDeposit = expectedAmountToReceivePostRebalanceFrom1 + expectedAmountToReceivePostRebalanceFrom2; @@ -2849,6 +3336,33 @@ contract SuperformRouterPlusTest is ProtocolActions { args.rebalanceToCallData = _callDataRebalanceToOneVaultxChain(totalAmountToDeposit, args.interimAsset); } + function _buildRebalanceSinglePositionToTwoVaultsXChainArgs() + internal + returns (ISuperformRouterPlus.RebalanceSinglePositionSyncArgs memory args) + { + args.id = superformId1; + args.sharesToRedeem = SuperPositions(SUPER_POSITIONS_SOURCE).balanceOf(deployer, superformId1); + args.rebalanceFromMsgValue = 1 ether; + args.rebalanceToMsgValue = 1 ether; + args.interimAsset = getContract(SOURCE_CHAIN, "USDC"); + args.slippage = 100; + args.receiverAddressSP = deployer; + args.callData = _callDataRebalanceFrom(args.interimAsset); + + uint256 decimal1 = MockERC20(getContract(SOURCE_CHAIN, "DAI")).decimals(); + uint256 decimal2 = MockERC20(args.interimAsset).decimals(); + uint256 previewRedeemAmount = IBaseForm(superform1).previewRedeemFrom(args.sharesToRedeem); + + if (decimal1 > decimal2) { + args.expectedAmountToReceivePostRebalanceFrom = previewRedeemAmount / (10 ** (decimal1 - decimal2)); + } else { + args.expectedAmountToReceivePostRebalanceFrom = previewRedeemAmount * 10 ** (decimal2 - decimal1); + } + + args.rebalanceToCallData = + _callDataRebalanceToTwoVaultxChain(args.expectedAmountToReceivePostRebalanceFrom, args.interimAsset); + } + function _buildInitiateXChainRebalanceArgs( uint64 REBALANCE_FROM, uint64 REBALANCE_TO, @@ -3680,37 +4194,46 @@ contract SuperformRouterPlusTest is ProtocolActions { function _callDataRebalanceFromTwoVaults(address interimToken) internal view returns (bytes memory) { uint256[] memory superformIds = new uint256[](2); - superformIds[0] = superformId1; - superformIds[1] = superformId2; + superformIds[0] = superformId1; // DAI vault + superformIds[1] = superformId2; // USDC vault uint256[] memory amounts = new uint256[](2); amounts[0] = 1e18; - amounts[1] = 1e18; + amounts[1] = 1e6; uint256[] memory outputAmounts = new uint256[](2); outputAmounts[0] = 1e18; - outputAmounts[1] = 1e18; + outputAmounts[1] = 1e6; uint256[] memory maxSlippages = new uint256[](2); maxSlippages[0] = 100; maxSlippages[1] = 100; LiqRequest[] memory liqReqs = new LiqRequest[](2); + /// @dev if interim is USDC then: + /// @dev - vault 1 will have txData (to swap from DAI external token to USDC interim token) + /// @dev - vault 2 will have no txData + /// @dev otherwise, if interim is DAI + /// @dev - vault 1 will have no txData + /// @dev - vault 2 will have txData (to swap from USDC external token to DAI interim token) + /// @dev overriding to false in using withdraw for same chain swap because true likely has issues... LiqBridgeTxDataArgs memory liqBridgeTxDataArgs = LiqBridgeTxDataArgs( 1, - getContract(SOURCE_CHAIN, "DAI"), - getContract(SOURCE_CHAIN, "DAI"), + interimToken == getContract(SOURCE_CHAIN, "USDC") + ? getContract(SOURCE_CHAIN, "DAI") + : getContract(SOURCE_CHAIN, "USDC"), interimToken, - superform1, + interimToken, + interimToken == getContract(SOURCE_CHAIN, "USDC") ? superform1 : superform2, SOURCE_CHAIN, SOURCE_CHAIN, SOURCE_CHAIN, false, getContract(SOURCE_CHAIN, "SuperformRouterPlus"), uint256(SOURCE_CHAIN), - 1e18, + interimToken == getContract(SOURCE_CHAIN, "USDC") ? 1e18 : 1e6, //1e18, - true, + false, /// @dev placeholder value, not used 0, 1, @@ -3719,9 +4242,13 @@ contract SuperformRouterPlusTest is ProtocolActions { address(0) ); - liqReqs[0] = - LiqRequest(_buildLiqBridgeTxData(liqBridgeTxDataArgs, true), interimToken, address(0), 1, SOURCE_CHAIN, 0); - liqReqs[1] = LiqRequest("", interimToken, address(0), 1, SOURCE_CHAIN, 0); + liqReqs[0] = interimToken == getContract(SOURCE_CHAIN, "USDC") + ? LiqRequest(_buildLiqBridgeTxData(liqBridgeTxDataArgs, true), interimToken, address(0), 1, SOURCE_CHAIN, 0) + : LiqRequest("", interimToken, address(0), 1, SOURCE_CHAIN, 0); + + liqReqs[1] = interimToken == getContract(SOURCE_CHAIN, "USDC") + ? LiqRequest("", interimToken, address(0), 1, SOURCE_CHAIN, 0) + : LiqRequest(_buildLiqBridgeTxData(liqBridgeTxDataArgs, true), interimToken, address(0), 1, SOURCE_CHAIN, 0); bool[] memory falseBool = new bool[](2); @@ -3838,6 +4365,87 @@ contract SuperformRouterPlusTest is ProtocolActions { return abi.encodeCall(IBaseRouter.singleDirectMultiVaultDeposit, SingleDirectMultiVaultStateReq(data)); } + function _callDataRebalanceToTwoVaultxChain( + uint256 amountToDeposit, + address interimToken + ) + internal + returns (bytes memory) + { + uint256 initialFork = vm.activeFork(); + vm.selectFork(FORKS[OP]); + + uint256[] memory superformIds = new uint256[](2); + superformIds[0] = superformId4OP; + superformIds[1] = superformId4OP; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = amountToDeposit / 2; + amounts[1] = amountToDeposit / 2; + + uint256[] memory outputAmounts = new uint256[](2); + outputAmounts[0] = IBaseForm(superform4OP).previewDepositTo(amounts[0]); + outputAmounts[1] = IBaseForm(superform4OP).previewDepositTo(amounts[1]); + + uint256[] memory maxSlippages = new uint256[](2); + maxSlippages[0] = 100; + maxSlippages[1] = 100; + + address underlyingToken = IBaseForm(superform4OP).getVaultAsset(); + LiqBridgeTxDataArgs memory liqBridgeTxDataArgs = LiqBridgeTxDataArgs( + 1, + interimToken, + interimToken, + underlyingToken, + getContract(SOURCE_CHAIN, "SuperformRouter"), + SOURCE_CHAIN, + OP, + OP, + false, + getContract(OP, "CoreStateRegistry"), + uint256(OP), + amounts[1], + //1e18, + false, + /// @dev placeholder value, not used + 0, + 1, + 1, + 1, + address(0) + ); + LiqRequest[] memory liqReqs = new LiqRequest[](2); + + liqReqs[0] = LiqRequest(_buildLiqBridgeTxData(liqBridgeTxDataArgs, false), interimToken, address(0), 1, OP, 0); + liqReqs[1] = LiqRequest(_buildLiqBridgeTxData(liqBridgeTxDataArgs, false), interimToken, address(0), 1, OP, 0); + + bool[] memory falseBoolean = new bool[](2); + + MultiVaultSFData memory data = MultiVaultSFData( + superformIds, + amounts, + outputAmounts, + maxSlippages, + liqReqs, + "", + falseBoolean, + falseBoolean, + deployer, + deployer, + "" + ); + + uint8[] memory ambIds = new uint8[](2); + ambIds[0] = 5; + ambIds[1] = 6; + + uint64 dstChainId = OP; + vm.selectFork(initialFork); + return abi.encodeCall( + IBaseRouter.singleXChainMultiVaultDeposit, SingleXChainMultiVaultStateReq(ambIds, dstChainId, data) + ); + } + function _callDataRebalanceToOneVaultxChain( uint256 amountToDeposit, address interimToken @@ -4027,14 +4635,14 @@ contract SuperformRouterPlusTest is ProtocolActions { ); } - function _directDeposit(uint256 superformId) internal { + function _directDeposit(uint256 superformId, uint256 amounts) internal { vm.selectFork(FORKS[SOURCE_CHAIN]); (address superform,,) = superformId.getSuperform(); SingleVaultSFData memory data = SingleVaultSFData( superformId, - 1e18, - 1e18, + amounts, + amounts, 100, LiqRequest("", IBaseForm(superform).getVaultAsset(), address(0), 1, SOURCE_CHAIN, 0), "", @@ -4475,4 +5083,29 @@ contract SuperformRouterPlusTest is ProtocolActions { calldata_ := add(data, 0x04) } } + + function test_setGlobalSlippage() public { + // Test invalid caller + vm.startPrank(address(12_345)); + vm.expectRevert(); + SuperformRouterPlus(getContract(SOURCE_CHAIN, "SuperformRouterPlus")).setGlobalSlippage(100); + vm.stopPrank(); + + // Test slippage greater than ENTIRE_SLIPPAGE + vm.startPrank(deployer); + vm.expectRevert(ISuperformRouterPlus.INVALID_GLOBAL_SLIPPAGE.selector); + SuperformRouterPlus(getContract(SOURCE_CHAIN, "SuperformRouterPlus")).setGlobalSlippage(1_000_000); + vm.stopPrank(); + + // Test slippage 0 + vm.startPrank(deployer); + vm.expectRevert(ISuperformRouterPlus.INVALID_GLOBAL_SLIPPAGE.selector); + SuperformRouterPlus(getContract(SOURCE_CHAIN, "SuperformRouterPlus")).setGlobalSlippage(0); + vm.stopPrank(); + + // Test slippage valid + vm.startPrank(deployer); + SuperformRouterPlus(getContract(SOURCE_CHAIN, "SuperformRouterPlus")).setGlobalSlippage(100); + vm.stopPrank(); + } } diff --git a/test/utils/BaseSetup.sol b/test/utils/BaseSetup.sol index 95770586c..dea923a1c 100644 --- a/test/utils/BaseSetup.sol +++ b/test/utils/BaseSetup.sol @@ -1209,6 +1209,10 @@ abstract contract BaseSetup is StdInvariant, Test { vars.superformRouterPlus = address(new SuperformRouterPlus{ salt: salt }(vars.superRegistry)); contracts[vars.chainId][bytes32(bytes("SuperformRouterPlus"))] = vars.superformRouterPlus; + /// Set the global slippage + SuperformRouterPlus(vars.superformRouterPlus).setGlobalSlippage(100); + + /// @dev deploy Superform Router Plus Async vars.superformRouterPlusAsync = address(new SuperformRouterPlusAsync{ salt: salt }(vars.superRegistry)); contracts[vars.chainId][bytes32(bytes("SuperformRouterPlusAsync"))] = vars.superformRouterPlusAsync;