diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 501a57084..bfc95523c 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -69,7 +69,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock @@ -111,7 +111,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock @@ -144,7 +144,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock registry-url: "https://registry.npmjs.org" @@ -241,7 +241,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock @@ -290,7 +290,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock registry-url: "https://registry.npmjs.org" @@ -358,7 +358,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock @@ -401,7 +401,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: "14.x" + node-version: "18.x" cache: "yarn" cache-dependency-path: solidity/yarn.lock diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/cross-chain/L1BitcoinDepositor.sol similarity index 63% rename from solidity/contracts/l2/L1BitcoinDepositor.sol rename to solidity/contracts/cross-chain/L1BitcoinDepositor.sol index 0d02cc05c..13757215a 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/cross-chain/L1BitcoinDepositor.sol @@ -13,9 +13,8 @@ // ▐████▌ ▐████▌ // ▐████▌ ▐████▌ -pragma solidity 0.8.17; +pragma solidity ^0.8.20; -import "@keep-network/random-beacon/contracts/Reimbursable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -23,7 +22,8 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../integrator/AbstractTBTCDepositor.sol"; import "../integrator/IBridge.sol"; import "../integrator/ITBTCVault.sol"; -import "./Wormhole.sol"; +import "./utils/Reimbursable.sol"; +import "./utils/Crosschain.sol"; /// @title L1BitcoinDepositor /// @notice This contract is part of the direct bridging mechanism allowing @@ -48,27 +48,12 @@ import "./Wormhole.sol"; /// on-chain event emitted by the `L2BitcoinDepositor` contract. /// Further iterations assumes those data are passed off-chain, e.g. /// through a REST API exposed by the relayer. -/// 3. The relayer uses the data to initialize a deposit on the L1 -/// chain by calling the `initializeDeposit` function of the -/// `L1BitcoinDepositor` contract. The `initializeDeposit` function -/// reveals the deposit to the tBTC Bridge so minting of ERC20 L1 TBTC -/// can occur. -/// 4. Once minting is complete, the `L1BitcoinDepositor` contract -/// receives minted ERC20 L1 TBTC. The relayer then calls the -/// `finalizeDeposit` function of the `L1BitcoinDepositor` contract -/// to transfer the minted ERC20 L1 TBTC to the L2 user address. This -/// is achieved using the Wormhole protocol. First, the `finalizeDeposit` -/// function initiates a Wormhole token transfer that locks the ERC20 -/// L1 TBTC within the Wormhole Token Bridge contract and assigns -/// Wormhole-wrapped L2 TBTC to the corresponding `L2WormholeGateway` -/// contract. Then, `finalizeDeposit` notifies the `L2BitcoinDepositor` -/// contract by sending a Wormhole message containing the VAA -/// of the Wormhole token transfer. The `L2BitcoinDepositor` contract -/// receives the Wormhole message, and calls the `L2WormholeGateway` -/// contract that redeems Wormhole-wrapped L2 TBTC from the Wormhole -/// Token Bridge and uses it to mint canonical L2 TBTC to the L2 user +/// 3. Once TBTC is minted on L1, the relayer calls `finalizeDeposit(...)` +/// to have the newly minted TBTC bridged to L2 for the user. +/// The details of that bridging are handled by `_transferTbtc(...)` +/// in whichever specialized child contract extends this abstract one. /// address. -contract L1BitcoinDepositor is +abstract contract L1BitcoinDepositor is AbstractTBTCDepositor, OwnableUpgradeable, Reimbursable @@ -102,24 +87,6 @@ contract L1BitcoinDepositor is mapping(uint256 => DepositState) public deposits; /// @notice ERC20 L1 TBTC token contract. IERC20Upgradeable public tbtcToken; - /// @notice `Wormhole` core contract on L1. - IWormhole public wormhole; - /// @notice `WormholeRelayer` contract on L1. - IWormholeRelayer public wormholeRelayer; - /// @notice Wormhole `TokenBridge` contract on L1. - IWormholeTokenBridge public wormholeTokenBridge; - /// @notice tBTC `L2WormholeGateway` contract on the corresponding L2 chain. - address public l2WormholeGateway; - /// @notice Wormhole chain ID of the corresponding L2 chain. - uint16 public l2ChainId; - /// @notice tBTC `L2BitcoinDepositor` contract on the corresponding L2 chain. - address public l2BitcoinDepositor; - /// @notice Gas limit necessary to execute the L2 part of the deposit - /// finalization. This value is used to calculate the payment for - /// the Wormhole Relayer that is responsible to execute the - /// deposit finalization on the corresponding L2 chain. Can be - /// updated by the owner. - uint256 public l2FinalizeDepositGasLimit; /// @notice Holds deferred gas reimbursements for deposit initialization /// (indexed by deposit key). Reimbursement for deposit /// initialization is paid out upon deposit finalization. This is @@ -160,8 +127,6 @@ contract L1BitcoinDepositor is uint256 tbtcAmount ); - event L2FinalizeDepositGasLimitUpdated(uint256 l2FinalizeDepositGasLimit); - event GasOffsetParametersUpdated( uint256 initializeDepositGasOffset, uint256 finalizeDepositGasOffset @@ -183,87 +148,19 @@ contract L1BitcoinDepositor is _; } - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - function initialize( + function __L1BitcoinDepositor_initialize( address _tbtcBridge, - address _tbtcVault, - address _wormhole, - address _wormholeRelayer, - address _wormholeTokenBridge, - address _l2WormholeGateway, - uint16 _l2ChainId - ) external initializer { + address _tbtcVault + ) internal { __AbstractTBTCDepositor_initialize(_tbtcBridge, _tbtcVault); - __Ownable_init(); - - require(_wormhole != address(0), "Wormhole address cannot be zero"); - require( - _wormholeRelayer != address(0), - "WormholeRelayer address cannot be zero" - ); - require( - _wormholeTokenBridge != address(0), - "WormholeTokenBridge address cannot be zero" - ); - require( - _l2WormholeGateway != address(0), - "L2WormholeGateway address cannot be zero" - ); tbtcToken = IERC20Upgradeable(ITBTCVault(_tbtcVault).tbtcToken()); - wormhole = IWormhole(_wormhole); - wormholeRelayer = IWormholeRelayer(_wormholeRelayer); - wormholeTokenBridge = IWormholeTokenBridge(_wormholeTokenBridge); - // slither-disable-next-line missing-zero-check - l2WormholeGateway = _l2WormholeGateway; - l2ChainId = _l2ChainId; - l2FinalizeDepositGasLimit = 500_000; + initializeDepositGasOffset = 60_000; finalizeDepositGasOffset = 20_000; reimburseTxMaxFee = false; } - /// @notice Sets the address of the `L2BitcoinDepositor` contract on the - /// corresponding L2 chain. This function solves the chicken-and-egg - /// problem of setting the `L2BitcoinDepositor` contract address - /// on the `L1BitcoinDepositor` contract and vice versa. - /// @param _l2BitcoinDepositor Address of the `L2BitcoinDepositor` contract. - /// @dev Requirements: - /// - Can be called only by the contract owner, - /// - The address must not be set yet, - /// - The new address must not be 0x0. - function attachL2BitcoinDepositor(address _l2BitcoinDepositor) - external - onlyOwner - { - require( - l2BitcoinDepositor == address(0), - "L2 Bitcoin Depositor already set" - ); - require( - _l2BitcoinDepositor != address(0), - "L2 Bitcoin Depositor must not be 0x0" - ); - l2BitcoinDepositor = _l2BitcoinDepositor; - } - - /// @notice Updates the gas limit necessary to execute the L2 part of the - /// deposit finalization. - /// @param _l2FinalizeDepositGasLimit New gas limit. - /// @dev Requirements: - /// - Can be called only by the contract owner. - function updateL2FinalizeDepositGasLimit(uint256 _l2FinalizeDepositGasLimit) - external - onlyOwner - { - l2FinalizeDepositGasLimit = _l2FinalizeDepositGasLimit; - emit L2FinalizeDepositGasLimitUpdated(_l2FinalizeDepositGasLimit); - } - /// @notice Updates the values of gas offset parameters. /// @dev Can be called only by the contract owner. The caller is responsible /// for validating parameters. @@ -308,7 +205,7 @@ contract L1BitcoinDepositor is /// data (funding transaction and components of the P2(W)SH deposit /// address) to the tBTC Bridge. Once tBTC minting is completed, /// this call should be followed by a call to `finalizeDeposit`. - /// Callers of `initializeDeposit` are eligible for a gas refund + /// Callers of `initializeDeposit` are eligible for a gas dgasd /// that is paid out upon deposit finalization (only if the /// reimbursement pool is attached and the given caller is /// authorized for refunds). @@ -334,8 +231,8 @@ contract L1BitcoinDepositor is /// 20-byte L1 address of the /// `L1BitcoinDepositor` contract. /// - /// L2 deposit owner address in the Wormhole - /// format, i.e. 32-byte value left-padded with 0. + /// L2 deposit owner address in the Bytes32 + /// format. /// /// 8-byte deposit blinding factor, as used in the /// tBTC bridge. @@ -383,9 +280,9 @@ contract L1BitcoinDepositor is "L2 deposit owner must not be 0x0" ); - // Convert the L2 deposit owner address into the Wormhole format and + // Convert the L2 deposit owner address into the Bytes32 format and // encode it as deposit extra data. - bytes32 extraData = WormholeUtils.toWormholeAddress(l2DepositOwner); + bytes32 extraData = CrosschainUtils.addressToBytes32(l2DepositOwner); // Input parameters do not have to be validated in any way. // The tBTC Bridge is responsible for validating whether the provided @@ -459,7 +356,7 @@ contract L1BitcoinDepositor is /// - `initializeDeposit` was called for the given deposit before, /// - ERC20 L1 TBTC was minted by tBTC Bridge to this contract, /// - The function was not called for the given deposit before, - /// - The call must carry a payment for the Wormhole Relayer that + /// - The call must carry a payment for the briding system that /// is responsible for executing the deposit finalization on the /// corresponding L2 chain. The payment must be equal to the /// value returned by the `quoteFinalizeDeposit` function. @@ -477,7 +374,7 @@ contract L1BitcoinDepositor is uint256 initialDepositAmount, uint256 tbtcAmount, // Deposit extra data is actually the L2 deposit owner - // address in Wormhole format. + // address in Bytes32 format. bytes32 l2DepositOwner ) = _finalizeDeposit(depositKey); @@ -495,7 +392,7 @@ contract L1BitcoinDepositor is // slither-disable-next-line reentrancy-events emit DepositFinalized( depositKey, - WormholeUtils.fromWormholeAddress(l2DepositOwner), + CrosschainUtils.bytes32ToAddress(l2DepositOwner), msg.sender, initialDepositAmount, tbtcAmount @@ -517,6 +414,7 @@ contract L1BitcoinDepositor is depositKey ]; if (reimbursement.receiver != address(0)) { + // slither-disable-next-line reentrancy-benign delete gasReimbursements[depositKey]; reimbursementPool.refund( @@ -529,7 +427,7 @@ contract L1BitcoinDepositor is // is authorized to receive reimbursements. if (reimbursementAuthorizations[msg.sender]) { // As this call is payable and this transaction carries out a - // msg.value that covers Wormhole cost, we need to reimburse + // msg.value that covers the Bridging cost, we need to reimburse // that as well. However, the `ReimbursementPool` issues refunds // based on gas spent. We need to convert msg.value accordingly // using the `_refundToGasSpent` function. @@ -552,7 +450,11 @@ contract L1BitcoinDepositor is /// @return Refund value as gas spent. /// @dev This function is the reverse of the logic used /// within `ReimbursementPool.refund`. - function _refundToGasSpent(uint256 refund) internal returns (uint256) { + function _refundToGasSpent(uint256 refund) + internal + virtual + returns (uint256) + { uint256 maxGasPrice = reimbursementPool.maxGasPrice(); uint256 staticGas = reimbursementPool.staticGas(); @@ -577,117 +479,9 @@ contract L1BitcoinDepositor is return gasSpent - staticGas; } - /// @notice Quotes the payment that must be attached to the `finalizeDeposit` - /// function call. The payment is necessary to cover the cost of - /// the Wormhole Relayer that is responsible for executing the - /// deposit finalization on the corresponding L2 chain. - /// @return cost The cost of the `finalizeDeposit` function call in WEI. - function quoteFinalizeDeposit() external view returns (uint256 cost) { - cost = _quoteFinalizeDeposit(wormhole.messageFee()); - } - - /// @notice Internal version of the `quoteFinalizeDeposit` function that - /// works with a custom Wormhole message fee. - /// @param messageFee Custom Wormhole message fee. - /// @return cost The cost of the `finalizeDeposit` function call in WEI. - /// @dev Implemented based on examples presented as part of the Wormhole SDK: - /// https://github.com/wormhole-foundation/hello-token/blob/8ec757248788dc12183f13627633e1d6fd1001bb/src/example-extensions/HelloTokenWithoutSDK.sol#L23 - function _quoteFinalizeDeposit(uint256 messageFee) - internal - view - returns (uint256 cost) - { - // Cost of delivering token and payload to `l2ChainId`. - (uint256 deliveryCost, ) = wormholeRelayer.quoteEVMDeliveryPrice( - l2ChainId, - 0, - l2FinalizeDepositGasLimit - ); - - // Total cost = delivery cost + cost of publishing the `sending token` - // Wormhole message. - cost = deliveryCost + messageFee; - } - - /// @notice Transfers ERC20 L1 TBTC to the L2 deposit owner using the Wormhole - /// protocol. The function initiates a Wormhole token transfer that - /// locks the ERC20 L1 TBTC within the Wormhole Token Bridge contract - /// and assigns Wormhole-wrapped L2 TBTC to the corresponding - /// `L2WormholeGateway` contract. Then, the function notifies the - /// `L2BitcoinDepositor` contract by sending a Wormhole message - /// containing the VAA of the Wormhole token transfer. The - /// `L2BitcoinDepositor` contract receives the Wormhole message, - /// and calls the `L2WormholeGateway` contract that redeems - /// Wormhole-wrapped L2 TBTC from the Wormhole Token Bridge and - /// uses it to mint canonical L2 TBTC to the L2 deposit owner address. - /// @param amount Amount of TBTC L1 ERC20 to transfer (1e18 precision). - /// @param l2Receiver Address of the L2 deposit owner. - /// @dev Requirements: - /// - The normalized amount (1e8 precision) must be greater than 0, - /// - The appropriate payment for the Wormhole Relayer must be - /// attached to the call (as calculated by `quoteFinalizeDeposit`). - /// @dev Implemented based on examples presented as part of the Wormhole SDK: - /// https://github.com/wormhole-foundation/hello-token/blob/8ec757248788dc12183f13627633e1d6fd1001bb/src/example-extensions/HelloTokenWithoutSDK.sol#L29 - function _transferTbtc(uint256 amount, bytes32 l2Receiver) internal { - // Wormhole supports the 1e8 precision at most. TBTC is 1e18 so - // the amount needs to be normalized. - amount = WormholeUtils.normalize(amount); - - require(amount > 0, "Amount too low to bridge"); - - // Cost of requesting a `finalizeDeposit` message to be sent to - // `l2ChainId` with a gasLimit of `l2FinalizeDepositGasLimit`. - uint256 wormholeMessageFee = wormhole.messageFee(); - uint256 cost = _quoteFinalizeDeposit(wormholeMessageFee); - - require(msg.value == cost, "Payment for Wormhole Relayer is too low"); - - // The Wormhole Token Bridge will pull the TBTC amount - // from this contract. We need to approve the transfer first. - tbtcToken.safeIncreaseAllowance(address(wormholeTokenBridge), amount); - - // Initiate a Wormhole token transfer that will lock L1 TBTC within - // the Wormhole Token Bridge contract and assign Wormhole-wrapped - // L2 TBTC to the corresponding `L2WormholeGateway` contract. - // slither-disable-next-line arbitrary-send-eth - uint64 transferSequence = wormholeTokenBridge.transferTokensWithPayload{ - value: wormholeMessageFee - }( - address(tbtcToken), - amount, - l2ChainId, - WormholeUtils.toWormholeAddress(l2WormholeGateway), - 0, // Nonce is a free field that is not relevant in this context. - abi.encode(l2Receiver) // Set the L2 receiver address as the transfer payload. - ); - - // Construct the VAA key corresponding to the above Wormhole token transfer. - WormholeTypes.VaaKey[] - memory additionalVaas = new WormholeTypes.VaaKey[](1); - additionalVaas[0] = WormholeTypes.VaaKey({ - chainId: wormhole.chainId(), - emitterAddress: WormholeUtils.toWormholeAddress( - address(wormholeTokenBridge) - ), - sequence: transferSequence - }); - - // The Wormhole token transfer initiated above must be finalized on - // the L2 chain. We achieve that by sending the transfer's VAA to the - // `L2BitcoinDepositor` contract. Once, the `L2BitcoinDepositor` - // contract receives it, it calls the `L2WormholeGateway` contract - // that redeems Wormhole-wrapped L2 TBTC from the Wormhole Token - // Bridge and use it to mint canonical L2 TBTC to the receiver address. - // slither-disable-next-line arbitrary-send-eth,unused-return - wormholeRelayer.sendVaasToEvm{value: cost - wormholeMessageFee}( - l2ChainId, - l2BitcoinDepositor, - bytes(""), // No payload needed. The L2 receiver address is already encoded in the Wormhole token transfer payload. - 0, // No receiver value needed. - l2FinalizeDepositGasLimit, - additionalVaas, - l2ChainId, // Set the L2 chain as the refund chain to avoid cross-chain refunds. - msg.sender // Set the caller as the refund receiver. - ); - } + /// @notice Generic function for bridging TBTC to L2. Overridden by child contracts. + /// @dev In child contracts, this can be LayerZero, Wormhole, or any bridging code. + /// @param amount Amount of TBTC in 1e18 precision. + /// @param l2Receiver L2 deposit owner (20 bytes zero‐padded to 32). + function _transferTbtc(uint256 amount, bytes32 l2Receiver) internal virtual; } diff --git a/solidity/contracts/l2/L2BitcoinDepositor.sol b/solidity/contracts/cross-chain/L2BitcoinDepositor.sol similarity index 98% rename from solidity/contracts/l2/L2BitcoinDepositor.sol rename to solidity/contracts/cross-chain/L2BitcoinDepositor.sol index 4ec53b05c..8e0871cbd 100644 --- a/solidity/contracts/l2/L2BitcoinDepositor.sol +++ b/solidity/contracts/cross-chain/L2BitcoinDepositor.sol @@ -13,12 +13,13 @@ // ▐████▌ ▐████▌ // ▐████▌ ▐████▌ -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../integrator/IBridge.sol"; -import "./Wormhole.sol"; +import "./wormhole/Wormhole.sol"; +import "./utils/Crosschain.sol"; /// @title IL2WormholeGateway /// @notice Interface to the `L2WormholeGateway` contract. @@ -173,7 +174,7 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { ); require( - WormholeUtils.fromWormholeAddress(sourceAddress) == + CrosschainUtils.bytes32ToAddress(sourceAddress) == l1BitcoinDepositor, "Source address is not the expected L1 Bitcoin depositor" ); diff --git a/solidity/contracts/l2/L2TBTC.sol b/solidity/contracts/cross-chain/L2TBTC.sol similarity index 98% rename from solidity/contracts/l2/L2TBTC.sol rename to solidity/contracts/cross-chain/L2TBTC.sol index 6f8c054e6..1afee41e3 100644 --- a/solidity/contracts/l2/L2TBTC.sol +++ b/solidity/contracts/cross-chain/L2TBTC.sol @@ -13,7 +13,7 @@ // ▐████▌ ▐████▌ // ▐████▌ ▐████▌ -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-ERC20PermitUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20BurnableUpgradeable.sol"; @@ -47,6 +47,7 @@ import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; /// and possibly performing other actions in the same transaction. /// The governance can recover ERC20 and ERC721 tokens sent mistakenly /// to L2TBTC token contract. +// slither-disable-next-line missing-inheritance contract L2TBTC is ERC20Upgradeable, ERC20BurnableUpgradeable, @@ -260,7 +261,7 @@ contract L2TBTC is /// caller's allowance. Emits a `Transfer` event with `to` set to /// the zero address. /// @dev Requirements: - /// - The che caller must have allowance for `accounts`'s tokens of at + /// - The caller must have allowance for `accounts`'s tokens of at /// least `amount`. /// - `account` must not be the zero address. /// - `account` must have at least `amount` tokens. diff --git a/solidity/contracts/cross-chain/layerzero/L1BitcoinDepositorLayerZero.sol b/solidity/contracts/cross-chain/layerzero/L1BitcoinDepositorLayerZero.sol new file mode 100644 index 000000000..c74a45981 --- /dev/null +++ b/solidity/contracts/cross-chain/layerzero/L1BitcoinDepositorLayerZero.sol @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {SafeERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import {IERC20MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; + +import {IOFT, SendParam, OFTReceipt} from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol"; +import {MessagingReceipt, MessagingFee} from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; + +import "../L1BitcoinDepositor.sol"; + +/// @title L1BitcoinDepositorLayerZero +/// @notice This contract is part of the direct bridging mechanism allowing +/// users to obtain ERC20 TBTC on supported L2 chains, without the need +/// to interact with the L1 tBTC ledger chain where minting occurs. +contract L1BitcoinDepositorLayerZero is L1BitcoinDepositor { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @notice tBTC `l1OFTAdapter` contract. + IOFT public l1OFTAdapter; + /// @notice LayerZero Destination Endpoint Id. + uint32 public destinationEndpointId; + + event TokensSent(MessagingReceipt msgReceipt, OFTReceipt oftReceipt); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @dev Initializes this contract. Must be called exactly once. + * @param _tbtcBridge Address of the tBTC Bridge on L1 + * @param _tbtcVault Address of the tBTC Vault on L1 + * @param _destinationEndpointId The LayerZero endpoint ID for the destination L2 + * @param _l1OFTAdapter The LayerZero OFT adapter on L1 that locks TBTC + */ + function initialize( + address _tbtcBridge, + address _tbtcVault, + uint32 _destinationEndpointId, + address _l1OFTAdapter + ) external initializer { + __L1BitcoinDepositor_initialize(_tbtcBridge, _tbtcVault); + __Ownable_init(); + + require( + _l1OFTAdapter != address(0), + "l1OFTAdapter address cannot be zero" + ); + l1OFTAdapter = IOFT(_l1OFTAdapter); + destinationEndpointId = _destinationEndpointId; + } + + /** + * @dev Given that this contract is set to receive any excess funds from LayerZero, this function + * Allows the owner to retrieve tokens from the contract and send to another wallet. + * If the token address is zero, it transfers the specified amount of native token to the given address. + * Otherwise, it transfers the specified amount of the given ERC20 token to the given address. + * @param _token The address of the token to retrieve. Use address(0) for native token. + * @param _to The address to which the tokens or native token will be sent. + * @param _amount The amount of tokens or native token to retrieve. + */ + function retrieveTokens( + address _token, + address _to, + uint256 _amount + ) external onlyOwner { + require( + _to != address(0), + "Cannot retrieve tokens to the zero address" + ); + + if (_token == address(0)) { + payable(_to).transfer(_amount); + } else { + IERC20Upgradeable(_token).safeTransfer(_to, _amount); + } + } + + /** + * @dev Transfers TBTC from L1 to L2 via the LayerZero OFTAdapter: + * + * 1. L1 TBTC is locked by the OFTAdapter on the L1 side. + * 2. A LayerZero message is sent across chains. + * 3. The corresponding L2 OFTAdapter receives the message, mints canonical + * TBTC, and delivers it to the final user (the `l2Receiver` address). + * + * @param amount Amount of TBTC in 1e18 precision + * @param l2Receiver L2 user’s address (padded to 32 bytes) + */ + // slither-disable-next-line arbitrary-send-eth + function _transferTbtc(uint256 amount, bytes32 l2Receiver) + internal + override + { + // Calculate the minimum amount without dust that the user should receive on L2. + uint8 tbtcDecimals = IERC20MetadataUpgradeable(address(tbtcToken)) + .decimals(); + uint256 minimumAmount = _calculateMinimumAmount(amount, tbtcDecimals); + + require(minimumAmount > 0, "minimumAmount too low to bridge"); + require(amount > 0, "Amount too low to bridge"); + + SendParam memory sendParam = SendParam({ + dstEid: destinationEndpointId, + to: l2Receiver, + amountLD: amount, + minAmountLD: minimumAmount, + extraOptions: bytes(""), + composeMsg: bytes(""), + oftCmd: bytes("") + }); + + // The second parameter is `_payInLzToken` which indicates whether we want to pay + // the bridging fee using LayerZero's ZRO token. Here it's set to `false` + // because we're paying the fee in the native chain currency. + MessagingFee memory msgFee = l1OFTAdapter.quoteSend(sendParam, false); + + require( + msg.value == msgFee.nativeFee, + "Payment for ZeroLayer is too low" + ); + + // The LayerZero Token Bridge will pull the TBTC amount + // from this contract. We need to approve the transfer first. + tbtcToken.safeIncreaseAllowance(address(l1OFTAdapter), amount); + + // Initiate a LayerZero token transfer that will mint L2 TBTC and + // send it to the user. + // solhint-disable-next-line check-send-result + (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) = l1OFTAdapter + .send{value: msgFee.nativeFee}( + sendParam, + msgFee, + address(this) // refundable address + ); + + // slither-disable-next-line reentrancy-events + emit TokensSent(msgReceipt, oftReceipt); + } + + /** + * @dev Retrieves the shared decimals of the OFT. + * @return The shared decimals of the OFT. + * + * @dev Sets an implicit cap on the amount of tokens, over uint64.max() will need some sort of outbound cap / totalSupply cap + * Lowest common decimal denominator between chains. + * Defaults to 6 decimal places to provide up to 18,446,744,073,709.551615 units (max uint64). + * For tokens exceeding this totalSupply(), they will need to override the sharedDecimals function with something smaller. + * ie. 4 sharedDecimals would be 1,844,674,407,370,955.1615 + */ + function _sharedDecimals() internal pure returns (uint8) { + return 6; + } + + /** + * @dev Calculates the minimal “no‐dust” bridging amount by removing + * any precision beyond the `_sharedDecimals()` from `_amount`. + * @param _amount Amount of TBTC in local decimals (e.g. 1e18). + * @param _localDecimals The local (L1) TBTC decimal precision (e.g. 18). + * @return The minimal amount eligible for bridging, after removing dust. + */ + function _calculateMinimumAmount(uint256 _amount, uint8 _localDecimals) + internal + pure + returns (uint256) + { + uint8 sharedDecimals = _sharedDecimals(); + + require(_localDecimals > sharedDecimals, "localDecimals too low"); + uint256 decimalConversionRate = 10**(_localDecimals - sharedDecimals); + + return _amount - (_amount % decimalConversionRate); + } +} diff --git a/solidity/contracts/cross-chain/layerzero/L1LockBoxAdapter.sol b/solidity/contracts/cross-chain/layerzero/L1LockBoxAdapter.sol new file mode 100644 index 000000000..fb5eb6d71 --- /dev/null +++ b/solidity/contracts/cross-chain/layerzero/L1LockBoxAdapter.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import {OFTAdapterUpgradeable} from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTAdapterUpgradeable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice L1LockBoxAdapter uses a deployed ERC-20 token and safeERC20 to interact with the OFTCore contract. +contract L1LockBoxAdapter is OFTAdapterUpgradeable { + using SafeERC20 for IERC20; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address _token, address _lzEndpoint) + OFTAdapterUpgradeable(_token, _lzEndpoint) + { + _disableInitializers(); + } + + /** + * @dev Initializes the L1LockBoxAdapter with the provided owner and locking limits. + * @param _owner The owner/delegate of the contract/OFTAdapter. + */ + function initialize(address _owner) external initializer { + __OFTAdapter_init(_owner); + __Ownable_init(); + } + + /** + * @dev Allows the owner to retrieve tokens from the contract. + * If the token address is zero, it transfers the specified amount of native token to the given address. + * Otherwise, it transfers the specified amount of the given ERC20 token to the given address. + * The function ensures that innerToken tokens cannot be retrieved. + * @param _token The address of the token to retrieve. Use address(0) for native token. + * @param _to The address to which the tokens or native token will be sent. + * @param _amount The amount of tokens or native token to retrieve. + */ + function retrieveTokens( + address _token, + address _to, + uint256 _amount + ) external onlyOwner { + require( + _to != address(0), + "Cannot retrieve tokens to the zero address" + ); + + if (_token == address(0)) { + payable(_to).transfer(_amount); + } else { + require(_token != address(innerToken), "Token is innerToken"); + IERC20(_token).safeTransfer(_to, _amount); + } + } + + /** + * @dev Helper function to convert address to Bytes32 for peer setup. + * @param _address The address needed to be converted. + * @return The converted address. + */ + function addressToBytes32(address _address) public pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } +} diff --git a/solidity/contracts/cross-chain/layerzero/L2MintAndBurnAdapter.sol b/solidity/contracts/cross-chain/layerzero/L2MintAndBurnAdapter.sol new file mode 100644 index 000000000..918a7625f --- /dev/null +++ b/solidity/contracts/cross-chain/layerzero/L2MintAndBurnAdapter.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import {OFTAdapterUpgradeable} from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTAdapterUpgradeable.sol"; +import {IERC20Metadata, IERC20} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {IL2TBTC} from "../utils/IL2TBTC.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/// @notice L2MintAndBurnAdapter uses a deployed ERC-20 token and safeERC20 to interact with the OFTCore contract. +contract L2MintAndBurnAdapter is OFTAdapterUpgradeable { + using SafeERC20 for IERC20; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address _token, address _lzEndpoint) + OFTAdapterUpgradeable(_token, _lzEndpoint) + { + _disableInitializers(); + } + + /** + * @dev Initializes the L2MintAndBurnAdapter with the provided owner and locking limits. + * @param _delegate The delegate of the contract/OFTAdapter. + */ + function initialize(address _delegate) external initializer { + __OFTAdapter_init(_delegate); + __Ownable_init(); + } + + /** + * @notice Indicates whether the OFT contract requires approval of the 'token()' to send. + * @return requiresApproval Needs approval of the underlying token implementation. + * + * @dev In the case of default OFTAdapter, approval is required. + * @dev In non-default OFTAdapter contracts with something like mint and burn privileges, it would NOT need approval. + */ + function approvalRequired() external pure override returns (bool) { + return false; + } + + /** + * @dev Helper function to convert address to Bytes32 for peer setup. + * @param _address The address needed to be converted. + * @return The converted address. + */ + function addressToBytes32(address _address) public pure returns (bytes32) { + return bytes32(uint256(uint160(_address))); + } + + /** + * @dev Allows the owner to retrieve tokens from the contract. + * If the token address is zero, it transfers the specified amount of native token to the given address. + * Otherwise, it transfers the specified amount of the given ERC20 token to the given address. + * The function ensures that innerToken tokens cannot be retrieved. + * @param _token The address of the token to retrieve. Use address(0) for native token. + * @param _to The address to which the tokens or native token will be sent. + * @param _amount The amount of tokens or native token to retrieve. + */ + function retrieveTokens( + address _token, + address _to, + uint256 _amount + ) external onlyOwner { + require( + _to != address(0), + "Cannot retrieve tokens to the zero address" + ); + + if (_token == address(0)) { + payable(_to).transfer(_amount); + } else { + require(_token != address(innerToken), "Token is innerToken"); + IERC20(_token).safeTransfer(_to, _amount); + } + } + + /** + * @dev Burns tokens from the sender's specified balance. + * @param _from The address to debit the tokens from. + * @param _amountLD The amount of tokens to send in local decimals. + * @param _minAmountLD The minimum amount to send in local decimals. + * @param _dstEid The destination chain ID. + * @return amountSentLD The amount sent in local decimals. + * @return amountReceivedLD The amount received in local decimals on the remote. + * @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out. + * IF the 'innerToken' applies something like a transfer fee, the default will NOT work... + * a pre/post balance check will need to be done to calculate the amountReceivedLD. + */ + function _debit( + address _from, + uint256 _amountLD, + uint256 _minAmountLD, + uint32 _dstEid + ) + internal + override + returns (uint256 amountSentLD, uint256 amountReceivedLD) + { + (amountSentLD, amountReceivedLD) = _debitView( + _amountLD, + _minAmountLD, + _dstEid + ); + /// The caller must have allowance for `accounts`'s tokens of at + /// least `amount`. + IL2TBTC(address(innerToken)).burnFrom(_from, amountSentLD); + } + + /** + * @dev Credits tokens to the specified address. + * @param _to The address to credit the tokens to. + * @param _amountLD The amount of tokens to credit in local decimals. + * @dev _srcEid The source chain ID. + * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals. + * + * @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, ie. 1 token in, 1 token out. + * IF the 'innerToken' applies something like a transfer fee, the default will NOT work... + * a pre/post balance check will need to be done to calculate the amountReceivedLD. + */ + function _credit( + address _to, + uint256 _amountLD, + uint32 /*_srcEid*/ + ) internal override returns (uint256 amountReceivedLD) { + // @dev Mints the tokens and transfers to the recipient. + IL2TBTC(address(innerToken)).mint(_to, _amountLD); + // @dev In the case of NON-default OFTAdapter, the amountLD MIGHT not be == amountReceivedLD. + return _amountLD; + } +} diff --git a/solidity/contracts/cross-chain/utils/Crosschain.sol b/solidity/contracts/cross-chain/utils/Crosschain.sol new file mode 100644 index 000000000..a3df89496 --- /dev/null +++ b/solidity/contracts/cross-chain/utils/Crosschain.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.17; + +/// @title CrosschainUtils +/// @notice Library for LayerZero utilities. +library CrosschainUtils { + /** + * @dev Helper function to convert address to Bytes32 for peer setup. + * @param _address The address needed to be converted. + * @return The converted address. + */ + function addressToBytes32(address _address) + internal + pure + returns (bytes32) + { + return bytes32(uint256(uint160(_address))); + } + + /** + * @dev Helper function to convert Bytes32 to address for peer setup. + * @param _address The address needed to be converted. + * @return The converted address. + */ + function bytes32ToAddress(bytes32 _address) + internal + pure + returns (address) + { + return address(uint160(uint256(_address))); + } +} diff --git a/solidity/contracts/cross-chain/utils/IL2TBTC.sol b/solidity/contracts/cross-chain/utils/IL2TBTC.sol new file mode 100644 index 000000000..315677748 --- /dev/null +++ b/solidity/contracts/cross-chain/utils/IL2TBTC.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/draft-IERC20PermitUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; + +/** + * @title IL2TBTC + * @notice Interface for interacting with the L2TBTC token contract. + * + * Exposes all public or external functions and state variables + * from the `L2TBTC` implementation in an interface form. + */ +interface IL2TBTC is IERC20Upgradeable, IERC20PermitUpgradeable { + //------------------------------------------------------------------------- + // Mint/Burn + //------------------------------------------------------------------------- + + /// @notice Mints `amount` tokens to `account` (only callable by a minter). + function mint(address account, uint256 amount) external; + + /// @notice Burns `amount` tokens from `account` using the caller’s allowance. + function burnFrom(address account, uint256 amount) external; +} diff --git a/solidity/contracts/cross-chain/utils/Reimbursable.sol b/solidity/contracts/cross-chain/utils/Reimbursable.sol new file mode 100644 index 000000000..cda100d22 --- /dev/null +++ b/solidity/contracts/cross-chain/utils/Reimbursable.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import "./ReimbursementPool.sol"; + +abstract contract Reimbursable { + // The variable should be initialized by the implementing contract. + // slither-disable-next-line uninitialized-state + ReimbursementPool public reimbursementPool; + + // Reserved storage space in case we need to add more variables, + // since there are upgradeable contracts that inherit from this one. + // See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + // slither-disable-next-line unused-state + uint256[49] private __gap; + + event ReimbursementPoolUpdated(address newReimbursementPool); + + modifier refundable(address receiver) { + uint256 gasStart = gasleft(); + _; + reimbursementPool.refund(gasStart - gasleft(), receiver); + } + + modifier onlyReimbursableAdmin() virtual { + _; + } + + function updateReimbursementPool(ReimbursementPool _reimbursementPool) + external + onlyReimbursableAdmin + { + emit ReimbursementPoolUpdated(address(_reimbursementPool)); + + reimbursementPool = _reimbursementPool; + } +} diff --git a/solidity/contracts/cross-chain/utils/ReimbursementPool.sol b/solidity/contracts/cross-chain/utils/ReimbursementPool.sol new file mode 100644 index 000000000..43df9ff41 --- /dev/null +++ b/solidity/contracts/cross-chain/utils/ReimbursementPool.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +contract ReimbursementPool is Ownable, ReentrancyGuard { + /// @notice Authorized contracts that can interact with the reimbursment pool. + /// Authorization can be granted and removed by the owner. + mapping(address => bool) public isAuthorized; + + /// @notice Static gas includes: + /// - cost of the refund function + /// - base transaction cost + uint256 public staticGas; + + /// @notice Max gas price used to reimburse a transaction submitter. Protects + /// against malicious operator-miners. + uint256 public maxGasPrice; + + event StaticGasUpdated(uint256 newStaticGas); + + event MaxGasPriceUpdated(uint256 newMaxGasPrice); + + event SendingEtherFailed(uint256 refundAmount, address receiver); + + event AuthorizedContract(address thirdPartyContract); + + event UnauthorizedContract(address thirdPartyContract); + + event FundsWithdrawn(uint256 withdrawnAmount, address receiver); + + constructor(uint256 _staticGas, uint256 _maxGasPrice) { + staticGas = _staticGas; + maxGasPrice = _maxGasPrice; + } + + /// @notice Receive ETH + receive() external payable {} + + /// @notice Refunds ETH to a spender for executing specific transactions. + /// @dev Ignoring the result of sending ETH to a receiver is made on purpose. + /// For EOA receiving ETH should always work. If a receiver is a smart + /// contract, then we do not want to fail a transaction, because in some + /// cases the refund is done at the very end of multiple calls where all + /// the previous calls were already paid off. It is a receiver's smart + /// contract resposibility to make sure it can receive ETH. + /// @dev Only authorized contracts are allowed calling this function. + /// @param gasSpent Gas spent on a transaction that needs to be reimbursed. + /// @param receiver Address where the reimbursment is sent. + function refund(uint256 gasSpent, address receiver) external nonReentrant { + require( + isAuthorized[msg.sender], + "Contract is not authorized for a refund" + ); + require(receiver != address(0), "Receiver's address cannot be zero"); + + uint256 gasPrice = tx.gasprice < maxGasPrice + ? tx.gasprice + : maxGasPrice; + + uint256 refundAmount = (gasSpent + staticGas) * gasPrice; + + /* solhint-disable avoid-low-level-calls */ + // slither-disable-next-line low-level-calls,unchecked-lowlevel + (bool sent, ) = receiver.call{value: refundAmount}(""); + /* solhint-enable avoid-low-level-calls */ + if (!sent) { + // slither-disable-next-line reentrancy-events + emit SendingEtherFailed(refundAmount, receiver); + } + } + + /// @notice Authorize a contract that can interact with this reimbursment pool. + /// Can be authorized by the owner only. + /// @param _contract Authorized contract. + function authorize(address _contract) external onlyOwner { + isAuthorized[_contract] = true; + + emit AuthorizedContract(_contract); + } + + /// @notice Unauthorize a contract that was previously authorized to interact + /// with this reimbursment pool. Can be unauthorized by the + /// owner only. + /// @param _contract Authorized contract. + function unauthorize(address _contract) external onlyOwner { + delete isAuthorized[_contract]; + + emit UnauthorizedContract(_contract); + } + + /// @notice Setting a static gas cost for executing a transaction. Can be set + /// by the owner only. + /// @param _staticGas Static gas cost. + function setStaticGas(uint256 _staticGas) external onlyOwner { + staticGas = _staticGas; + + emit StaticGasUpdated(_staticGas); + } + + /// @notice Setting a max gas price for transactions. Can be set by the + /// owner only. + /// @param _maxGasPrice Max gas price used to reimburse tx submitters. + function setMaxGasPrice(uint256 _maxGasPrice) external onlyOwner { + maxGasPrice = _maxGasPrice; + + emit MaxGasPriceUpdated(_maxGasPrice); + } + + /// @notice Withdraws all ETH from this pool which are sent to a given + /// address. Can be set by the owner only. + /// @param receiver An address where ETH is sent. + function withdrawAll(address receiver) external onlyOwner { + withdraw(address(this).balance, receiver); + } + + /// @notice Withdraws ETH amount from this pool which are sent to a given + /// address. Can be set by the owner only. + /// @param amount Amount to withdraw from the pool. + /// @param receiver An address where ETH is sent. + function withdraw(uint256 amount, address receiver) public onlyOwner { + require( + address(this).balance >= amount, + "Insufficient contract balance" + ); + require(receiver != address(0), "Receiver's address cannot be zero"); + + emit FundsWithdrawn(amount, receiver); + + /* solhint-disable avoid-low-level-calls */ + // slither-disable-next-line low-level-calls,arbitrary-send + (bool sent, ) = receiver.call{value: amount}(""); + /* solhint-enable avoid-low-level-calls */ + require(sent, "Failed to send Ether"); + } +} diff --git a/solidity/contracts/cross-chain/wormhole/L1BitcoinDepositorWormhole.sol b/solidity/contracts/cross-chain/wormhole/L1BitcoinDepositorWormhole.sol new file mode 100644 index 000000000..50c03f4e5 --- /dev/null +++ b/solidity/contracts/cross-chain/wormhole/L1BitcoinDepositorWormhole.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import "./Wormhole.sol"; +import "../L1BitcoinDepositor.sol"; +import "../utils/Crosschain.sol"; + +/// @title L1BitcoinDepositorWormhole +/// @notice This contract is part of the direct bridging mechanism allowing +/// users to obtain ERC20 TBTC on supported L2 chains, without the need +/// to interact with the L1 tBTC ledger chain where minting occurs. +contract L1BitcoinDepositorWormhole is L1BitcoinDepositor { + using SafeERC20Upgradeable for IERC20Upgradeable; + + /// @notice `Wormhole` core contract on L1. + IWormhole public wormhole; + /// @notice `WormholeRelayer` contract on L1. + IWormholeRelayer public wormholeRelayer; + /// @notice Wormhole `TokenBridge` contract on L1. + IWormholeTokenBridge public wormholeTokenBridge; + /// @notice tBTC `L2WormholeGateway` contract on the corresponding L2 chain. + address public l2WormholeGateway; + /// @notice Wormhole chain ID of the corresponding L2 chain. + uint16 public l2ChainId; + /// @notice tBTC `L2BitcoinDepositor` contract on the corresponding L2 chain. + address public l2BitcoinDepositor; + /// @notice Gas limit necessary to execute the L2 part of the deposit + /// finalization. This value is used to calculate the payment for + /// the Wormhole Relayer that is responsible to execute the + /// deposit finalization on the corresponding L2 chain. Can be + /// updated by the owner. + uint256 public l2FinalizeDepositGasLimit; + + event L2FinalizeDepositGasLimitUpdated(uint256 l2FinalizeDepositGasLimit); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _tbtcBridge, + address _tbtcVault, + address _wormhole, + address _wormholeRelayer, + address _wormholeTokenBridge, + address _l2WormholeGateway, + uint16 _l2ChainId + ) external initializer { + __L1BitcoinDepositor_initialize(_tbtcBridge, _tbtcVault); + __Ownable_init(); + + require(_wormhole != address(0), "Wormhole address cannot be zero"); + require( + _wormholeRelayer != address(0), + "WormholeRelayer address cannot be zero" + ); + require( + _wormholeTokenBridge != address(0), + "WormholeTokenBridge address cannot be zero" + ); + require( + _l2WormholeGateway != address(0), + "L2WormholeGateway address cannot be zero" + ); + + wormhole = IWormhole(_wormhole); + wormholeRelayer = IWormholeRelayer(_wormholeRelayer); + wormholeTokenBridge = IWormholeTokenBridge(_wormholeTokenBridge); + // slither-disable-next-line missing-zero-check + l2WormholeGateway = _l2WormholeGateway; + l2ChainId = _l2ChainId; + l2FinalizeDepositGasLimit = 500_000; + } + + /// @notice Sets the address of the `L2BitcoinDepositor` contract on the + /// corresponding L2 chain. This function solves the chicken-and-egg + /// problem of setting the `L2BitcoinDepositor` contract address + /// on the `L1BitcoinDepositor` contract and vice versa. + /// @param _l2BitcoinDepositor Address of the `L2BitcoinDepositor` contract. + /// @dev Requirements: + /// - Can be called only by the contract owner, + /// - The address must not be set yet, + /// - The new address must not be 0x0. + function attachL2BitcoinDepositor(address _l2BitcoinDepositor) + external + onlyOwner + { + require( + l2BitcoinDepositor == address(0), + "L2 Bitcoin Depositor already set" + ); + require( + _l2BitcoinDepositor != address(0), + "L2 Bitcoin Depositor must not be 0x0" + ); + l2BitcoinDepositor = _l2BitcoinDepositor; + } + + /// @notice Updates the gas limit necessary to execute the L2 part of the + /// deposit finalization. + /// @param _l2FinalizeDepositGasLimit New gas limit. + /// @dev Requirements: + /// - Can be called only by the contract owner. + function updateL2FinalizeDepositGasLimit(uint256 _l2FinalizeDepositGasLimit) + external + onlyOwner + { + l2FinalizeDepositGasLimit = _l2FinalizeDepositGasLimit; + emit L2FinalizeDepositGasLimitUpdated(_l2FinalizeDepositGasLimit); + } + + /// @notice Quotes the payment that must be attached to the `finalizeDeposit` + /// function call. The payment is necessary to cover the cost of + /// the Wormhole Relayer that is responsible for executing the + /// deposit finalization on the corresponding L2 chain. + /// @return cost The cost of the `finalizeDeposit` function call in WEI. + function quoteFinalizeDeposit() external view returns (uint256 cost) { + cost = _quoteFinalizeDeposit(wormhole.messageFee()); + } + + /// @notice Internal version of the `quoteFinalizeDeposit` function that + /// works with a custom Wormhole message fee. + /// @param messageFee Custom Wormhole message fee. + /// @return cost The cost of the `finalizeDeposit` function call in WEI. + /// @dev Implemented based on examples presented as part of the Wormhole SDK: + /// https://github.com/wormhole-foundation/hello-token/blob/8ec757248788dc12183f13627633e1d6fd1001bb/src/example-extensions/HelloTokenWithoutSDK.sol#L23 + function _quoteFinalizeDeposit(uint256 messageFee) + internal + view + returns (uint256 cost) + { + // Cost of delivering token and payload to `l2ChainId`. + (uint256 deliveryCost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + l2ChainId, + 0, + l2FinalizeDepositGasLimit + ); + + // Total cost = delivery cost + cost of publishing the `sending token` + // Wormhole message. + cost = deliveryCost + messageFee; + } + + /// @notice Transfers ERC20 L1 TBTC to the L2 deposit owner using the Wormhole + /// protocol. The function initiates a Wormhole token transfer that + /// locks the ERC20 L1 TBTC within the Wormhole Token Bridge contract + /// and assigns Wormhole-wrapped L2 TBTC to the corresponding + /// `L2WormholeGateway` contract. Then, the function notifies the + /// `L2BitcoinDepositor` contract by sending a Wormhole message + /// containing the VAA of the Wormhole token transfer. The + /// `L2BitcoinDepositor` contract receives the Wormhole message, + /// and calls the `L2WormholeGateway` contract that redeems + /// Wormhole-wrapped L2 TBTC from the Wormhole Token Bridge and + /// uses it to mint canonical L2 TBTC to the L2 deposit owner address. + /// @param amount Amount of TBTC L1 ERC20 to transfer (1e18 precision). + /// @param l2Receiver Address of the L2 deposit owner. + /// @dev Requirements: + /// - The normalized amount (1e8 precision) must be greater than 0, + /// - The appropriate payment for the Wormhole Relayer must be + /// attached to the call (as calculated by `quoteFinalizeDeposit`). + /// @dev Implemented based on examples presented as part of the Wormhole SDK: + /// https://github.com/wormhole-foundation/hello-token/blob/8ec757248788dc12183f13627633e1d6fd1001bb/src/example-extensions/HelloTokenWithoutSDK.sol#L29 + function _transferTbtc(uint256 amount, bytes32 l2Receiver) + internal + override + { + // Wormhole supports the 1e8 precision at most. TBTC is 1e18 so + // the amount needs to be normalized. + amount = WormholeUtils.normalize(amount); + + require(amount > 0, "Amount too low to bridge"); + + // Cost of requesting a `finalizeDeposit` message to be sent to + // `l2ChainId` with a gasLimit of `l2FinalizeDepositGasLimit`. + uint256 wormholeMessageFee = wormhole.messageFee(); + uint256 cost = _quoteFinalizeDeposit(wormholeMessageFee); + + require(msg.value == cost, "Payment for Wormhole Relayer is too low"); + + // The Wormhole Token Bridge will pull the TBTC amount + // from this contract. We need to approve the transfer first. + tbtcToken.safeIncreaseAllowance(address(wormholeTokenBridge), amount); + + // Initiate a Wormhole token transfer that will lock L1 TBTC within + // the Wormhole Token Bridge contract and assign Wormhole-wrapped + // L2 TBTC to the corresponding `L2WormholeGateway` contract. + // slither-disable-next-line arbitrary-send-eth + uint64 transferSequence = wormholeTokenBridge.transferTokensWithPayload{ + value: wormholeMessageFee + }( + address(tbtcToken), + amount, + l2ChainId, + CrosschainUtils.addressToBytes32(l2WormholeGateway), + 0, // Nonce is a free field that is not relevant in this context. + abi.encode(l2Receiver) // Set the L2 receiver address as the transfer payload. + ); + + // Construct the VAA key corresponding to the above Wormhole token transfer. + WormholeTypes.VaaKey[] + memory additionalVaas = new WormholeTypes.VaaKey[](1); + additionalVaas[0] = WormholeTypes.VaaKey({ + chainId: wormhole.chainId(), + emitterAddress: CrosschainUtils.addressToBytes32( + address(wormholeTokenBridge) + ), + sequence: transferSequence + }); + + // The Wormhole token transfer initiated above must be finalized on + // the L2 chain. We achieve that by sending the transfer's VAA to the + // `L2BitcoinDepositor` contract. Once, the `L2BitcoinDepositor` + // contract receives it, it calls the `L2WormholeGateway` contract + // that redeems Wormhole-wrapped L2 TBTC from the Wormhole Token + // Bridge and use it to mint canonical L2 TBTC to the receiver address. + // slither-disable-next-line arbitrary-send-eth,unused-return + wormholeRelayer.sendVaasToEvm{value: cost - wormholeMessageFee}( + l2ChainId, + l2BitcoinDepositor, + bytes(""), // No payload needed. The L2 receiver address is already encoded in the Wormhole token transfer payload. + 0, // No receiver value needed. + l2FinalizeDepositGasLimit, + additionalVaas, + l2ChainId, // Set the L2 chain as the refund chain to avoid cross-chain refunds. + msg.sender // Set the caller as the refund receiver. + ); + } +} diff --git a/solidity/contracts/l2/L2WormholeGateway.sol b/solidity/contracts/cross-chain/wormhole/L2WormholeGateway.sol similarity index 97% rename from solidity/contracts/l2/L2WormholeGateway.sol rename to solidity/contracts/cross-chain/wormhole/L2WormholeGateway.sol index 148ddbc74..d1418a5c3 100644 --- a/solidity/contracts/l2/L2WormholeGateway.sol +++ b/solidity/contracts/cross-chain/wormhole/L2WormholeGateway.sol @@ -13,7 +13,7 @@ // ▐████▌ ▐████▌ // ▐████▌ ▐████▌ -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; @@ -21,7 +21,8 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeab import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; import "./Wormhole.sol"; -import "./L2TBTC.sol"; +import "../L2TBTC.sol"; +import "../utils/Crosschain.sol"; /// @title L2WormholeGateway /// @notice Selected cross-ecosystem bridges are given the minting authority for @@ -319,9 +320,9 @@ contract L2WormholeGateway is function toWormholeAddress(address _address) external pure - returns (bytes32) + returns (bytes32 wormholeAddress) { - return WormholeUtils.toWormholeAddress(_address); + wormholeAddress = CrosschainUtils.addressToBytes32(_address); } /// @notice Converts Wormhole address into Ethereum format. @@ -329,8 +330,8 @@ contract L2WormholeGateway is function fromWormholeAddress(bytes32 _address) public pure - returns (address) + returns (address ethereumAddress) { - return WormholeUtils.fromWormholeAddress(_address); + ethereumAddress = CrosschainUtils.bytes32ToAddress(_address); } } diff --git a/solidity/contracts/l2/Wormhole.sol b/solidity/contracts/cross-chain/wormhole/Wormhole.sol similarity index 90% rename from solidity/contracts/l2/Wormhole.sol rename to solidity/contracts/cross-chain/wormhole/Wormhole.sol index 2721d6a44..63ce01deb 100644 --- a/solidity/contracts/l2/Wormhole.sol +++ b/solidity/contracts/cross-chain/wormhole/Wormhole.sol @@ -13,7 +13,7 @@ // ▐████▌ ▐████▌ // ▐████▌ ▐████▌ -pragma solidity ^0.8.17; +pragma solidity ^0.8.20; /// @title WormholeTypes /// @notice Namespace which groups all types relevant to Wormhole interfaces. @@ -127,26 +127,6 @@ interface IWormholeTokenBridge { /// @title WormholeUtils /// @notice Library for Wormhole utilities. library WormholeUtils { - /// @notice Converts Ethereum address into Wormhole format. - /// @param _address The address to convert. - function toWormholeAddress(address _address) - internal - pure - returns (bytes32) - { - return bytes32(uint256(uint160(_address))); - } - - /// @notice Converts Wormhole address into Ethereum format. - /// @param _address The address to convert. - function fromWormholeAddress(bytes32 _address) - internal - pure - returns (address) - { - return address(uint160(uint256(_address))); - } - /// @dev Eliminates the dust that cannot be bridged with Wormhole /// due to the decimal shift in the Wormhole Bridge contract. /// See https://github.com/wormhole-foundation/wormhole/blob/96682bdbeb7c87bfa110eade0554b3d8cbf788d2/ethereum/contracts/bridge/Bridge.sol#L276-L288 diff --git a/solidity/contracts/test/ReimbursableImplStub.sol b/solidity/contracts/test/ReimbursableImplStub.sol new file mode 100644 index 000000000..969d735cc --- /dev/null +++ b/solidity/contracts/test/ReimbursableImplStub.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.6; + +import {Reimbursable} from "../cross-chain/utils/Reimbursable.sol"; + +contract ReimbursableImplStub is Reimbursable { + address public admin; + + constructor(address _admin) { + admin = _admin; + } + + modifier onlyReimbursableAdmin() override { + require(admin == msg.sender, "Caller is not the admin"); + _; + } +} diff --git a/solidity/contracts/test/TestERC20.sol b/solidity/contracts/test/TestERC20.sol index 0202ac8b1..0a2b6f55f 100644 --- a/solidity/contracts/test/TestERC20.sol +++ b/solidity/contracts/test/TestERC20.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "@thesis/solidity-contracts/contracts/token/ERC20WithPermit.sol"; diff --git a/solidity/contracts/test/WormholeBridgeStub.sol b/solidity/contracts/test/WormholeBridgeStub.sol index 7afe9413c..55fbd201e 100644 --- a/solidity/contracts/test/WormholeBridgeStub.sol +++ b/solidity/contracts/test/WormholeBridgeStub.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity 0.8.17; +pragma solidity ^0.8.20; import "./TestERC20.sol"; -import "../l2/Wormhole.sol"; +import "../cross-chain/wormhole/Wormhole.sol"; /// @dev Stub contract used in L2WormholeGateway unit tests. /// Stub contract is used instead of a smock because of the token transfer diff --git a/solidity/hardhat.config.ts b/solidity/hardhat.config.ts index 2eef2dcf2..7d836883f 100644 --- a/solidity/hardhat.config.ts +++ b/solidity/hardhat.config.ts @@ -62,6 +62,15 @@ const config: HardhatUserConfig = { }, }, }, + { + version: "0.8.20", + settings: { + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, ], overrides: { "@keep-network/ecdsa/contracts/WalletRegistry.sol": diff --git a/solidity/package.json b/solidity/package.json index 781cd2806..b99f4372b 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -34,6 +34,11 @@ "@keep-network/ecdsa": "development", "@keep-network/random-beacon": "development", "@keep-network/tbtc": "development", + "@layerzerolabs/lz-evm-protocol-v2": "^3.0.71", + "@layerzerolabs/oapp-evm": "^0.3.1", + "@layerzerolabs/oapp-evm-upgradeable": "^0.1.1", + "@layerzerolabs/oft-evm": "^3.1.2", + "@layerzerolabs/oft-evm-upgradeable": "^3.0.1", "@openzeppelin/contracts": "^4.8.1", "@openzeppelin/contracts-upgradeable": "^4.8.1", "@thesis/solidity-contracts": "github:thesis/solidity-contracts#4985bcf" @@ -58,7 +63,7 @@ "eslint": "^7.32.0", "ethereum-waffle": "^3.4.0", "ethers": "^5.5.3", - "hardhat": "^2.10.0", + "hardhat": "^2.11.0", "hardhat-contract-sizer": "^2.5.0", "hardhat-dependency-compiler": "^1.1.2", "hardhat-deploy": "^0.11.11", diff --git a/solidity/test/Reimbursable.test.ts b/solidity/test/Reimbursable.test.ts new file mode 100644 index 000000000..e112ef98f --- /dev/null +++ b/solidity/test/Reimbursable.test.ts @@ -0,0 +1,74 @@ +import { ethers, helpers } from "hardhat" +import { expect } from "chai" + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" +import type { ReimbursableImplStub } from "../typechain" + +describe("Reimbursable", () => { + let reimbursableImplStub: ReimbursableImplStub + let deployer: SignerWithAddress + let admin: SignerWithAddress + let thirdParty: SignerWithAddress + let contractToUpdate: SignerWithAddress + + before(async () => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;({ deployer } = await helpers.signers.getNamedSigners()) + + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;[admin, thirdParty, contractToUpdate] = + await helpers.signers.getUnnamedSigners() + + const ReimbursableImplStub = await ethers.getContractFactory( + "ReimbursableImplStub", + deployer + ) + reimbursableImplStub = (await ReimbursableImplStub.deploy( + admin.address + )) as ReimbursableImplStub + }) + + describe("updateReimbursementPool", () => { + context("when a caller is the deployer", () => { + it("should revert", async () => { + await expect( + reimbursableImplStub + .connect(deployer) + .updateReimbursementPool(contractToUpdate.address) + ).to.be.revertedWith("Caller is not the admin") + }) + }) + + context("when a caller is not the admin", () => { + it("should revert", async () => { + await expect( + reimbursableImplStub + .connect(thirdParty) + .updateReimbursementPool(contractToUpdate.address) + ).to.be.revertedWith("Caller is not the admin") + }) + }) + + context("when a caller is the admin", () => { + it("should update a reimbursement contract", async () => { + await reimbursableImplStub + .connect(admin) + .updateReimbursementPool(contractToUpdate.address) + + expect(await reimbursableImplStub.reimbursementPool()).to.be.equal( + contractToUpdate.address + ) + }) + + it("should emit ReimbursementPoolUpdated event", async () => { + await expect( + reimbursableImplStub + .connect(admin) + .updateReimbursementPool(contractToUpdate.address) + ) + .to.emit(reimbursableImplStub, "ReimbursementPoolUpdated") + .withArgs(contractToUpdate.address) + }) + }) + }) +}) diff --git a/solidity/test/ReimbursementPool.test.ts b/solidity/test/ReimbursementPool.test.ts new file mode 100644 index 000000000..82400313a --- /dev/null +++ b/solidity/test/ReimbursementPool.test.ts @@ -0,0 +1,511 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import { ethers, waffle, helpers, deployments } from "hardhat" +import { expect } from "chai" + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" +import type { ContractTransaction } from "ethers" +import type { ReimbursementPool } from "../typechain" + +import { constants } from "./fixtures" + +const ZERO_ADDRESS = ethers.constants.AddressZero +const { createSnapshot, restoreSnapshot } = helpers.snapshot +const { provider } = waffle + +describe("ReimbursementPool", () => { + let owner: SignerWithAddress + let thirdParty: SignerWithAddress + let refundee: SignerWithAddress + let thirdPartyContract: SignerWithAddress + let reimbursementPool: ReimbursementPool + + // prettier-ignore + before(async () => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;({ deployer: owner } = await helpers.signers.getNamedSigners()) + + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;[thirdParty, thirdPartyContract, refundee] = await helpers.signers.getUnnamedSigners() + }) + + beforeEach("load test fixture", async () => { + await deployments.fixture() + reimbursementPool = await helpers.contracts.getContract("ReimbursementPool") + }) + + describe("transfer ETH", () => { + context("when a third party funds a reimbursment pool", () => { + it("should send ETH to the Reimbursment Pool", async () => { + let reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + expect(reimbursementPoolBalance).to.be.equal(0) + + await thirdParty.sendTransaction({ + to: reimbursementPool.address, + value: ethers.utils.parseEther("1.0"), // Send 1.0 ETH + }) + + reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + + expect(reimbursementPoolBalance).to.be.equal( + ethers.utils.parseEther("1.0") + ) + }) + }) + + context("when the owner funds a reimbursment pool", () => { + it("should send ETH to the Reimbursment Pool", async () => { + let reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + expect(reimbursementPoolBalance).to.be.equal(0) + + await owner.sendTransaction({ + to: reimbursementPool.address, + value: ethers.utils.parseEther("1.0"), // Send 1.0 ETH + }) + + reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + + expect(reimbursementPoolBalance).to.be.equal( + ethers.utils.parseEther("1.0") + ) + }) + }) + }) + + describe("withdrawAll", () => { + beforeEach(async () => { + await thirdParty.sendTransaction({ + to: reimbursementPool.address, + value: ethers.utils.parseEther("10.0"), // Send 10.0 ETH + }) + }) + + context("when withdrawing all the funds as a non owner", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(thirdParty) + .withdrawAll(thirdPartyContract.address) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when widhrawing all the funds as an owner", () => { + it("should withdraw entire ETH balance", async () => { + let reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + expect(reimbursementPoolBalance).to.be.equal( + ethers.utils.parseEther("10.0") + ) + + const thirdPartyBalanceBefore = await provider.getBalance( + thirdParty.address + ) + + await reimbursementPool.connect(owner).withdrawAll(thirdParty.address) + + reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + expect(reimbursementPoolBalance).to.be.equal(0) + + const thirdPartyBalanceAfter = await provider.getBalance( + thirdParty.address + ) + const thirdPartyBalanceDiff = thirdPartyBalanceAfter.sub( + thirdPartyBalanceBefore + ) + expect(thirdPartyBalanceDiff).to.be.equal( + ethers.utils.parseEther("10.0") + ) + }) + + it("should emit FundsWithdrawn event", async () => { + await expect( + reimbursementPool.connect(owner).withdrawAll(thirdParty.address) + ) + .to.emit(reimbursementPool, "FundsWithdrawn") + .withArgs(ethers.utils.parseEther("10.0"), thirdParty.address) + }) + }) + + context("when receiver is zero address", () => { + it("should revert", async () => { + await expect( + reimbursementPool.connect(owner).withdrawAll(ZERO_ADDRESS) + ).to.be.revertedWith("Receiver's address cannot be zero") + }) + }) + }) + + describe("withdraw", () => { + beforeEach(async () => { + await createSnapshot() + + await thirdParty.sendTransaction({ + to: reimbursementPool.address, + value: ethers.utils.parseEther("10.0"), // Send 10.0 ETH + }) + }) + + afterEach(async () => { + await restoreSnapshot() + }) + + context("when withdrawing funds as a non owner", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(thirdParty) + .withdraw( + ethers.utils.parseEther("2.0"), + thirdPartyContract.address + ) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when widhrawing funds as an owner", () => { + it("should withdraw ETH balance", async () => { + let reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + expect(reimbursementPoolBalance).to.be.equal( + ethers.utils.parseEther("10.0") + ) + + const thirdPartyBalanceBefore = await provider.getBalance( + thirdParty.address + ) + + await reimbursementPool + .connect(owner) + .withdraw(ethers.utils.parseEther("2.0"), thirdParty.address) + + reimbursementPoolBalance = await provider.getBalance( + reimbursementPool.address + ) + expect(reimbursementPoolBalance).to.be.equal( + ethers.utils.parseEther("8.0") + ) + + const thirdPartyBalanceAfter = await provider.getBalance( + thirdParty.address + ) + const thirdPartyBalanceDiff = thirdPartyBalanceAfter.sub( + thirdPartyBalanceBefore + ) + expect(thirdPartyBalanceDiff).to.be.equal( + ethers.utils.parseEther("2.0") + ) + }) + + it("should emit FundsWithdrawn event", async () => { + await expect( + reimbursementPool + .connect(owner) + .withdraw(ethers.utils.parseEther("2.0"), thirdParty.address) + ) + .to.emit(reimbursementPool, "FundsWithdrawn") + .withArgs(ethers.utils.parseEther("2.0"), thirdParty.address) + }) + }) + + context("when receiver is zero address", () => { + it("should revert", async () => { + await expect( + reimbursementPool.connect(owner).withdraw(42, ZERO_ADDRESS) + ).to.be.revertedWith("Receiver's address cannot be zero") + }) + }) + + context("when withdrawing more than the pool's balance", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(owner) + .withdraw(ethers.utils.parseEther("42.0"), ZERO_ADDRESS) + ).to.be.revertedWith("Insufficient contract balance") + }) + }) + }) + + describe("refund", () => { + beforeEach(async () => { + await createSnapshot() + + await thirdParty.sendTransaction({ + to: reimbursementPool.address, + value: ethers.utils.parseEther("10.0"), // Send 10.0 ETH + }) + }) + + afterEach(async () => { + await restoreSnapshot() + }) + + context("when contract is not authorized", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(thirdParty) + .refund(ethers.utils.parseEther("2.0"), thirdParty.address) + ).to.be.revertedWith("Contract is not authorized for a refund") + }) + }) + + context("when contract is authorized", () => { + beforeEach(async () => { + await createSnapshot() + + await reimbursementPool + .connect(owner) + .authorize(thirdPartyContract.address) + }) + + afterEach(async () => { + await restoreSnapshot() + }) + + context("when tx gas price is lower than the max gas price", () => { + it("should refund based on tx.gasprice", async () => { + const refundeeBalanceBefore = await provider.getBalance( + refundee.address + ) + + const tx = await reimbursementPool + .connect(thirdPartyContract) + .refund(50000, refundee.address) + + const refundeeBalanceAfter = await provider.getBalance( + refundee.address + ) + const refundeeBalanceDiff = refundeeBalanceAfter.sub( + refundeeBalanceBefore + ) + // consumed gas: 50k + 40.8k = 90.8k + // refund: 90.8k * tx.gasPrice + const expectedRefund = ethers.BigNumber.from(90800).mul(tx.gasPrice) + expect(refundeeBalanceDiff).to.be.equal(expectedRefund) + }) + + it("should not emit SendingEtherFailed event", async () => { + await expect( + reimbursementPool + .connect(thirdPartyContract) + .refund(50000, refundee.address) + ).not.to.emit(reimbursementPool, "SendingEtherFailed") + }) + }) + + context("when tx gas price is higher than the max gas price", () => { + it("should refund based on max gas price", async () => { + await reimbursementPool + .connect(owner) + .authorize(thirdPartyContract.address) + + await reimbursementPool + .connect(owner) + .setMaxGasPrice(ethers.utils.parseUnits("1.0", "gwei")) + + const refundeeBalanceBefore = await provider.getBalance( + refundee.address + ) + + await reimbursementPool + .connect(thirdPartyContract) + .refund(50000, refundee.address) + + const refundeeBalanceAfter = await provider.getBalance( + refundee.address + ) + const refundeeBalanceDiff = refundeeBalanceAfter.sub( + refundeeBalanceBefore + ) + // gas spent + static gas => 50k + 40.8k + expect(refundeeBalanceDiff).to.be.eq( + ethers.utils.parseUnits("90800", "gwei") + ) + }) + }) + + context("when receiver address is zero", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(thirdPartyContract) + .refund(50000, ZERO_ADDRESS) + ).to.be.revertedWith("Receiver's address cannot be zero") + }) + }) + + context("when no funds available in the pool", () => { + let tx: Promise + + beforeEach(async () => { + await createSnapshot() + + await reimbursementPool + .connect(owner) + .setMaxGasPrice(ethers.utils.parseUnits("1.0", "gwei")) + + await reimbursementPool.connect(owner).withdrawAll(thirdParty.address) + + tx = reimbursementPool + .connect(thirdPartyContract) + .refund(50000, refundee.address) + }) + + afterEach(async () => { + await restoreSnapshot() + }) + + it("should not revert", async () => { + await expect(tx).to.not.be.reverted + }) + + it("should emit SendingEtherFailed event", async () => { + // gas spent + static gas => 50k + 40.8k + await expect(tx) + .to.emit(reimbursementPool, "SendingEtherFailed") + .withArgs( + ethers.utils.parseUnits("90800", "gwei"), + refundee.address + ) + }) + }) + }) + }) + + describe("authorize", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(thirdParty) + .authorize(thirdPartyContract.address) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + it("should authorize a contract", async () => { + const tx = await reimbursementPool + .connect(owner) + .authorize(thirdPartyContract.address) + + expect(await reimbursementPool.isAuthorized(thirdPartyContract.address)) + .to.be.true + + await expect(tx) + .to.emit(reimbursementPool, "AuthorizedContract") + .withArgs(thirdPartyContract.address) + }) + }) + }) + + describe("unauthorize", () => { + beforeEach(async () => { + await createSnapshot() + + await reimbursementPool + .connect(owner) + .authorize(thirdPartyContract.address) + }) + + afterEach(async () => { + await restoreSnapshot() + }) + + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + reimbursementPool + .connect(thirdParty) + .unauthorize(thirdPartyContract.address) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + it("should unauthorize a contract", async () => { + const tx = await reimbursementPool + .connect(owner) + .unauthorize(thirdPartyContract.address) + + expect(await reimbursementPool.isAuthorized(thirdPartyContract.address)) + .to.be.false + + await expect(tx) + .to.emit(reimbursementPool, "UnauthorizedContract") + .withArgs(thirdPartyContract.address) + }) + }) + }) + + describe("setStaticGas", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + reimbursementPool.connect(thirdParty).setStaticGas(42) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + it("should set the static gas cost", async () => { + expect(await reimbursementPool.staticGas()).to.be.equal( + constants.reimbursementPoolStaticGas + ) + + const tx = await reimbursementPool.connect(owner).setStaticGas(42000) + + await expect(tx) + .to.emit(reimbursementPool, "StaticGasUpdated") + .withArgs(42000) + + expect(await reimbursementPool.staticGas()).to.be.equal(42000) + }) + }) + }) + + describe("setMaxGasPrice", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + reimbursementPool.connect(thirdParty).setMaxGasPrice(42) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + it("should set the max gas price", async () => { + expect(await reimbursementPool.maxGasPrice()).to.be.equal( + constants.reimbursementPoolMaxGasPrice + ) + const newMaxGasPrice = ethers.utils.parseUnits("21", "gwei") + + const tx = await reimbursementPool + .connect(owner) + .setMaxGasPrice(newMaxGasPrice) + + await expect(tx) + .to.emit(reimbursementPool, "MaxGasPriceUpdated") + .withArgs(newMaxGasPrice) + + expect(await reimbursementPool.maxGasPrice()).to.be.equal( + newMaxGasPrice + ) + }) + }) + }) +}) diff --git a/solidity/test/fixtures/index.ts b/solidity/test/fixtures/index.ts index 21f35eed5..f435ebe25 100644 --- a/solidity/test/fixtures/index.ts +++ b/solidity/test/fixtures/index.ts @@ -28,6 +28,8 @@ export const constants = { fraudChallengeDefeatTimeout: 604800, // 1 week fraudSlashingAmount: to1ePrecision(100, 18), // 100 T fraudNotifierRewardMultiplier: 100, // 100% + reimbursementPoolStaticGas: 40_800, + reimbursementPoolMaxGasPrice: to1ePrecision(500, 9), // 500 Gwei walletCreationPeriod: 604800, // 1 week walletCreationMinBtcBalance: to1ePrecision(1, 8), // 1 BTC walletCreationMaxBtcBalance: to1ePrecision(100, 8), // 100 BTC diff --git a/solidity/yarn.lock b/solidity/yarn.lock index 9722617f2..92f259b7d 100644 --- a/solidity/yarn.lock +++ b/solidity/yarn.lock @@ -1575,6 +1575,11 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -1680,6 +1685,35 @@ "@summa-tx/relay-sol" "^2.0.2" openzeppelin-solidity "2.3.0" +"@layerzerolabs/lz-evm-protocol-v2@^3.0.71": + version "3.0.71" + resolved "https://registry.yarnpkg.com/@layerzerolabs/lz-evm-protocol-v2/-/lz-evm-protocol-v2-3.0.71.tgz#5a417e9274ee144b910db1e243a2ead6c3b69d74" + integrity sha512-CYbjRU7EYIjKVWWPV0rh/ofFgvbUZBhTqjE4Ot+cIKg08IorLxvQ/7BR09XznGAThQHkEgwsoCZdvLVDgqMuOw== + +"@layerzerolabs/oapp-evm-upgradeable@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@layerzerolabs/oapp-evm-upgradeable/-/oapp-evm-upgradeable-0.1.1.tgz#c4b9021115938000d2343c69718f136ee5bdd032" + integrity sha512-9rTJz4+QvL8/okR6EI05+M4Qz+G8c1Tozbu6P1CgBf9ZsSC2rGy5x2DGLjvzma44N1pPeL95lFK8MKu8TMpD6g== + dependencies: + ethers "^5.7.2" + +"@layerzerolabs/oapp-evm@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@layerzerolabs/oapp-evm/-/oapp-evm-0.3.1.tgz#79ebeba4f9a9b1b72e0674775ce4e9e027303d1b" + integrity sha512-SOuvqy1s+tWwgixR+hw5d9izS8z2tiO8XwGddlnSte24e9lxnOrqwMaOdIJt0GorTJk5rdUgrMlBW0HVwYMZDw== + dependencies: + ethers "^5.7.2" + +"@layerzerolabs/oft-evm-upgradeable@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@layerzerolabs/oft-evm-upgradeable/-/oft-evm-upgradeable-3.0.1.tgz#8aa033e4c72f9d4167ae6a48ef971de392f3ab12" + integrity sha512-FIWIB0Pda5zNTRf/3L3Da7IqU1sQkvO4EumD/mV2E4o5iTe8QQOW9vFuxmQMJDs4hTdh8K8OTVY2OOolGAvDig== + +"@layerzerolabs/oft-evm@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@layerzerolabs/oft-evm/-/oft-evm-3.1.2.tgz#599056728ea065038092084ba490579f8e88de55" + integrity sha512-2aqbnZW8SuLzhSBpQEraEl4LJYi97oNqxg5GTWoIvQNebQ/qkbppEs6kaEQxFqgo+I78v6Vu3SB7XIwpvG7K/w== + "@ledgerhq/cryptoassets@^5.53.0": version "5.53.0" resolved "https://registry.yarnpkg.com/@ledgerhq/cryptoassets/-/cryptoassets-5.53.0.tgz#11dcc93211960c6fd6620392e4dd91896aaabe58" @@ -1770,6 +1804,54 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@nomicfoundation/edr-darwin-arm64@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-arm64/-/edr-darwin-arm64-0.8.0.tgz#70a23214a2dd2941fcb55e47bb4653514d2dae06" + integrity sha512-sKTmOu/P5YYhxT0ThN2Pe3hmCE/5Ag6K/eYoiavjLWbR7HEb5ZwPu2rC3DpuUk1H+UKJqt7o4/xIgJxqw9wu6A== + +"@nomicfoundation/edr-darwin-x64@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-darwin-x64/-/edr-darwin-x64-0.8.0.tgz#89c11ae510b3ac5c0e5268cd3a6b04194552112f" + integrity sha512-8ymEtWw1xf1Id1cc42XIeE+9wyo3Dpn9OD/X8GiaMz9R70Ebmj2g+FrbETu8o6UM+aL28sBZQCiCzjlft2yWAg== + +"@nomicfoundation/edr-linux-arm64-gnu@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-gnu/-/edr-linux-arm64-gnu-0.8.0.tgz#02c1b4f426576af4e464320e340855139a00fe9b" + integrity sha512-h/wWzS2EyQuycz+x/SjMRbyA+QMCCVmotRsgM1WycPARvVZWIVfwRRsKoXKdCftsb3S8NTprqBdJlOmsFyETFA== + +"@nomicfoundation/edr-linux-arm64-musl@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-arm64-musl/-/edr-linux-arm64-musl-0.8.0.tgz#9b432dca973068f16a33abb70260e904494638dd" + integrity sha512-gnWxDgdkka0O9GpPX/gZT3REeKYV28Guyg13+Vj/bbLpmK1HmGh6Kx+fMhWv+Ht/wEmGDBGMCW1wdyT/CftJaQ== + +"@nomicfoundation/edr-linux-x64-gnu@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-gnu/-/edr-linux-x64-gnu-0.8.0.tgz#72954e5fd875df17c43d4ef3fcc381e3312e1347" + integrity sha512-DTMiAkgAx+nyxcxKyxFZk1HPakXXUCgrmei7r5G7kngiggiGp/AUuBBWFHi8xvl2y04GYhro5Wp+KprnLVoAPA== + +"@nomicfoundation/edr-linux-x64-musl@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-linux-x64-musl/-/edr-linux-x64-musl-0.8.0.tgz#0d59390c512106010d6f4d94b7fffd99fb7fd8ae" + integrity sha512-iTITWe0Zj8cNqS0xTblmxPbHVWwEtMiDC+Yxwr64d7QBn/1W0ilFQ16J8gB6RVVFU3GpfNyoeg3tUoMpSnrm6Q== + +"@nomicfoundation/edr-win32-x64-msvc@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr-win32-x64-msvc/-/edr-win32-x64-msvc-0.8.0.tgz#d14225c513372fda54684de1229cc793ffe48c12" + integrity sha512-mNRDyd/C3j7RMcwapifzv2K57sfA5xOw8g2U84ZDvgSrXVXLC99ZPxn9kmolb+dz8VMm9FONTZz9ESS6v8DTnA== + +"@nomicfoundation/edr@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@nomicfoundation/edr/-/edr-0.8.0.tgz#63441bb24c1804b6d27b075d0d29f3a02d94fc4f" + integrity sha512-dwWRrghSVBQDpt0wP+6RXD8BMz2i/9TI34TcmZqeEAZuCLei3U9KZRgGTKVAM1rMRvrpf5ROfPqrWNetKVUTag== + dependencies: + "@nomicfoundation/edr-darwin-arm64" "0.8.0" + "@nomicfoundation/edr-darwin-x64" "0.8.0" + "@nomicfoundation/edr-linux-arm64-gnu" "0.8.0" + "@nomicfoundation/edr-linux-arm64-musl" "0.8.0" + "@nomicfoundation/edr-linux-x64-gnu" "0.8.0" + "@nomicfoundation/edr-linux-x64-musl" "0.8.0" + "@nomicfoundation/edr-win32-x64-msvc" "0.8.0" + "@nomicfoundation/ethereumjs-block@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz#fdd5c045e7baa5169abeed0e1202bf94e4481c49" @@ -1800,6 +1882,13 @@ lru-cache "^5.1.1" memory-level "^1.0.0" +"@nomicfoundation/ethereumjs-common@4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.4.tgz#9901f513af2d4802da87c66d6f255b510bef5acb" + integrity sha512-9Rgb658lcWsjiicr5GzNCjI1llow/7r0k50dLL95OJ+6iZJcVbi15r3Y0xh2cIO+zgX0WIHcbzIu6FeQf9KPrg== + dependencies: + "@nomicfoundation/ethereumjs-util" "9.0.4" + "@nomicfoundation/ethereumjs-common@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz#f6bcc7753994555e49ab3aa517fc8bcf89c280b9" @@ -1834,6 +1923,11 @@ mcl-wasm "^0.7.1" rustbn.js "~0.2.0" +"@nomicfoundation/ethereumjs-rlp@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.4.tgz#66c95256fc3c909f6fb18f6a586475fc9762fa30" + integrity sha512-8H1S3s8F6QueOc/X92SdrA4RDenpiAEqMg5vJH99kcQaCy/a3Q6fgseo75mgWlbanGJXSlAPtnCeG9jvfTYXlw== + "@nomicfoundation/ethereumjs-rlp@^4.0.0", "@nomicfoundation/ethereumjs-rlp@^4.0.0-beta.2": version "4.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz#d9a9c5f0f10310c8849b6525101de455a53e771d" @@ -1862,6 +1956,16 @@ ethereum-cryptography "0.1.3" readable-stream "^3.6.0" +"@nomicfoundation/ethereumjs-tx@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.4.tgz#b0ceb58c98cc34367d40a30d255d6315b2f456da" + integrity sha512-Xjv8wAKJGMrP1f0n2PeyfFCCojHd7iS3s/Ab7qzF1S64kxZ8Z22LCMynArYsVqiFx6rzYy548HNVEyI+AYN/kw== + dependencies: + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-tx@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz#59dc7452b0862b30342966f7052ab9a1f7802f52" @@ -1872,6 +1976,14 @@ "@nomicfoundation/ethereumjs-util" "^8.0.0" ethereum-cryptography "0.1.3" +"@nomicfoundation/ethereumjs-util@9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.4.tgz#84c5274e82018b154244c877b76bc049a4ed7b38" + integrity sha512-sLOzjnSrlx9Bb9EFNtHzK/FJFsfg2re6bsGqinFinH1gCqVfz9YYlXiMWwDM4C/L4ywuHFCYwfKTVr/QHQcU0Q== + dependencies: + "@nomicfoundation/ethereumjs-rlp" "5.0.4" + ethereum-cryptography "0.1.3" + "@nomicfoundation/ethereumjs-util@^8.0.0", "@nomicfoundation/ethereumjs-util@^8.0.0-rc.3": version "8.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz#deb2b15d2c308a731e82977aefc4e61ca0ece6c5" @@ -1880,7 +1992,7 @@ "@nomicfoundation/ethereumjs-rlp" "^4.0.0-beta.2" ethereum-cryptography "0.1.3" -"@nomicfoundation/ethereumjs-vm@^6.0.0", "@nomicfoundation/ethereumjs-vm@^6.0.0-rc.3": +"@nomicfoundation/ethereumjs-vm@^6.0.0-rc.3": version "6.0.0" resolved "https://registry.yarnpkg.com/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz#2bb50d332bf41790b01a3767ffec3987585d1de6" integrity sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w== @@ -2777,13 +2889,6 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - abstract-level@^1.0.0, abstract-level@^1.0.2, abstract-level@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-1.0.3.tgz#78a67d3d84da55ee15201486ab44c09560070741" @@ -2908,6 +3013,13 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + ansi-colors@3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" @@ -4005,6 +4117,20 @@ body-parser@1.20.0, body-parser@^1.16.0: type-is "~1.6.18" unpipe "1.0.0" +boxen@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" + widest-line "^3.1.0" + wrap-ansi "^7.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -4317,7 +4443,7 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: +camelcase@^6.0.0, camelcase@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -4482,20 +4608,12 @@ chokidar@3.5.3, chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" -chokidar@^3.4.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== +chokidar@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" + readdirp "^4.0.1" chownr@^1.1.4: version "1.1.4" @@ -4557,6 +4675,11 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -4732,6 +4855,11 @@ commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^8.1.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + compare-versions@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-5.0.1.tgz#14c6008436d994c3787aba38d4087fabe858555e" @@ -6428,7 +6556,7 @@ ethers@^5.0.1, ethers@^5.0.2: "@ethersproject/web" "5.3.0" "@ethersproject/wordlists" "5.3.0" -ethers@^5.2.0, ethers@^5.6.8: +ethers@^5.2.0, ethers@^5.6.8, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -6552,11 +6680,6 @@ ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: is-hex-prefixed "1.0.0" strip-hex-prefix "1.0.0" -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - eventemitter3@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" @@ -6747,6 +6870,11 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fdir@^6.4.3: + version "6.4.3" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" + integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== + fetch-ponyfill@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" @@ -6847,7 +6975,7 @@ find-up@3.0.0, find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@5.0.0: +find-up@5.0.0, find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== @@ -7108,7 +7236,7 @@ fsevents@~2.1.1: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -7506,59 +7634,53 @@ hardhat-gas-reporter@^1.0.8: eth-gas-reporter "^0.2.25" sha1 "^1.1.1" -hardhat@^2.10.0: - version "2.12.5" - resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.12.5.tgz#e3cd4d6dae35cb9505055967bd7e15e6adf3aa03" - integrity sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A== +hardhat@^2.11.0: + version "2.22.19" + resolved "https://registry.yarnpkg.com/hardhat/-/hardhat-2.22.19.tgz#92eb6f59e75b0dded841fecf16260a5e3f6eb4eb" + integrity sha512-jptJR5o6MCgNbhd7eKa3mrteR+Ggq1exmE5RUL5ydQEVKcZm0sss5laa86yZ0ixIavIvF4zzS7TdGDuyopj0sQ== dependencies: "@ethersproject/abi" "^5.1.2" "@metamask/eth-sig-util" "^4.0.0" - "@nomicfoundation/ethereumjs-block" "^4.0.0" - "@nomicfoundation/ethereumjs-blockchain" "^6.0.0" - "@nomicfoundation/ethereumjs-common" "^3.0.0" - "@nomicfoundation/ethereumjs-evm" "^1.0.0" - "@nomicfoundation/ethereumjs-rlp" "^4.0.0" - "@nomicfoundation/ethereumjs-statemanager" "^1.0.0" - "@nomicfoundation/ethereumjs-trie" "^5.0.0" - "@nomicfoundation/ethereumjs-tx" "^4.0.0" - "@nomicfoundation/ethereumjs-util" "^8.0.0" - "@nomicfoundation/ethereumjs-vm" "^6.0.0" + "@nomicfoundation/edr" "^0.8.0" + "@nomicfoundation/ethereumjs-common" "4.0.4" + "@nomicfoundation/ethereumjs-tx" "5.0.4" + "@nomicfoundation/ethereumjs-util" "9.0.4" "@nomicfoundation/solidity-analyzer" "^0.1.0" "@sentry/node" "^5.18.1" "@types/bn.js" "^5.1.0" "@types/lru-cache" "^5.1.0" - abort-controller "^3.0.0" adm-zip "^0.4.16" aggregate-error "^3.0.0" ansi-escapes "^4.3.0" - chalk "^2.4.2" - chokidar "^3.4.0" + boxen "^5.1.2" + chokidar "^4.0.0" ci-info "^2.0.0" debug "^4.1.1" enquirer "^2.3.0" env-paths "^2.2.0" ethereum-cryptography "^1.0.3" ethereumjs-abi "^0.6.8" - find-up "^2.1.0" + find-up "^5.0.0" fp-ts "1.19.3" fs-extra "^7.0.1" - glob "7.2.0" immutable "^4.0.0-rc.12" io-ts "1.10.4" + json-stream-stringify "^3.1.4" keccak "^3.0.2" lodash "^4.17.11" mnemonist "^0.38.0" mocha "^10.0.0" p-map "^4.0.0" - qs "^6.7.0" + picocolors "^1.1.0" raw-body "^2.4.1" resolve "1.17.0" semver "^6.3.0" - solc "0.7.3" + solc "0.8.26" source-map-support "^0.5.13" stacktrace-parser "^0.1.10" + tinyglobby "^0.2.6" tsort "0.0.1" - undici "^5.4.0" + undici "^5.14.0" uuid "^8.3.2" ws "^7.4.6" @@ -8485,6 +8607,11 @@ json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" +json-stream-stringify@^3.1.4: + version "3.1.6" + resolved "https://registry.yarnpkg.com/json-stream-stringify/-/json-stream-stringify-3.1.6.tgz#ebe32193876fb99d4ec9f612389a8d8e2b5d54d4" + integrity sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog== + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -10161,11 +10288,21 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -10442,7 +10579,7 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" -qs@^6.4.0, qs@^6.7.0, qs@^6.9.4: +qs@^6.4.0, qs@^6.9.4: version "6.10.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg== @@ -10577,6 +10714,11 @@ readable-stream@~1.0.15: isarray "0.0.1" string_decoder "~0.10.x" +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + readdirp@~3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839" @@ -10584,13 +10726,6 @@ readdirp@~3.2.0: dependencies: picomatch "^2.0.4" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -11283,18 +11418,16 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -solc@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/solc/-/solc-0.7.3.tgz#04646961bd867a744f63d2b4e3c0701ffdc7d78a" - integrity sha512-GAsWNAjGzIDg7VxzP6mPjdurby3IkGCjQcM8GFYZT6RyaoUZKmMU6Y7YwG+tFGhv7dwZ8rmR4iwFDrrD99JwqA== +solc@0.8.26: + version "0.8.26" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.26.tgz#afc78078953f6ab3e727c338a2fefcd80dd5b01a" + integrity sha512-yiPQNVf5rBFHwN6SIf3TUUvVAFKcQqmSUFeq+fb6pNRCo0ZCgpYOZDi3BVoezCPIAcKrVYd/qXlBLUP9wVrZ9g== dependencies: command-exists "^1.2.8" - commander "3.0.2" + commander "^8.1.0" follow-redirects "^1.12.1" - fs-extra "^0.30.0" js-sha3 "0.8.0" memorystream "^0.3.1" - require-from-string "^2.0.0" semver "^5.5.0" tmp "0.0.33" @@ -11565,7 +11698,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.3: +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11968,6 +12101,14 @@ tiny-secp256k1@^1.1.3: elliptic "^6.4.0" nan "^2.13.2" +tinyglobby@^0.2.6: + version "0.2.12" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" + integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== + dependencies: + fdir "^6.4.3" + picomatch "^4.0.2" + tmp@0.0.33, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -12385,6 +12526,13 @@ underscore@>1.4.4: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +undici@^5.14.0: + version "5.28.5" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.5.tgz#b2b94b6bf8f1d919bc5a6f31f2c01deb02e54d4b" + integrity sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA== + dependencies: + "@fastify/busboy" "^2.0.0" + undici@^5.4.0: version "5.10.0" resolved "https://registry.yarnpkg.com/undici/-/undici-5.10.0.tgz#dd9391087a90ccfbd007568db458674232ebf014" @@ -13676,6 +13824,13 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + wif@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704"