diff --git a/contracts/old/MOR.sol b/contracts/MOR.sol similarity index 94% rename from contracts/old/MOR.sol rename to contracts/MOR.sol index e3a83f9..9110c9f 100644 --- a/contracts/old/MOR.sol +++ b/contracts/MOR.sol @@ -5,7 +5,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20, ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol"; -import {IMOR, IERC20, IERC165} from "../interfaces/old/IMOR.sol"; +import {IMOR, IERC20, IERC165} from "./interfaces/IMOR.sol"; contract MOR is IMOR, ERC20Capped, ERC20Burnable, Ownable { constructor(uint256 cap_) ERC20("MOR", "MOR") ERC20Capped(cap_) {} diff --git a/contracts/capital-protocol/DistributorV2.sol b/contracts/capital-protocol/DistributorV2.sol index f87d396..fe595ed 100644 --- a/contracts/capital-protocol/DistributorV2.sol +++ b/contracts/capital-protocol/DistributorV2.sol @@ -15,7 +15,7 @@ import {IRewardsController} from "../interfaces/aave/IRewardsController.sol"; import {DecimalsConverter} from "@solarity/solidity-lib/libs/decimals/DecimalsConverter.sol"; import {IDistributor, IERC165} from "../interfaces/capital-protocol/IDistributor.sol"; -import {IL1SenderV2} from "../interfaces/capital-protocol/IL1SenderV2.sol"; +import {IL1SenderV2} from "../interfaces/capital-protocol/old/IL1SenderV2.sol"; import {IChainLinkDataConsumer} from "../interfaces/capital-protocol/IChainLinkDataConsumer.sol"; import {IDepositPool} from "../interfaces/capital-protocol/IDepositPool.sol"; import {IRewardPool} from "../interfaces/capital-protocol/IRewardPool.sol"; diff --git a/contracts/capital-protocol/L1SenderV3.sol b/contracts/capital-protocol/L1SenderV3.sol new file mode 100644 index 0000000..d12d836 --- /dev/null +++ b/contracts/capital-protocol/L1SenderV3.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import {ISwapRouter} from "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; +import {TransferHelper} from "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol"; + +import {ILayerZeroEndpoint} from "@layerzerolabs/lz-evm-sdk-v1-0.7/contracts/interfaces/ILayerZeroEndpoint.sol"; + +import {IGatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/IGatewayRouter.sol"; + +import {IL1SenderV3, IERC165} from "../interfaces/capital-protocol/IL1SenderV3.sol"; +import {IDistributor} from "../interfaces/capital-protocol/IDistributor.sol"; +import {IWStETH} from "../interfaces/tokens/IWStETH.sol"; +import {IL1ERC20Bridge} from "../interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IL1ERC20Bridge.sol"; + +contract L1SenderV3 is IL1SenderV3, OwnableUpgradeable, UUPSUpgradeable { + /** @dev stETH token address */ + address public stETH; + + /** @dev `Distributor` contract address. */ + address public distributor; + + /** @dev The config for Arbitrum bridge. Send wstETH to the Arbitrum */ + TokenBridgeConfig public tokenBridgeConfig; + + /** @dev The config for LayerZero. Send MOR mint message to the Arbitrum */ + MessageBridgeConfig public messageBridgeConfig; + + /** @dev UPGRADE `L1SenderV2` storage updates, add Uniswap integration */ + address public uniswapSwapRouter; + + /**********************************************************************************************/ + /*** Init, IERC165 ***/ + /**********************************************************************************************/ + + constructor() { + _disableInitializers(); + } + + function L1SenderV3__init() external initializer { + __Ownable_init(); + __UUPSUpgradeable_init(); + } + + function supportsInterface(bytes4 interfaceId_) external pure returns (bool) { + return interfaceId_ == type(IL1SenderV3).interfaceId || interfaceId_ == type(IERC165).interfaceId; + } + + /**********************************************************************************************/ + /*** Global contract management functionality for the contract `owner()` ***/ + /**********************************************************************************************/ + + function setStETh(address value_) external onlyOwner { + require(value_ != address(0), "L1S: invalid stETH address"); + + stETH = value_; + + emit stETHSet(value_); + } + + function setDistributor(address value_) external onlyOwner { + require(IERC165(value_).supportsInterface(type(IDistributor).interfaceId), "L1S: invalid distributor address"); + + distributor = value_; + + emit DistributorSet(value_); + } + + /** + * https://docs.uniswap.org/contracts/v3/reference/deployments/ethereum-deployments + */ + function setUniswapSwapRouter(address value_) external onlyOwner { + require(value_ != address(0), "L1S: invalid `uniswapSwapRouter` address"); + + uniswapSwapRouter = value_; + + emit UniswapSwapRouterSet(value_); + } + + /**********************************************************************************************/ + /*** LayerZero functionality ***/ + /**********************************************************************************************/ + + /** + * @dev https://docs.layerzero.network/v1/deployments/deployed-contracts + * Gateway - see `EndpointV1` at the link + * Receiver - `L2MessageReceiver` address + * Receiver Chain Id - see `EndpointId` at the link + * Zro Payment Address - the address of the ZRO token holder who would pay for the transaction + * Adapter Params - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination + */ + function setMessageBridgeConfig(MessageBridgeConfig calldata config_) external onlyOwner { + messageBridgeConfig = config_; + + emit MessageBridgeConfigSet(messageBridgeConfig); + } + + function sendMintMessage(address user_, uint256 amount_, address refundTo_) external payable { + require(_msgSender() == distributor, "L1S: the `msg.sender` isn't `distributor`"); + + MessageBridgeConfig storage config = messageBridgeConfig; + + bytes memory receiverAndSenderAddresses_ = abi.encodePacked(config.receiver, address(this)); + bytes memory payload_ = abi.encode(user_, amount_); + + // https://docs.layerzero.network/v1/developers/evm/evm-guides/send-messages + ILayerZeroEndpoint(config.gateway).send{value: msg.value}( + config.receiverChainId, + receiverAndSenderAddresses_, + payload_, + payable(refundTo_), + config.zroPaymentAddress, + config.adapterParams + ); + + emit MintMessageSent(user_, amount_); + } + + /**********************************************************************************************/ + /*** Lido bridge functionality ***/ + /**********************************************************************************************/ + + /** + * @dev https://docs.lido.fi/deployed-contracts/#base + * wstETH - see `WstETH ERC20Bridged (proxy)` at the link + * Gateway - see `L1ERC20TokenBridge (proxy)` at the link + * Receiver - token receiver address on L2 + */ + function setTokenBridgeConfig(TokenBridgeConfig calldata config_) external onlyOwner { + require(stETH != address(0), "L1S: stETH is not set"); + require(config_.receiver != address(0), "L1S: invalid receiver"); + + TokenBridgeConfig memory oldConfig_ = tokenBridgeConfig; + + if (oldConfig_.wstETH != address(0)) { + IERC20(stETH).approve(oldConfig_.wstETH, 0); + + address oldGateway_; + try IGatewayRouter(oldConfig_.gateway).getGateway(oldConfig_.wstETH) returns (address gateway_) { + oldGateway_ = gateway_; + } catch { + oldGateway_ = oldConfig_.gateway; + } + IERC20(oldConfig_.wstETH).approve(oldGateway_, 0); + } + + IERC20(stETH).approve(config_.wstETH, type(uint256).max); + IERC20(config_.wstETH).approve(config_.gateway, type(uint256).max); + + tokenBridgeConfig = config_; + + emit TokenBridgeConfigSet(tokenBridgeConfig); + } + + function sendWstETH(uint32 l2Gas_, bytes calldata data_) external onlyOwner { + TokenBridgeConfig memory config_ = tokenBridgeConfig; + require(config_.wstETH != address(0), "L1S: wstETH isn't set"); + + uint256 stETHBalance_ = IERC20(stETH).balanceOf(address(this)); + if (stETHBalance_ > 0) { + IWStETH(config_.wstETH).wrap(stETHBalance_); + } + + uint256 amount_ = IWStETH(config_.wstETH).balanceOf(address(this)); + + IL1ERC20Bridge l1ERC20Bridge_ = IL1ERC20Bridge(config_.gateway); + l1ERC20Bridge_.depositERC20To( + config_.wstETH, + l1ERC20Bridge_.l2Token(), + config_.receiver, + amount_, + l2Gas_, + data_ + ); + + emit TokenSent(amount_, config_.receiver, l2Gas_, data_); + } + + /**********************************************************************************************/ + /*** Uniswap functionality ***/ + /**********************************************************************************************/ + + /** + * @dev https://docs.uniswap.org/contracts/v3/guides/swaps/multihop-swaps + * + * Multiple pool swaps are encoded through bytes called a `path`. A path is a sequence + * of token addresses and poolFees that define the pools used in the swaps. + * The format for pool encoding is (tokenIn, fee, tokenOut/tokenIn, fee, tokenOut) where + * tokenIn/tokenOut parameter is the shared token across the pools. + * Since we are swapping DAI to USDC and then USDC to WETH9 the path encoding is (DAI, 0.3%, USDC, 0.3%, WETH9). + */ + function swapExactInputMultihop( + address[] calldata tokens_, + uint24[] calldata poolsFee_, + uint256 amountIn_, + uint256 amountOutMinimum_, + uint256 deadline_ + ) external onlyOwner returns (uint256) { + require(tokens_.length >= 2 && tokens_.length == poolsFee_.length + 1, "L1S: invalid array length"); + require(amountIn_ != 0, "L1S: invalid `amountIn_` value"); + require(amountOutMinimum_ != 0, "L1S: invalid `amountOutMinimum_` value"); + + TransferHelper.safeApprove(tokens_[0], uniswapSwapRouter, amountIn_); + + // START create the `path` + bytes memory path_; + for (uint256 i = 0; i < poolsFee_.length; i++) { + path_ = abi.encodePacked(path_, tokens_[i], poolsFee_[i]); + } + path_ = abi.encodePacked(path_, tokens_[tokens_.length - 1]); + // END + + ISwapRouter.ExactInputParams memory params_ = ISwapRouter.ExactInputParams({ + path: path_, + recipient: address(this), + deadline: deadline_, + amountIn: amountIn_, + amountOutMinimum: amountOutMinimum_ + }); + + uint256 amountOut_ = ISwapRouter(uniswapSwapRouter).exactInput(params_); + + emit TokensSwapped(path_, amountIn_, amountOut_); + + return amountOut_; + } + + /**********************************************************************************************/ + /*** UUPS ***/ + /**********************************************************************************************/ + + function version() external pure returns (uint256) { + return 3; + } + + function _authorizeUpgrade(address) internal view override onlyOwner {} +} diff --git a/contracts/capital-protocol/old/L2MessageReceiver.sol b/contracts/capital-protocol/L2MessageReceiver.sol similarity index 96% rename from contracts/capital-protocol/old/L2MessageReceiver.sol rename to contracts/capital-protocol/L2MessageReceiver.sol index ce932a3..e4a2829 100644 --- a/contracts/capital-protocol/old/L2MessageReceiver.sol +++ b/contracts/capital-protocol/L2MessageReceiver.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.20; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import {IMOROFT} from "../../interfaces/IMOROFT.sol"; -import {IL2MessageReceiver} from "../../interfaces/capital-protocol/old/IL2MessageReceiver.sol"; +import {IMOROFT} from "../interfaces/IMOROFT.sol"; +import {IL2MessageReceiver} from "../interfaces/capital-protocol/IL2MessageReceiver.sol"; contract L2MessageReceiver is IL2MessageReceiver, OwnableUpgradeable, UUPSUpgradeable { address public rewardToken; diff --git a/contracts/capital-protocol/old/Distributor.sol b/contracts/capital-protocol/old/Distributor.sol index d4f9ed1..ae66272 100644 --- a/contracts/capital-protocol/old/Distributor.sol +++ b/contracts/capital-protocol/old/Distributor.sol @@ -15,7 +15,7 @@ import {IRewardsController} from "../../interfaces/aave/IRewardsController.sol"; import {DecimalsConverter} from "@solarity/solidity-lib/libs/decimals/DecimalsConverter.sol"; import {IDistributor, IERC165} from "../../interfaces/capital-protocol/IDistributor.sol"; -import {IL1SenderV2} from "../../interfaces/capital-protocol/IL1SenderV2.sol"; +import {IL1SenderV2} from "../../interfaces/capital-protocol/old/IL1SenderV2.sol"; import {IChainLinkDataConsumer} from "../../interfaces/capital-protocol/IChainLinkDataConsumer.sol"; import {IDepositPool} from "../../interfaces/capital-protocol/IDepositPool.sol"; import {IRewardPool} from "../../interfaces/capital-protocol/IRewardPool.sol"; diff --git a/contracts/capital-protocol/L1SenderV2.sol b/contracts/capital-protocol/old/L1SenderV2.sol similarity index 97% rename from contracts/capital-protocol/L1SenderV2.sol rename to contracts/capital-protocol/old/L1SenderV2.sol index 7b3810c..a7aaba8 100644 --- a/contracts/capital-protocol/L1SenderV2.sol +++ b/contracts/capital-protocol/old/L1SenderV2.sol @@ -12,9 +12,9 @@ import {ILayerZeroEndpoint} from "@layerzerolabs/lz-evm-sdk-v1-0.7/contracts/int import {IGatewayRouter} from "@arbitrum/token-bridge-contracts/contracts/tokenbridge/libraries/gateway/IGatewayRouter.sol"; -import {IL1SenderV2, IERC165} from "../interfaces/capital-protocol/IL1SenderV2.sol"; -import {IDistributor} from "../interfaces/capital-protocol/IDistributor.sol"; -import {IWStETH} from "../interfaces/tokens/IWStETH.sol"; +import {IL1SenderV2, IERC165} from "../../interfaces/capital-protocol/old/IL1SenderV2.sol"; +import {IDistributor} from "../../interfaces/capital-protocol/IDistributor.sol"; +import {IWStETH} from "../../interfaces/tokens/IWStETH.sol"; contract L1SenderV2 is IL1SenderV2, OwnableUpgradeable, UUPSUpgradeable { /** @dev stETH token address */ diff --git a/contracts/interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IBridgeableTokens.sol b/contracts/interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IBridgeableTokens.sol new file mode 100644 index 0000000..70cd8df --- /dev/null +++ b/contracts/interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IBridgeableTokens.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.20; + +interface IBridgeableTokens { + function l1Token() external returns (address); + function l2Token() external returns (address); +} diff --git a/contracts/interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IL1ERC20Bridge.sol b/contracts/interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IL1ERC20Bridge.sol new file mode 100644 index 0000000..f9899a8 --- /dev/null +++ b/contracts/interfaces/@lidofinance/lido-l2/contracts/optimism/interfaces/IL1ERC20Bridge.sol @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2022 Lido +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.20; + +import {IBridgeableTokens} from "./IBridgeableTokens.sol"; + +/// @notice The L1 Standard bridge locks bridged tokens on the L1 side, sends deposit messages +/// on the L2 side, and finalizes token withdrawals from L2. +interface IL1ERC20Bridge is IBridgeableTokens { + event ERC20DepositInitiated( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + event ERC20WithdrawalFinalized( + address indexed _l1Token, + address indexed _l2Token, + address indexed _from, + address _to, + uint256 _amount, + bytes _data + ); + + /// @notice get the address of the corresponding L2 bridge contract. + /// @return Address of the corresponding L2 bridge contract. + function l2TokenBridge() external returns (address); + + /// @notice deposit an amount of the ERC20 to the caller's balance on L2. + /// @param l1Token_ Address of the L1 ERC20 we are depositing + /// @param l2Token_ Address of the L1 respective L2 ERC20 + /// @param amount_ Amount of the ERC20 to deposit + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + /// @param data_ Optional data to forward to L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function depositERC20( + address l1Token_, + address l2Token_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) external; + + /// @notice deposit an amount of ERC20 to a recipient's balance on L2. + /// @param l1Token_ Address of the L1 ERC20 we are depositing + /// @param l2Token_ Address of the L1 respective L2 ERC20 + /// @param to_ L2 address to credit the withdrawal to. + /// @param amount_ Amount of the ERC20 to deposit. + /// @param l2Gas_ Gas limit required to complete the deposit on L2. + /// @param data_ Optional data to forward to L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) external; + + /// @notice Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the + /// L1 ERC20 token. + /// @dev This call will fail if the initialized withdrawal from L2 has not been finalized. + /// @param l1Token_ Address of L1 token to finalizeWithdrawal for. + /// @param l2Token_ Address of L2 token where withdrawal was initiated. + /// @param from_ L2 address initiating the transfer. + /// @param to_ L1 address to credit the withdrawal to. + /// @param amount_ Amount of the ERC20 to deposit. + /// @param data_ Data provided by the sender on L2. This data is provided + /// solely as a convenience for external contracts. Aside from enforcing a maximum + /// length, these contracts provide no guarantees about its content. + function finalizeERC20Withdrawal( + address l1Token_, + address l2Token_, + address from_, + address to_, + uint256 amount_, + bytes calldata data_ + ) external; +} diff --git a/contracts/interfaces/old/IMOR.sol b/contracts/interfaces/IMOR.sol similarity index 100% rename from contracts/interfaces/old/IMOR.sol rename to contracts/interfaces/IMOR.sol diff --git a/contracts/interfaces/capital-protocol/IL1SenderV3.sol b/contracts/interfaces/capital-protocol/IL1SenderV3.sol new file mode 100644 index 0000000..e6289d3 --- /dev/null +++ b/contracts/interfaces/capital-protocol/IL1SenderV3.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; + +/** + * @title IL1SenderV3 + * @notice Defines the basic interface for the L1SenderV3 + */ +interface IL1SenderV3 is IERC165 { + event stETHSet(address stETH); + event DistributorSet(address distributor); + event UniswapSwapRouterSet(address uniswapSwapRouter); + event MessageBridgeConfigSet(MessageBridgeConfig messageBridgeConfig); + event MintMessageSent(address user, uint256 amount); + event TokenBridgeConfigSet(TokenBridgeConfig tokenBridgeConfig); + event TokenSent(uint256 amount, address receiver, uint32 l2Gas, bytes data); + event TokensSwapped(bytes path, uint256 amountIn, uint256 amountOut); + + /** + * @notice The structure that stores the deposit token's (stETH) data. + * @param wstETH The address of wrapped deposit token. + * @param gateway The address of token's gateway. + * @param receiver The address of wrapped token's receiver on L2. + */ + struct TokenBridgeConfig { + address wstETH; + address gateway; + address receiver; + } + + /** + * @notice The structure that stores the reward token's (MOR) data. + * @param gateway The address of token's gateway. + * @param receiver The address of token's receiver on L2. + * @param receiverChainId The chain id of receiver. + * @param zroPaymentAddress The address of ZKSync payment contract. + * @param adapterParams The parameters for the adapter. + */ + struct MessageBridgeConfig { + address gateway; + address receiver; + uint16 receiverChainId; + address zroPaymentAddress; + bytes adapterParams; + } + + /** + * @notice The function to receive the stETH contract address. + * @return The stETH contract address. + */ + function stETH() external view returns (address); + + /** + * @notice The function to receive the `Distributor` contract address. + * @return The `Distributor` contract address. + */ + function distributor() external view returns (address); + + /** + * @notice The function to receive the Uniswap `SwapRouter` contract address. + * @return The Uniswap `SwapRouter` contract address. + */ + function uniswapSwapRouter() external view returns (address); + + /** + * @notice The function to set the stETH address + * @dev Only for the contract `owner()`. + * @param value_ stETH contract address + */ + function setStETh(address value_) external; + + /** + * @notice The function to set the `distributor` value + * @dev Only for the contract `owner()`. + * @param value_ stETH contract address + */ + function setDistributor(address value_) external; + + /** + * @notice The function to set the `uniswapSwapRouter` value + * @dev Only for the contract `owner()`. + * @param value_ `uniswapSwapRouter` contract address + */ + function setUniswapSwapRouter(address value_) external; + + /** + * @notice The function to set the LayerZero config + * @dev Only for the contract `owner()`. + * @param config_ Config + */ + function setMessageBridgeConfig(MessageBridgeConfig calldata config_) external; + + /** + * @notice The function to send the reward token mint message to the `L1SenderV2`. + * @param user_ The user's address receiver . + * @param amount_ The amount of reward token to mint. + * @param refundTo_ The address to refund the overpaid gas. + */ + function sendMintMessage(address user_, uint256 amount_, address refundTo_) external payable; + + /** + * @notice The function to set the token bridge config for wstETH transfer to L2 + * @dev Only for the contract `owner()`. + * @param config_ Config + */ + function setTokenBridgeConfig(TokenBridgeConfig calldata config_) external; + + /** + * @notice The function to send all current balance of the deposit token to the L2. + * @param l2Gas_ Gas limit required to complete the deposit on L2. + * @param data_ Optional data to forward to L2. This data is provided + * solely as a convenience for external contracts. Aside from enforcing a maximum + * length, these contracts provide no guarantees about its content. + */ + function sendWstETH(uint32 l2Gas_, bytes calldata data_) external; + + /** + * @notice The function to swap the tokens on the contract. + * @param tokens_ Token for the swap. + * @param poolsFee_ Pools fee for the swap. + * @param amountIn_ Amount IN to swap. + * @param amountOutMinimum_ Minimal amount OUT to receive. + * @param deadline_ The unix time after which a swap will fail, to protect against long-pending transactions and wild swings in prices. + */ + function swapExactInputMultihop( + address[] calldata tokens_, + uint24[] calldata poolsFee_, + uint256 amountIn_, + uint256 amountOutMinimum_, + uint256 deadline_ + ) external returns (uint256); + + /** + * @notice The function to get the contract version. + * @return The current contract version + */ + function version() external pure returns (uint256); +} diff --git a/contracts/interfaces/capital-protocol/old/IL2MessageReceiver.sol b/contracts/interfaces/capital-protocol/IL2MessageReceiver.sol similarity index 100% rename from contracts/interfaces/capital-protocol/old/IL2MessageReceiver.sol rename to contracts/interfaces/capital-protocol/IL2MessageReceiver.sol diff --git a/contracts/interfaces/capital-protocol/IL1SenderV2.sol b/contracts/interfaces/capital-protocol/old/IL1SenderV2.sol similarity index 100% rename from contracts/interfaces/capital-protocol/IL1SenderV2.sol rename to contracts/interfaces/capital-protocol/old/IL1SenderV2.sol diff --git a/contracts/mock/InterfaceMock.sol b/contracts/mock/InterfaceMock.sol index aee5240..5dff0e0 100644 --- a/contracts/mock/InterfaceMock.sol +++ b/contracts/mock/InterfaceMock.sol @@ -6,7 +6,8 @@ import {IDepositPool} from "../interfaces/capital-protocol/IDepositPool.sol"; import {IRewardPool} from "../interfaces/capital-protocol/IRewardPool.sol"; import {IChainLinkDataConsumer} from "../interfaces/capital-protocol/IChainLinkDataConsumer.sol"; import {IDistributor} from "../interfaces/capital-protocol/IDistributor.sol"; -import {IL1SenderV2} from "../interfaces/capital-protocol/IL1SenderV2.sol"; +import {IL1SenderV2} from "../interfaces/capital-protocol/old/IL1SenderV2.sol"; +import {IL1SenderV3} from "../interfaces/capital-protocol/IL1SenderV3.sol"; contract InterfaceMock { function getIBuilderSubnetsInterfaceId() public pure returns (bytes4) { @@ -33,6 +34,10 @@ contract InterfaceMock { return type(IL1SenderV2).interfaceId; } + function getIL1SenderV3InterfaceId() public pure returns (bytes4) { + return type(IL1SenderV3).interfaceId; + } + function getIERC165InterfaceId() public pure returns (bytes4) { return type(IERC165).interfaceId; } diff --git a/contracts/mock/capital-protocol/@lidofinance/L1ERC20BridgeMock.sol b/contracts/mock/capital-protocol/@lidofinance/L1ERC20BridgeMock.sol new file mode 100644 index 0000000..329bdba --- /dev/null +++ b/contracts/mock/capital-protocol/@lidofinance/L1ERC20BridgeMock.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract L1ERC20BridgeMock { + uint256 preventWarning; + + function l2Token() external view returns (address) { + return address(this); + } + + function depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) external { + IERC20(l1Token_).transferFrom(msg.sender, to_, amount_); + + preventWarning = uint256(uint160(l2Token_)) + l2Gas_ + uint256(uint160(bytes20(data_))); + } +} diff --git a/contracts/mock/capital-protocol/DistributorMock.sol b/contracts/mock/capital-protocol/DistributorMock.sol index 8601bce..b22a119 100644 --- a/contracts/mock/capital-protocol/DistributorMock.sol +++ b/contracts/mock/capital-protocol/DistributorMock.sol @@ -7,7 +7,7 @@ import {AavePoolDataProviderMock} from "./aave/AavePoolDataProviderMock.sol"; import {AavePoolMock} from "./aave/AavePoolMock.sol"; import {IDistributor, IERC165} from "../../interfaces/capital-protocol/IDistributor.sol"; -import {IL1SenderV2} from "../../interfaces/capital-protocol/IL1SenderV2.sol"; +import {IL1SenderV2} from "../../interfaces/capital-protocol/old/IL1SenderV2.sol"; import "../tokens/ERC20Token.sol"; diff --git a/contracts/mock/capital-protocol/L1SenderMock.sol b/contracts/mock/capital-protocol/L1SenderMock.sol index 112be36..0295eef 100644 --- a/contracts/mock/capital-protocol/L1SenderMock.sol +++ b/contracts/mock/capital-protocol/L1SenderMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {IL1SenderV2, IERC165} from "../../interfaces/capital-protocol/IL1SenderV2.sol"; +import {IL1SenderV2, IERC165} from "../../interfaces/capital-protocol/old/IL1SenderV2.sol"; contract L1SenderMock is UUPSUpgradeable, IERC165 { mapping(address => uint256) public minted; diff --git a/deploy/capital-protocol/l1-sender/1_v2_v3_testing.ts b/deploy/capital-protocol/l1-sender/1_v2_v3_testing.ts new file mode 100644 index 0000000..05a95d5 --- /dev/null +++ b/deploy/capital-protocol/l1-sender/1_v2_v3_testing.ts @@ -0,0 +1,128 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Deployer } from '@solarity/hardhat-migrate'; + +import { + ERC1967Proxy__factory, + L1SenderV2, + L1SenderV2__factory, + L1SenderV3, + L1SenderV3__factory, + L2MessageReceiver, + L2MessageReceiver__factory, + MOR, + MOR__factory, +} from '@/generated-types/ethers'; +import { ZERO_ADDR } from '@/scripts/utils/constants'; + +const distributorAddress = '0xDf1AC1AC255d91F5f4B1E3B4Aef57c5350F64C7A'; // Ethereum +const l1SenderAddress = '0x6Fd2674E13a42E588f83Ae74e5F22a4EE24eD75A'; // Ethereum +const morAddress = '0x98e3CFBdB9707dF6107Cb1A7BD03036052EAa20e'; // Base +const l2MessageReceiverAddress = '0xB69DbF7C9aB4597D3b3BC284Cc8771D580299baD'; // Base +const stEThAddress = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; // Ethereum +const wstETHAddress = '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'; // Ethereum + +const layerZeroEthereumToArbitrumConfig = { + gateway: '0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675', + receiver: '0xd4a8ECcBe696295e68572A98b1aA70Aa9277d427', + receiverChainId: 110, + zroPaymentAddress: ZERO_ADDR, + adapterParams: '0x', +}; + +const layerZeroEthereumToBaseConfig = { + gateway: '0x66A71Dcef29A0fFBDBE3c6a460a3B5BC225Cd675', + receiver: l2MessageReceiverAddress, + receiverChainId: 184, + zroPaymentAddress: ZERO_ADDR, + adapterParams: '0x', +}; + +const layerZeroBaseToEthereumConfig = { + gateway: '0xb6319cC6c8c27A8F5dAF0dD3DF91EA35C4720dd7', + sender: l1SenderAddress, + senderChainId: 101, +}; + +const tokenArbitrumConfig = { + wstETH: wstETHAddress, + gateway: '0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef', + receiver: '0x47176B2Af9885dC6C4575d4eFd63895f7Aaa4790', +}; + +const tokenBaseConfig = { + wstETH: wstETHAddress, + gateway: '0x9de443AdC5A411E83F1878Ef24C3F52C61571e72', // https://docs.lido.fi/deployed-contracts/#base + receiver: '0x040ef6fb6592a70291954e2a6a1a8f320ff10626', // Personal wallet +}; + +module.exports = async function (deployer: Deployer) { + await _ethereumSetup(deployer); + await _baseSetup(deployer); +}; + +const _ethereumSetup = async (deployer: Deployer) => { + const l1SenderV2 = await _deployL1SenderV2(deployer); + // const l1SenderV2 = await deployer.deployed(L1SenderV3__factory, l1SenderAddress); + await l1SenderV2.setDistributor(distributorAddress); + await l1SenderV2.setStETh(stEThAddress); + await l1SenderV2.setLayerZeroConfig(layerZeroEthereumToArbitrumConfig); + await l1SenderV2.setArbitrumBridgeConfig(tokenArbitrumConfig); + + const l1SenderV3 = await _upgradeL1SenderV2toV3(deployer, l1SenderV2); + await l1SenderV3.setMessageBridgeConfig(layerZeroEthereumToBaseConfig); + await l1SenderV3.setTokenBridgeConfig(tokenBaseConfig); +}; + +const _baseSetup = async (deployer: Deployer) => { + const mor = await _deployMOR(deployer); + const l2MessageReceiver = await _deployL2MessageReceiver(deployer); + + // const mor = await deployer.deployed(MOR__factory, morAddress); + // const l2MessageReceiver = await deployer.deployed(L2MessageReceiver__factory, l2MessageReceiverAddress); + + await l2MessageReceiver.setParams(mor, layerZeroBaseToEthereumConfig); +}; + +const _deployL2MessageReceiver = async (deployer: Deployer): Promise => { + const impl = await deployer.deploy(L2MessageReceiver__factory); + const proxy = await deployer.deploy( + ERC1967Proxy__factory, + [ + await impl.getAddress(), + L2MessageReceiver__factory.createInterface().encodeFunctionData('L2MessageReceiver__init'), + ], + { + name: `L2MessageReceiver Proxy`, + }, + ); + + return await deployer.deployed(L2MessageReceiver__factory, await proxy.getAddress()); +}; + +const _deployMOR = async (deployer: Deployer): Promise => { + return await deployer.deploy(MOR__factory, ['42000000000000000000000000']); +}; + +const _deployL1SenderV2 = async (deployer: Deployer): Promise => { + const impl = await deployer.deploy(L1SenderV2__factory); + const proxy = await deployer.deploy( + ERC1967Proxy__factory, + [await impl.getAddress(), L1SenderV2__factory.createInterface().encodeFunctionData('L1SenderV2__init')], + { + name: `L1SenderV2 Proxy`, + }, + ); + + return await deployer.deployed(L1SenderV2__factory, await proxy.getAddress()); +}; + +const _upgradeL1SenderV2toV3 = async (deployer: Deployer, l1SenderV2: L1SenderV2): Promise => { + const impl = await deployer.deploy(L1SenderV3__factory); + await l1SenderV2.upgradeTo(await impl.getAddress()); + + return await deployer.deployed(L1SenderV3__factory, await l1SenderV2.getAddress()); +}; + +// npx hardhat migrate --path-to-migrations ./deploy/capital-protocol/l1-sender --only 1 +// npx hardhat migrate --path-to-migrations ./deploy/capital-protocol/l1-sender --only 1 --network base --verify +// npx hardhat migrate --path-to-migrations ./deploy/capital-protocol/l1-sender --only 1 --network mainnet --verify diff --git a/hardhat.config.ts b/hardhat.config.ts index a7802e8..8cd8785 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -29,11 +29,6 @@ function forceTypechain() { return process.env.TYPECHAIN_FORCE === 'false'; } -// 19183235 - 64607378777276381331 -// 19183263 - 502357441883879637000 -// 19183265 -- -// 19183274 - - - const config: HardhatUserConfig = { networks: { hardhat: { @@ -42,7 +37,6 @@ const config: HardhatUserConfig = { // gasPrice: 624381666, // forking: { // url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, - // blockNumber: 23444760, // }, // forking: { // url: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_KEY}`, @@ -90,56 +84,21 @@ const config: HardhatUserConfig = { gasMultiplier: 1.2, timeout: 1000000000000000, }, - goerli: { - url: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`, - accounts: privateKey(), - gasMultiplier: 1.2, - }, sepolia: { url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_KEY}`, accounts: privateKey(), gasMultiplier: 1.1, }, - chapel: { - url: 'https://data-seed-prebsc-1-s1.binance.org:8545', - accounts: privateKey(), - gasMultiplier: 1.2, - timeout: 60000, - }, - mumbai: { url: `https://polygon-mumbai.blockpi.network/v1/rpc/public`, accounts: privateKey(), gasMultiplier: 1.1 }, - polygonAmoy: { - url: `https://polygon-amoy.blockpi.network/v1/rpc/public`, - accounts: privateKey(), - gasMultiplier: 1.1, - }, - fuji: { - url: `https://avalanche-fuji.infura.io/v3/${process.env.INFURA_KEY}`, - accounts: privateKey(), - gasMultiplier: 1.2, - }, - bsc: { url: 'https://bsc-dataseed.binance.org/', accounts: privateKey(), gasMultiplier: 1.2 }, - ethereum: { + mainnet: { url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`, accounts: privateKey(), gasMultiplier: 1.2, }, - polygon: { url: `https://matic-mainnet.chainstacklabs.com`, accounts: privateKey(), gasMultiplier: 1.2 }, - avalanche: { - url: `https://api.avax.network/ext/bc/C/rpc`, - accounts: privateKey(), - gasMultiplier: 1.2, - timeout: 60000, - }, arbitrum: { url: `https://arbitrum-mainnet.infura.io/v3/${process.env.INFURA_KEY}`, accounts: privateKey(), gasMultiplier: 1.2, }, - arbitrum_goerli: { - url: `https://arbitrum-goerli.infura.io/v3/${process.env.INFURA_KEY}`, - accounts: privateKey(), - gasMultiplier: 1.2, - }, arbitrum_sepolia: { url: `https://arbitrum-sepolia.infura.io/v3/${process.env.INFURA_KEY}`, accounts: privateKey(), @@ -187,8 +146,27 @@ const config: HardhatUserConfig = { chainId: 80002, urls: { apiURL: 'https://api-amoy.polygonscan.com/api', browserURL: 'https://amoy.polygonscan.com' }, }, + { + network: 'mainnet', + chainId: 1, + urls: { + apiURL: `https://api.etherscan.io/v2/api?chainid=1&apikey=${process.env.ETHERSCAN_KEY}`, + browserURL: 'https://etherscan.io', + }, + }, + { + network: 'base', + chainId: 8453, + urls: { + apiURL: `https://api.etherscan.io/v2/api?chainid=8453&apikey=${process.env.ETHERSCAN_KEY}`, + browserURL: 'https://basescan.org', + }, + }, ], }, + sourcify: { + enabled: true, + }, migrate: { pathToMigrations: './deploy/', // only: 1, diff --git a/test/capital-protocol/L1SenderV3.test.ts b/test/capital-protocol/L1SenderV3.test.ts new file mode 100644 index 0000000..6ed2699 --- /dev/null +++ b/test/capital-protocol/L1SenderV3.test.ts @@ -0,0 +1,449 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +import { + deployArbitrumBridgeGatewayRouterMock, + deployDistributorMock, + deployERC20Token, + deployInterfaceMock, + deployL1ERC20BridgeMock, + deployL1SenderV2, + deployL1SenderV3, + deployL2MessageReceiver, + deployLZEndpointMock, + deployRewardPoolMock, + deployStETHMock, + deployUniswapSwapRouterMock, + deployWstETHMock, +} from '../helpers/deployers'; + +import { + DistributorMock, + L1ERC20BridgeMock, + L1SenderV3, + StETHMock, + UniswapSwapRouterMock, + WStETHMock, +} from '@/generated-types/ethers'; +import { ZERO_ADDR } from '@/scripts/utils/constants'; +import { wei } from '@/scripts/utils/utils'; +import { Reverter } from '@/test/helpers/reverter'; + +describe('L1SenderV3', () => { + const reverter = new Reverter(); + + let OWNER: SignerWithAddress; + let BOB: SignerWithAddress; + + let stETH: StETHMock; + let wstETH: WStETHMock; + let l1Sender: L1SenderV3; + let distributor: DistributorMock; + // let arbitrumBridgeGatewayRouterMock: ArbitrumBridgeGatewayRouterMock; + let l1ERC20BridgeMock: L1ERC20BridgeMock; + let uniswapSwapRouterMock: UniswapSwapRouterMock; + + before(async () => { + [OWNER, BOB] = await ethers.getSigners(); + + stETH = await deployStETHMock(); + wstETH = await deployWstETHMock(stETH); + distributor = await deployDistributorMock(await deployRewardPoolMock(), await deployERC20Token()); + // arbitrumBridgeGatewayRouterMock = await deployArbitrumBridgeGatewayRouterMock(); + l1ERC20BridgeMock = await deployL1ERC20BridgeMock(); + uniswapSwapRouterMock = await deployUniswapSwapRouterMock(); + l1Sender = await deployL1SenderV3(); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe('UUPS proxy functionality', () => { + describe('#constructor', () => { + it('should disable initialize function', async () => { + const reason = 'Initializable: contract is already initialized'; + + await expect(l1Sender.connect(OWNER).L1SenderV3__init()).to.be.revertedWith(reason); + }); + }); + + describe('#_authorizeUpgrade', () => { + it('should upgrade to the new version', async () => { + const [factory] = await Promise.all([ethers.getContractFactory('L1SenderMock')]); + const contract = await factory.deploy(); + + await l1Sender.upgradeTo(contract); + expect(await l1Sender.version()).to.eq(666); + }); + + it('should revert if caller is not the owner', async () => { + await expect(l1Sender.connect(BOB).upgradeTo(ZERO_ADDR)).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('#version()', () => { + it('should return correct version', async () => { + expect(await l1Sender.version()).to.eq(3); + }); + }); + }); + + describe('#supportsInterface', () => { + it('should support IL1SenderV3', async () => { + const interfaceMock = await deployInterfaceMock(); + expect(await l1Sender.supportsInterface(await interfaceMock.getIL1SenderV3InterfaceId())).to.be.true; + }); + it('should support IERC165', async () => { + const interfaceMock = await deployInterfaceMock(); + expect(await l1Sender.supportsInterface(await interfaceMock.getIERC165InterfaceId())).to.be.true; + }); + }); + + describe('#setStETh', () => { + it('should correctly set new value', async () => { + await l1Sender.setStETh(stETH); + expect(await l1Sender.stETH()).to.be.equal(stETH); + }); + it('should revert when invalid distributor address', async () => { + await expect(l1Sender.setStETh(ZERO_ADDR)).to.be.revertedWith('L1S: invalid stETH address'); + }); + it('should revert if not called by the owner', async () => { + await expect(l1Sender.connect(BOB).setStETh(stETH)).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('#setDistribution', () => { + it('should correctly set new value', async () => { + await l1Sender.setDistributor(distributor); + expect(await l1Sender.distributor()).to.be.equal(distributor); + }); + it('should revert when invalid distributor address', async () => { + await expect(l1Sender.setDistributor(await deployRewardPoolMock())).to.be.revertedWith( + 'L1S: invalid distributor address', + ); + }); + it('should revert if not called by the owner', async () => { + await expect(l1Sender.connect(BOB).setDistributor(distributor)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setUniswapSwapRouter', () => { + it('should correctly set new value', async () => { + await l1Sender.setUniswapSwapRouter(uniswapSwapRouterMock); + expect(await l1Sender.uniswapSwapRouter()).to.be.equal(uniswapSwapRouterMock); + }); + it('should revert when invalid `uniswapSwapRouter` address', async () => { + await expect(l1Sender.setUniswapSwapRouter(ZERO_ADDR)).to.be.revertedWith( + 'L1S: invalid `uniswapSwapRouter` address', + ); + }); + it('should revert if not called by the owner', async () => { + await expect(l1Sender.connect(BOB).setUniswapSwapRouter(uniswapSwapRouterMock)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setMessageBridgeConfig', () => { + const config = { + gateway: '', + receiver: '', + receiverChainId: 0, + zroPaymentAddress: ZERO_ADDR, + adapterParams: '0x', + }; + beforeEach(async () => { + config.gateway = await BOB.getAddress(); + config.receiver = await OWNER.getAddress(); + }); + + it('should set new config', async () => { + await l1Sender.setMessageBridgeConfig(config); + + expect(await l1Sender.messageBridgeConfig()).to.be.deep.equal([ + config.gateway, + config.receiver, + 0, + ZERO_ADDR, + '0x', + ]); + }); + it('should revert if not called by the owner', async () => { + await expect(l1Sender.connect(BOB).setMessageBridgeConfig(config)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#setArbitrumBridgeConfig', () => { + it('should correctly set new config', async () => { + await l1Sender.setStETh(stETH); + + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }; + + await l1Sender.setTokenBridgeConfig(config); + + expect(await l1Sender.tokenBridgeConfig()).to.be.deep.equal([ + await wstETH.getAddress(), + await l1ERC20BridgeMock.getAddress(), + await BOB.getAddress(), + ]); + + expect(await stETH.allowance(l1Sender, wstETH)).to.be.equal(ethers.MaxUint256); + expect(await wstETH.allowance(l1Sender, l1ERC20BridgeMock)).to.be.equal(ethers.MaxUint256); + }); + it('should correctly reset new config', async () => { + await l1Sender.setStETh(stETH); + + const config1 = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }; + await l1Sender.setTokenBridgeConfig(config1); + + const config2 = { ...config1 }; + config2.wstETH = await deployWstETHMock(await deployStETHMock()); + config2.gateway = await deployL1ERC20BridgeMock(); + config2.receiver = OWNER; + await l1Sender.setTokenBridgeConfig(config2); + + expect(await l1Sender.tokenBridgeConfig()).to.be.deep.equal([ + await config2.wstETH.getAddress(), + await config2.gateway.getAddress(), + await config2.receiver.getAddress(), + ]); + + expect(await stETH.allowance(l1Sender, wstETH)).to.be.equal(0); + expect(await wstETH.allowance(l1Sender, l1ERC20BridgeMock)).to.be.equal(0); + + expect(await stETH.allowance(l1Sender, config2.wstETH)).to.be.equal(ethers.MaxUint256); + expect(await config2.wstETH.allowance(l1Sender, config2.gateway)).to.be.equal(ethers.MaxUint256); + }); + it('should correctly reset new config after the update from v2 to v3', async () => { + // Imitate L1SenderV2 `setArbitrumBridgeConfig` + const l1SenderV2Contract = await deployL1SenderV2(); + + const arbitrumBridgeGatewayRouterMock = await deployArbitrumBridgeGatewayRouterMock(); + await l1SenderV2Contract.setStETh(stETH); + await l1SenderV2Contract.setArbitrumBridgeConfig({ + wstETH: wstETH, + gateway: arbitrumBridgeGatewayRouterMock, + receiver: BOB, + }); + expect(await l1SenderV2Contract.arbitrumBridgeConfig()).to.be.deep.equal([ + await wstETH.getAddress(), + await arbitrumBridgeGatewayRouterMock.getAddress(), + await BOB.getAddress(), + ]); + + // Upgrade to v3 + const l1SenderV3Factory = await ethers.getContractFactory('L1SenderV3'); + const l1SenderV3Impl = await l1SenderV3Factory.deploy(); + await l1SenderV2Contract.upgradeTo(l1SenderV3Impl); + const l1SenderV3 = l1SenderV3Impl.attach(l1SenderV2Contract) as L1SenderV3; + + const wstETHNew = await deployWstETHMock(await deployStETHMock()); + await l1SenderV3.setTokenBridgeConfig({ + wstETH: wstETHNew, + gateway: l1ERC20BridgeMock, + receiver: OWNER, + }); + + expect(await l1SenderV3.tokenBridgeConfig()).to.be.deep.equal([ + await wstETHNew.getAddress(), + await l1ERC20BridgeMock.getAddress(), + await OWNER.getAddress(), + ]); + + expect(await stETH.allowance(l1SenderV3, wstETH)).to.be.equal(0); + expect(await wstETH.allowance(l1SenderV3, arbitrumBridgeGatewayRouterMock)).to.be.equal(0); + + expect(await stETH.allowance(l1SenderV3, wstETHNew)).to.be.equal(ethers.MaxUint256); + expect(await wstETHNew.allowance(l1SenderV3, l1ERC20BridgeMock)).to.be.equal(ethers.MaxUint256); + }); + it('should revert when stETH is not set', async () => { + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: ZERO_ADDR, + }; + + await expect(l1Sender.setTokenBridgeConfig(config)).to.be.revertedWith('L1S: stETH is not set'); + }); + it('should revert when invalid receiver', async () => { + await l1Sender.setStETh(stETH); + + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: ZERO_ADDR, + }; + + await expect(l1Sender.setTokenBridgeConfig(config)).to.be.revertedWith('L1S: invalid receiver'); + }); + it('should revert if not called by the owner', async () => { + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }; + + await expect(l1Sender.connect(BOB).setTokenBridgeConfig(config)).to.be.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('#sendWstETH', () => { + beforeEach(async () => { + await l1Sender.setStETh(stETH); + }); + it('should send stETH tokens to another address', async () => { + await l1Sender.setTokenBridgeConfig({ + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }); + + await stETH.mint(l1Sender, wei(100)); + + await l1Sender.sendWstETH(1, '0x'); + + expect(await stETH.balanceOf(l1Sender)).to.eq(0); + expect(await wstETH.balanceOf(BOB)).to.eq(wei(100)); + }); + it('should send wstETH tokens to another address', async () => { + await l1Sender.setTokenBridgeConfig({ + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }); + + await wstETH.mint(l1Sender, wei(100)); + + await l1Sender.sendWstETH(1, '0x'); + + expect(await stETH.balanceOf(l1Sender)).to.eq(0); + expect(await wstETH.balanceOf(BOB)).to.eq(wei(100)); + }); + it("should revert when wstETH isn't set", async () => { + await expect(l1Sender.sendWstETH(1, '0x')).to.be.revertedWith("L1S: wstETH isn't set"); + }); + it('should revert if not called by the owner', async () => { + await expect(l1Sender.connect(BOB).sendWstETH(1, '0x')).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('#sendMintMessage', () => { + it('should send mint message', async () => { + const lzEndpointMockL1 = await deployLZEndpointMock(101); + const lzEndpointMockL2 = await deployLZEndpointMock(110); + const l2MessageReceiver = await deployL2MessageReceiver(); + + const mor = await deployERC20Token(); + await l2MessageReceiver.setParams(mor, { + gateway: lzEndpointMockL2, + sender: l1Sender, + senderChainId: 101, + }); + + await lzEndpointMockL1.setDestLzEndpoint(l2MessageReceiver, lzEndpointMockL2); + + await l1Sender.setMessageBridgeConfig({ + gateway: lzEndpointMockL1, + receiver: l2MessageReceiver, + receiverChainId: 110, + zroPaymentAddress: ZERO_ADDR, + adapterParams: '0x', + }); + + const distributorMock = await deployDistributorMock(await deployRewardPoolMock(), await deployERC20Token()); + + await l1Sender.setDistributor(distributorMock); + await distributorMock.sendMintMessageToL1Sender(l1Sender, BOB, wei(1), OWNER, { + value: ethers.parseEther('20'), + }); + expect(await mor.balanceOf(BOB)).to.eq(wei(1)); + }); + it('should revert if not called by the owner', async () => { + await expect(l1Sender.sendMintMessage(BOB, '999', OWNER, { value: ethers.parseEther('0.1') })).to.be.revertedWith( + "L1S: the `msg.sender` isn't `distributor`", + ); + }); + }); + + describe('#swapExactInputMultihop', () => { + it('should swap tokens', async () => { + const tokenIn = await deployERC20Token(); + + await tokenIn.mint(l1Sender, wei(90)); + await wstETH.mint(uniswapSwapRouterMock, wei(90)); + + await l1Sender.setStETh(stETH); + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }; + await l1Sender.setTokenBridgeConfig(config); + + await l1Sender.setUniswapSwapRouter(uniswapSwapRouterMock); + await l1Sender.swapExactInputMultihop([tokenIn, wstETH], [100], wei(90), wei(40), 0); + + expect(await tokenIn.balanceOf(l1Sender)).to.eq(wei(0)); + expect(await wstETH.balanceOf(l1Sender)).to.eq(wei(90)); + }); + it('should revert when invalid `amountIn_` value', async () => { + await l1Sender.setStETh(stETH); + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }; + await l1Sender.setTokenBridgeConfig(config); + + await expect(l1Sender.swapExactInputMultihop([stETH, wstETH], [100], wei(0), wei(40), 0)).to.be.revertedWith( + 'L1S: invalid `amountIn_` value', + ); + }); + it('should revert when invalid `amountIn_` value', async () => { + await l1Sender.setStETh(stETH); + const config = { + wstETH: wstETH, + gateway: l1ERC20BridgeMock, + receiver: BOB, + }; + await l1Sender.setTokenBridgeConfig(config); + + await expect(l1Sender.swapExactInputMultihop([stETH, wstETH], [100], wei(10), wei(0), 0)).to.be.revertedWith( + 'L1S: invalid `amountOutMinimum_` value', + ); + }); + it('should revert when invalid array length', async () => { + await expect(l1Sender.swapExactInputMultihop([stETH], [100], wei(10), wei(0), 0)).to.be.revertedWith( + 'L1S: invalid array length', + ); + + await expect(l1Sender.swapExactInputMultihop([stETH, wstETH], [100, 100], wei(10), wei(0), 0)).to.be.revertedWith( + 'L1S: invalid array length', + ); + }); + it('should revert if not called by the owner', async () => { + await expect( + l1Sender.connect(BOB).swapExactInputMultihop([stETH, wstETH], [100], wei(90), wei(40), 0), + ).to.be.revertedWith('Ownable: caller is not the owner'); + }); + }); +}); + +// npx hardhat test "test/capital-protocol/L1SenderV3.test.ts" +// npx hardhat coverage --solcoverjs ./.solcover.ts --testfiles "test/capital-protocol/L1SenderV3.test.ts" diff --git a/test/capital-protocol/L1SenderV2.test.ts b/test/capital-protocol/old/L1SenderV2.test.ts similarity index 99% rename from test/capital-protocol/L1SenderV2.test.ts rename to test/capital-protocol/old/L1SenderV2.test.ts index 4dc5b5e..c36086a 100644 --- a/test/capital-protocol/L1SenderV2.test.ts +++ b/test/capital-protocol/old/L1SenderV2.test.ts @@ -14,7 +14,7 @@ import { deployStETHMock, deployUniswapSwapRouterMock, deployWstETHMock, -} from '../helpers/deployers'; +} from '../../helpers/deployers'; import { ArbitrumBridgeGatewayRouterMock, diff --git a/test/helpers/deployers/capital-protocol/index.ts b/test/helpers/deployers/capital-protocol/index.ts index 3527b25..fe04d50 100644 --- a/test/helpers/deployers/capital-protocol/index.ts +++ b/test/helpers/deployers/capital-protocol/index.ts @@ -1,7 +1,8 @@ +export * from './old'; export * from './chain-link-data-consumer'; export * from './deposit-pool'; export * from './distributor'; export * from './distributor-v2'; -export * from './l1-sender-v2'; +export * from './l1-sender-v3'; export * from './l2-message-receiver'; export * from './reward-pool'; diff --git a/test/helpers/deployers/capital-protocol/l1-sender-v3.ts b/test/helpers/deployers/capital-protocol/l1-sender-v3.ts new file mode 100644 index 0000000..af60c18 --- /dev/null +++ b/test/helpers/deployers/capital-protocol/l1-sender-v3.ts @@ -0,0 +1,18 @@ +import { ethers } from 'hardhat'; + +import { L1SenderV3 } from '@/generated-types/ethers'; + +export const deployL1SenderV3 = async (): Promise => { + const [implFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('L1SenderV3'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const impl = await implFactory.deploy(); + const proxy = await proxyFactory.deploy(impl, '0x'); + const contract = impl.attach(proxy) as L1SenderV3; + + await contract.L1SenderV3__init(); + + return contract; +}; diff --git a/test/helpers/deployers/capital-protocol/old/index.ts b/test/helpers/deployers/capital-protocol/old/index.ts new file mode 100644 index 0000000..81ff909 --- /dev/null +++ b/test/helpers/deployers/capital-protocol/old/index.ts @@ -0,0 +1 @@ +export * from './l1-sender-v2'; diff --git a/test/helpers/deployers/capital-protocol/l1-sender-v2.ts b/test/helpers/deployers/capital-protocol/old/l1-sender-v2.ts similarity index 100% rename from test/helpers/deployers/capital-protocol/l1-sender-v2.ts rename to test/helpers/deployers/capital-protocol/old/l1-sender-v2.ts diff --git a/test/helpers/deployers/l1-sender-v2.ts b/test/helpers/deployers/l1-sender-v2.ts new file mode 100644 index 0000000..da4b7d7 --- /dev/null +++ b/test/helpers/deployers/l1-sender-v2.ts @@ -0,0 +1,18 @@ +import { ethers } from 'hardhat'; + +import { L1SenderV2 } from '@/generated-types/ethers'; + +export const deployL1SenderV2 = async (): Promise => { + const [implFactory, proxyFactory] = await Promise.all([ + ethers.getContractFactory('L1SenderV2'), + ethers.getContractFactory('ERC1967Proxy'), + ]); + + const impl = await implFactory.deploy(); + const proxy = await proxyFactory.deploy(impl, '0x'); + const contract = impl.attach(proxy) as L1SenderV2; + + await contract.L1SenderV2__init(); + + return contract; +}; diff --git a/test/helpers/deployers/mock/capital-protocol/index.ts b/test/helpers/deployers/mock/capital-protocol/index.ts index 88efeb6..f46ff76 100644 --- a/test/helpers/deployers/mock/capital-protocol/index.ts +++ b/test/helpers/deployers/mock/capital-protocol/index.ts @@ -6,6 +6,7 @@ export * from './chain-link-aggregator-v3-mock'; export * from './chain-link-data-consumer-mock'; export * from './deposit-pool-mock'; export * from './distributor-mock'; +export * from './l1-erc20-bridge-mock'; export * from './l1-sender-mock'; export * from './lz-endpoint-mock'; export * from './reward-pool-mock'; diff --git a/test/helpers/deployers/mock/capital-protocol/l1-erc20-bridge-mock.ts b/test/helpers/deployers/mock/capital-protocol/l1-erc20-bridge-mock.ts new file mode 100644 index 0000000..7cce64b --- /dev/null +++ b/test/helpers/deployers/mock/capital-protocol/l1-erc20-bridge-mock.ts @@ -0,0 +1,11 @@ +import { ethers } from 'hardhat'; + +import { L1ERC20BridgeMock } from '@/generated-types/ethers'; + +export const deployL1ERC20BridgeMock = async (): Promise => { + const [factory] = await Promise.all([ethers.getContractFactory('L1ERC20BridgeMock')]); + + const contract = await factory.deploy(); + + return contract; +};