diff --git a/README.md b/README.md index b1800a0..d2a9ef7 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,12 @@ You can optionally set VERIFY to `true` in order to publish the source code to E ### Hardhat tasks -In order to update onchain rebalancer or repayer routes to reflect what is put into configuration, execute the following tasks: +In order to update onchain rebalancer or repayer configurations to reflect what is put into configuration, execute the following tasks: ``` npm run hardhat -- update-routes-rebalancer --network BASE npm run hardhat -- update-routes-repayer --network BASE +npm run hardhat -- add-tokens-repayer --network BASE ``` It will produce a list of instructions for the admin multisig. diff --git a/contracts/LiquidityPool.sol b/contracts/LiquidityPool.sol index e0ff057..ff88cc5 100644 --- a/contracts/LiquidityPool.sol +++ b/contracts/LiquidityPool.sol @@ -547,6 +547,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner { } function balance(IERC20 token) external view override returns (uint256) { + if (paused || borrowPaused) return 0; if (token == NATIVE_TOKEN) token = WRAPPED_NATIVE_TOKEN; return _balance(token); } diff --git a/contracts/Repayer.sol b/contracts/Repayer.sol index 270e054..7de894b 100644 --- a/contracts/Repayer.sol +++ b/contracts/Repayer.sol @@ -13,7 +13,7 @@ import {CCTPAdapter} from "./utils/CCTPAdapter.sol"; import {AcrossAdapter} from "./utils/AcrossAdapter.sol"; import {StargateAdapter} from "./utils/StargateAdapter.sol"; import {EverclearAdapter} from "./utils/EverclearAdapter.sol"; -import {OptimismStandardBridgeAdapter} from "./utils/OptimismStandardBridgeAdapter.sol"; +import {SuperchainStandardBridgeAdapter} from "./utils/SuperchainStandardBridgeAdapter.sol"; import {ERC7201Helper} from "./utils/ERC7201Helper.sol"; /// @title Performs repayment to Liquidity Pools on same/different chains. @@ -28,7 +28,7 @@ contract Repayer is AcrossAdapter, StargateAdapter, EverclearAdapter, - OptimismStandardBridgeAdapter + SuperchainStandardBridgeAdapter { using SafeERC20 for IERC20; using BitMaps for BitMaps.BitMap; @@ -37,6 +37,7 @@ contract Repayer is Domain immutable public DOMAIN; IERC20 immutable public ASSETS; bytes32 constant public REPAYER_ROLE = "REPAYER_ROLE"; + bytes32 constant public SET_TOKENS_ROLE = "SET_TOKENS_ROLE"; IWrappedNativeToken immutable public WRAPPED_NATIVE_TOKEN; /// @custom:storage-location erc7201:sprinter.storage.Repayer @@ -44,6 +45,8 @@ contract Repayer is mapping(address pool => BitMaps.BitMap) allowedRoutes; EnumerableSet.AddressSet knownPools; mapping(address pool => bool) poolSupportsAllTokens; + mapping(address inputToken => + mapping(bytes32 outputToken => BitMaps.BitMap destinationDomains)) inputOutputTokens; } bytes32 private constant STORAGE_LOCATION = 0xa6615d19cc0b2a17ee46271ca76cd3f303efb9bf682e7eb5c4e7290e895cde00; @@ -63,6 +66,7 @@ contract Repayer is Provider provider ); event ProcessRepay(IERC20 token, uint256 amount, address destinationPool, Provider provider); + event SetInputOutputToken(address inputToken, Domain destinationDomain, bytes32 outputToken, bool isAllowed); error ZeroAmount(); error InsufficientBalance(); @@ -71,6 +75,16 @@ contract Repayer is error UnsupportedProvider(); error InvalidPoolAssets(); + struct DestinationToken { + Domain destinationDomain; + bytes32 outputToken; + } + + struct InputOutputToken { + address inputToken; + DestinationToken[] destinationTokens; + } + constructor( Domain localDomain, IERC20 assets, @@ -80,13 +94,14 @@ contract Repayer is address everclearFeeAdapter, address wrappedNativeToken, address stargateTreasurer, - address optimismBridge + address optimismBridge, + address baseBridge ) CCTPAdapter(cctpTokenMessenger, cctpMessageTransmitter) AcrossAdapter(acrossSpokePool) StargateAdapter(stargateTreasurer) EverclearAdapter(everclearFeeAdapter) - OptimismStandardBridgeAdapter(optimismBridge, wrappedNativeToken) + SuperchainStandardBridgeAdapter(optimismBridge, baseBridge, wrappedNativeToken) { ERC7201Helper.validateStorageLocation( STORAGE_LOCATION, @@ -106,14 +121,18 @@ contract Repayer is function initialize( address admin, address repayer, + address setTokens, address[] calldata pools, Domain[] calldata domains, Provider[] calldata providers, - bool[] calldata poolSupportsAllTokens + bool[] calldata poolSupportsAllTokens, + InputOutputToken[] calldata inputOutputTokens ) external initializer() { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(REPAYER_ROLE, repayer); + _grantRole(SET_TOKENS_ROLE, setTokens); _setRoute(pools, domains, providers, poolSupportsAllTokens, true); + _setInputOutputTokens(inputOutputTokens, true); } function setRoute( @@ -126,6 +145,13 @@ contract Repayer is _setRoute(pools, domains, providers, poolSupportsAllTokens, isAllowed); } + function setInputOutputTokens( + InputOutputToken[] calldata inputOutputTokens, + bool isAllowed + ) external onlyRole(SET_TOKENS_ROLE) { + _setInputOutputTokens(inputOutputTokens, isAllowed); + } + /// @notice If the selected provider requires native currency payment to cover fees, /// then caller has to include it in the transaction. It is then the responsibility /// of the Adapter to forward the payment and return any change back to the caller. @@ -167,17 +193,37 @@ contract Repayer is initiateTransferCCTP(token, amount, destinationPool, destinationDomain); } else if (provider == Provider.ACROSS) { - initiateTransferAcross(token, amount, destinationPool, destinationDomain, extraData); + initiateTransferAcross( + token, + amount, + destinationPool, + destinationDomain, + extraData, + $.inputOutputTokens[address(token)] + ); } else if (provider == Provider.EVERCLEAR) { - initiateTransferEverclear(token, amount, destinationPool, destinationDomain, extraData); + initiateTransferEverclear( + token, + amount, + destinationPool, + destinationDomain, + extraData, + $.inputOutputTokens[address(token)] + ); } else if (provider == Provider.STARGATE) { initiateTransferStargate(token, amount, destinationPool, destinationDomain, extraData, _msgSender()); } else - if (provider == Provider.OPTIMISM_STANDARD_BRIDGE) { - initiateTransferOptimismStandardBridge( - token, amount, destinationPool, destinationDomain, extraData, DOMAIN + if (provider == Provider.SUPERCHAIN_STANDARD_BRIDGE) { + initiateTransferSuperchainStandardBridge( + token, + amount, + destinationPool, + destinationDomain, + extraData, + DOMAIN, + $.inputOutputTokens[address(token)] ); } else { // Unreachable atm, but could become so when more providers are added to enum. @@ -244,6 +290,25 @@ contract Repayer is } } + function _setInputOutputTokens( + InputOutputToken[] calldata inputOutputTokens, + bool isAllowed + ) internal { + RepayerStorage storage $ = _getStorage(); + for (uint256 i = 0; i < inputOutputTokens.length; ++i) { + address inputToken = inputOutputTokens[i].inputToken; + DestinationToken[] calldata destinationTokens = inputOutputTokens[i].destinationTokens; + for (uint256 j = 0; j < destinationTokens.length; ++j) { + DestinationToken calldata destinationToken = destinationTokens[j]; + Domain destinationDomain = destinationToken.destinationDomain; + bytes32 outputToken = destinationToken.outputToken; + require(destinationDomain != DOMAIN, UnsupportedDomain()); + $.inputOutputTokens[inputToken][outputToken].setTo(uint256(destinationDomain), isAllowed); + emit SetInputOutputToken(inputToken, destinationDomain, outputToken, isAllowed); + } + } + } + function getAllRoutes() external view returns ( address[] memory pools, @@ -293,6 +358,14 @@ contract Repayer is return _getStorage().allowedRoutes[pool].get(_toIndex(domain, provider)); } + function isOutputTokenAllowed( + address inputToken, + Domain destinationDomain, + bytes32 outputToken + ) public view returns (bool) { + return _getStorage().inputOutputTokens[inputToken][outputToken].get(uint256(destinationDomain)); + } + function _getStorage() private pure returns (RepayerStorage storage $) { assembly { $.slot := STORAGE_LOCATION diff --git a/contracts/interfaces/IRoute.sol b/contracts/interfaces/IRoute.sol index e71f8e9..bcf2aab 100644 --- a/contracts/interfaces/IRoute.sol +++ b/contracts/interfaces/IRoute.sol @@ -27,7 +27,7 @@ interface IRoute { ACROSS, STARGATE, EVERCLEAR, - OPTIMISM_STANDARD_BRIDGE + SUPERCHAIN_STANDARD_BRIDGE } enum PoolType { diff --git a/contracts/interfaces/IOptimism.sol b/contracts/interfaces/ISuperchainStandardBridge.sol similarity index 98% rename from contracts/interfaces/IOptimism.sol rename to contracts/interfaces/ISuperchainStandardBridge.sol index 46b56d2..2689262 100644 --- a/contracts/interfaces/IOptimism.sol +++ b/contracts/interfaces/ISuperchainStandardBridge.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; -interface IOptimismStandardBridge { +interface ISuperchainStandardBridge { /// @notice Emitted when an ERC20 bridge is initiated to the other chain. /// @param localToken Address of the ERC20 on this chain. diff --git a/contracts/testing/TestRepayer.sol b/contracts/testing/TestRepayer.sol index fee171a..2d9f939 100644 --- a/contracts/testing/TestRepayer.sol +++ b/contracts/testing/TestRepayer.sol @@ -13,7 +13,8 @@ contract TestRepayer is Repayer { address everclearFeeAdapter, address wrappedNativeToken, address stargateTreasurer, - address optimismBridge + address optimismBridge, + address baseBridge ) Repayer( localDomain, assets, @@ -23,7 +24,8 @@ contract TestRepayer is Repayer { everclearFeeAdapter, wrappedNativeToken, stargateTreasurer, - optimismBridge + optimismBridge, + baseBridge ) {} function domainCCTP(Domain destinationDomain) public pure override returns (uint32) { diff --git a/contracts/testing/TestOptimism.sol b/contracts/testing/TestSuperchain.sol similarity index 73% rename from contracts/testing/TestOptimism.sol rename to contracts/testing/TestSuperchain.sol index 67db8fd..3b6935d 100644 --- a/contracts/testing/TestOptimism.sol +++ b/contracts/testing/TestSuperchain.sol @@ -2,11 +2,11 @@ pragma solidity 0.8.28; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IOptimismStandardBridge} from "../interfaces/IOptimism.sol"; +import {ISuperchainStandardBridge} from "../interfaces/ISuperchainStandardBridge.sol"; -contract TestOptimismStandardBridge is IOptimismStandardBridge { - error OptimismBridgeWrongRemoteToken(); - error OptimismBridgeWrongMinGasLimit(); +contract TestSuperchainStandardBridge is ISuperchainStandardBridge { + error SuperchainStandardBridgeWrongRemoteToken(); + error SuperchainStandardBridgeWrongMinGasLimit(); function bridgeERC20To( address _localToken, @@ -18,7 +18,7 @@ contract TestOptimismStandardBridge is IOptimismStandardBridge { ) external override { require( _localToken != _remoteToken, - OptimismBridgeWrongRemoteToken() + SuperchainStandardBridgeWrongRemoteToken() ); // To simulate revert. SafeERC20.safeTransferFrom(IERC20(_localToken), msg.sender, address(this), _amount); emit ERC20BridgeInitiated( @@ -34,7 +34,7 @@ contract TestOptimismStandardBridge is IOptimismStandardBridge { function bridgeETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable override { require( _minGasLimit > 0, - OptimismBridgeWrongMinGasLimit() + SuperchainStandardBridgeWrongMinGasLimit() ); // To simulate revert. emit ETHBridgeInitiated(address(this), _to, msg.value, _extraData); } diff --git a/contracts/utils/AcrossAdapter.sol b/contracts/utils/AcrossAdapter.sol index c036b96..e387382 100644 --- a/contracts/utils/AcrossAdapter.sol +++ b/contracts/utils/AcrossAdapter.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.28; import {V3SpokePoolInterface} from ".././interfaces/IAcross.sol"; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AdapterHelper} from "./AdapterHelper.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; abstract contract AcrossAdapter is AdapterHelper { using SafeERC20 for IERC20; @@ -22,7 +23,8 @@ abstract contract AcrossAdapter is AdapterHelper { uint256 amount, address destinationPool, Domain destinationDomain, - bytes calldata extraData + bytes calldata extraData, + mapping(bytes32 => BitMaps.BitMap) storage outputTokens ) internal notPayable { require(address(ACROSS_SPOKE_POOL) != address(0), ZeroAddress()); token.forceApprove(address(ACROSS_SPOKE_POOL), amount); @@ -38,6 +40,9 @@ abstract contract AcrossAdapter is AdapterHelper { // then we will need to remove this requirement. // Until then we leave it here as a protective measure on potential offchain component calculation errors. require(outputAmount >= (amount * 9980 / 10000), SlippageTooHigh()); + if (outputToken != address(0)) { + _validateOutputToken(_addressToBytes32(outputToken), destinationDomain, outputTokens); + } ACROSS_SPOKE_POOL.depositV3( address(this), destinationPool, diff --git a/contracts/utils/AdapterHelper.sol b/contracts/utils/AdapterHelper.sol index db7e03f..bc0e34a 100644 --- a/contracts/utils/AdapterHelper.sol +++ b/contracts/utils/AdapterHelper.sol @@ -1,11 +1,15 @@ // SPDX-License-Identifier: LGPL-3.0-only pragma solidity 0.8.28; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; import {IRoute} from ".././interfaces/IRoute.sol"; abstract contract AdapterHelper is IRoute { + using BitMaps for BitMaps.BitMap; + error SlippageTooHigh(); error NotPayable(); + error InvalidOutputToken(); modifier notPayable() { require(msg.value == 0, NotPayable()); @@ -47,4 +51,12 @@ abstract contract AdapterHelper is IRoute { function _addressToBytes32(address addr) internal pure returns (bytes32) { return bytes32(uint256(uint160(addr))); } + + function _validateOutputToken( + bytes32 outputToken, + Domain destinationDomain, + mapping(bytes32 outputToken => BitMaps.BitMap destinationDomains) storage outputTokens + ) internal view { + require(outputTokens[outputToken].get(uint256(destinationDomain)), InvalidOutputToken()); + } } diff --git a/contracts/utils/EverclearAdapter.sol b/contracts/utils/EverclearAdapter.sol index 0f344a2..4895793 100644 --- a/contracts/utils/EverclearAdapter.sol +++ b/contracts/utils/EverclearAdapter.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.28; import {IFeeAdapterV2} from ".././interfaces/IEverclear.sol"; import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {AdapterHelper} from "./AdapterHelper.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; abstract contract EverclearAdapter is AdapterHelper { using SafeERC20 for IERC20; @@ -22,7 +23,8 @@ abstract contract EverclearAdapter is AdapterHelper { uint256 amount, address destinationPool, Domain destinationDomain, - bytes calldata extraData + bytes calldata extraData, + mapping(bytes32 => BitMaps.BitMap) storage outputTokens ) internal { require(address(EVERCLEAR_FEE_ADAPTER) != address(0), ZeroAddress()); token.forceApprove(address(EVERCLEAR_FEE_ADAPTER), amount); @@ -33,6 +35,7 @@ abstract contract EverclearAdapter is AdapterHelper { IFeeAdapterV2.FeeParams memory feeParams ) = abi.decode(extraData, (bytes32, uint256, uint48, IFeeAdapterV2.FeeParams)); require(amountOutMin >= (amount * 9980 / 10000), SlippageTooHigh()); + _validateOutputToken(outputAsset, destinationDomain, outputTokens); uint32[] memory destinations = new uint32[](1); destinations[0] = domainChainId(destinationDomain); EVERCLEAR_FEE_ADAPTER.newIntent{value: msg.value}( diff --git a/contracts/utils/OptimismStandardBridgeAdapter.sol b/contracts/utils/OptimismStandardBridgeAdapter.sol deleted file mode 100644 index 28b2c49..0000000 --- a/contracts/utils/OptimismStandardBridgeAdapter.sol +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity 0.8.28; - -import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IOptimismStandardBridge} from ".././interfaces/IOptimism.sol"; -import {IWrappedNativeToken} from ".././interfaces/IWrappedNativeToken.sol"; -import {AdapterHelper} from "./AdapterHelper.sol"; - -abstract contract OptimismStandardBridgeAdapter is AdapterHelper { - using SafeERC20 for IERC20; - - IOptimismStandardBridge immutable public OPTIMISM_STANDARD_BRIDGE; - IWrappedNativeToken immutable private WRAPPED_NATIVE_TOKEN; - - constructor( - address optimismStandardBridge, - address wrappedNativeToken - ) { - // No check for address(0) to allow deployment on chains where Optimism Standard Bridge is not available - OPTIMISM_STANDARD_BRIDGE = IOptimismStandardBridge(optimismStandardBridge); - WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(wrappedNativeToken); - } - - function initiateTransferOptimismStandardBridge( - IERC20 token, - uint256 amount, - address destinationPool, - Domain destinationDomain, - bytes calldata extraData, - Domain localDomain - ) internal notPayable{ - // We are only interested in fast L1->L2 bridging, because the reverse is slow. - require( - localDomain == Domain.ETHEREUM && destinationDomain == Domain.OP_MAINNET, - UnsupportedDomain() - ); - require(address(OPTIMISM_STANDARD_BRIDGE) != address(0), ZeroAddress()); - // WARNING: Contract doesn't maintain an input/output token mapping which could result in a mismatch. - // If the output token is wrong then the input tokens will be lost. - // Notice: In case of minGasLimit being too low, the message could be retried on the destination chain. - (address outputToken, uint32 minGasLimit) = abi.decode(extraData, (address, uint32)); - if (token == WRAPPED_NATIVE_TOKEN) { - WRAPPED_NATIVE_TOKEN.withdraw(amount); - OPTIMISM_STANDARD_BRIDGE.bridgeETHTo{value: amount}(destinationPool, minGasLimit, ""); - return; - } - - require(outputToken != address(0), ZeroAddress()); - token.forceApprove(address(OPTIMISM_STANDARD_BRIDGE), amount); - OPTIMISM_STANDARD_BRIDGE.bridgeERC20To( - address(token), - outputToken, - destinationPool, - amount, - minGasLimit, - "" // message - ); - } -} diff --git a/contracts/utils/SuperchainStandardBridgeAdapter.sol b/contracts/utils/SuperchainStandardBridgeAdapter.sol new file mode 100644 index 0000000..80ae5fa --- /dev/null +++ b/contracts/utils/SuperchainStandardBridgeAdapter.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.28; + +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ISuperchainStandardBridge} from ".././interfaces/ISuperchainStandardBridge.sol"; +import {IWrappedNativeToken} from ".././interfaces/IWrappedNativeToken.sol"; +import {AdapterHelper} from "./AdapterHelper.sol"; + +abstract contract SuperchainStandardBridgeAdapter is AdapterHelper { + using SafeERC20 for IERC20; + + ISuperchainStandardBridge immutable public OPTIMISM_STANDARD_BRIDGE; + ISuperchainStandardBridge immutable public BASE_STANDARD_BRIDGE; + IWrappedNativeToken immutable private WRAPPED_NATIVE_TOKEN; + + constructor( + address optimismStandardBridge, + address baseStandardBridge, + address wrappedNativeToken + ) { + // No check for address(0) to allow deployment on chains where Standard Bridge is not available + OPTIMISM_STANDARD_BRIDGE = ISuperchainStandardBridge(optimismStandardBridge); + BASE_STANDARD_BRIDGE = ISuperchainStandardBridge(baseStandardBridge); + WRAPPED_NATIVE_TOKEN = IWrappedNativeToken(wrappedNativeToken); + } + + function initiateTransferSuperchainStandardBridge( + IERC20 token, + uint256 amount, + address destinationPool, + Domain destinationDomain, + bytes calldata extraData, + Domain localDomain, + mapping(bytes32 => BitMaps.BitMap) storage outputTokens + ) internal notPayable { + // We are only interested in fast L1->L2 bridging, because the reverse is slow. + require(localDomain == Domain.ETHEREUM, UnsupportedDomain()); + ISuperchainStandardBridge standardBridge; + if (destinationDomain == Domain.OP_MAINNET) { + standardBridge = OPTIMISM_STANDARD_BRIDGE; + } else if (destinationDomain == Domain.BASE) { + standardBridge = BASE_STANDARD_BRIDGE; + } else { + revert UnsupportedDomain(); + } + require(address(standardBridge) != address(0), ZeroAddress()); + // WARNING: Contract doesn't maintain an input/output token mapping which could result in a mismatch. + // If the output token is wrong then the input tokens will be lost. + // Notice: In case of minGasLimit being too low, the message could be retried on the destination chain. + (address outputToken, uint32 minGasLimit, bytes memory message) = + abi.decode(extraData, (address, uint32, bytes)); + if (token == WRAPPED_NATIVE_TOKEN) { + require(outputToken == address(0), InvalidOutputToken()); + WRAPPED_NATIVE_TOKEN.withdraw(amount); + standardBridge.bridgeETHTo{value: amount}(destinationPool, minGasLimit, message); + return; + } + + _validateOutputToken(_addressToBytes32(outputToken), destinationDomain, outputTokens); + token.forceApprove(address(standardBridge), amount); + standardBridge.bridgeERC20To( + address(token), + outputToken, + destinationPool, + amount, + minGasLimit, + message + ); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index bb1415d..519b382 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -400,6 +400,61 @@ task("update-routes-repayer", "Update Repayer routes based on current network co } }); +task("add-tokens-repayer", "Add input output tokens based on current network configs") +.addOptionalParam("repayer", "Repayer address or id", "Repayer", types.string) +.setAction(async (args: { + repayer: string, +}, hre) => { + const {resolveProxyXAddress, toBytes32} = await loadTestHelpers(); + const { + getNetworkConfig, getHardhatNetworkConfig, getInputOutputTokens, flattenInputOutputTokens, + } = await loadScriptHelpers(); + let {network, config} = await getNetworkConfig(); + if (!network) { + ({network, config} = await getHardhatNetworkConfig()); + } + const inputOutputTokens = getInputOutputTokens(network, config); + + const [sender] = await hre.ethers.getSigners(); + + const targetAddress = await resolveProxyXAddress(args.repayer); + const target = (await hre.ethers.getContractAt("Repayer", targetAddress, sender)) as Repayer; + const filteredInputOutputTokens: Repayer.InputOutputTokenStruct[] = []; + for (const entry of inputOutputTokens) { + const alreadyAllowed = await Promise.all(entry.destinationTokens.map( + el => target.isOutputTokenAllowed(entry.inputToken, el.destinationDomain, el.outputToken) + )); + entry.destinationTokens = entry.destinationTokens.filter((_, index) => !alreadyAllowed[index]); + if (entry.destinationTokens.length > 0) { + filteredInputOutputTokens.push(entry); + } + } + + const hasRole = await target.hasRole(toBytes32("SET_TOKENS_ROLE"), sender); + console.log(`Following tokens will be added to ${targetAddress}.`); + console.table(flattenInputOutputTokens(filteredInputOutputTokens)); + + if (filteredInputOutputTokens.length > 0) { + if (hasRole) { + await target.setInputOutputTokens(filteredInputOutputTokens, true); + console.log("Done."); + } else { + console.log("To add missing tokens execute the following transaction."); + console.log(`To: ${targetAddress}`); + console.log("Function: setInputOutputTokens"); + console.log("Params:"); + console.log("isAllowed: true"); + for (const entry of filteredInputOutputTokens) { + console.log(`inputToken: ${entry.inputToken}`); + console.log("destinationTokens:"); + console.table(entry.destinationTokens); + } + } + } else { + console.log("There are no missing tokens to add."); + } +}); + task("sign-borrow", "Sign a Liquidity Pool borrow request for testing purposes") .addParam("caller", "Address that will call borrow or borrowAndSwap") .addOptionalParam("token", "Token to borrow") @@ -449,7 +504,7 @@ task("sign-borrow", "Sign a Liquidity Pool borrow request for testing purposes") }; const token = await hre.ethers.getContractAt("IERC20", hre.ethers.ZeroAddress, signer); - const borrowToken = args.token || config.USDC; + const borrowToken = args.token || config.Tokens.USDC; const amount = args.amount; const target = args.target || borrowToken; const data = args.data || (await token.transfer.populateTransaction(signer, amount)).data; diff --git a/network.config.ts b/network.config.ts index 7d93738..d589b6b 100644 --- a/network.config.ts +++ b/network.config.ts @@ -10,37 +10,52 @@ export const LiquidityPoolPublicUSDC = "LiquidityPoolPublicUSDC"; export const LiquidityPoolUSDCStablecoin = "LiquidityPoolUSDCStablecoin"; export const LiquidityPoolAaveUSDCLongTerm = "LiquidityPoolAaveUSDCLongTerm"; export const ERC4626AdapterUSDC = "ERC4626AdapterUSDC"; + export const LiquidityPoolAaveUSDCLongTermV2 = "LiquidityPoolAaveUSDCLongTerm-V2-e09cc75"; export const LiquidityPoolAaveUSDCV2 = "LiquidityPoolAaveUSDC-V2-3601cc4"; export const LiquidityPoolUSDCV2 = "LiquidityPoolUSDC-V2-3601cc4"; export const LiquidityPoolUSDCStablecoinV2 = "LiquidityPoolUSDCStablecoin-V2-3601cc4"; + export const LiquidityPoolAaveUSDCV3 = "LiquidityPoolAaveUSDC-V3-e09cc75"; export const LiquidityPoolUSDCV3 = "LiquidityPoolUSDC-V3-e09cc75"; export const LiquidityPoolUSDCStablecoinV3 = "LiquidityPoolUSDCStablecoin-V3-e09cc75"; + +export const LiquidityPoolAaveUSDCV4 = "LiquidityPoolAaveUSDC-V4-TBD"; +export const LiquidityPoolUSDCV4 = "LiquidityPoolUSDC-V4-TBD"; +export const LiquidityPoolPublicUSDCV2 = "LiquidityPoolPublicUSDC-V2-TBD"; +export const LiquidityPoolUSDCStablecoinV4 = "LiquidityPoolUSDCStablecoin-V4-TBD"; +export const LiquidityPoolAaveUSDCLongTermV3 = "LiquidityPoolAaveUSDCLongTerm-V3-TBD"; +export const ERC4626AdapterUSDCV2 = "ERC4626AdapterUSDC-V2-TBD"; export const LiquidityPoolAaveUSDCLongTermVersions = [ LiquidityPoolAaveUSDCLongTerm, LiquidityPoolAaveUSDCLongTermV2, + LiquidityPoolAaveUSDCLongTermV3, ] as const; export const LiquidityPoolAaveUSDCVersions = [ LiquidityPoolAaveUSDC, LiquidityPoolAaveUSDCV2, LiquidityPoolAaveUSDCV3, + LiquidityPoolAaveUSDCV4, ] as const; export const LiquidityPoolUSDCVersions = [ LiquidityPoolUSDC, LiquidityPoolUSDCV2, LiquidityPoolUSDCV3, + LiquidityPoolUSDCV4, ] as const; export const LiquidityPoolUSDCStablecoinVersions = [ LiquidityPoolUSDCStablecoin, LiquidityPoolUSDCStablecoinV2, LiquidityPoolUSDCStablecoinV3, + LiquidityPoolUSDCStablecoinV4, ] as const; export const LiquidityPoolPublicUSDCVersions = [ LiquidityPoolPublicUSDC, + LiquidityPoolPublicUSDCV2, ] as const; export const ERC4626AdapterUSDCVersions = [ ERC4626AdapterUSDC, + ERC4626AdapterUSDCV2, ] as const; export enum Network { @@ -67,7 +82,15 @@ export enum Provider { ACROSS = "ACROSS", EVERCLEAR = "EVERCLEAR", STARGATE = "STARGATE", - OPTIMISM_STANDARD_BRIDGE = "OPTIMISM_STANDARD_BRIDGE", + SUPERCHAIN_STANDARD_BRIDGE = "SUPERCHAIN_STANDARD_BRIDGE", +} + +export enum Token { + USDC = "USDC", + USDT = "USDT", + DAI = "DAI", + WETH = "WETH", + WBTC = "WBTC", } interface CCTPConfig { @@ -140,7 +163,14 @@ export interface NetworkConfig { StargateTreasurer?: string; EverclearFeeAdapter?: string; OptimismStandardBridge?: string; - USDC: string; + BaseStandardBridge?: string; + Tokens: { + [Token.USDC]: string; + [Token.USDT]?: string; + [Token.DAI]?: string; + [Token.WETH]?: string; + [Token.WBTC]?: string; + }; WrappedNativeToken: string; RebalancerRoutes?: RebalancerRoutesConfig; RepayerRoutes?: RepayerRoutesConfig; @@ -150,6 +180,7 @@ export interface NetworkConfig { Pauser: string; RebalanceCaller: string; // Address that can trigger funds movement between pools. RepayerCaller: string; + SetInputOutputTokens: string; MpcAddress: string; SignerAddress: string; Hub?: HubConfig; @@ -162,6 +193,10 @@ export interface NetworkConfig { Stage?: NetworkConfig; } +export type PartialNetworksConfig = { + [key in Network]?: NetworkConfig; +}; + type NetworksConfig = { [key in Network]: NetworkConfig; }; @@ -177,7 +212,14 @@ export const networkConfig: NetworksConfig = { StargateTreasurer: "0x1041D127b2d4BC700F0F563883bC689502606918", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", - USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + Tokens: { + USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + WETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + WBTC: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + }, WrappedNativeToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -185,6 +227,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalancerRoutes: { @@ -210,19 +253,30 @@ export const networkConfig: NetworksConfig = { Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, Provider.STARGATE, ], [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.STARGATE], - [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.STARGATE], + [Network.BASE]: [ + Provider.CCTP, + Provider.ACROSS, + Provider.EVERCLEAR, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + Provider.STARGATE + ], }, }, [LiquidityPoolUSDC]: { SupportsAllTokens: false, Domains: { - [Network.OP_MAINNET]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.OP_MAINNET]: [ + Provider.CCTP, + Provider.ACROSS, + Provider.EVERCLEAR, + Provider.SUPERCHAIN_STANDARD_BRIDGE + ], [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], - [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.SUPERCHAIN_STANDARD_BRIDGE], }, }, [LiquidityPoolUSDCStablecoin]: { @@ -255,7 +309,14 @@ export const networkConfig: NetworksConfig = { StargateTreasurer: "0x1041D127b2d4BC700F0F563883bC689502606918", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", OptimismStandardBridge: "0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1", - USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + BaseStandardBridge: "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + Tokens: { + USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + DAI: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + WETH: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + WBTC: "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + }, WrappedNativeToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", IsTest: false, Admin: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", @@ -263,6 +324,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalanceCaller: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RepayerCaller: "0xECf983dD6Ecd4245fBAAF608594033AB0660D225", + SetInputOutputTokens: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalancerRoutes: { @@ -285,16 +347,31 @@ export const networkConfig: NetworksConfig = { SupportsAllTokens: true, Domains: { [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], - [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], - [Network.OP_MAINNET]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR, Provider.SUPERCHAIN_STANDARD_BRIDGE], + [Network.OP_MAINNET]: [ + Provider.CCTP, + Provider.ACROSS, + Provider.EVERCLEAR, + Provider.SUPERCHAIN_STANDARD_BRIDGE + ], }, }, [LiquidityPoolUSDCV3]: { SupportsAllTokens: false, Domains: { [Network.ARBITRUM_ONE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], - [Network.BASE]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], - [Network.OP_MAINNET]: [Provider.CCTP, Provider.ACROSS, Provider.EVERCLEAR], + [Network.BASE]: [ + Provider.CCTP, + Provider.ACROSS, + Provider.EVERCLEAR, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + ], + [Network.OP_MAINNET]: [ + Provider.CCTP, + Provider.ACROSS, + Provider.EVERCLEAR, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + ], }, }, [LiquidityPoolAaveUSDCLongTermV2]: { @@ -315,7 +392,9 @@ export const networkConfig: NetworksConfig = { }, StargateTreasurer: "0xC2b638Cb5042c1B3c5d5C969361fB50569840583", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + Tokens: { + USDC: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + }, WrappedNativeToken: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -323,6 +402,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerRoutes: { @@ -368,7 +448,13 @@ export const networkConfig: NetworksConfig = { StargateTreasurer: "0x644abb1e17291b4403966119d15Ab081e4a487e9", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", OptimismStandardBridge: "0x4200000000000000000000000000000000000010", - USDC: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + Tokens: { + USDC: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + USDT: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + DAI: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + WETH: "0x4200000000000000000000000000000000000006", + WBTC: "0x68f180fcCe6836688e9084f035309E29Bf0A2095", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -376,6 +462,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalancerRoutes: { @@ -455,7 +542,13 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x6f26Bf09B1C792e3228e5467807a900A503c0281", StargateTreasurer: "0x644abb1e17291b4403966119d15Ab081e4a487e9", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + Tokens: { + USDC: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + USDT: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + DAI: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + WETH: "0x4200000000000000000000000000000000000006", + WBTC: "0x68f180fcCe6836688e9084f035309E29Bf0A2095", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: false, Admin: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", @@ -463,6 +556,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalanceCaller: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RepayerCaller: "0xECf983dD6Ecd4245fBAAF608594033AB0660D225", + SetInputOutputTokens: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalancerRoutes: { @@ -536,7 +630,13 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A", StargateTreasurer: "0x146c8e409C113ED87C6183f4d25c50251DFfbb3a", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + Tokens: { + USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + USDT: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + DAI: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + WETH: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + WBTC: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + }, WrappedNativeToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -544,6 +644,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalancerRoutes: { @@ -625,7 +726,13 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A", StargateTreasurer: "0x146c8e409C113ED87C6183f4d25c50251DFfbb3a", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + Tokens: { + USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + USDT: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + DAI: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + WETH: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + WBTC: "0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f", + }, WrappedNativeToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", IsTest: false, Admin: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", @@ -633,6 +740,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalanceCaller: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RepayerCaller: "0xECf983dD6Ecd4245fBAAF608594033AB0660D225", + SetInputOutputTokens: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalancerRoutes: { @@ -713,7 +821,11 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", StargateTreasurer: "0xd47b03ee6d86Cf251ee7860FB2ACf9f91B9fD4d7", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Tokens: { + USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + DAI: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + WETH: "0x4200000000000000000000000000000000000006", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -721,6 +833,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", Hub: { @@ -807,7 +920,11 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", StargateTreasurer: "0xd47b03ee6d86Cf251ee7860FB2ACf9f91B9fD4d7", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Tokens: { + USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + DAI: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + WETH: "0x4200000000000000000000000000000000000006", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: false, Admin: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", @@ -815,6 +932,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RebalanceCaller: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", RepayerCaller: "0xECf983dD6Ecd4245fBAAF608594033AB0660D225", + SetInputOutputTokens: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x2D5B6C193C39D2AECb4a99052074E6F325258a0f", Hub: { @@ -897,7 +1015,12 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096", StargateTreasurer: "0x36ed193dc7160D3858EC250e69D12B03Ca087D08", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + Tokens: { + USDC: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", + DAI: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + WETH: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + WBTC: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + }, WrappedNativeToken: "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -905,6 +1028,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerRoutes: { @@ -954,7 +1078,10 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", StargateTreasurer: "0x6D205337F45D6850c3c3006e28d5b52c8a432c35", EverclearFeeAdapter: "0x877Fd0A881B63eBE413124EeE6abbCD7E82cf10b", - USDC: "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + Tokens: { + USDC: "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + WETH: "0x4200000000000000000000000000000000000006", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -962,6 +1089,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalancerRoutes: { @@ -1020,7 +1148,9 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x4e8E101924eDE233C13e2D8622DC8aED2872d505", StargateTreasurer: "0x0a6A15964fEe494A881338D65940430797F0d97C", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + Tokens: { + USDC: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + }, WrappedNativeToken: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -1028,6 +1158,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerRoutes: { @@ -1067,7 +1198,13 @@ export const networkConfig: NetworksConfig = { AcrossV3SpokePool: "0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75", StargateTreasurer: "0xf5F74d2508e97A3a7CCA2ccb75c8325D66b46152", EverclearFeeAdapter: "0xAa7ee09f745a3c5De329EB0CD67878Ba87B70Ffe", - USDC: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + Tokens: { + USDC: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + USDT: "0xA219439258ca9da29E9Cc4cE5596924745e12B93", + DAI: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", + WETH: "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f", + WBTC: "0x3aAB2285ddcDdaD8edf438C1bAB47e1a9D05a9b4", + }, WrappedNativeToken: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", IsTest: false, Admin: "0x4eA9E682BA79bC403523c9e8D98A05EaF3810636", @@ -1075,6 +1212,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RebalanceCaller: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerCaller: "0x9A5B33bd11329116A55F764c604a5152eE8Ca292", + SetInputOutputTokens: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", MpcAddress: "0x3F68D470701522F1c9bb21CF44a33dBFa8E299C2", SignerAddress: "0x83B8D2eAda788943c3e80892f37f9c102271C1D6", RepayerRoutes: { @@ -1117,7 +1255,9 @@ export const networkConfig: NetworksConfig = { }, AcrossV3SpokePool: "0x5ef6C01E11889d86803e0B23e3cB3F9E9d97B662", StargateTreasurer: "0x41945d449bd72AE0E237Eade565D8Bde2aa5e969", - USDC: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + Tokens: { + USDC: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + }, WrappedNativeToken: "0xC558DBdd856501FCd9aaF1E62eae57A9F0629a3c", IsTest: true, Admin: "0xcf2d403c75ba3481ae7b190b1cd3246b5afe9120", @@ -1125,6 +1265,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0xcc5dd1eec29dbe028e61e91db5da4d453be48d90", RebalanceCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RepayerCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", + SetInputOutputTokens: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", USDCPool: true, @@ -1136,7 +1277,9 @@ export const networkConfig: NetworksConfig = { TokenMessenger: "0xeb08f243e5d3fcff26a9e38ae5520a669f4019d0", MessageTransmitter: "0xa9fb1b3009dcb79e2fe346c16a604b8fa8ae0a79", }, - USDC: "0x5425890298aed601595a70ab815c96711a31bc65", + Tokens: { + USDC: "0x5425890298aed601595a70ab815c96711a31bc65", + }, WrappedNativeToken: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c", IsTest: true, Admin: "0xcf2d403c75ba3481ae7b190b1cd3246b5afe9120", @@ -1144,6 +1287,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0xcc5dd1eec29dbe028e61e91db5da4d453be48d90", RebalanceCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RepayerCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", + SetInputOutputTokens: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", AavePool: { @@ -1160,7 +1304,9 @@ export const networkConfig: NetworksConfig = { }, AcrossV3SpokePool: "0x4e8E101924eDE233C13e2D8622DC8aED2872d505", StargateTreasurer: "0x7470E97cc02b0D5be6CFFAd3fd8012755db16156", - USDC: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", + Tokens: { + USDC: "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: true, Admin: "0xcf2d403c75ba3481ae7b190b1cd3246b5afe9120", @@ -1168,6 +1314,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0xcc5dd1eec29dbe028e61e91db5da4d453be48d90", RebalanceCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RepayerCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", + SetInputOutputTokens: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RebalancerRoutes: { @@ -1211,7 +1358,9 @@ export const networkConfig: NetworksConfig = { }, AcrossV3SpokePool: "0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75", StargateTreasurer: "0xd1E255BB6354D237172802646B0d6dDCFC8c509E", - USDC: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", + Tokens: { + USDC: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", + }, WrappedNativeToken: "0x1dF462e2712496373A347f8ad10802a5E95f053D", IsTest: true, Admin: "0xcf2d403c75ba3481ae7b190b1cd3246b5afe9120", @@ -1219,6 +1368,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0xcc5dd1eec29dbe028e61e91db5da4d453be48d90", RebalanceCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RepayerCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", + SetInputOutputTokens: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RebalancerRoutes: { @@ -1261,7 +1411,9 @@ export const networkConfig: NetworksConfig = { MessageTransmitter: "0x7865fAfC2db2093669d92c0F33AeEF291086BEFD", }, AcrossV3SpokePool: "0x82B564983aE7274c86695917BBf8C99ECb6F0F8F", - USDC: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + Tokens: { + USDC: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + }, WrappedNativeToken: "0x4200000000000000000000000000000000000006", IsTest: true, Admin: "0xcf2d403c75ba3481ae7b190b1cd3246b5afe9120", @@ -1269,6 +1421,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0xcc5dd1eec29dbe028e61e91db5da4d453be48d90", RebalanceCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RepayerCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", + SetInputOutputTokens: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", Hub: { @@ -1322,7 +1475,9 @@ export const networkConfig: NetworksConfig = { MessageTransmitter: "0x7865fAfC2db2093669d92c0F33AeEF291086BEFD", }, AcrossV3SpokePool: "0xd08baaE74D6d2eAb1F3320B2E1a53eeb391ce8e5", - USDC: "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582", + Tokens: { + USDC: "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582", + }, WrappedNativeToken: "0x0000000000000000000000000000000000000000", IsTest: true, Admin: "0xcf2d403c75ba3481ae7b190b1cd3246b5afe9120", @@ -1330,6 +1485,7 @@ export const networkConfig: NetworksConfig = { Pauser: "0xcc5dd1eec29dbe028e61e91db5da4d453be48d90", RebalanceCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", RepayerCaller: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", + SetInputOutputTokens: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", MpcAddress: "0x6adAF8c96151962198a9b73132c16E99F4682Eb5", SignerAddress: "0x20ad9b208767e98dba19346f88b2686f00dbcf58", }, @@ -1346,7 +1502,8 @@ export interface StandaloneRepayerConfig { StargateTreasurer?: string; EverclearFeeAdapter?: string; OptimismStandardBridge?: string; - USDC: string; + BaseStandardBridge?: string; + // Repayer tokens are used from the general network config. WrappedNativeToken: string; RepayerRoutes: RepayerRoutesConfig; IsTest: boolean; @@ -1371,7 +1528,6 @@ export const repayerConfig: StandaloneRepayersConfig = { AcrossV3SpokePool: "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", StargateTreasurer: "0xd47b03ee6d86Cf251ee7860FB2ACf9f91B9fD4d7", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", WrappedNativeToken: "0x4200000000000000000000000000000000000006", RepayerRoutes: { "0xa21007B5BC5E2B488063752d1BE43C0f3f376743": { @@ -1398,7 +1554,6 @@ export const repayerConfig: StandaloneRepayersConfig = { AcrossV3SpokePool: "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A", StargateTreasurer: "0x146c8e409C113ED87C6183f4d25c50251DFfbb3a", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", WrappedNativeToken: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", RepayerRoutes: { "0xa21007B5BC5E2B488063752d1BE43C0f3f376743": { @@ -1425,7 +1580,6 @@ export const repayerConfig: StandaloneRepayersConfig = { AcrossV3SpokePool: "0x6f26Bf09B1C792e3228e5467807a900A503c0281", StargateTreasurer: "0x644abb1e17291b4403966119d15Ab081e4a487e9", EverclearFeeAdapter: "0xd0185bfb8107c5b2336bC73cE3fdd9Bfb504540e", - USDC: "0x0b2c639c533813f4Aa9D7837CAf62653d097Ff85", WrappedNativeToken: "0x4200000000000000000000000000000000000006", RepayerRoutes: { "0xa21007B5BC5E2B488063752d1BE43C0f3f376743": { diff --git a/package.json b/package.json index 3520939..e65c0b1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "compile": "hardhat compile", "deploy": "hardhat run ./scripts/deploy.ts", - "deploy-local": "hardhat run ./scripts/deploy.ts --network localhost", + "deploy-local": "VERIFY=false hardhat run ./scripts/deploy.ts --network localhost", "deploy-basesepolia": "hardhat run ./scripts/deploy.ts --network BASE_SEPOLIA", "deploy-ethereumsepolia": "hardhat run ./scripts/deploy.ts --network ETHEREUM_SEPOLIA", "deploy-arbitrumsepolia": "hardhat run ./scripts/deploy.ts --network ARBITRUM_SEPOLIA", diff --git a/scripts/common.ts b/scripts/common.ts index c005d26..798fe8c 100644 --- a/scripts/common.ts +++ b/scripts/common.ts @@ -1,4 +1,4 @@ -import {isAddress, getAddress} from "ethers"; +import {isAddress, getAddress, zeroPadValue, stripZerosLeft} from "ethers"; import {Network, Provider} from "../network.config"; export function assert(condition: any, message: string): asserts condition { @@ -28,13 +28,26 @@ export function isSet(input?: string): boolean { return false; } +export function addressToBytes32(address: any) { + return zeroPadValue(address.toString(), 32); +} + +export function bytes32ToToken(bytes32: any) { + // Making sure vanity addresses are not truncated. + const token = zeroPadValue(stripZerosLeft(bytes32), 20); + if (isAddress(token)) { + return getAddress(token); // Checksum. + } + return token; +} + export const ProviderSolidity = { LOCAL: 0n, CCTP: 1n, ACROSS: 2n, STARGATE: 3n, EVERCLEAR: 4n, - OPTIMISM_STANDARD_BRIDGE: 5n, + SUPERCHAIN_STANDARD_BRIDGE: 5n, }; export const DomainSolidity = { @@ -79,7 +92,7 @@ export const SolidityProvider: { [n: number]: Provider } = { 2: Provider.ACROSS, 3: Provider.STARGATE, 4: Provider.EVERCLEAR, - 5: Provider.OPTIMISM_STANDARD_BRIDGE, + 5: Provider.SUPERCHAIN_STANDARD_BRIDGE, }; export const CCTPDomain: { [n: number]: Network } = { diff --git a/scripts/deploy.ts b/scripts/deploy.ts index 688b188..633acd8 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -5,12 +5,11 @@ import {MaxUint256} from "ethers"; import {toBytes32, resolveProxyXAddress, resolveXAddress, getContractAt, resolveXAddresses} from "../test/helpers"; import { getVerifier, deployProxyX, getHardhatNetworkConfig, getNetworkConfig, percentsToBps, - getProxyXAdmin, + getProxyXAdmin, getInputOutputTokens, flattenInputOutputTokens, } from "./helpers"; import { assert, isSet, ProviderSolidity, DomainSolidity, DEFAULT_ADMIN_ROLE, ZERO_ADDRESS, - sameAddress, - assertAddress, + sameAddress, assertAddress, } from "./common"; import { SprinterUSDCLPShare, LiquidityHub, SprinterLiquidityMining, @@ -51,12 +50,13 @@ export async function main() { assert(config.AavePool! || config.AavePoolLongTerm! || config.USDCPool! || config.USDCStablecoinPool!, "At least one pool should be present."); - assertAddress(config.USDC, "USDC must be an address"); + assertAddress(config.Tokens.USDC, "USDC must be an address"); assertAddress(config.Admin, "Admin must be an address"); assertAddress(config.WithdrawProfit, "WithdrawProfit must be an address"); assertAddress(config.Pauser, "Pauser must be an address"); assertAddress(config.RebalanceCaller, "RebalanceCaller must be an address"); assertAddress(config.RepayerCaller, "RepayerCaller must be an address"); + assertAddress(config.SetInputOutputTokens, "SetInputOutputTokens must be an address"); assertAddress(config.MpcAddress, "MpcAddress must be an address"); assertAddress(config.SignerAddress, "SignerAddress must be an address"); assertAddress(config.WrappedNativeToken, "WrappedNativeToken must be an address"); @@ -121,6 +121,9 @@ export async function main() { if (!config.OptimismStandardBridge) { config.OptimismStandardBridge = ZERO_ADDRESS; } + if (!config.BaseStandardBridge) { + config.BaseStandardBridge = ZERO_ADDRESS; + } let mainPool: LiquidityPool | undefined = undefined; let aavePoolLongTerm: LiquidityPoolAaveLongTerm; @@ -134,7 +137,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, config.AavePoolLongTerm.AaveAddressesProvider, deployer, config.MpcAddress, @@ -179,7 +182,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, config.AavePool.AaveAddressesProvider, deployer, config.MpcAddress, @@ -223,7 +226,7 @@ export async function main() { "LiquidityPool", deployer, {}, - [config.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, config.SignerAddress], + [config.Tokens.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, config.SignerAddress], id, )) as LiquidityPool; console.log(`${id}: ${usdcPool.target}`); @@ -250,7 +253,7 @@ export async function main() { "LiquidityPoolStablecoin", deployer, {}, - [config.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, config.SignerAddress], + [config.Tokens.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, config.SignerAddress], id, )) as LiquidityPool; console.log(`${id}: ${usdcStablecoinPool.target}`); @@ -279,7 +282,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, @@ -305,7 +308,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, targetVault, deployer, ], @@ -328,7 +331,7 @@ export async function main() { rebalancerVersion, deployer, config.Admin, - [DomainSolidity[network], config.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter], + [DomainSolidity[network], config.Tokens.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter], [ config.Admin, config.RebalanceCaller, @@ -381,6 +384,7 @@ export async function main() { const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; repayerRoutes.Pools = await resolveXAddresses(repayerRoutes.Pools || [], false); + const inputOutputTokens = getInputOutputTokens(network, config); const repayerId = "Repayer"; let repayer: Repayer; @@ -399,7 +403,7 @@ export async function main() { config.Admin, [ DomainSolidity[network], - config.USDC, + config.Tokens.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter, config.AcrossV3SpokePool, @@ -407,14 +411,17 @@ export async function main() { config.WrappedNativeToken, config.StargateTreasurer, config.OptimismStandardBridge, + config.BaseStandardBridge, ], [ config.Admin, config.RepayerCaller, + config.SetInputOutputTokens, repayerRoutes.Pools, repayerRoutes.Domains.map(el => DomainSolidity[el]), repayerRoutes.Providers.map(el => ProviderSolidity[el]), repayerRoutes.SupportsAllTokens, + inputOutputTokens, ], repayerId, ); @@ -444,7 +451,7 @@ export async function main() { config.Admin, [lpToken, mainPool], [ - config.USDC, + config.Tokens.USDC, config.Admin, config.Hub.AssetsAdjuster, config.Hub.DepositProfit, @@ -525,7 +532,7 @@ export async function main() { console.log(`LiquidityPool Withdraw Profit: ${config.WithdrawProfit}`); console.log(`LiquidityPool Pauser: ${config.Pauser}`); console.log(`MPC Address: ${config.MpcAddress}`); - console.log(`USDC: ${config.USDC}`); + console.log(`USDC: ${config.Tokens.USDC}`); console.log(`Signer Address: ${config.SignerAddress}`); console.log(`Rebalancer: ${rebalancer.target}`); console.log(`RebalancerProxyAdmin: ${rebalancerAdmin.target}`); @@ -556,6 +563,10 @@ export async function main() { } console.table(transposedRoutes); } + if (inputOutputTokens.length > 0) { + console.log("InputOutputTokens:"); + console.table(flattenInputOutputTokens(inputOutputTokens)); + } await verifier.verify(process.env.VERIFY === "true"); } diff --git a/scripts/deployERC4626Adapter.ts b/scripts/deployERC4626Adapter.ts index 054ef25..cc0af6c 100644 --- a/scripts/deployERC4626Adapter.ts +++ b/scripts/deployERC4626Adapter.ts @@ -42,7 +42,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, targetVault, deployer, ], diff --git a/scripts/deployRepayer.ts b/scripts/deployRepayer.ts index baa4c26..2bff6f2 100644 --- a/scripts/deployRepayer.ts +++ b/scripts/deployRepayer.ts @@ -1,10 +1,14 @@ import dotenv from "dotenv"; dotenv.config(); import hre from "hardhat"; -import {isAddress} from "ethers"; -import {getVerifier, deployProxyX, getHardhatNetworkConfig, getNetworkConfig, addLocalPools} from "./helpers"; +import { + getVerifier, deployProxyX, getHardhatNetworkConfig, getNetworkConfig, addLocalPools, + getInputOutputTokens, flattenInputOutputTokens, +} from "./helpers"; import {resolveXAddress} from "../test/helpers"; -import {isSet, assert, ProviderSolidity, DomainSolidity, ZERO_ADDRESS} from "./common"; +import { + isSet, assert, ProviderSolidity, DomainSolidity, ZERO_ADDRESS, assertAddress, +} from "./common"; import {Repayer} from "../typechain-types"; import { Network, NetworkConfig, Provider, @@ -27,10 +31,11 @@ export async function main() { id += "-DeployTest"; } - assert(isAddress(config.USDC), "USDC must be an address"); - assert(isAddress(config.Admin), "Admin must be an address"); - assert(isAddress(config.RepayerCaller), "RepayerCaller must be an address"); - assert(isAddress(config.WrappedNativeToken), "WrappedNativeToken must be an address"); + assertAddress(config.Tokens.USDC, "USDC must be an address"); + assertAddress(config.Admin, "Admin must be an address"); + assertAddress(config.RepayerCaller, "RepayerCaller must be an address"); + assertAddress(config.SetInputOutputTokens, "SetInputOutputTokens must be an address"); + assertAddress(config.WrappedNativeToken, "WrappedNativeToken must be an address"); const repayerRoutes: {Pool: string, Domain: Network, Provider: Provider, SupportsAllTokens: boolean}[] = []; for (const [pool, domainProviders] of Object.entries(config.RepayerRoutes || {})) { @@ -65,7 +70,11 @@ export async function main() { if (!config.OptimismStandardBridge) { config.OptimismStandardBridge = ZERO_ADDRESS; } + if (!config.BaseStandardBridge) { + config.BaseStandardBridge = ZERO_ADDRESS; + } + const inputOutputTokens = getInputOutputTokens(network, config); const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; const {target: repayer, targetAdmin: repayerAdmin} = await deployProxyX( @@ -75,7 +84,7 @@ export async function main() { config.Admin, [ DomainSolidity[network], - config.USDC, + config.Tokens.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter, config.AcrossV3SpokePool, @@ -83,14 +92,17 @@ export async function main() { config.WrappedNativeToken, config.StargateTreasurer, config.OptimismStandardBridge, + config.BaseStandardBridge, ], [ config.Admin, config.RepayerCaller, + config.SetInputOutputTokens, repayerRoutes.map(el => el.Pool), repayerRoutes.map(el => DomainSolidity[el.Domain]), repayerRoutes.map(el => ProviderSolidity[el.Provider]), repayerRoutes.map(el => el.SupportsAllTokens), + inputOutputTokens, ], id, verifier, @@ -102,6 +114,10 @@ export async function main() { console.log("RepayerRoutes:"); console.table(repayerRoutes); } + if (inputOutputTokens.length > 0) { + console.log("InputOutputTokens:"); + console.table(flattenInputOutputTokens(inputOutputTokens)); + } await verifier.verify(process.env.VERIFY === "true"); } diff --git a/scripts/deployStandaloneRepayer.ts b/scripts/deployStandaloneRepayer.ts index 1879677..715fb25 100644 --- a/scripts/deployStandaloneRepayer.ts +++ b/scripts/deployStandaloneRepayer.ts @@ -1,13 +1,19 @@ import dotenv from "dotenv"; dotenv.config(); import hre from "hardhat"; -import {isAddress, getAddress} from "ethers"; -import {getVerifier, deployProxyX, getHardhatStandaloneRepayerConfig, getStandaloneRepayerConfig} from "./helpers"; +import {getAddress} from "ethers"; +import { + getVerifier, deployProxyX, getHardhatStandaloneRepayerConfig, getStandaloneRepayerConfig, + getInputOutputTokens, flattenInputOutputTokens, +} from "./helpers"; import {resolveXAddress, toBytes32} from "../test/helpers"; -import {isSet, assert, ProviderSolidity, DomainSolidity, ZERO_ADDRESS, DEFAULT_ADMIN_ROLE} from "./common"; +import { + isSet, assert, ProviderSolidity, DomainSolidity, ZERO_ADDRESS, DEFAULT_ADMIN_ROLE, assertAddress, +} from "./common"; import {Repayer} from "../typechain-types"; import { Network, StandaloneRepayerConfig, StandaloneRepayerEnv, Provider, + networkConfig, } from "../network.config"; export async function main() { @@ -33,11 +39,11 @@ export async function main() { id += "-DeployTest"; } - assert(isAddress(config.USDC), "USDC must be an address"); - assert(isAddress(config.Admin), "Admin must be an address"); + assertAddress(networkConfig[network].Tokens.USDC, "USDC must be an address"); + assertAddress(config.Admin, "Admin must be an address"); assert(config.RepayerCallers.length > 0, "RepayerCallers must not be empty"); - config.RepayerCallers.forEach(el => assert(isAddress(el), "Each RepayerCaller must be an address")); - assert(isAddress(config.WrappedNativeToken), "WrappedNativeToken must be an address"); + config.RepayerCallers.forEach(el => assertAddress(el, "Each RepayerCaller must be an address")); + assertAddress(config.WrappedNativeToken, "WrappedNativeToken must be an address"); const repayerRoutes: {Pool: string, Domain: Network, Provider: Provider, SupportsAllTokens: boolean}[] = []; for (const [pool, domainProviders] of Object.entries(config.RepayerRoutes || {})) { @@ -71,7 +77,11 @@ export async function main() { if (!config.OptimismStandardBridge) { config.OptimismStandardBridge = ZERO_ADDRESS; } + if (!config.BaseStandardBridge) { + config.BaseStandardBridge = ZERO_ADDRESS; + } + const inputOutputTokens = getInputOutputTokens(network, networkConfig[network]); const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; const {target: repayer, targetAdmin: repayerAdmin} = await deployProxyX( @@ -81,7 +91,7 @@ export async function main() { config.Admin, [ DomainSolidity[network], - config.USDC, + networkConfig[network].Tokens.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter, config.AcrossV3SpokePool, @@ -89,14 +99,17 @@ export async function main() { config.WrappedNativeToken, config.StargateTreasurer, config.OptimismStandardBridge, + config.BaseStandardBridge, ], [ deployer, config.RepayerCallers[0], + config.Admin, repayerRoutes.map(el => el.Pool), repayerRoutes.map(el => DomainSolidity[el.Domain]), repayerRoutes.map(el => ProviderSolidity[el.Provider]), repayerRoutes.map(el => el.SupportsAllTokens), + inputOutputTokens, ], id, verifier, @@ -115,6 +128,10 @@ export async function main() { console.log("RepayerRoutes:"); console.table(repayerRoutes); } + if (inputOutputTokens.length > 0) { + console.log("InputOutputTokens:"); + console.table(flattenInputOutputTokens(inputOutputTokens)); + } if (getAddress(deployer.address) != getAddress(config.Admin)) { await repayer.grantRole(DEFAULT_ADMIN_ROLE, config.Admin); diff --git a/scripts/deployUSDCPool.ts b/scripts/deployUSDCPool.ts index 1387a5a..d1eedc9 100644 --- a/scripts/deployUSDCPool.ts +++ b/scripts/deployUSDCPool.ts @@ -39,7 +39,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, diff --git a/scripts/deployUSDCPoolAave.ts b/scripts/deployUSDCPoolAave.ts index 0840d62..ea544f4 100644 --- a/scripts/deployUSDCPoolAave.ts +++ b/scripts/deployUSDCPoolAave.ts @@ -32,7 +32,7 @@ export async function main() { assertAddress(config.Admin, "Admin must be an address"); assertAddress(config.WithdrawProfit, "WithdrawProfit must be an address"); assertAddress(config.Pauser, "Pauser must be an address"); - assertAddress(config.USDC, "USDC must be an address"); + assertAddress(config.Tokens.USDC, "USDC must be an address"); assertAddress(config.MpcAddress, "MpcAddress must be an address"); assertAddress(config.WrappedNativeToken, "WrappedNativeToken must be an address"); assertAddress(config.SignerAddress, "SignerAddress must be an address"); @@ -48,7 +48,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, config.AavePool.AaveAddressesProvider, deployer, config.MpcAddress, diff --git a/scripts/deployUSDCPoolAaveLongTerm.ts b/scripts/deployUSDCPoolAaveLongTerm.ts index bcae744..dbec4a9 100644 --- a/scripts/deployUSDCPoolAaveLongTerm.ts +++ b/scripts/deployUSDCPoolAaveLongTerm.ts @@ -34,7 +34,7 @@ export async function main() { assertAddress(config.Admin, "Admin must be an address"); assertAddress(config.WithdrawProfit, "WithdrawProfit must be an address"); assertAddress(config.Pauser, "Pauser must be an address"); - assertAddress(config.USDC, "USDC must be an address"); + assertAddress(config.Tokens.USDC, "USDC must be an address"); assertAddress(config.MpcAddress, "MpcAddress must be an address"); assertAddress(config.WrappedNativeToken, "WrappedNativeToken must be an address"); assertAddress(config.SignerAddress, "SignerAddress must be an address"); @@ -52,7 +52,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, config.AavePoolLongTerm.AaveAddressesProvider, deployer, config.MpcAddress, diff --git a/scripts/deployUSDCPublicPool.ts b/scripts/deployUSDCPublicPool.ts index 752ed8f..b96fb16 100644 --- a/scripts/deployUSDCPublicPool.ts +++ b/scripts/deployUSDCPublicPool.ts @@ -38,7 +38,7 @@ export async function main() { deployer, {}, [ - config.USDC, + config.Tokens.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, diff --git a/scripts/deployUSDCStablecoinPool.ts b/scripts/deployUSDCStablecoinPool.ts index 9285dbe..d65fee0 100644 --- a/scripts/deployUSDCStablecoinPool.ts +++ b/scripts/deployUSDCStablecoinPool.ts @@ -37,7 +37,7 @@ export async function main() { const usdcPoolStablecoin: LiquidityPoolStablecoin = (await verifier.deployX( "LiquidityPoolStablecoin", deployer, {}, [ - config.USDC, + config.Tokens.USDC, deployer, config.MpcAddress, config.WrappedNativeToken, diff --git a/scripts/helpers.ts b/scripts/helpers.ts index 4edfb98..e2065b0 100644 --- a/scripts/helpers.ts +++ b/scripts/helpers.ts @@ -5,9 +5,11 @@ import { resolveXAddress, resolveProxyXAddress, assertCode, } from "../test/helpers"; import { - TransparentUpgradeableProxy, ProxyAdmin, + TransparentUpgradeableProxy, ProxyAdmin, Repayer, } from "../typechain-types"; -import {sleep, DEFAULT_PROXY_TYPE, assert, assertAddress} from "./common"; +import { + sleep, DEFAULT_PROXY_TYPE, assert, assertAddress, DomainSolidity, addressToBytes32, bytes32ToToken, SolidityDomain +} from "./common"; import { networkConfig, Network, NetworkConfig, StandaloneRepayerEnv, StandaloneRepayerConfig, repayerConfig, @@ -18,6 +20,8 @@ import { LiquidityPoolAaveUSDCLongTermVersions, LiquidityPoolPublicUSDCVersions, ERC4626AdapterUSDCVersions, + PartialNetworksConfig, + Token, } from "../network.config"; export async function resolveAddresses(input: any[]): Promise { @@ -292,6 +296,67 @@ export async function addLocalPools( } } +export function getNetworkConfigsForCurrentEnv(config: NetworkConfig): PartialNetworksConfig { + const networkConfigs: PartialNetworksConfig = {}; + let isTest = false; + let isStage = false; + if (config.IsTest) { + isTest = true; + } else if (process.env.DEPLOY_TYPE === "STAGE") { + isStage = true; + } + for (const network of Object.values(Network)) { + if (isTest === networkConfig[network].IsTest) { + networkConfigs[network] = networkConfig[network]; + } else if (isStage && networkConfig[network].Stage) { + networkConfigs[network] = networkConfig[network].Stage; + } + } + return networkConfigs; +} + +export function getInputOutputTokens(network: Network, config: NetworkConfig) { + const envConfigs = getNetworkConfigsForCurrentEnv(config); + const inputOutputTokens: Repayer.InputOutputTokenStruct[] = []; + for (const [tokenSymbol, tokenAddress] of Object.entries(config.Tokens) as [Token, string][]) { + const inputToken: Repayer.InputOutputTokenStruct = { + inputToken: tokenAddress, + destinationTokens: [], + }; + for (const [envNetwork, envConfig] of Object.entries(envConfigs) as [Network, NetworkConfig][]) { + if (envNetwork === network) continue; + if (envConfig.Tokens[tokenSymbol]) { + inputToken.destinationTokens.push({ + destinationDomain: DomainSolidity[envNetwork], + outputToken: addressToBytes32(envConfig.Tokens[tokenSymbol]), + }); + } + } + if (inputToken.destinationTokens.length > 0) { + inputOutputTokens.push(inputToken); + } + } + return inputOutputTokens; +} + +export function flattenInputOutputTokens(inputOutputTokens: Repayer.InputOutputTokenStruct[]) { + const flatInputOutputTokens: { + InputToken: string; + Domain: Network; + OutputToken: string; + }[] = []; + for (const entry of inputOutputTokens) { + for (const destinationToken of entry.destinationTokens) { + flatInputOutputTokens.push({ + InputToken: entry.inputToken as string, + Domain: SolidityDomain[Number(destinationToken.destinationDomain)], + OutputToken: bytes32ToToken(destinationToken.outputToken), + }); + } + } + return flatInputOutputTokens; +} + export async function getNetworkConfig() { let network: Network; let config: NetworkConfig; @@ -316,7 +381,7 @@ export async function getNetworkConfig() { } export async function getHardhatNetworkConfig() { - assert(hre.network.name === "hardhat", "Only for Hardhat network"); + assert(hre.network.name === "hardhat" || hre.network.name === "localhost", "Only for Hardhat or localhost network"); const network = Network.BASE; const [deployer, opsAdmin, superAdmin, mpc] = await hre.ethers.getSigners(); process.env.DEPLOYER_ADDRESS = await resolveAddress(deployer); diff --git a/scripts/upgradeRebalancer.ts b/scripts/upgradeRebalancer.ts index 71ad043..e690d34 100644 --- a/scripts/upgradeRebalancer.ts +++ b/scripts/upgradeRebalancer.ts @@ -25,7 +25,7 @@ export async function main() { ({network, config} = await getHardhatNetworkConfig()); } - assert(isAddress(config.USDC), "USDC must be an address"); + assert(isAddress(config.Tokens.USDC), "USDC must be an address"); if (!config.CCTP) { config.CCTP = { TokenMessenger: ZERO_ADDRESS, @@ -41,7 +41,7 @@ export async function main() { rebalancerAddress, rebalancerVersion, deployer, - [DomainSolidity[network], config.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter], + [DomainSolidity[network], config.Tokens.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter], "Rebalancer", ); diff --git a/scripts/upgradeRepayer.ts b/scripts/upgradeRepayer.ts index bd793e8..3343c3e 100644 --- a/scripts/upgradeRepayer.ts +++ b/scripts/upgradeRepayer.ts @@ -25,7 +25,7 @@ export async function main() { ({network, config} = await getHardhatNetworkConfig()); } - assert(isAddress(config.USDC), "USDC must be an address"); + assert(isAddress(config.Tokens.USDC), "USDC must be an address"); assert(isAddress(config.WrappedNativeToken), "WrappedNativeToken must be an address"); if (!config.CCTP) { config.CCTP = { @@ -45,6 +45,9 @@ export async function main() { if (!config.OptimismStandardBridge) { config.OptimismStandardBridge = ZERO_ADDRESS; } + if (!config.BaseStandardBridge) { + config.BaseStandardBridge = ZERO_ADDRESS; + } const repayerAddress = await getDeployProxyXAddress("Repayer"); const repayerVersion = config.IsTest ? "TestRepayer" : "Repayer"; @@ -56,7 +59,7 @@ export async function main() { deployer, [ DomainSolidity[network], - config.USDC, + config.Tokens.USDC, config.CCTP.TokenMessenger, config.CCTP.MessageTransmitter, config.AcrossV3SpokePool, @@ -64,6 +67,7 @@ export async function main() { config.WrappedNativeToken, config.StargateTreasurer, config.OptimismStandardBridge, + config.BaseStandardBridge, ], "Repayer", ); diff --git a/specific-fork-test/ethereum/Repayer.ts b/specific-fork-test/ethereum/Repayer.ts index 935c48f..eb21a20 100644 --- a/specific-fork-test/ethereum/Repayer.ts +++ b/specific-fork-test/ethereum/Repayer.ts @@ -10,6 +10,7 @@ import { import { ProviderSolidity as Provider, DomainSolidity as Domain, DEFAULT_ADMIN_ROLE, assertAddress, ETH, ZERO_ADDRESS, + addressToBytes32, } from "../../scripts/common"; import { TransparentUpgradeableProxy, ProxyAdmin, @@ -19,7 +20,7 @@ import {networkConfig} from "../../network.config"; describe("Repayer", function () { const deployAll = async () => { - const [deployer, admin, repayUser, user] = await hre.ethers.getSigners(); + const [deployer, admin, repayUser, user, setTokensUser] = await hre.ethers.getSigners(); await setCode(repayUser.address, "0x00"); const forkNetworkConfig = networkConfig.ETHEREUM; @@ -27,7 +28,7 @@ describe("Repayer", function () { const REPAYER_ROLE = toBytes32("REPAYER_ROLE"); const DEPOSIT_PROFIT_ROLE = toBytes32("DEPOSIT_PROFIT_ROLE"); - const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.USDC); + const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); const liquidityPool = (await deploy( "TestLiquidityPool", deployer, @@ -61,9 +62,13 @@ describe("Repayer", function () { forkNetworkConfig.StargateTreasurer! ); const optimismStandardBridge = await hre.ethers.getContractAt( - "IOptimismStandardBridge", + "ISuperchainStandardBridge", forkNetworkConfig.OptimismStandardBridge! ); + const baseStandardBridge = await hre.ethers.getContractAt( + "ISuperchainStandardBridge", + forkNetworkConfig.BaseStandardBridge! + ); const everclearFeeAdapter = await hre.ethers.getContractAt("IFeeAdapterV2", forkNetworkConfig.EverclearFeeAdapter!); const weth = await hre.ethers.getContractAt("IWrappedNativeToken", forkNetworkConfig.WrappedNativeToken); @@ -80,15 +85,31 @@ describe("Repayer", function () { weth, stargateTreasurer, optimismStandardBridge, + baseStandardBridge, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, - [liquidityPool, liquidityPool2, liquidityPool], - [Domain.ETHEREUM, Domain.ETHEREUM, Domain.OP_MAINNET], - [Provider.LOCAL, Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], - [true, false, true], + setTokensUser, + [liquidityPool, liquidityPool2, liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.ETHEREUM, Domain.OP_MAINNET, Domain.BASE], + [Provider.LOCAL, Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE, Provider.SUPERCHAIN_STANDARD_BRIDGE], + [true, false, true, true], + [ + { + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.OP_MAINNET, outputToken: addressToBytes32(networkConfig.OP_MAINNET.Tokens.USDC)} + ] + }, + { + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.BASE, outputToken: addressToBytes32(networkConfig.BASE.Tokens.USDC)} + ] + }, + ], )).data; const repayerProxy = (await deployX( "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer", {}, @@ -101,10 +122,10 @@ describe("Repayer", function () { await liquidityPool.grantRole(DEPOSIT_PROFIT_ROLE, repayer); return { - deployer, admin, repayUser, user, usdc, + deployer, admin, repayUser, user, usdc, setTokensUser, USDC_DEC, liquidityPool, liquidityPool2, repayer, repayerProxy, repayerAdmin, cctpTokenMessenger, cctpMessageTransmitter, REPAYER_ROLE, DEFAULT_ADMIN_ROLE, acrossV3SpokePool, weth, - stargateTreasurer, everclearFeeAdapter, forkNetworkConfig, optimismStandardBridge, + stargateTreasurer, everclearFeeAdapter, forkNetworkConfig, optimismStandardBridge, baseStandardBridge, }; }; @@ -122,23 +143,23 @@ describe("Repayer", function () { await usdc.connect(usdcOwner).transfer(repayer, 10n * USDC_DEC); const amount = 4n * USDC_DEC; - const outputToken = networkConfig.OP_MAINNET.USDC; + const outputToken = networkConfig.OP_MAINNET.Tokens.USDC; const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [outputToken, minGasLimit] + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x1234"] ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, liquidityPool, Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) .to.emit(repayer, "InitiateRepay") - .withArgs(usdc.target, amount, liquidityPool.target, Domain.OP_MAINNET, Provider.OPTIMISM_STANDARD_BRIDGE); + .withArgs(usdc.target, amount, liquidityPool.target, Domain.OP_MAINNET, Provider.SUPERCHAIN_STANDARD_BRIDGE); await expect(tx) .to.emit(usdc, "Transfer") .withArgs(repayer.target, optimismStandardBridge.target, amount); @@ -150,7 +171,7 @@ describe("Repayer", function () { repayer, liquidityPool, amount, - "0x" + "0x1234" ); }); @@ -162,27 +183,107 @@ describe("Repayer", function () { const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [ZERO_ADDRESS, minGasLimit] + ["address", "uint32", "bytes"], + [ZERO_ADDRESS, minGasLimit, "0x1234"] ); const tx = repayer.connect(repayUser).initiateRepay( weth, amount, liquidityPool, Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) .to.emit(repayer, "InitiateRepay") - .withArgs(weth.target, amount, liquidityPool.target, Domain.OP_MAINNET, Provider.OPTIMISM_STANDARD_BRIDGE); + .withArgs(weth.target, amount, liquidityPool.target, Domain.OP_MAINNET, Provider.SUPERCHAIN_STANDARD_BRIDGE); await expect(tx) .to.emit(optimismStandardBridge, "ETHBridgeInitiated") .withArgs( repayer, liquidityPool, amount, - "0x" + "0x1234" + ); + expect(await getBalance(repayer)).to.equal(0n); + expect(await weth.balanceOf(repayer)).to.equal(0n); + }); + + it("Should allow repayer to initiate Base repay on fork", async function () { + const {repayer, USDC_DEC, usdc, repayUser, liquidityPool, baseStandardBridge} = await loadFixture(deployAll); + + assertAddress(process.env.USDC_OWNER_ETH_ADDRESS, "Env variables not configured (USDC_OWNER_ETH_ADDRESS missing)"); + const USDC_OWNER_ETH_ADDRESS = process.env.USDC_OWNER_ETH_ADDRESS; + const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ETH_ADDRESS); + await setBalance(USDC_OWNER_ETH_ADDRESS, 10n ** 18n); + + expect(await repayer.BASE_STANDARD_BRIDGE()) + .to.equal(baseStandardBridge.target); + + await usdc.connect(usdcOwner).transfer(repayer, 10n * USDC_DEC); + + const amount = 4n * USDC_DEC; + const outputToken = networkConfig.BASE.Tokens.USDC; + const minGasLimit = 100000n; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x1234"] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.BASE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + extraData + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(usdc.target, amount, liquidityPool.target, Domain.BASE, Provider.SUPERCHAIN_STANDARD_BRIDGE); + await expect(tx) + .to.emit(usdc, "Transfer") + .withArgs(repayer.target, baseStandardBridge.target, amount); + await expect(tx) + .to.emit(baseStandardBridge, "ERC20BridgeInitiated") + .withArgs( + usdc, + outputToken, + repayer, + liquidityPool, + amount, + "0x1234" + ); + }); + + it("Should allow repayer to initiate native token Base repay on fork", async function () { + const {repayer, repayUser, liquidityPool, baseStandardBridge, weth} = await loadFixture(deployAll); + + const amount = 4n * ETH; + await repayUser.sendTransaction({to: repayer, value: amount}); + + const minGasLimit = 100000n; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint32", "bytes"], + [ZERO_ADDRESS, minGasLimit, "0x1234"] + ); + const tx = repayer.connect(repayUser).initiateRepay( + weth, + amount, + liquidityPool, + Domain.BASE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + extraData + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(weth.target, amount, liquidityPool.target, Domain.BASE, Provider.SUPERCHAIN_STANDARD_BRIDGE); + await expect(tx) + .to.emit(baseStandardBridge, "ETHBridgeInitiated") + .withArgs( + repayer, + liquidityPool, + amount, + "0x1234" ); expect(await getBalance(repayer)).to.equal(0n); expect(await weth.balanceOf(repayer)).to.equal(0n); diff --git a/test/ERC4626Adapter.ts b/test/ERC4626Adapter.ts index 62c0864..aa44079 100644 --- a/test/ERC4626Adapter.ts +++ b/test/ERC4626Adapter.ts @@ -33,7 +33,7 @@ describe("ERC4626Adapter", function () { ] = await hre.ethers.getSigners(); await setCode(user2.address, "0x00"); - const USDC_ADDRESS = networkConfig.BASE.USDC; + const USDC_ADDRESS = networkConfig.BASE.Tokens.USDC; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); diff --git a/test/LiquidityPool.ts b/test/LiquidityPool.ts index 0e8f6a2..354dcca 100644 --- a/test/LiquidityPool.ts +++ b/test/LiquidityPool.ts @@ -24,7 +24,7 @@ describe("LiquidityPool", function () { ] = await hre.ethers.getSigners(); await setCode(user2.address, "0x00"); - const USDC_ADDRESS = networkConfig.BASE.USDC; + const USDC_ADDRESS = networkConfig.BASE.Tokens.USDC; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); @@ -671,7 +671,7 @@ describe("LiquidityPool", function () { await usdc.connect(usdcOwner).approve(liquidityPool, amountLiquidity); await expect(liquidityPool.connect(usdcOwner).depositWithPull(amountLiquidity)) .to.emit(liquidityPool, "Deposit").withArgs(usdcOwner, amountLiquidity); - expect(await liquidityPool.balance(usdc)).to.eq(amountLiquidity + amountLiquidity); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should withdraw liquidity", async function () { @@ -936,8 +936,11 @@ describe("LiquidityPool", function () { }); it("Should NOT borrow if borrowing is paused", async function () { - const {liquidityPool, user, user2, withdrawProfit, mpc_signer, usdc, USDC_DEC} = await loadFixture(deployAll); + const { + liquidityPool, user, user2, withdrawProfit, mpc_signer, usdc, USDC_DEC, usdcOwner + } = await loadFixture(deployAll); + await usdc.connect(usdcOwner).transfer(liquidityPool, 1000n * USDC_DEC); // Pause borrowing await expect(liquidityPool.connect(withdrawProfit).pauseBorrow()) .to.emit(liquidityPool, "BorrowPaused"); @@ -962,6 +965,7 @@ describe("LiquidityPool", function () { 2000000000n, signature)) .to.be.revertedWithCustomError(liquidityPool, "BorrowingIsPaused"); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should NOT borrow if the contract is paused", async function () { diff --git a/test/LiquidityPoolAave.ts b/test/LiquidityPoolAave.ts index bb7fd22..0e42c35 100644 --- a/test/LiquidityPoolAave.ts +++ b/test/LiquidityPoolAave.ts @@ -37,7 +37,7 @@ describe("LiquidityPoolAave", function () { const aavePoolAddress = await aavePoolAddressesProvider.getPool(); const aavePool = await hre.ethers.getContractAt("IAavePool", aavePoolAddress); - const USDC_ADDRESS = forkNetworkConfig.USDC; + const USDC_ADDRESS = forkNetworkConfig.Tokens.USDC; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); @@ -1150,10 +1150,12 @@ describe("LiquidityPoolAave", function () { .to.emit(liquidityPool, "Paused"); await eurc.connect(eurcOwner).transfer(liquidityPool, amountToBorrow); - + await time.increase(3600); await expect(liquidityPool.connect(user).repay([eurc])) - .to.emit(liquidityPool, "Repaid"); + .to.emit(liquidityPool, "Repaid"); + expect(await liquidityPool.balance(eurc)).to.eq(0n); + await liquidityPool.connect(pauser).unpause(); expect(await eurc.balanceOf(liquidityPool)).to.be.lessThan(amountToBorrow); expect(await liquidityPool.balance(eurc)).to.be.lessThan(availableBefore + 1n * EURC_DEC); expect(await liquidityPool.balance(eurc)).to.be.greaterThan(availableBefore - 1n * EURC_DEC); @@ -1184,7 +1186,7 @@ describe("LiquidityPoolAave", function () { await expect(liquidityPool.connect(usdcOwner).depositWithPull(amountCollateral)) .to.emit(liquidityPool, "SuppliedToAave").withArgs(amountCollateral); expect(await aToken.balanceOf(liquidityPool)).to.be.greaterThanOrEqual(amountCollateral * 2n - 1n); - expectAlmostEqual(await liquidityPool.balance(usdc), 100n * USDC_DEC); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should borrow and repay different tokens", async function () { @@ -2002,8 +2004,13 @@ describe("LiquidityPoolAave", function () { }); it("Should NOT borrow if borrowing is paused", async function () { - const {liquidityPool, user, user2, withdrawProfit, mpc_signer, eurc, EURC_DEC} = await loadFixture(deployAll); - + const { + liquidityPool, user, user2, withdrawProfit, mpc_signer, eurc, EURC_DEC, usdc, USDC_DEC, usdcOwner, + liquidityAdmin + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool, amountCollateral); + await liquidityPool.connect(liquidityAdmin).deposit(amountCollateral); // Pause borrowing await expect(liquidityPool.connect(withdrawProfit).pauseBorrow()) .to.emit(liquidityPool, "BorrowPaused"); @@ -2028,6 +2035,7 @@ describe("LiquidityPoolAave", function () { 2000000000n, signature)) .to.be.revertedWithCustomError(liquidityPool, "BorrowingIsPaused"); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should NOT borrow if the contract is paused", async function () { diff --git a/test/LiquidityPoolAaveLongTerm.ts b/test/LiquidityPoolAaveLongTerm.ts index d1869ea..d081456 100644 --- a/test/LiquidityPoolAaveLongTerm.ts +++ b/test/LiquidityPoolAaveLongTerm.ts @@ -38,7 +38,7 @@ describe("LiquidityPoolAaveLongTerm", function () { const aavePoolAddress = await aavePoolAddressesProvider.getPool(); const aavePool = await hre.ethers.getContractAt("IAavePool", aavePoolAddress); - const USDC_ADDRESS = forkNetworkConfig.USDC; + const USDC_ADDRESS = forkNetworkConfig.Tokens.USDC; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); @@ -1625,6 +1625,8 @@ describe("LiquidityPoolAaveLongTerm", function () { await time.increase(3600); await expect(liquidityPool.connect(user).repay([eurc])) .to.emit(liquidityPool, "Repaid"); + expect(await liquidityPool.balance(eurc)).to.eq(0n); + await liquidityPool.connect(pauser).unpause(); expect(await eurc.balanceOf(liquidityPool)).to.be.lessThan(amountToBorrow); expect(await liquidityPool.balance(eurc)).to.be.lessThan(availableBefore + amountToBorrow + 1n * EURC_DEC); expect(await liquidityPool.balance(eurc)).to.be.greaterThan(availableBefore + amountToBorrow - 1n * EURC_DEC); @@ -1671,6 +1673,8 @@ describe("LiquidityPoolAaveLongTerm", function () { await time.increase(3600); await expect(liquidityPool.connect(user).repayPartial([eurc], [amountToBorrow])) .to.emit(liquidityPool, "Repaid"); + expect(await liquidityPool.balance(eurc)).to.eq(0n); + await liquidityPool.connect(pauser).unpause(); expect(await eurc.allowance(liquidityPool, aavePool)).to.eq(0); expect(await eurc.balanceOf(liquidityPool)).to.eq(amountToBorrow); expect(await liquidityPool.balance(eurc)).to.be.lessThan(availableBefore + amountToBorrow + 1n * EURC_DEC); @@ -1702,7 +1706,7 @@ describe("LiquidityPoolAaveLongTerm", function () { await expect(liquidityPool.connect(usdcOwner).depositWithPull(amountCollateral)) .to.emit(liquidityPool, "SuppliedToAave").withArgs(amountCollateral); expect(await aToken.balanceOf(liquidityPool)).to.be.greaterThanOrEqual(amountCollateral * 2n - 1n); - expectAlmostEqual(await liquidityPool.balance(usdc), 100n * USDC_DEC); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should borrow and repay different tokens", async function () { @@ -2884,8 +2888,13 @@ describe("LiquidityPoolAaveLongTerm", function () { }); it("Should NOT borrow if borrowing is paused", async function () { - const {liquidityPool, user, user2, withdrawProfit, mpc_signer, eurc, EURC_DEC} = await loadFixture(deployAll); - + const { + liquidityPool, user, user2, withdrawProfit, mpc_signer, eurc, EURC_DEC, usdc, USDC_DEC, usdcOwner, + liquidityAdmin + } = await loadFixture(deployAll); + const amountCollateral = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool, amountCollateral); + await liquidityPool.connect(liquidityAdmin).deposit(amountCollateral); // Pause borrowing await expect(liquidityPool.connect(withdrawProfit).pauseBorrow()) .to.emit(liquidityPool, "BorrowPaused"); @@ -2910,6 +2919,7 @@ describe("LiquidityPoolAaveLongTerm", function () { 2000000000n, signature)) .to.be.revertedWithCustomError(liquidityPool, "BorrowingIsPaused"); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should NOT borrow if the contract is paused", async function () { @@ -4143,6 +4153,7 @@ describe("LiquidityPoolAaveLongTerm", function () { .to.emit(liquidityPool, "SuppliedToAave").withArgs(amount); expect(await aToken.balanceOf(liquidityPool)).to.be.greaterThanOrEqual(amount - 2n); + await time.increase(3600); await expect(liquidityPool.connect(liquidityAdmin).withdraw(user, amount)) .to.emit(liquidityPool, "WithdrawnFromAave").withArgs(user.address, amount); diff --git a/test/LiquidityPoolStablecoin.ts b/test/LiquidityPoolStablecoin.ts index 7b202e4..9d47964 100644 --- a/test/LiquidityPoolStablecoin.ts +++ b/test/LiquidityPoolStablecoin.ts @@ -27,7 +27,7 @@ describe("LiquidityPoolStablecoin", function () { const forkNetworkConfig = networkConfig.BASE; - const USDC_ADDRESS = forkNetworkConfig.USDC; + const USDC_ADDRESS = forkNetworkConfig.Tokens.USDC; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); @@ -614,7 +614,7 @@ describe("LiquidityPoolStablecoin", function () { await usdc.connect(usdcOwner).approve(liquidityPool, amountLiquidity); await expect(liquidityPool.connect(usdcOwner).depositWithPull(amountLiquidity)) .to.emit(liquidityPool, "Deposit").withArgs(usdcOwner, amountLiquidity); - expect(await liquidityPool.balance(usdc)).to.eq(amountLiquidity * 2n); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should withdraw liquidity", async function () { @@ -843,9 +843,11 @@ describe("LiquidityPoolStablecoin", function () { it("Should NOT borrow if borrowing is paused", async function () { const { - liquidityPool, user, user2, withdrawProfit, mpc_signer, usdc, USDC_DEC + liquidityPool, user, user2, withdrawProfit, mpc_signer, usdc, USDC_DEC, usdcOwner, liquidityAdmin } = await loadFixture(deployAll); - + const amountCollateral = 1000n * USDC_DEC; + await usdc.connect(usdcOwner).transfer(liquidityPool, amountCollateral); + await liquidityPool.connect(liquidityAdmin).deposit(amountCollateral); // Pause borrowing await expect(liquidityPool.connect(withdrawProfit).pauseBorrow()) .to.emit(liquidityPool, "BorrowPaused"); @@ -870,6 +872,7 @@ describe("LiquidityPoolStablecoin", function () { 2000000000n, signature)) .to.be.revertedWithCustomError(liquidityPool, "BorrowingIsPaused"); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should NOT borrow if the contract is paused", async function () { diff --git a/test/PublicLiquidityPool.ts b/test/PublicLiquidityPool.ts index 408924a..88f696c 100644 --- a/test/PublicLiquidityPool.ts +++ b/test/PublicLiquidityPool.ts @@ -39,7 +39,7 @@ describe("PublicLiquidityPool", function () { ] = await hre.ethers.getSigners(); await setCode(user2.address, "0x00"); - const USDC_ADDRESS = networkConfig.BASE.USDC; + const USDC_ADDRESS = networkConfig.BASE.Tokens.USDC; const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; if (!USDC_OWNER_ADDRESS) throw new Error("Env variables not configured (USDC_OWNER_ADDRESS missing)"); const usdc = await hre.ethers.getContractAt("ERC20", USDC_ADDRESS); @@ -758,7 +758,7 @@ describe("PublicLiquidityPool", function () { expect(await liquidityPool.totalDeposited()).to.eq(amountLiquidity + amountLiquidity); expect(await liquidityPool.totalAssets()).to.eq(amountLiquidity + amountLiquidity); expect(await liquidityPool.totalSupply()).to.eq(amountLiquidity + amountLiquidity); - expect(await liquidityPool.balance(usdc)).to.eq(amountLiquidity + amountLiquidity); + expect(await liquidityPool.balance(usdc)).to.eq(0n); expect(await liquidityPool.balanceOf(lp)).to.eq(amountLiquidity); expect(await liquidityPool.balanceOf(usdcOwner)).to.eq(amountLiquidity); expect(await usdc.balanceOf(liquidityPool)).to.eq(amountLiquidity + amountLiquidity); @@ -1166,8 +1166,11 @@ describe("PublicLiquidityPool", function () { }); it("Should NOT borrow if borrowing is paused", async function () { - const {liquidityPool, user, user2, withdrawProfit, mpc_signer, usdc, USDC_DEC} = await loadFixture(deployAll); - + const {liquidityPool, user, user2, withdrawProfit, mpc_signer, usdc, USDC_DEC, lp} = await loadFixture(deployAll); + + const amountLiquidity = 1000n * USDC_DEC; + await usdc.connect(lp).approve(liquidityPool, amountLiquidity); + await liquidityPool.connect(lp)[ERC4626Deposit](amountLiquidity, lp); // Pause borrowing await expect(liquidityPool.connect(withdrawProfit).pauseBorrow()) .to.emit(liquidityPool, "BorrowPaused"); @@ -1192,6 +1195,7 @@ describe("PublicLiquidityPool", function () { 2000000000n, signature)) .to.be.revertedWithCustomError(liquidityPool, "BorrowingIsPaused"); + expect(await liquidityPool.balance(usdc)).to.eq(0n); }); it("Should NOT borrow if the contract is paused", async function () { diff --git a/test/Repayer.ts b/test/Repayer.ts index 34e5b73..df2e265 100644 --- a/test/Repayer.ts +++ b/test/Repayer.ts @@ -3,20 +3,20 @@ import { } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import {expect} from "chai"; import hre from "hardhat"; -import {AbiCoder, zeroPadValue} from "ethers"; +import {AbiCoder} from "ethers"; import {anyValue} from "@nomicfoundation/hardhat-chai-matchers/withArgs"; import { getCreateAddress, getContractAt, deploy, deployX, toBytes32, getBalance, } from "./helpers"; import { ProviderSolidity as Provider, DomainSolidity as Domain, ZERO_ADDRESS, - DEFAULT_ADMIN_ROLE, assertAddress, ETH, ZERO_BYTES32, + DEFAULT_ADMIN_ROLE, assertAddress, ETH, addressToBytes32, } from "../scripts/common"; import { TestUSDC, TransparentUpgradeableProxy, ProxyAdmin, TestLiquidityPool, Repayer, TestCCTPTokenMessenger, TestCCTPMessageTransmitter, TestAcrossV3SpokePool, TestStargate, MockStargateTreasurerTrue, MockStargateTreasurerFalse, - TestOptimismStandardBridge, IWrappedNativeToken + TestSuperchainStandardBridge, IWrappedNativeToken } from "../typechain-types"; import {networkConfig} from "../network.config"; @@ -27,13 +27,9 @@ async function now() { return BigInt(await time.latest()); } -function addressToBytes32(address: any) { - return zeroPadValue(address.toString(), 32); -} - describe("Repayer", function () { const deployAll = async () => { - const [deployer, admin, repayUser, user] = await hre.ethers.getSigners(); + const [deployer, admin, repayUser, user, setTokensUser] = await hre.ethers.getSigners(); await setCode(repayUser.address, "0x00"); const forkNetworkConfig = networkConfig.BASE; @@ -71,8 +67,11 @@ describe("Repayer", function () { await deploy("MockStargateTreasurerFalse", deployer, {}) ) as MockStargateTreasurerFalse; const optimismBridge = ( - await deploy("TestOptimismStandardBridge", deployer, {}) - ) as TestOptimismStandardBridge; + await deploy("TestSuperchainStandardBridge", deployer, {}) + ) as TestSuperchainStandardBridge; + const baseBridge = ( + await deploy("TestSuperchainStandardBridge", deployer, {}) + ) as TestSuperchainStandardBridge; const USDC_DEC = 10n ** (await usdc.decimals()); @@ -100,15 +99,31 @@ describe("Repayer", function () { weth, stargateTreasurerTrue, optimismBridge, + baseBridge, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, + setTokensUser, [liquidityPool, liquidityPool2, liquidityPool, liquidityPool], [Domain.BASE, Domain.BASE, Domain.ETHEREUM, Domain.ARBITRUM_ONE], [Provider.LOCAL, Provider.LOCAL, Provider.CCTP, Provider.CCTP], [true, false, true, true], + [ + { + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(eurc.target)} + ] + }, + { + inputToken: eurc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(usdc.target)} + ] + }, + ], )).data; const repayerProxy = (await deployX( "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer", {}, @@ -123,13 +138,14 @@ describe("Repayer", function () { USDC_DEC, eurc, EURC_DEC, eurcOwner, liquidityPool, liquidityPool2, repayer, repayerProxy, repayerAdmin, cctpTokenMessenger, cctpMessageTransmitter, REPAYER_ROLE, DEFAULT_ADMIN_ROLE, acrossV3SpokePool, weth, stargateTreasurerTrue, stargateTreasurerFalse, everclearFeeAdapter, forkNetworkConfig, optimismBridge, + baseBridge, setTokensUser, }; }; it("Should have default values", async function () { const {liquidityPool, liquidityPool2, repayer, usdc, REPAYER_ROLE, DEFAULT_ADMIN_ROLE, cctpTokenMessenger, cctpMessageTransmitter, admin, repayUser, deployer, acrossV3SpokePool, - stargateTreasurerTrue, optimismBridge, + stargateTreasurerTrue, optimismBridge, baseBridge, setTokensUser, eurc, } = await loadFixture(deployAll); expect(await repayer.ASSETS()).to.equal(usdc.target); @@ -138,6 +154,7 @@ describe("Repayer", function () { expect(await repayer.ACROSS_SPOKE_POOL()).to.equal(acrossV3SpokePool.target); expect(await repayer.STARGATE_TREASURER()).to.equal(stargateTreasurerTrue.target); expect(await repayer.OPTIMISM_STANDARD_BRIDGE()).to.equal(optimismBridge.target); + expect(await repayer.BASE_STANDARD_BRIDGE()).to.equal(baseBridge.target); expect(await repayer.REPAYER_ROLE()).to.equal(REPAYER_ROLE); expect(await repayer.isRouteAllowed(liquidityPool, Domain.BASE, Provider.LOCAL)).to.be.true; expect(await repayer.isRouteAllowed(liquidityPool2, Domain.BASE, Provider.LOCAL)).to.be.true; @@ -163,15 +180,22 @@ describe("Repayer", function () { expect(await repayer.domainChainId(Domain.ARBITRUM_ONE)).to.equal(42161n); expect(await repayer.domainChainId(Domain.BASE)).to.equal(8453n); expect(await repayer.domainChainId(Domain.POLYGON_MAINNET)).to.equal(137n); + expect(await repayer.domainChainId(Domain.UNICHAIN)).to.equal(130n); + expect(await repayer.domainChainId(Domain.BSC)).to.equal(56n); + expect(await repayer.domainChainId(Domain.LINEA)).to.equal(59144n); expect(await repayer.getAllRoutes()).to.deep.equal([ [liquidityPool.target, liquidityPool.target, liquidityPool.target, liquidityPool2.target], [Domain.ETHEREUM, Domain.ARBITRUM_ONE, Domain.BASE, Domain.BASE], [Provider.CCTP, Provider.CCTP, Provider.LOCAL, Provider.LOCAL], [true, true, true, false] ]); + expect(await repayer.isOutputTokenAllowed(usdc, Domain.ETHEREUM, addressToBytes32(eurc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(eurc, Domain.ETHEREUM, addressToBytes32(usdc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(usdc, Domain.OP_MAINNET, addressToBytes32(eurc.target))).to.be.false; + expect(await repayer.isOutputTokenAllowed(usdc, Domain.ETHEREUM, addressToBytes32(usdc.target))).to.be.false; await expect(repayer.connect(admin).initialize( - admin, repayUser, [], [], [], [] + admin, repayUser, setTokensUser, [], [], [], [], [] )).to.be.reverted; }); @@ -315,6 +339,170 @@ describe("Repayer", function () { )).to.be.revertedWithCustomError(repayer, "AccessControlUnauthorizedAccount(address,bytes32)"); }); + it("Should allow SET_TOKENS_ROLE to allow output tokens", async function () { + const {repayer, usdc, USDC_DEC, admin, repayUser, + liquidityPool, user, eurc, setTokensUser, + } = await loadFixture(deployAll); + + await usdc.transfer(repayer, 10n * USDC_DEC); + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ETHEREUM], + [Provider.ACROSS], + [true], + ALLOWED + ); + + const amount = 4n * USDC_DEC; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "address", "uint32", "uint32", "uint32"], + [usdc.target, amount, user.address, 1n, 2n, 3n] + ); + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + const tx = repayer.connect(setTokensUser).setInputOutputTokens( + [ + { + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(usdc.target)}, + {destinationDomain: Domain.AVALANCHE, outputToken: addressToBytes32(eurc.target)} + ] + }, + { + inputToken: eurc, + destinationTokens: [ + {destinationDomain: Domain.OP_MAINNET, outputToken: addressToBytes32(eurc.target)} + ] + }, + ], + ALLOWED + ); + await expect(tx) + .to.emit(repayer, "SetInputOutputToken") + .withArgs(usdc.target, Domain.ETHEREUM, addressToBytes32(usdc.target), true); + + expect(await repayer.isOutputTokenAllowed(usdc, Domain.ETHEREUM, addressToBytes32(eurc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(eurc, Domain.ETHEREUM, addressToBytes32(usdc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(eurc, Domain.ETHEREUM, addressToBytes32(eurc.target))).to.be.false; + expect(await repayer.isOutputTokenAllowed(usdc, Domain.ETHEREUM, addressToBytes32(usdc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(usdc, Domain.AVALANCHE, addressToBytes32(eurc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(eurc, Domain.OP_MAINNET, addressToBytes32(eurc.target))).to.be.true; + await repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData + ); + }); + + it("Should allow SET_TOKENS_ROLE to deny output tokens", async function () { + const {repayer, usdc, USDC_DEC, admin, repayUser, + liquidityPool, user, eurc, setTokensUser, + } = await loadFixture(deployAll); + + await usdc.transfer(repayer, 10n * USDC_DEC); + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ETHEREUM], + [Provider.ACROSS], + [true], + ALLOWED + ); + + const amount = 4n * USDC_DEC; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "address", "uint32", "uint32", "uint32"], + [eurc.target, amount, user.address, 1n, 2n, 3n] + ); + await repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData + ); + + const tx = repayer.connect(setTokensUser).setInputOutputTokens( + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(eurc.target)} + ] + }], + DISALLOWED + ); + await expect(tx) + .to.emit(repayer, "SetInputOutputToken") + .withArgs(usdc.target, Domain.ETHEREUM, addressToBytes32(eurc.target), false); + + expect(await repayer.isOutputTokenAllowed(usdc, Domain.ETHEREUM, addressToBytes32(eurc.target))).to.be.false; + expect(await repayer.isOutputTokenAllowed(eurc, Domain.ETHEREUM, addressToBytes32(usdc.target))).to.be.true; + expect(await repayer.isOutputTokenAllowed(eurc, Domain.ETHEREUM, addressToBytes32(eurc.target))).to.be.false; + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + }); + + it("Should NOT allow SET_TOKENS_ROLE to allow output tokens for local domain", async function () { + const {repayer, usdc, setTokensUser, + } = await loadFixture(deployAll); + + await expect(repayer.connect(setTokensUser).setInputOutputTokens( + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.BASE, outputToken: addressToBytes32(usdc.target)} + ] + }], + ALLOWED + )).to.be.revertedWithCustomError(repayer, "UnsupportedDomain()"); + }); + + it("Should NOT allow others to allow output tokens", async function () { + const {repayer, usdc, user, + } = await loadFixture(deployAll); + + await expect(repayer.connect(user).setInputOutputTokens( + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(usdc.target)} + ] + }], + ALLOWED + )).to.be.revertedWithCustomError(repayer, "AccessControlUnauthorizedAccount(address,bytes32)"); + }); + + it("Should NOT allow others to deny output tokens", async function () { + const {repayer, usdc, user, eurc, + } = await loadFixture(deployAll); + + await expect(repayer.connect(user).setInputOutputTokens( + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(eurc.target)} + ] + }], + DISALLOWED + )).to.be.revertedWithCustomError(repayer, "AccessControlUnauthorizedAccount(address,bytes32)"); + }); + it("Should allow repayer to initiate CCTP repay", async function () { const {repayer, usdc, USDC_DEC, repayUser, liquidityPool, cctpTokenMessenger @@ -397,6 +585,60 @@ describe("Repayer", function () { }); it("Should allow repayer to initiate Across repay with a different token", async function () { + const {repayer, EURC_DEC, admin, repayUser, usdc, + liquidityPool, acrossV3SpokePool, eurc, user, eurcOwner, + } = await loadFixture(deployAll); + + await eurc.connect(eurcOwner).transfer(repayer, 10n * EURC_DEC); + + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ETHEREUM], + [Provider.ACROSS], + [true], + ALLOWED + ); + const amount = 4n * EURC_DEC; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "address", "uint32", "uint32", "uint32"], + [usdc.target, amount * 998n / 1000n, user.address, 1n, 2n, 3n] + ); + const tx = repayer.connect(repayUser).initiateRepay( + eurc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(eurc.target, amount, liquidityPool.target, Domain.ETHEREUM, Provider.ACROSS); + await expect(tx) + .to.emit(eurc, "Transfer") + .withArgs(repayer.target, acrossV3SpokePool.target, amount); + await expect(tx) + .to.emit(acrossV3SpokePool, "FundsDeposited") + .withArgs( + addressToBytes32(eurc.target), + addressToBytes32(usdc.target), + amount, + amount * 998n / 1000n, + 1n, + 1337n, + 1n, + 2n, + 3n, + addressToBytes32(repayer.target), + addressToBytes32(liquidityPool.target), + addressToBytes32(user.address), + "0x" + ); + + expect(await eurc.balanceOf(repayer)).to.equal(6n * EURC_DEC); + }); + + it("Should allow repayer to initiate Across repay with a different token and no output token", async function () { const {repayer, EURC_DEC, admin, repayUser, liquidityPool, acrossV3SpokePool, eurc, user, eurcOwner, } = await loadFixture(deployAll); @@ -453,18 +695,18 @@ describe("Repayer", function () { it("Should allow repayer to initiate Across repay with SpokePool on fork", async function () { const {deployer, repayer, USDC_DEC, admin, repayUser, repayerAdmin, repayerProxy, liquidityPool, cctpTokenMessenger, cctpMessageTransmitter, weth, stargateTreasurerTrue, everclearFeeAdapter, - optimismBridge, + optimismBridge, baseBridge, setTokensUser, } = await loadFixture(deployAll); const acrossV3SpokePoolFork = await hre.ethers.getContractAt( "V3SpokePoolInterface", networkConfig.BASE.AcrossV3SpokePool! ); - const USDC_BASE_ADDRESS = networkConfig.BASE.USDC; + const USDC_BASE_ADDRESS = networkConfig.BASE.Tokens.USDC; assertAddress(process.env.USDC_OWNER_ADDRESS, "Env variables not configured (USDC_OWNER_ADDRESS missing)"); const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; - const usdc = await hre.ethers.getContractAt("ERC20", networkConfig.BASE.USDC); + const usdc = await hre.ethers.getContractAt("ERC20", networkConfig.BASE.Tokens.USDC); const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ADDRESS); const repayerImpl2 = ( @@ -482,6 +724,7 @@ describe("Repayer", function () { weth, stargateTreasurerTrue, optimismBridge, + baseBridge, ) ) as Repayer; @@ -499,6 +742,16 @@ describe("Repayer", function () { [true], ALLOWED ); + + await repayer.connect(setTokensUser).setInputOutputTokens( + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: addressToBytes32(USDC_BASE_ADDRESS)} + ] + }], + ALLOWED + ); const amount = 4n * USDC_DEC; const currentTime = await now(); const extraData = AbiCoder.defaultAbiCoder().encode( @@ -627,14 +880,44 @@ describe("Repayer", function () { )).to.be.revertedWithCustomError(repayer, "NotPayable()"); }); + it("Should revert Across repay if output token is not allowed", async function () { + const {repayer, EURC_DEC, admin, repayUser, + liquidityPool, eurc, user, eurcOwner, + } = await loadFixture(deployAll); + + await eurc.connect(eurcOwner).transfer(repayer, 10n * EURC_DEC); + + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ETHEREUM], + [Provider.ACROSS], + [true], + ALLOWED + ); + const amount = 4n * EURC_DEC; + const fillDeadlineError = 0n; + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint256", "address", "uint32", "uint32", "uint32"], + [user.address, amount, user.address, 1n, fillDeadlineError, 3n] + ); + await expect(repayer.connect(repayUser).initiateRepay( + eurc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.ACROSS, + extraData + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + }); + it("Should allow repayer to initiate Everclear repay on fork", async function () { const {repayer, USDC_DEC, admin, repayUser, - liquidityPool, everclearFeeAdapter, forkNetworkConfig, + liquidityPool, everclearFeeAdapter, forkNetworkConfig, setTokensUser, } = await loadFixture(deployAll); assertAddress(process.env.USDC_OWNER_ADDRESS, "Env variables not configured (USDC_OWNER_ADDRESS missing)"); const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; - const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.USDC); + const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ADDRESS); await usdc.connect(usdcOwner).transfer(repayer, 100000n * USDC_DEC); @@ -672,6 +955,15 @@ describe("Repayer", function () { ["bytes32", "uint256", "uint48", "tuple(uint256, uint256, bytes)"], [apiTx[3], apiTx[5], apiTx[6], apiTx[8]] ); + await repayer.connect(setTokensUser).setInputOutputTokens( + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: apiTx[3]} + ] + }], + ALLOWED + ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, @@ -694,7 +986,7 @@ describe("Repayer", function () { it("Should allow repayer to initiate Everclear repay with other token", async function () { const {deployer, repayer, weth, admin, repayUser, - liquidityPool, everclearFeeAdapter, forkNetworkConfig, + liquidityPool, everclearFeeAdapter, forkNetworkConfig, setTokensUser, } = await loadFixture(deployAll); await deployer.sendTransaction({to: repayer, value: 10n * ETH}); @@ -731,6 +1023,15 @@ describe("Repayer", function () { ["bytes32", "uint256", "uint48", "tuple(uint256, uint256, bytes)"], [apiTx[3], apiTx[5], apiTx[6], apiTx[8]] ); + await repayer.connect(setTokensUser).setInputOutputTokens( + [{ + inputToken: weth, + destinationTokens: [ + {destinationDomain: Domain.ETHEREUM, outputToken: apiTx[3]} + ] + }], + ALLOWED + ); const tx = repayer.connect(repayUser).initiateRepay( weth, amount, @@ -754,12 +1055,12 @@ describe("Repayer", function () { it("Should revert Everclear repay if call to Everclear reverts", async function () { const {repayer, USDC_DEC, admin, repayUser, - liquidityPool, forkNetworkConfig, + liquidityPool, forkNetworkConfig, eurc, } = await loadFixture(deployAll); assertAddress(process.env.USDC_OWNER_ADDRESS, "Env variables not configured (USDC_OWNER_ADDRESS missing)"); const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; - const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.USDC); + const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ADDRESS); await usdc.connect(usdcOwner).transfer(repayer, 10n * USDC_DEC); @@ -775,7 +1076,7 @@ describe("Repayer", function () { const extraData = AbiCoder.defaultAbiCoder().encode( ["bytes32", "uint256", "uint48", "tuple(uint256, uint256, bytes)"], - [ZERO_BYTES32, amount, 0, [0, 0, "0x"]] + [addressToBytes32(eurc.target), amount, 0, [0, 0, "0x"]] ); await expect(repayer.connect(repayUser).initiateRepay( usdc, @@ -787,11 +1088,50 @@ describe("Repayer", function () { )).to.be.reverted; }); - it("Should allow repayer to initiate Optimism repay with mock bridge", async function () { + it("Should revert Everclear repay if output token is not allowed", async function () { + const {repayer, USDC_DEC, admin, repayUser, + liquidityPool, forkNetworkConfig, user, + } = await loadFixture(deployAll); + + assertAddress(process.env.USDC_OWNER_ADDRESS, "Env variables not configured (USDC_OWNER_ADDRESS missing)"); + const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; + const usdc = await hre.ethers.getContractAt("ERC20", forkNetworkConfig.Tokens.USDC); + const usdcOwner = await hre.ethers.getImpersonatedSigner(USDC_OWNER_ADDRESS); + + await usdc.connect(usdcOwner).transfer(repayer, 10n * USDC_DEC); + + await repayer.connect(admin).setRoute( + [liquidityPool], + [Domain.ETHEREUM], + [Provider.EVERCLEAR], + [true], + ALLOWED + ); + const amount = 4n * USDC_DEC; + + const extraData = AbiCoder.defaultAbiCoder().encode( + ["bytes32", "uint256", "uint48", "tuple(uint256, uint256, bytes)"], + [addressToBytes32(user.address), amount, 0, [0, 0, "0x"]] + ); + await expect(repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.ETHEREUM, + Provider.EVERCLEAR, + extraData + )).to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); + }); + + it("Should allow repayer to initiate Superchain Optimism repay with mock bridge", async function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, - acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, } = await loadFixture(deployAll); + const amount = 4n * USDC_DEC; + const outputToken = networkConfig.OP_MAINNET.Tokens.USDC; + const minGasLimit = 100000n; const repayerImpl = ( await deployX("Repayer", deployer, "Repayer2", {}, @@ -804,15 +1144,23 @@ describe("Repayer", function () { weth, stargateTreasurerTrue, optimismBridge, + baseBridge, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, + setTokensUser, [liquidityPool, liquidityPool], [Domain.ETHEREUM, Domain.OP_MAINNET], - [Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], + [Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE], [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.OP_MAINNET, outputToken: addressToBytes32(outputToken)} + ] + }], )).data; const repayerProxy = (await deployX( "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, @@ -822,30 +1170,98 @@ describe("Repayer", function () { await usdc.transfer(repayer, 10n * USDC_DEC); + const extraData = AbiCoder.defaultAbiCoder().encode( + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x1234"] + ); + const tx = repayer.connect(repayUser).initiateRepay( + usdc, + amount, + liquidityPool, + Domain.OP_MAINNET, + Provider.SUPERCHAIN_STANDARD_BRIDGE, + extraData + ); + await expect(tx) + .to.emit(repayer, "InitiateRepay") + .withArgs(usdc.target, amount, liquidityPool.target, Domain.OP_MAINNET, Provider.SUPERCHAIN_STANDARD_BRIDGE); + await expect(tx) + .to.emit(optimismBridge, "ERC20BridgeInitiated") + .withArgs(usdc.target, outputToken, optimismBridge.target, liquidityPool.target, amount, "0x1234"); + }); + + it("Should allow repayer to initiate Superchain Base repay with mock bridge", async function () { + const { + USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, + } = await loadFixture(deployAll); const amount = 4n * USDC_DEC; - const outputToken = networkConfig.OP_MAINNET.USDC; + const outputToken = networkConfig.BASE.Tokens.USDC; const minGasLimit = 100000n; + + const repayerImpl = ( + await deployX("Repayer", deployer, "Repayer2", {}, + Domain.ETHEREUM, + usdc, + cctpTokenMessenger, + cctpMessageTransmitter, + acrossV3SpokePool, + everclearFeeAdapter, + weth, + stargateTreasurerTrue, + optimismBridge, + baseBridge, + ) + ) as Repayer; + const repayerInit = (await repayerImpl.initialize.populateTransaction( + admin, + repayUser, + setTokensUser, + [liquidityPool, liquidityPool], + [Domain.ETHEREUM, Domain.BASE], + [Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE], + [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.BASE, outputToken: addressToBytes32(outputToken)} + ] + }], + )).data; + const repayerProxy = (await deployX( + "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, + repayerImpl, admin, repayerInit + )) as TransparentUpgradeableProxy; + const repayer = (await getContractAt("Repayer", repayerProxy, deployer)) as Repayer; + + await usdc.transfer(repayer, 10n * USDC_DEC); + const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [outputToken, minGasLimit] + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x1234"] ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, liquidityPool, - Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Domain.BASE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) .to.emit(repayer, "InitiateRepay") - .withArgs(usdc.target, amount, liquidityPool.target, Domain.OP_MAINNET, Provider.OPTIMISM_STANDARD_BRIDGE); + .withArgs(usdc.target, amount, liquidityPool.target, Domain.BASE, Provider.SUPERCHAIN_STANDARD_BRIDGE); + await expect(tx) + .to.emit(baseBridge, "ERC20BridgeInitiated") + .withArgs(usdc.target, outputToken, baseBridge.target, liquidityPool.target, amount, "0x1234"); }); - it("Should revert Optimism repay if call to Optimism reverts", async function () { + it("Should revert Superchain repay if call to Standard Bridge reverts", async function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, - acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, } = await loadFixture(deployAll); const repayerImpl = ( @@ -859,15 +1275,23 @@ describe("Repayer", function () { weth, stargateTreasurerTrue, optimismBridge, + baseBridge, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, + setTokensUser, [liquidityPool, liquidityPool], [Domain.ETHEREUM, Domain.OP_MAINNET], - [Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], + [Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE], [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.OP_MAINNET, outputToken: addressToBytes32(usdc.target)} + ] + }], )).data; const repayerProxy = (await deployX( "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, @@ -881,25 +1305,26 @@ describe("Repayer", function () { const outputToken = usdc.target; const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [outputToken, minGasLimit] + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x"], ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, liquidityPool, Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) - .to.be.revertedWithCustomError(optimismBridge, "OptimismBridgeWrongRemoteToken"); + .to.be.revertedWithCustomError(optimismBridge, "SuperchainStandardBridgeWrongRemoteToken"); }); - it("Should revert Optimism repay if native currency is sent along", async function () { + it("Should revert Standard Bridge repay if native currency is sent along", async function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, - acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, } = await loadFixture(deployAll); const repayerImpl = ( @@ -913,15 +1338,23 @@ describe("Repayer", function () { weth, stargateTreasurerTrue, optimismBridge, + baseBridge, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, + setTokensUser, [liquidityPool, liquidityPool], [Domain.ETHEREUM, Domain.OP_MAINNET], - [Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], + [Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE], [true, true], + [{ + inputToken: usdc, + destinationTokens: [ + {destinationDomain: Domain.OP_MAINNET, outputToken: addressToBytes32(usdc.target)} + ] + }], )).data; const repayerProxy = (await deployX( "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, @@ -935,15 +1368,15 @@ describe("Repayer", function () { const outputToken = usdc.target; const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [outputToken, minGasLimit] + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x"], ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, liquidityPool, Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData, {value: 1n} ); @@ -951,10 +1384,11 @@ describe("Repayer", function () { .to.be.revertedWithCustomError(repayer, "NotPayable()"); }); - it("Should revert Optimism repay if output token is zero address", async function () { + it("Should revert Standard Bridge repay if output token is not allowed", async function () { const { USDC_DEC, usdc, repayUser, liquidityPool, optimismBridge, cctpTokenMessenger, cctpMessageTransmitter, - acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, + acrossV3SpokePool, everclearFeeAdapter, weth, stargateTreasurerTrue, admin, deployer, baseBridge, + setTokensUser, } = await loadFixture(deployAll); const repayerImpl = ( @@ -968,15 +1402,18 @@ describe("Repayer", function () { weth, stargateTreasurerTrue, optimismBridge, + baseBridge, ) ) as Repayer; const repayerInit = (await repayerImpl.initialize.populateTransaction( admin, repayUser, + setTokensUser, [liquidityPool, liquidityPool], [Domain.ETHEREUM, Domain.OP_MAINNET], - [Provider.LOCAL, Provider.OPTIMISM_STANDARD_BRIDGE], + [Provider.LOCAL, Provider.SUPERCHAIN_STANDARD_BRIDGE], [true, true], + [], )).data; const repayerProxy = (await deployX( "TransparentUpgradeableProxy", deployer, "TransparentUpgradeableProxyRepayer2", {}, @@ -990,22 +1427,22 @@ describe("Repayer", function () { const outputToken = ZERO_ADDRESS; const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [outputToken, minGasLimit] + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x"], ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, liquidityPool, Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) - .to.be.revertedWithCustomError(repayer, "ZeroAddress()"); + .to.be.revertedWithCustomError(repayer, "InvalidOutputToken()"); }); - it("Should NOT allow repayer to initiate Optimism repay on invalid route", async function () { + it("Should NOT allow repayer to initiate Superchain Standard Bridge repay on invalid route", async function () { const {repayer, USDC_DEC, usdc, admin, repayUser, liquidityPool} = await loadFixture(deployAll); await usdc.transfer(repayer, 10n * USDC_DEC); @@ -1013,7 +1450,7 @@ describe("Repayer", function () { await repayer.connect(admin).setRoute( [liquidityPool], [Domain.ETHEREUM], - [Provider.OPTIMISM_STANDARD_BRIDGE], + [Provider.SUPERCHAIN_STANDARD_BRIDGE], [true], ALLOWED ); @@ -1021,15 +1458,15 @@ describe("Repayer", function () { const outputToken = usdc.target; const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [outputToken, minGasLimit] + ["address", "uint32", "bytes"], + [outputToken, minGasLimit, "0x"], ); const tx = repayer.connect(repayUser).initiateRepay( usdc, amount, liquidityPool, Domain.ETHEREUM, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) @@ -1313,7 +1750,7 @@ describe("Repayer", function () { it("Should revert Stargate repay if the pool is not registered", async function () { const {repayer, USDC_DEC, usdc, admin, repayUser, liquidityPool, deployer, cctpTokenMessenger, cctpMessageTransmitter, acrossV3SpokePool, weth, stargateTreasurerFalse, repayerAdmin, repayerProxy, - everclearFeeAdapter, optimismBridge, + everclearFeeAdapter, optimismBridge, baseBridge, } = await loadFixture(deployAll); await usdc.transfer(repayer, 10n * USDC_DEC); @@ -1338,6 +1775,7 @@ describe("Repayer", function () { weth, stargateTreasurerFalse, optimismBridge, + baseBridge, ) ) as Repayer; @@ -1444,7 +1882,7 @@ describe("Repayer", function () { it("Should allow repayer to initiate Stargate repay on fork and refund unspent fee", async function () { const { repayer, USDC_DEC, admin, repayUser, liquidityPool, deployer, cctpTokenMessenger, cctpMessageTransmitter, - acrossV3SpokePool, weth, repayerAdmin, repayerProxy, everclearFeeAdapter, optimismBridge, + acrossV3SpokePool, weth, repayerAdmin, repayerProxy, everclearFeeAdapter, optimismBridge, baseBridge, } = await loadFixture(deployAll); const stargatePoolUsdcAddress = "0x27a16dc786820B16E5c9028b75B99F6f604b5d26"; @@ -1453,7 +1891,7 @@ describe("Repayer", function () { "IStargate", stargatePoolUsdcAddress ); - const USDC_BASE_ADDRESS = networkConfig.BASE.USDC; + const USDC_BASE_ADDRESS = networkConfig.BASE.Tokens.USDC; assertAddress(process.env.USDC_OWNER_ADDRESS, "Env variables not configured (USDC_OWNER_ADDRESS missing)"); const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; @@ -1475,6 +1913,7 @@ describe("Repayer", function () { weth, stargateTreasurer, optimismBridge, + baseBridge, ) ) as Repayer; @@ -1538,7 +1977,7 @@ describe("Repayer", function () { const {repayer, USDC_DEC, admin, repayUser, liquidityPool} = await loadFixture(deployAll); const stargatePoolUsdcAddress = "0x27a16dc786820B16E5c9028b75B99F6f604b5d26"; - const USDC_BASE_ADDRESS = networkConfig.BASE.USDC; + const USDC_BASE_ADDRESS = networkConfig.BASE.Tokens.USDC; assertAddress(process.env.USDC_OWNER_ADDRESS, "Env variables not configured (USDC_OWNER_ADDRESS missing)"); const USDC_OWNER_ADDRESS = process.env.USDC_OWNER_ADDRESS; @@ -1613,7 +2052,7 @@ describe("Repayer", function () { it("Should unwrap enough native tokens on initiate repay", async function () { const { repayer, repayUser, liquidityPool, optimismBridge, usdc, cctpTokenMessenger, - cctpMessageTransmitter, repayerAdmin, admin, repayerProxy, deployer, + cctpMessageTransmitter, repayerAdmin, admin, repayerProxy, deployer, baseBridge, } = await loadFixture(deployAll); const wrappedAmount = 10n * ETH; @@ -1641,6 +2080,7 @@ describe("Repayer", function () { weth, ZERO_ADDRESS, optimismBridge, + baseBridge, ) ) as Repayer; @@ -1650,27 +2090,27 @@ describe("Repayer", function () { await repayer.connect(admin).setRoute( [liquidityPool], [Domain.OP_MAINNET], - [Provider.OPTIMISM_STANDARD_BRIDGE], + [Provider.SUPERCHAIN_STANDARD_BRIDGE], [true], ALLOWED ); const minGasLimit = 100000n; const extraData = AbiCoder.defaultAbiCoder().encode( - ["address", "uint32"], - [ZERO_ADDRESS, minGasLimit] + ["address", "uint32", "bytes"], + [ZERO_ADDRESS, minGasLimit, "0x1234"], ); const tx = repayer.connect(repayUser).initiateRepay( weth, repayAmount, liquidityPool, Domain.OP_MAINNET, - Provider.OPTIMISM_STANDARD_BRIDGE, + Provider.SUPERCHAIN_STANDARD_BRIDGE, extraData ); await expect(tx) .to.emit(repayer, "InitiateRepay") - .withArgs(weth.target, repayAmount, liquidityPool.target, Domain.OP_MAINNET, Provider.OPTIMISM_STANDARD_BRIDGE); + .withArgs(weth.target, repayAmount, liquidityPool.target, Domain.OP_MAINNET, Provider.SUPERCHAIN_STANDARD_BRIDGE); await expect(tx) .to.emit(weth, "Deposit") .withArgs(repayer.target, 1n * ETH); diff --git a/test/WETHLiquidityPool.ts b/test/WETHLiquidityPool.ts index d11d274..7e05f6e 100644 --- a/test/WETHLiquidityPool.ts +++ b/test/WETHLiquidityPool.ts @@ -1066,7 +1066,7 @@ describe("WETHLiquidityPool", function () { await weth.connect(wethOwner).approve(liquidityPool, amountLiquidity); await expect(liquidityPool.connect(wethOwner).depositWithPull(amountLiquidity)) .to.emit(liquidityPool, "Deposit").withArgs(wethOwner, amountLiquidity); - expect(await liquidityPool.balance(weth)).to.eq(amountLiquidity + amountLiquidity); + expect(await liquidityPool.balance(weth)).to.eq(0n); }); it("Should withdraw liquidity", async function () { @@ -1339,8 +1339,11 @@ describe("WETHLiquidityPool", function () { it("Should NOT borrow if borrowing is paused", async function () { const { liquidityPool, user, user2, withdrawProfit, mpc_signer, - weth, WETH_DEC + weth, WETH_DEC, wethOwner, liquidityAdmin } = await loadFixture(deployAll); + const amountLiquidity = 100n * WETH_DEC; + await weth.connect(wethOwner).transfer(liquidityPool, amountLiquidity); + await liquidityPool.connect(liquidityAdmin).deposit(amountLiquidity); // Pause borrowing await expect(liquidityPool.connect(withdrawProfit).pauseBorrow()) @@ -1366,6 +1369,7 @@ describe("WETHLiquidityPool", function () { 2000000000n, signature)) .to.be.revertedWithCustomError(liquidityPool, "BorrowingIsPaused"); + expect(await liquidityPool.balance(weth)).to.eq(0n); }); it("Should NOT borrow if the contract is paused", async function () {