From 970fed2baf295b0457f6dc317563a074f010614b Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 28 Feb 2024 14:48:22 +0100 Subject: [PATCH 01/24] Draft implementation of the `L2BitcoinDepositor` contract Here we present a draft implementation of the `L2BitcoinDepositor` contract that acts as an entrypoint of the tBTC direct bridging feature on the given L2 chain. This contract exposes the `initializeDeposit` function that takes the deposit data (funding tx, reveal info, original depositor) and relays it to the `L1BitcoinDepositor` contract using the Wormhole Relayer infrastructure. --- solidity/contracts/l2/L2BitcoinDepositor.sol | 164 +++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 solidity/contracts/l2/L2BitcoinDepositor.sol diff --git a/solidity/contracts/l2/L2BitcoinDepositor.sol b/solidity/contracts/l2/L2BitcoinDepositor.sol new file mode 100644 index 000000000..24f5f47d9 --- /dev/null +++ b/solidity/contracts/l2/L2BitcoinDepositor.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.17; + +import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import "../integrator/IBridge.sol"; + +/// @title IWormholeRelayer +/// @notice Wormhole Relayer interface. Contains only selected functions +/// used by L2BitcoinDepositor. +/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L74 +interface IWormholeRelayer { + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L122 + function sendPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + uint16 refundChain, + address refundAddress + ) external payable returns (uint64 sequence); + + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L442 + function quoteEVMDeliveryPrice( + uint16 targetChain, + uint256 receiverValue, + uint256 gasLimit + ) + external + view + returns ( + uint256 nativePriceQuote, + uint256 targetChainRefundPerGasUnused + ); +} + +// TODO: Document this contract. +contract L2BitcoinDepositor is OwnableUpgradeable { + using BTCUtils for bytes; + + // TODO: Document state variables. + IWormholeRelayer public wormholeRelayer; + uint16 public l2ChainId; + uint16 public l1ChainId; + address public l1BitcoinDepositor; + uint256 public l1InitializeDepositGasLimit; + + event DepositInitialized( + uint256 indexed depositKey, + address indexed l2DepositOwner, + address indexed l2Sender, + uint64 wormholeMessageSequence + ); + + event L1InitializeDepositGasLimitUpdated( + uint256 l1InitializeDepositGasLimit + ); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _wormholeRelayer, + uint16 _l2ChainId, + uint16 _l1ChainId, + address _l1BitcoinDepositor + ) external initializer { + __Ownable_init(); + + wormholeRelayer = IWormholeRelayer(_wormholeRelayer); + l2ChainId = _l2ChainId; + l1ChainId = _l1ChainId; + l1BitcoinDepositor = _l1BitcoinDepositor; + l1InitializeDepositGasLimit = 200_000; + } + + // TODO: Document this function. + function updateL1InitializeDepositGasLimit( + uint256 _l1InitializeDepositGasLimit + ) external onlyOwner { + l1InitializeDepositGasLimit = _l1InitializeDepositGasLimit; + emit L1InitializeDepositGasLimitUpdated(_l1InitializeDepositGasLimit); + } + + // TODO: Document this function. + function initializeDeposit( + IBridgeTypes.BitcoinTxInfo calldata fundingTx, + IBridgeTypes.DepositRevealInfo calldata reveal, + address l2DepositOwner + ) external payable { + require( + l2DepositOwner != address(0), + "L2 deposit owner must not be 0x0" + ); + + // Cost of requesting a `initializeDeposit` message to be sent to + // `l1Chain` with a gasLimit of `l1InitializeDepositGasLimit`. + uint256 cost = quoteInitializeDeposit(); + + require(msg.value == cost, "Payment for Wormhole Relayer is too low"); + + uint64 wormholeMessageSequence = wormholeRelayer.sendPayloadToEvm{ + value: cost + }( + l1ChainId, + l1BitcoinDepositor, + abi.encode(fundingTx, reveal, l2DepositOwner), // Message payload. + 0, // No receiver value needed. + l1InitializeDepositGasLimit, + l2ChainId, // Set this L2 chain as the refund chain. + msg.sender // Set the caller as the refund receiver. + ); + + uint256 depositKey = uint256( + keccak256( + abi.encodePacked( + abi + .encodePacked( + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime + ) + .hash256View(), + reveal.fundingOutputIndex + ) + ) + ); + + emit DepositInitialized( + depositKey, + l2DepositOwner, + msg.sender, + wormholeMessageSequence + ); + } + + // TODO: Document this function. + function quoteInitializeDeposit() public view returns (uint256 cost) { + (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + l1ChainId, + 0, // No receiver value needed. + l1InitializeDepositGasLimit + ); + } +} From 924100358b449f722956a198b112cb6e97991a39 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 28 Feb 2024 16:51:01 +0100 Subject: [PATCH 02/24] Draft implementation of the `L1BitcoinDepositor` contract Here we present a draft implementation of the `L1BitcoinDepositor` contract that acts as the central point of the tBTC direct bridging feature on the L1 Ethereum chain where TBTC minting occurs. This contract exposes the `receiveWormholeMessages` function that receives a message from `L2BitcoinDepositor` contract (using the Wormhole Relayer infrastructure), extracts deposit data from it, and initiates the deposit on the tBTC `Bridge` side. Moreover, the contract also exposes the `initializeDeposit` function directly. The goal here is to satisfy future use cases that don't rely on Wormhole Relayer for cross-chain messaging. --- solidity/contracts/l2/L1BitcoinDepositor.sol | 182 +++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 solidity/contracts/l2/L1BitcoinDepositor.sol diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol new file mode 100644 index 000000000..acccf6011 --- /dev/null +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import "../integrator/AbstractTBTCDepositor.sol"; +import "../integrator/IBridge.sol"; + +/// @title IWormholeReceiver +/// @notice Wormhole Receiver interface. Contains only selected functions +/// used by L1BitcoinDepositor. +/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeReceiver.sol#L8 +interface IWormholeReceiver { + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeReceiver.sol#L44 + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalMessages, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable; +} + +// TODO: Document this contract. +contract L1BitcoinDepositor is + AbstractTBTCDepositor, + IWormholeReceiver, + OwnableUpgradeable +{ + /// @notice Reflects the deposit state: + /// - Unknown deposit has not been initialized yet. + /// - Initialized deposit has been initialized with a call to + /// `initializeDeposit` function and is known to this contract. + /// - Finalized deposit led to tBTC ERC20 minting and was finalized + /// with a call to `finalizeDeposit` function that deposited tBTC + /// to the Portal contract. + enum DepositState { + Unknown, + Initialized, + Finalized + } + + /// @notice Holds the deposit state, keyed by the deposit key calculated for + /// the individual deposit during the call to `initializeDeposit` + /// function. + mapping(uint256 => DepositState) public deposits; + + // TODO: Document other state variables. + address public wormholeRelayer; + address public wormholeTokenBridge; + uint16 public l2ChainId; + address public l2BitcoinDepositor; + + event DepositInitialized( + uint256 indexed depositKey, + address indexed l2DepositOwner, + address indexed l1Sender + ); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _tbtcBridge, + address _tbtcVault, + address _wormholeRelayer, + address _wormholeTokenBridge, + uint16 _l2ChainId, + address _l2BitcoinDepositor + ) external initializer { + __AbstractTBTCDepositor_initialize(_tbtcBridge, _tbtcVault); + __Ownable_init(); + + wormholeRelayer = _wormholeRelayer; + wormholeTokenBridge = _wormholeTokenBridge; + l2ChainId = _l2ChainId; + l2BitcoinDepositor = _l2BitcoinDepositor; + } + + // TODO: Document this function. + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 + ) external payable { + require( + msg.sender == address(wormholeRelayer), + "Caller is not Wormhole Relayer" + ); + + require( + sourceChain == l2ChainId, + "Source chain is not the expected L2 chain" + ); + + require( + fromWormholeAddress(sourceAddress) == l2BitcoinDepositor, + "Source address is not the expected L2 Bitcoin depositor" + ); + + ( + IBridgeTypes.BitcoinTxInfo memory fundingTx, + IBridgeTypes.DepositRevealInfo memory reveal, + address l2DepositOwner + ) = abi.decode( + payload, + ( + IBridgeTypes.BitcoinTxInfo, + IBridgeTypes.DepositRevealInfo, + address + ) + ); + + initializeDeposit(fundingTx, reveal, l2DepositOwner); + } + + // TODO: Document this function. + function initializeDeposit( + IBridgeTypes.BitcoinTxInfo memory fundingTx, + IBridgeTypes.DepositRevealInfo memory reveal, + address l2DepositOwner + ) public { + require( + l2DepositOwner != address(0), + "L2 deposit owner must not be 0x0" + ); + + // TODO: Document how the Bridge works and why we don't need to validate input parameters. + (uint256 depositKey, ) = _initializeDeposit( + fundingTx, + reveal, + toWormholeAddress(l2DepositOwner) + ); + + require( + deposits[depositKey] == DepositState.Unknown, + "Wrong deposit state" + ); + + deposits[depositKey] = DepositState.Initialized; + + emit DepositInitialized(depositKey, l2DepositOwner, msg.sender); + } + + /// @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))); + } +} From df436e1e2d51a5ce756619a597b797a34ed83a47 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 28 Feb 2024 16:55:19 +0100 Subject: [PATCH 03/24] Change `calldata` to `memory` in `AbstractTBTCDepositor` contract So far, the `_initializeDeposit` accepted `calldata` parameters. This does not work for integrator contracts that obtain deposit parameters as `memory` objects (the conversion `memory -> calldata` is not possible). To overcome this problem, we are changing parameters of `_initializeDeposit` to be `memory` as well. This is not breaking integrators that receive deposit parameters as `calldata` because the `calldata -> memory` conversion is fine. --- solidity/contracts/integrator/AbstractTBTCDepositor.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/solidity/contracts/integrator/AbstractTBTCDepositor.sol b/solidity/contracts/integrator/AbstractTBTCDepositor.sol index 0ba4928ba..10910ed9a 100644 --- a/solidity/contracts/integrator/AbstractTBTCDepositor.sol +++ b/solidity/contracts/integrator/AbstractTBTCDepositor.sol @@ -133,8 +133,8 @@ abstract contract AbstractTBTCDepositor { /// as the Bridge won't allow the same deposit to be revealed twice. // slither-disable-next-line dead-code function _initializeDeposit( - IBridgeTypes.BitcoinTxInfo calldata fundingTx, - IBridgeTypes.DepositRevealInfo calldata reveal, + IBridgeTypes.BitcoinTxInfo memory fundingTx, + IBridgeTypes.DepositRevealInfo memory reveal, bytes32 extraData ) internal returns (uint256 depositKey, uint256 initialDepositAmount) { require(reveal.vault == address(tbtcVault), "Vault address mismatch"); @@ -278,7 +278,7 @@ abstract contract AbstractTBTCDepositor { /// @param txInfo Bitcoin transaction data, see `IBridgeTypes.BitcoinTxInfo` struct. /// @return txHash Bitcoin transaction hash. // slither-disable-next-line dead-code - function _calculateBitcoinTxHash(IBridgeTypes.BitcoinTxInfo calldata txInfo) + function _calculateBitcoinTxHash(IBridgeTypes.BitcoinTxInfo memory txInfo) internal view returns (bytes32) From 34971b0ef87d3fc3e7fb6a2cffd355bc54f240ef Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 29 Feb 2024 14:43:09 +0100 Subject: [PATCH 04/24] Expose `tbtcToken` from the `ITBTCVault` interface This will be useful to get TBTC token address directly from the vault contract. --- solidity/contracts/integrator/ITBTCVault.sol | 3 +++ solidity/contracts/test/TestTBTCDepositor.sol | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/solidity/contracts/integrator/ITBTCVault.sol b/solidity/contracts/integrator/ITBTCVault.sol index a9f665637..881d27602 100644 --- a/solidity/contracts/integrator/ITBTCVault.sol +++ b/solidity/contracts/integrator/ITBTCVault.sol @@ -25,4 +25,7 @@ interface ITBTCVault { /// @dev See {TBTCVault#optimisticMintingFeeDivisor} function optimisticMintingFeeDivisor() external view returns (uint32); + + /// @dev See {TBTCVault#tbtcToken} + function tbtcToken() external view returns (address); } diff --git a/solidity/contracts/test/TestTBTCDepositor.sol b/solidity/contracts/test/TestTBTCDepositor.sol index a54155b11..8bd914c6d 100644 --- a/solidity/contracts/test/TestTBTCDepositor.sol +++ b/solidity/contracts/test/TestTBTCDepositor.sol @@ -220,4 +220,8 @@ contract MockTBTCVault is ITBTCVault { function setOptimisticMintingFeeDivisor(uint32 value) external { optimisticMintingFeeDivisor = value; } + + function tbtcToken() external view returns (address) { + revert("Not implemented"); + } } From 5a179fd48b7667cad105276b9beadc74623b2dfb Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 29 Feb 2024 14:51:38 +0100 Subject: [PATCH 05/24] Finalize deposit flow Here we implement the finalization of the deposit flow. This process is started on the `L1BitcoinDepositor` side once the tBTC `Bridge` completes minting of the TBTC token. The process consists of multiple steps: 1. The `L1BitcoinDepositor` marks the given deposit as finalized and determines the amount of TBTC minted. 2. The `L1BitcoinDepositor` initiates a Wormhole token transfer. This transfer locks minted TBTC on L1 within the Wormhole Token Bridge and unlocks Wormhole-wrapped L2 TBTC for the `L2WormholeGateway` contract. 3. The `L2BitcoinDepositor` sends the transfer VAA to `L2BitcoinDepositor`. 4. The `L2BitcoinDepositor` finalizes the transfer by calling the `L2WormholeGateway` that redeems Wormhole-wrapped L2 TBTC from the Wormhole Token Bridge and uses it to mint canonical L2 TBTC for the L2 deposit owner. --- solidity/contracts/l2/L1BitcoinDepositor.sol | 191 ++++++++++++++---- solidity/contracts/l2/L2BitcoinDepositor.sol | 77 ++++--- solidity/contracts/l2/L2WormholeGateway.sol | 60 +----- solidity/contracts/l2/Wormhole.sol | 170 ++++++++++++++++ .../contracts/test/WormholeBridgeStub.sol | 2 +- 5 files changed, 378 insertions(+), 122 deletions(-) create mode 100644 solidity/contracts/l2/Wormhole.sol diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index acccf6011..f21570a56 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -15,25 +15,14 @@ pragma solidity ^0.8.17; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../integrator/AbstractTBTCDepositor.sol"; import "../integrator/IBridge.sol"; - -/// @title IWormholeReceiver -/// @notice Wormhole Receiver interface. Contains only selected functions -/// used by L1BitcoinDepositor. -/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeReceiver.sol#L8 -interface IWormholeReceiver { - /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeReceiver.sol#L44 - function receiveWormholeMessages( - bytes memory payload, - bytes[] memory additionalMessages, - bytes32 sourceAddress, - uint16 sourceChain, - bytes32 deliveryHash - ) external payable; -} +import "../integrator/ITBTCVault.sol"; +import "./Wormhole.sol"; // TODO: Document this contract. contract L1BitcoinDepositor is @@ -41,6 +30,8 @@ contract L1BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { + using SafeERC20 for IERC20; + /// @notice Reflects the deposit state: /// - Unknown deposit has not been initialized yet. /// - Initialized deposit has been initialized with a call to @@ -60,10 +51,14 @@ contract L1BitcoinDepositor is mapping(uint256 => DepositState) public deposits; // TODO: Document other state variables. - address public wormholeRelayer; - address public wormholeTokenBridge; + IERC20 public tbtcToken; + IWormhole public wormhole; + IWormholeRelayer public wormholeRelayer; + IWormholeTokenBridge public wormholeTokenBridge; + address public l2WormholeGateway; uint16 public l2ChainId; address public l2BitcoinDepositor; + uint256 public l2FinalizeDepositGasLimit; event DepositInitialized( uint256 indexed depositKey, @@ -71,6 +66,16 @@ contract L1BitcoinDepositor is address indexed l1Sender ); + event DepositFinalized( + uint256 indexed depositKey, + address indexed l2DepositOwner, + address indexed l1Sender, + uint256 initialAmount, + uint256 tbtcAmount + ); + + event L2FinalizeDepositGasLimitUpdated(uint256 l2FinalizeDepositGasLimit); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -79,18 +84,33 @@ contract L1BitcoinDepositor is function initialize( address _tbtcBridge, address _tbtcVault, + address _wormhole, address _wormholeRelayer, address _wormholeTokenBridge, + address _l2WormholeGateway, uint16 _l2ChainId, address _l2BitcoinDepositor ) external initializer { __AbstractTBTCDepositor_initialize(_tbtcBridge, _tbtcVault); __Ownable_init(); - wormholeRelayer = _wormholeRelayer; - wormholeTokenBridge = _wormholeTokenBridge; + tbtcToken = IERC20(ITBTCVault(_tbtcVault).tbtcToken()); + wormhole = IWormhole(_wormhole); + wormholeRelayer = IWormholeRelayer(_wormholeRelayer); + wormholeTokenBridge = IWormholeTokenBridge(_wormholeTokenBridge); + l2WormholeGateway = _l2WormholeGateway; l2ChainId = _l2ChainId; l2BitcoinDepositor = _l2BitcoinDepositor; + l2FinalizeDepositGasLimit = 200_000; + } + + // TODO: Document this function. + function updateL2FinalizeDepositGasLimit(uint256 _l2FinalizeDepositGasLimit) + external + onlyOwner + { + l2FinalizeDepositGasLimit = _l2FinalizeDepositGasLimit; + emit L2FinalizeDepositGasLimitUpdated(_l2FinalizeDepositGasLimit); } // TODO: Document this function. @@ -112,7 +132,8 @@ contract L1BitcoinDepositor is ); require( - fromWormholeAddress(sourceAddress) == l2BitcoinDepositor, + WormholeUtils.fromWormholeAddress(sourceAddress) == + l2BitcoinDepositor, "Source address is not the expected L2 Bitcoin depositor" ); @@ -143,11 +164,15 @@ contract L1BitcoinDepositor is "L2 deposit owner must not be 0x0" ); + // Convert the L2 deposit owner address into the Wormhole format and + // encode it as deposit extra data. + bytes32 extraData = WormholeUtils.toWormholeAddress(l2DepositOwner); + // TODO: Document how the Bridge works and why we don't need to validate input parameters. (uint256 depositKey, ) = _initializeDeposit( fundingTx, reveal, - toWormholeAddress(l2DepositOwner) + extraData ); require( @@ -160,23 +185,119 @@ contract L1BitcoinDepositor is emit DepositInitialized(depositKey, l2DepositOwner, msg.sender); } - /// @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))); + // TODO: Document this function. + function finalizeDeposit(uint256 depositKey) external payable { + require( + deposits[depositKey] == DepositState.Initialized, + "Wrong deposit state" + ); + + deposits[depositKey] = DepositState.Finalized; + + ( + uint256 initialDepositAmount, + uint256 tbtcAmount, + // Deposit extra data is actually the L2 deposit owner + // address in Wormhole format. + bytes32 l2DepositOwner + ) = _finalizeDeposit(depositKey); + + emit DepositFinalized( + depositKey, + WormholeUtils.fromWormholeAddress(l2DepositOwner), + msg.sender, + initialDepositAmount, + tbtcAmount + ); + + transferTbtc(tbtcAmount, l2DepositOwner); + } + + // TODO: Document this function. + function quoteFinalizeDeposit() public view returns (uint256 cost) { + cost = _quoteFinalizeDeposit(wormhole.messageFee()); } - /// @notice Converts Wormhole address into Ethereum format. - /// @param _address The address to convert. - function fromWormholeAddress(bytes32 _address) + // TODO: Document this function. + function _quoteFinalizeDeposit(uint256 messageFee) internal - pure - returns (address) + view + returns (uint256 cost) { - return address(uint160(uint256(_address))); + // 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; + } + + // TODO: Document this function. + 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 `finalize` 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 transfer Wormhole-wrapped + // L2 TBTC to the corresponding `L2WormholeGateway` contract. + 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. + ); + + // Get Wormhole chain ID for this L1 chain. + uint16 l1ChainId = wormhole.chainId(); + + // Construct VAA representing the above Wormhole token transfer. + WormholeTypes.VaaKey[] + memory additionalVaas = new WormholeTypes.VaaKey[](1); + additionalVaas[0] = WormholeTypes.VaaKey({ + chainId: l1ChainId, + 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 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. + 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, + l1ChainId, // Set this L1 chain as the refund chain. + msg.sender // Set the caller as the refund receiver. + ); } } diff --git a/solidity/contracts/l2/L2BitcoinDepositor.sol b/solidity/contracts/l2/L2BitcoinDepositor.sol index 24f5f47d9..afec86e4d 100644 --- a/solidity/contracts/l2/L2BitcoinDepositor.sol +++ b/solidity/contracts/l2/L2BitcoinDepositor.sol @@ -19,43 +19,21 @@ import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../integrator/IBridge.sol"; +import "./Wormhole.sol"; -/// @title IWormholeRelayer -/// @notice Wormhole Relayer interface. Contains only selected functions -/// used by L2BitcoinDepositor. -/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L74 -interface IWormholeRelayer { - /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L122 - function sendPayloadToEvm( - uint16 targetChain, - address targetAddress, - bytes memory payload, - uint256 receiverValue, - uint256 gasLimit, - uint16 refundChain, - address refundAddress - ) external payable returns (uint64 sequence); - - /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L442 - function quoteEVMDeliveryPrice( - uint16 targetChain, - uint256 receiverValue, - uint256 gasLimit - ) - external - view - returns ( - uint256 nativePriceQuote, - uint256 targetChainRefundPerGasUnused - ); +// TODO: Document this interface. +interface IL2WormholeGateway { + // TODO: Document this function. + function receiveTbtc(bytes memory vaa) external; } // TODO: Document this contract. -contract L2BitcoinDepositor is OwnableUpgradeable { +contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { using BTCUtils for bytes; // TODO: Document state variables. IWormholeRelayer public wormholeRelayer; + IL2WormholeGateway public l2WormholeGateway; uint16 public l2ChainId; uint16 public l1ChainId; address public l1BitcoinDepositor; @@ -79,6 +57,7 @@ contract L2BitcoinDepositor is OwnableUpgradeable { function initialize( address _wormholeRelayer, + address _l2WormholeGateway, uint16 _l2ChainId, uint16 _l1ChainId, address _l1BitcoinDepositor @@ -86,6 +65,7 @@ contract L2BitcoinDepositor is OwnableUpgradeable { __Ownable_init(); wormholeRelayer = IWormholeRelayer(_wormholeRelayer); + l2WormholeGateway = IL2WormholeGateway(_l2WormholeGateway); l2ChainId = _l2ChainId; l1ChainId = _l1ChainId; l1BitcoinDepositor = _l1BitcoinDepositor; @@ -112,7 +92,7 @@ contract L2BitcoinDepositor is OwnableUpgradeable { ); // Cost of requesting a `initializeDeposit` message to be sent to - // `l1Chain` with a gasLimit of `l1InitializeDepositGasLimit`. + // `l1ChainId` with a gasLimit of `l1InitializeDepositGasLimit`. uint256 cost = quoteInitializeDeposit(); require(msg.value == cost, "Payment for Wormhole Relayer is too low"); @@ -161,4 +141,41 @@ contract L2BitcoinDepositor is OwnableUpgradeable { l1InitializeDepositGasLimit ); } + + // TODO: Document this function. + function receiveWormholeMessages( + bytes memory, + bytes[] memory additionalVaas, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 + ) external payable { + require( + msg.sender == address(wormholeRelayer), + "Caller is not Wormhole Relayer" + ); + + require( + sourceChain == l1ChainId, + "Source chain is not the expected L1 chain" + ); + + require( + WormholeUtils.fromWormholeAddress(sourceAddress) == + l1BitcoinDepositor, + "Source address is not the expected L1 Bitcoin depositor" + ); + + require( + additionalVaas.length == 1, + "Expected 1 additional VAA key for token transfer" + ); + + finalizeDeposit(additionalVaas[0]); + } + + // TODO: Document this function. + function finalizeDeposit(bytes memory vaa) internal { + l2WormholeGateway.receiveTbtc(vaa); + } } diff --git a/solidity/contracts/l2/L2WormholeGateway.sol b/solidity/contracts/l2/L2WormholeGateway.sol index 88fec4514..8b2dfa2a4 100644 --- a/solidity/contracts/l2/L2WormholeGateway.sol +++ b/solidity/contracts/l2/L2WormholeGateway.sol @@ -20,51 +20,9 @@ import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable. import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "./Wormhole.sol"; import "./L2TBTC.sol"; -/// @title IWormholeTokenBridge -/// @notice Wormhole Token Bridge interface. Contains only selected functions -/// used by L2WormholeGateway. -interface IWormholeTokenBridge { - function completeTransferWithPayload(bytes memory encodedVm) - external - returns (bytes memory); - - function parseTransferWithPayload(bytes memory encoded) - external - pure - returns (TransferWithPayload memory transfer); - - function transferTokens( - address token, - uint256 amount, - uint16 recipientChain, - bytes32 recipient, - uint256 arbiterFee, - uint32 nonce - ) external payable returns (uint64 sequence); - - function transferTokensWithPayload( - address token, - uint256 amount, - uint16 recipientChain, - bytes32 recipient, - uint32 nonce, - bytes memory payload - ) external payable returns (uint64 sequence); - - struct TransferWithPayload { - uint8 payloadID; - uint256 amount; - bytes32 tokenAddress; - uint16 tokenChain; - bytes32 to; - uint16 toChain; - bytes32 fromAddress; - bytes payload; - } -} - /// @title L2WormholeGateway /// @notice Selected cross-ecosystem bridges are given the minting authority for /// tBTC token on L2 and sidechains. This contract gives a minting @@ -216,7 +174,7 @@ contract L2WormholeGateway is // Normalize the amount to bridge. The dust can not be bridged due to // the decimal shift in the Wormhole Bridge contract. - amount = normalize(amount); + amount = WormholeUtils.normalize(amount); // Check again after dropping the dust. require(amount != 0, "Amount too low to bridge"); @@ -362,7 +320,7 @@ contract L2WormholeGateway is pure returns (bytes32) { - return bytes32(uint256(uint160(_address))); + return WormholeUtils.toWormholeAddress(_address); } /// @notice Converts Wormhole address into Ethereum format. @@ -372,16 +330,6 @@ contract L2WormholeGateway is 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 - function normalize(uint256 amount) internal pure returns (uint256) { - // slither-disable-next-line divide-before-multiply - amount /= 10**10; - amount *= 10**10; - return amount; + return WormholeUtils.fromWormholeAddress(_address); } } diff --git a/solidity/contracts/l2/Wormhole.sol b/solidity/contracts/l2/Wormhole.sol new file mode 100644 index 000000000..975643cc6 --- /dev/null +++ b/solidity/contracts/l2/Wormhole.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ██████████████ ▐████▌ ██████████████ +// ██████████████ ▐████▌ ██████████████ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ +// ▐████▌ ▐████▌ + +pragma solidity ^0.8.17; + +/// @title WormholeTypes +/// @notice Namespace which groups all types relevant to Wormhole interfaces. +library WormholeTypes { + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L22 + struct VaaKey { + uint16 chainId; + bytes32 emitterAddress; + uint64 sequence; + } +} + +/// @title IWormhole +/// @notice Wormhole interface. +/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormhole.sol#L6 +interface IWormhole { + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormhole.sol#L109 + function chainId() external view returns (uint16); + + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormhole.sol#L117 + function messageFee() external view returns (uint256); +} + +/// @title IWormholeRelayer +/// @notice Wormhole Relayer interface. +/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L74 +interface IWormholeRelayer { + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L122 + function sendPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + uint16 refundChain, + address refundAddress + ) external payable returns (uint64 sequence); + + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L442 + function quoteEVMDeliveryPrice( + uint16 targetChain, + uint256 receiverValue, + uint256 gasLimit + ) + external + view + returns ( + uint256 nativePriceQuote, + uint256 targetChainRefundPerGasUnused + ); + + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L182 + function sendVaasToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + WormholeTypes.VaaKey[] memory vaaKeys, + uint16 refundChain, + address refundAddress + ) external payable returns (uint64 sequence); +} + +/// @title IWormholeReceiver +/// @notice Wormhole Receiver interface. +/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeReceiver.sol#L8 +interface IWormholeReceiver { + /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeReceiver.sol#L44 + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalVaas, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable; +} + +/// @title IWormholeTokenBridge +/// @notice Wormhole Token Bridge interface. +/// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/ITokenBridge.sol#L9 +interface IWormholeTokenBridge { + function completeTransferWithPayload(bytes memory encodedVm) + external + returns (bytes memory); + + function parseTransferWithPayload(bytes memory encoded) + external + pure + returns (TransferWithPayload memory transfer); + + function transferTokens( + address token, + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + uint256 arbiterFee, + uint32 nonce + ) external payable returns (uint64 sequence); + + function transferTokensWithPayload( + address token, + uint256 amount, + uint16 recipientChain, + bytes32 recipient, + uint32 nonce, + bytes memory payload + ) external payable returns (uint64 sequence); + + struct TransferWithPayload { + uint8 payloadID; + uint256 amount; + bytes32 tokenAddress; + uint16 tokenChain; + bytes32 to; + uint16 toChain; + bytes32 fromAddress; + bytes payload; + } +} + +/// @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 + function normalize(uint256 amount) internal pure returns (uint256) { + // slither-disable-next-line divide-before-multiply + amount /= 10**10; + amount *= 10**10; + return amount; + } +} diff --git a/solidity/contracts/test/WormholeBridgeStub.sol b/solidity/contracts/test/WormholeBridgeStub.sol index 91bd9d417..7afe9413c 100644 --- a/solidity/contracts/test/WormholeBridgeStub.sol +++ b/solidity/contracts/test/WormholeBridgeStub.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.17; import "./TestERC20.sol"; -import "../l2/L2WormholeGateway.sol"; +import "../l2/Wormhole.sol"; /// @dev Stub contract used in L2WormholeGateway unit tests. /// Stub contract is used instead of a smock because of the token transfer From 7f81fbe3ab2a336c20cacb9e53a1bf8d4d8eedd7 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 1 Mar 2024 12:40:12 +0100 Subject: [PATCH 06/24] Deployment adjustments Here we adjust the new contracts for deployment: - We are introducing the `attach*BitcoinDepositor` functions to solve the chicken & egg problem that occurs upon deployment - We are adjusting gas limits for Wormhole to real-world values - We are getting rid of cross-chain Wormhole refunds that don't work for small amounts --- solidity/contracts/l2/L1BitcoinDepositor.sol | 27 ++++++++++++++------ solidity/contracts/l2/L2BitcoinDepositor.sol | 25 +++++++++++++----- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index f21570a56..f84ec3ab9 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -88,8 +88,7 @@ contract L1BitcoinDepositor is address _wormholeRelayer, address _wormholeTokenBridge, address _l2WormholeGateway, - uint16 _l2ChainId, - address _l2BitcoinDepositor + uint16 _l2ChainId ) external initializer { __AbstractTBTCDepositor_initialize(_tbtcBridge, _tbtcVault); __Ownable_init(); @@ -100,8 +99,23 @@ contract L1BitcoinDepositor is wormholeTokenBridge = IWormholeTokenBridge(_wormholeTokenBridge); l2WormholeGateway = _l2WormholeGateway; l2ChainId = _l2ChainId; + l2FinalizeDepositGasLimit = 500_000; + } + + // TODO: Document this function. + 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; - l2FinalizeDepositGasLimit = 200_000; } // TODO: Document this function. @@ -269,14 +283,11 @@ contract L1BitcoinDepositor is abi.encode(l2Receiver) // Set the L2 receiver address as the transfer payload. ); - // Get Wormhole chain ID for this L1 chain. - uint16 l1ChainId = wormhole.chainId(); - // Construct VAA representing the above Wormhole token transfer. WormholeTypes.VaaKey[] memory additionalVaas = new WormholeTypes.VaaKey[](1); additionalVaas[0] = WormholeTypes.VaaKey({ - chainId: l1ChainId, + chainId: wormhole.chainId(), emitterAddress: WormholeUtils.toWormholeAddress( address(wormholeTokenBridge) ), @@ -296,7 +307,7 @@ contract L1BitcoinDepositor is 0, // No receiver value needed. l2FinalizeDepositGasLimit, additionalVaas, - l1ChainId, // Set this L1 chain as the refund chain. + 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/L2BitcoinDepositor.sol b/solidity/contracts/l2/L2BitcoinDepositor.sol index afec86e4d..f4727f59f 100644 --- a/solidity/contracts/l2/L2BitcoinDepositor.sol +++ b/solidity/contracts/l2/L2BitcoinDepositor.sol @@ -34,7 +34,6 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { // TODO: Document state variables. IWormholeRelayer public wormholeRelayer; IL2WormholeGateway public l2WormholeGateway; - uint16 public l2ChainId; uint16 public l1ChainId; address public l1BitcoinDepositor; uint256 public l1InitializeDepositGasLimit; @@ -58,20 +57,32 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { function initialize( address _wormholeRelayer, address _l2WormholeGateway, - uint16 _l2ChainId, - uint16 _l1ChainId, - address _l1BitcoinDepositor + uint16 _l1ChainId ) external initializer { __Ownable_init(); wormholeRelayer = IWormholeRelayer(_wormholeRelayer); l2WormholeGateway = IL2WormholeGateway(_l2WormholeGateway); - l2ChainId = _l2ChainId; l1ChainId = _l1ChainId; - l1BitcoinDepositor = _l1BitcoinDepositor; l1InitializeDepositGasLimit = 200_000; } + // TODO: Document this function. + function attachL1BitcoinDepositor(address _l1BitcoinDepositor) + external + onlyOwner + { + require( + l1BitcoinDepositor == address(0), + "L1 Bitcoin Depositor already set" + ); + require( + _l1BitcoinDepositor != address(0), + "L1 Bitcoin Depositor must not be 0x0" + ); + l1BitcoinDepositor = _l1BitcoinDepositor; + } + // TODO: Document this function. function updateL1InitializeDepositGasLimit( uint256 _l1InitializeDepositGasLimit @@ -105,7 +116,7 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { abi.encode(fundingTx, reveal, l2DepositOwner), // Message payload. 0, // No receiver value needed. l1InitializeDepositGasLimit, - l2ChainId, // Set this L2 chain as the refund chain. + l1ChainId, // Set the L1 chain as the refund chain to avoid cross-chain refunds. msg.sender // Set the caller as the refund receiver. ); From 533e889c3cd16fb7e46dbba2235c26c1cad97bb7 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 1 Mar 2024 12:41:40 +0100 Subject: [PATCH 07/24] Deployment scripts for Base Here we add deployment scripts for both `L*BitcoinDepositor` contracts. We use Base as the reference chain. --- .../00_deploy_base_l1_bitcoin_depositor.ts | 53 +++++++++++++++++++ .../01_attach_base_l2_bitcoin_depositor.ts | 25 +++++++++ ...fer_base_l1_bitcoin_depositor_ownership.ts | 19 +++++++ .../25_deploy_base_l2_bitcoin_depositor.ts | 48 +++++++++++++++++ .../26_attach_base_l1_bitcoin_depositor.ts | 25 +++++++++ ...fer_base_l2_bitcoin_depositor_ownership.ts | 19 +++++++ cross-chain/base/hardhat.config.ts | 9 ++-- 7 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 cross-chain/base/deploy_l1/00_deploy_base_l1_bitcoin_depositor.ts create mode 100644 cross-chain/base/deploy_l1/01_attach_base_l2_bitcoin_depositor.ts create mode 100644 cross-chain/base/deploy_l1/03_transfer_base_l1_bitcoin_depositor_ownership.ts create mode 100644 cross-chain/base/deploy_l2/25_deploy_base_l2_bitcoin_depositor.ts create mode 100644 cross-chain/base/deploy_l2/26_attach_base_l1_bitcoin_depositor.ts create mode 100644 cross-chain/base/deploy_l2/27_transfer_base_l2_bitcoin_depositor_ownership.ts diff --git a/cross-chain/base/deploy_l1/00_deploy_base_l1_bitcoin_depositor.ts b/cross-chain/base/deploy_l1/00_deploy_base_l1_bitcoin_depositor.ts new file mode 100644 index 000000000..6bf2e6f29 --- /dev/null +++ b/cross-chain/base/deploy_l1/00_deploy_base_l1_bitcoin_depositor.ts @@ -0,0 +1,53 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" +import { getWormholeChains } from "../deploy_helpers/wormhole_chains" + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { ethers, getNamedAccounts, helpers, deployments } = hre + const { deployer } = await getNamedAccounts() + const l2Deployments = hre.companionNetworks.l2.deployments + + const wormholeChains = getWormholeChains(hre.network.name) + + const tbtcBridge = await deployments.get("Bridge") + const tbtcVault = await deployments.get("TBTCVault") + const wormhole = await deployments.get("Wormhole") + const wormholeRelayer = await deployments.get("WormholeRelayer") + const wormholeTokenBridge = await deployments.get("TokenBridge") + const baseWormholeGateway = await l2Deployments.get("BaseWormholeGateway") + + const [, proxyDeployment] = await helpers.upgrades.deployProxy( + "BaseL1BitcoinDepositor", + { + contractName: + "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:L1BitcoinDepositor", + initializerArgs: [ + tbtcBridge.address, + tbtcVault.address, + wormhole.address, + wormholeRelayer.address, + wormholeTokenBridge.address, + baseWormholeGateway.address, + wormholeChains.l2ChainId, + ], + factoryOpts: { signer: await ethers.getSigner(deployer) }, + proxyOpts: { + kind: "transparent", + }, + } + ) + + if (hre.network.tags.etherscan) { + // We use `verify` instead of `verify:verify` as the `verify` task is defined + // in "@openzeppelin/hardhat-upgrades" to perform Etherscan verification + // of Proxy and Implementation contracts. + await hre.run("verify", { + address: proxyDeployment.address, + constructorArgsParams: proxyDeployment.args, + }) + } +} + +export default func + +func.tags = ["BaseL1BitcoinDepositor"] diff --git a/cross-chain/base/deploy_l1/01_attach_base_l2_bitcoin_depositor.ts b/cross-chain/base/deploy_l1/01_attach_base_l2_bitcoin_depositor.ts new file mode 100644 index 000000000..c35aaa21d --- /dev/null +++ b/cross-chain/base/deploy_l1/01_attach_base_l2_bitcoin_depositor.ts @@ -0,0 +1,25 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts } = hre + const { deployer } = await getNamedAccounts() + const { execute } = deployments + const l2Deployments = hre.companionNetworks.l2.deployments + + const baseL2BitcoinDepositor = await l2Deployments.get( + "BaseL2BitcoinDepositor" + ) + + await execute( + "BaseL1BitcoinDepositor", + { from: deployer, log: true, waitConfirmations: 1 }, + "attachL2BitcoinDepositor", + baseL2BitcoinDepositor.address + ) +} + +export default func + +func.tags = ["AttachBaseL2BitcoinDepositor"] +func.dependencies = ["BaseL1BitcoinDepositor"] diff --git a/cross-chain/base/deploy_l1/03_transfer_base_l1_bitcoin_depositor_ownership.ts b/cross-chain/base/deploy_l1/03_transfer_base_l1_bitcoin_depositor_ownership.ts new file mode 100644 index 000000000..7c7d230de --- /dev/null +++ b/cross-chain/base/deploy_l1/03_transfer_base_l1_bitcoin_depositor_ownership.ts @@ -0,0 +1,19 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { getNamedAccounts, helpers } = hre + const { deployer, governance } = await getNamedAccounts() + + await helpers.ownable.transferOwnership( + "BaseL1BitcoinDepositor", + governance, + deployer + ) +} + +export default func + +func.tags = ["TransferBaseL1BitcoinDepositorOwnership"] +func.dependencies = ["BaseL1BitcoinDepositor"] +func.runAtTheEnd = true diff --git a/cross-chain/base/deploy_l2/25_deploy_base_l2_bitcoin_depositor.ts b/cross-chain/base/deploy_l2/25_deploy_base_l2_bitcoin_depositor.ts new file mode 100644 index 000000000..fd24fd0b2 --- /dev/null +++ b/cross-chain/base/deploy_l2/25_deploy_base_l2_bitcoin_depositor.ts @@ -0,0 +1,48 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" +import { getWormholeChains } from "../deploy_helpers/wormhole_chains" + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { ethers, getNamedAccounts, helpers, deployments } = hre + const { deployer } = await getNamedAccounts() + + const wormholeChains = getWormholeChains(hre.network.name) + + const baseWormholeRelayer = await deployments.get("BaseWormholeRelayer") + const baseWormholeGateway = await deployments.get("BaseWormholeGateway") + + const [, proxyDeployment] = await helpers.upgrades.deployProxy( + "BaseL2BitcoinDepositor", + { + contractName: + "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:L2BitcoinDepositor", + initializerArgs: [ + baseWormholeRelayer.address, + baseWormholeGateway.address, + wormholeChains.l1ChainId, + ], + factoryOpts: { signer: await ethers.getSigner(deployer) }, + proxyOpts: { + kind: "transparent", + }, + } + ) + + // Contracts can be verified on L2 Base Etherscan in a similar way as we + // do it on L1 Etherscan + if (hre.network.tags.basescan) { + // We use `verify` instead of `verify:verify` as the `verify` task is defined + // in "@openzeppelin/hardhat-upgrades" to verify the proxy’s implementation + // contract, the proxy itself and any proxy-related contracts, as well as + // link the proxy to the implementation contract’s ABI on (Ether)scan. + await hre.run("verify", { + address: proxyDeployment.address, + constructorArgsParams: [], + }) + } +} + +export default func + +func.tags = ["BaseL2BitcoinDepositor"] +func.dependencies = ["BaseWormholeGateway"] diff --git a/cross-chain/base/deploy_l2/26_attach_base_l1_bitcoin_depositor.ts b/cross-chain/base/deploy_l2/26_attach_base_l1_bitcoin_depositor.ts new file mode 100644 index 000000000..d945af44b --- /dev/null +++ b/cross-chain/base/deploy_l2/26_attach_base_l1_bitcoin_depositor.ts @@ -0,0 +1,25 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts } = hre + const { deployer } = await getNamedAccounts() + const { execute } = deployments + const l1Deployments = hre.companionNetworks.l1.deployments + + const baseL1BitcoinDepositor = await l1Deployments.get( + "BaseL1BitcoinDepositor" + ) + + await execute( + "BaseL2BitcoinDepositor", + { from: deployer, log: true, waitConfirmations: 1 }, + "attachL1BitcoinDepositor", + baseL1BitcoinDepositor.address + ) +} + +export default func + +func.tags = ["AttachBaseL1BitcoinDepositor"] +func.dependencies = ["BaseL2BitcoinDepositor"] diff --git a/cross-chain/base/deploy_l2/27_transfer_base_l2_bitcoin_depositor_ownership.ts b/cross-chain/base/deploy_l2/27_transfer_base_l2_bitcoin_depositor_ownership.ts new file mode 100644 index 000000000..2c499eead --- /dev/null +++ b/cross-chain/base/deploy_l2/27_transfer_base_l2_bitcoin_depositor_ownership.ts @@ -0,0 +1,19 @@ +import type { HardhatRuntimeEnvironment } from "hardhat/types" +import type { DeployFunction } from "hardhat-deploy/types" + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { getNamedAccounts, helpers } = hre + const { deployer, governance } = await getNamedAccounts() + + await helpers.ownable.transferOwnership( + "BaseL2BitcoinDepositor", + governance, + deployer + ) +} + +export default func + +func.tags = ["TransferBaseL2BitcoinDepositorOwnership"] +func.dependencies = ["BaseL2BitcoinDepositor"] +func.runAtTheEnd = true diff --git a/cross-chain/base/hardhat.config.ts b/cross-chain/base/hardhat.config.ts index 9ab642587..9e47a214a 100644 --- a/cross-chain/base/hardhat.config.ts +++ b/cross-chain/base/hardhat.config.ts @@ -52,6 +52,9 @@ const config: HardhatUserConfig = { ? process.env.L1_ACCOUNTS_PRIVATE_KEYS.split(",") : undefined, tags: ["etherscan"], + companionNetworks: { + l2: "baseSepolia", + }, }, mainnet: { url: process.env.L1_CHAIN_API_URL || "", @@ -88,9 +91,9 @@ const config: HardhatUserConfig = { // In case of deployment failing with underpriced transaction error set // the `gasPrice` parameter. // gasPrice: 1000000000, - // companionNetworks: { - // l1: "sepolia", - // }, + companionNetworks: { + l1: "sepolia", + }, }, base: { url: process.env.L2_CHAIN_API_URL || "", From e927830d725e1ee6aa6e9147e3893df655e6b160 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 1 Mar 2024 12:43:41 +0100 Subject: [PATCH 08/24] Deployment artifacts for Base Sepolia We are testing the implementation by deploying it on Base Sepolia chain. Here are the relevant deployment artifacts. --- cross-chain/base/.openzeppelin/sepolia.json | 233 +++++++ .../base/.openzeppelin/unknown-84532.json | 133 ++++ .../test/BaseL1BitcoinDepositorUpgraded.sol | 15 + .../test/BaseL2BitcoinDepositorUpgraded.sol | 15 + .../base/deploy_helpers/wormhole_chains.ts | 31 + .../baseSepolia/BaseL2BitcoinDepositor.json | 427 +++++++++++++ cross-chain/base/deployments/sepolia/.chainId | 1 + .../sepolia/BaseL1BitcoinDepositor.json | 588 ++++++++++++++++++ .../baseSepolia/BaseWormholeRelayer.json | 3 + cross-chain/base/external/sepolia/Bridge.json | 3 + .../base/external/sepolia/TBTCVault.json | 3 + .../base/external/sepolia/Wormhole.json | 3 + .../external/sepolia/WormholeRelayer.json | 3 + 13 files changed, 1458 insertions(+) create mode 100644 cross-chain/base/.openzeppelin/sepolia.json create mode 100644 cross-chain/base/contracts/test/BaseL1BitcoinDepositorUpgraded.sol create mode 100644 cross-chain/base/contracts/test/BaseL2BitcoinDepositorUpgraded.sol create mode 100644 cross-chain/base/deploy_helpers/wormhole_chains.ts create mode 100644 cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json create mode 100644 cross-chain/base/deployments/sepolia/.chainId create mode 100644 cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json create mode 100644 cross-chain/base/external/baseSepolia/BaseWormholeRelayer.json create mode 100644 cross-chain/base/external/sepolia/Bridge.json create mode 100644 cross-chain/base/external/sepolia/TBTCVault.json create mode 100644 cross-chain/base/external/sepolia/Wormhole.json create mode 100644 cross-chain/base/external/sepolia/WormholeRelayer.json diff --git a/cross-chain/base/.openzeppelin/sepolia.json b/cross-chain/base/.openzeppelin/sepolia.json new file mode 100644 index 000000000..1a5db310f --- /dev/null +++ b/cross-chain/base/.openzeppelin/sepolia.json @@ -0,0 +1,233 @@ +{ + "manifestVersion": "3.2", + "admin": { + "address": "0x59dCF5B9f5C5F00C6EdaCD80D88fc1A2F8536787", + "txHash": "0x692393d9edd20bea6eb7179f3c23ac1037c41342597cbb016d16562b3be00255" + }, + "proxies": [ + { + "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "txHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "kind": "transparent" + } + ], + "impls": { + "d1bd959487df6a914f345d6db574fc494c26bc8818086a7af9b2a0dd9a48e771": { + "address": "0xc3BD9ce767534eE4932A4C6FaD4B911094E41CF0", + "txHash": "0xe2d7aa67f89bf83afa4272a412cf4be9136399128ed92c4ec94cf8689704ab2b", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "bridge", + "offset": 0, + "slot": "0", + "type": "t_contract(IBridge)3078", + "contract": "AbstractTBTCDepositor", + "src": "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol:95" + }, + { + "label": "tbtcVault", + "offset": 0, + "slot": "1", + "type": "t_contract(ITBTCVault)3104", + "contract": "AbstractTBTCDepositor", + "src": "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol:96" + }, + { + "label": "__gap", + "offset": 0, + "slot": "2", + "type": "t_array(t_uint256)47_storage", + "contract": "AbstractTBTCDepositor", + "src": "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol:111" + }, + { + "label": "_initialized", + "offset": 0, + "slot": "49", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "49", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "50", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "100", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "deposits", + "offset": 0, + "slot": "150", + "type": "t_mapping(t_uint256,t_enum(DepositState)3127)", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:64" + }, + { + "label": "tbtcToken", + "offset": 0, + "slot": "151", + "type": "t_contract(IERC20)4902", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:69" + }, + { + "label": "wormhole", + "offset": 0, + "slot": "152", + "type": "t_contract(IWormhole)3987", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:70" + }, + { + "label": "wormholeRelayer", + "offset": 0, + "slot": "153", + "type": "t_contract(IWormholeRelayer)4047", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:71" + }, + { + "label": "wormholeTokenBridge", + "offset": 0, + "slot": "154", + "type": "t_contract(IWormholeTokenBridge)4132", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:72" + }, + { + "label": "l2WormholeGateway", + "offset": 0, + "slot": "155", + "type": "t_address", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:73" + }, + { + "label": "l2ChainId", + "offset": 20, + "slot": "155", + "type": "t_uint16", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:77" + }, + { + "label": "l2BitcoinDepositor", + "offset": 0, + "slot": "156", + "type": "t_address", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:77" + }, + { + "label": "l2FinalizeDepositGasLimit", + "offset": 0, + "slot": "157", + "type": "t_uint256", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:77" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)47_storage": { + "label": "uint256[47]", + "numberOfBytes": "1504" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IBridge)3078": { + "label": "contract IBridge", + "numberOfBytes": "20" + }, + "t_contract(IERC20)4902": { + "label": "contract IERC20", + "numberOfBytes": "20" + }, + "t_contract(ITBTCVault)3104": { + "label": "contract ITBTCVault", + "numberOfBytes": "20" + }, + "t_contract(IWormhole)3987": { + "label": "contract IWormhole", + "numberOfBytes": "20" + }, + "t_contract(IWormholeRelayer)4047": { + "label": "contract IWormholeRelayer", + "numberOfBytes": "20" + }, + "t_contract(IWormholeTokenBridge)4132": { + "label": "contract IWormholeTokenBridge", + "numberOfBytes": "20" + }, + "t_enum(DepositState)3127": { + "label": "enum L1BitcoinDepositor.DepositState", + "members": [ + "Unknown", + "Initialized", + "Finalized" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_uint256,t_enum(DepositState)3127)": { + "label": "mapping(uint256 => enum L1BitcoinDepositor.DepositState)", + "numberOfBytes": "32" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + } + } +} diff --git a/cross-chain/base/.openzeppelin/unknown-84532.json b/cross-chain/base/.openzeppelin/unknown-84532.json index c94e49b5b..ddcd1f325 100644 --- a/cross-chain/base/.openzeppelin/unknown-84532.json +++ b/cross-chain/base/.openzeppelin/unknown-84532.json @@ -14,6 +14,11 @@ "address": "0xc3D46e0266d95215589DE639cC4E93b79f88fc6C", "txHash": "0xc362207cb5c36e09f5eae5bcc206c760e01af0a042fb7a48f6a4b2078eafcd24", "kind": "transparent" + }, + { + "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "txHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "kind": "transparent" } ], "impls": { @@ -453,6 +458,134 @@ } } } + }, + "158e85b6f8306c53687c3df734c1d1b840db0b13301465860bf98e15ff8b1b13": { + "address": "0x1a53759DE2eADf73bd0b05c07a4F1F5B7912dA3d", + "txHash": "0xe1026e1340ff20ac460112ac2de7718f92f87897d00f6ad15cd59530f8179e4a", + "layout": { + "solcVersion": "0.8.17", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:62", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:67" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "52", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "wormholeRelayer", + "offset": 0, + "slot": "101", + "type": "t_contract(IWormholeRelayer)4047", + "contract": "L2BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:49" + }, + { + "label": "l2WormholeGateway", + "offset": 0, + "slot": "102", + "type": "t_contract(IL2WormholeGateway)3662", + "contract": "L2BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:52" + }, + { + "label": "l1ChainId", + "offset": 20, + "slot": "102", + "type": "t_uint16", + "contract": "L2BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:53" + }, + { + "label": "l1BitcoinDepositor", + "offset": 0, + "slot": "103", + "type": "t_address", + "contract": "L2BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:54" + }, + { + "label": "l1InitializeDepositGasLimit", + "offset": 0, + "slot": "104", + "type": "t_uint256", + "contract": "L2BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IL2WormholeGateway)3662": { + "label": "contract IL2WormholeGateway", + "numberOfBytes": "20" + }, + "t_contract(IWormholeRelayer)4047": { + "label": "contract IWormholeRelayer", + "numberOfBytes": "20" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } } } } diff --git a/cross-chain/base/contracts/test/BaseL1BitcoinDepositorUpgraded.sol b/cross-chain/base/contracts/test/BaseL1BitcoinDepositorUpgraded.sol new file mode 100644 index 000000000..eeed54f12 --- /dev/null +++ b/cross-chain/base/contracts/test/BaseL1BitcoinDepositorUpgraded.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.17; + +import "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol"; + +/// @notice L1BitcoinDepositor for Base - upgraded version. +/// @dev This contract is intended solely for testing purposes. +contract BaseL1BitcoinDepositorUpgraded is L1BitcoinDepositor { + string public newVar; + + function initializeV2(string memory _newVar) public { + newVar = _newVar; + } +} \ No newline at end of file diff --git a/cross-chain/base/contracts/test/BaseL2BitcoinDepositorUpgraded.sol b/cross-chain/base/contracts/test/BaseL2BitcoinDepositorUpgraded.sol new file mode 100644 index 000000000..ca5d32a25 --- /dev/null +++ b/cross-chain/base/contracts/test/BaseL2BitcoinDepositorUpgraded.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.17; + +import "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol"; + +/// @notice L2BitcoinDepositor for Base - upgraded version. +/// @dev This contract is intended solely for testing purposes. +contract BaseL2BitcoinDepositorUpgraded is L2BitcoinDepositor { + string public newVar; + + function initializeV2(string memory _newVar) public { + newVar = _newVar; + } +} \ No newline at end of file diff --git a/cross-chain/base/deploy_helpers/wormhole_chains.ts b/cross-chain/base/deploy_helpers/wormhole_chains.ts new file mode 100644 index 000000000..6ead540a1 --- /dev/null +++ b/cross-chain/base/deploy_helpers/wormhole_chains.ts @@ -0,0 +1,31 @@ +export type WormholeChains = { + l1ChainId: number + l2ChainId: number +} + +/** + * Returns Wormhole L1 and L2 chain IDs for the given network. + * Source: https://docs.wormhole.com/wormhole/reference/constants#chain-ids + * @param network Network name. + */ +export function getWormholeChains(network: string): WormholeChains { + let l1ChainId: number + let l2ChainId: number + + switch (network) { + case "mainnet": + case "base": + l1ChainId = 2 // L1 Ethereum mainnet + l2ChainId = 30 // L2 Base mainnet + break + case "sepolia": + case "baseSepolia": + l1ChainId = 10002 // L1 Ethereum Sepolia testnet + l2ChainId = 10004 // L2 Base Sepolia testnet + break + default: + throw new Error("Unsupported network") + } + + return { l1ChainId, l2ChainId } +} diff --git a/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json b/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json new file mode 100644 index 000000000..fe2c88640 --- /dev/null +++ b/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json @@ -0,0 +1,427 @@ +{ + "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "depositKey", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2DepositOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2Sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "wormholeMessageSequence", + "type": "uint64" + } + ], + "name": "DepositInitialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "l1InitializeDepositGasLimit", + "type": "uint256" + } + ], + "name": "L1InitializeDepositGasLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l1BitcoinDepositor", + "type": "address" + } + ], + "name": "attachL1BitcoinDepositor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_wormholeRelayer", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2WormholeGateway", + "type": "address" + }, + { + "internalType": "uint16", + "name": "_l1ChainId", + "type": "uint16" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes4", + "name": "version", + "type": "bytes4" + }, + { + "internalType": "bytes", + "name": "inputVector", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "outputVector", + "type": "bytes" + }, + { + "internalType": "bytes4", + "name": "locktime", + "type": "bytes4" + } + ], + "internalType": "struct IBridgeTypes.BitcoinTxInfo", + "name": "fundingTx", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "fundingOutputIndex", + "type": "uint32" + }, + { + "internalType": "bytes8", + "name": "blindingFactor", + "type": "bytes8" + }, + { + "internalType": "bytes20", + "name": "walletPubKeyHash", + "type": "bytes20" + }, + { + "internalType": "bytes20", + "name": "refundPubKeyHash", + "type": "bytes20" + }, + { + "internalType": "bytes4", + "name": "refundLocktime", + "type": "bytes4" + }, + { + "internalType": "address", + "name": "vault", + "type": "address" + } + ], + "internalType": "struct IBridgeTypes.DepositRevealInfo", + "name": "reveal", + "type": "tuple" + }, + { + "internalType": "address", + "name": "l2DepositOwner", + "type": "address" + } + ], + "name": "initializeDeposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "l1BitcoinDepositor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1ChainId", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1InitializeDepositGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l2WormholeGateway", + "outputs": [ + { + "internalType": "contract IL2WormholeGateway", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "quoteInitializeDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "cost", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "additionalVaas", + "type": "bytes[]" + }, + { + "internalType": "bytes32", + "name": "sourceAddress", + "type": "bytes32" + }, + { + "internalType": "uint16", + "name": "sourceChain", + "type": "uint16" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "receiveWormholeMessages", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l1InitializeDepositGasLimit", + "type": "uint256" + } + ], + "name": "updateL1InitializeDepositGasLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wormholeRelayer", + "outputs": [ + { + "internalType": "contract IWormholeRelayer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "receipt": { + "to": null, + "from": "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", + "contractAddress": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "transactionIndex": 4, + "gasUsed": "726908", + "logsBloom": "0x00000000000000000000000000000000400000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080202000001000000000000002000000000000000000000020000000000000000000800000000800080000000000000000000400000000200000000000000000000000000000000000080000000000000800020000000000000000000000000000400000000000000000000000000000001000000000020000000008000000020040000000000000400000000000000000020000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a", + "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "logs": [ + { + "transactionIndex": 4, + "blockNumber": 6731912, + "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x0000000000000000000000001a53759de2eadf73bd0b05c07a4f1f5b7912da3d" + ], + "data": "0x", + "logIndex": 0, + "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + }, + { + "transactionIndex": 4, + "blockNumber": 6731912, + "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000068ad60cc5e8f3b7cc53beab321cf0e6036962dbc" + ], + "data": "0x", + "logIndex": 1, + "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + }, + { + "transactionIndex": 4, + "blockNumber": 6731912, + "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "topics": [ + "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 2, + "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + }, + { + "transactionIndex": 4, + "blockNumber": 6731912, + "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "topics": [ + "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2f6c5b73239c39360ee0ea95047565dab13e3c7", + "logIndex": 3, + "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + } + ], + "blockNumber": 6731912, + "cumulativeGasUsed": "834783", + "status": 1, + "byzantium": true + }, + "numDeployments": 1, + "implementation": "0x1a53759DE2eADf73bd0b05c07a4F1F5B7912dA3d", + "devdoc": "Contract deployed as upgradable proxy" +} \ No newline at end of file diff --git a/cross-chain/base/deployments/sepolia/.chainId b/cross-chain/base/deployments/sepolia/.chainId new file mode 100644 index 000000000..bd8d1cd44 --- /dev/null +++ b/cross-chain/base/deployments/sepolia/.chainId @@ -0,0 +1 @@ +11155111 \ No newline at end of file diff --git a/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json b/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json new file mode 100644 index 000000000..198538443 --- /dev/null +++ b/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json @@ -0,0 +1,588 @@ +{ + "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "depositKey", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2DepositOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l1Sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "initialAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tbtcAmount", + "type": "uint256" + } + ], + "name": "DepositFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "depositKey", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "l2DepositOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "l1Sender", + "type": "address" + } + ], + "name": "DepositInitialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "l2FinalizeDepositGasLimit", + "type": "uint256" + } + ], + "name": "L2FinalizeDepositGasLimitUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "SATOSHI_MULTIPLIER", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_l2BitcoinDepositor", + "type": "address" + } + ], + "name": "attachL2BitcoinDepositor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "bridge", + "outputs": [ + { + "internalType": "contract IBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "deposits", + "outputs": [ + { + "internalType": "enum L1BitcoinDepositor.DepositState", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "depositKey", + "type": "uint256" + } + ], + "name": "finalizeDeposit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_tbtcBridge", + "type": "address" + }, + { + "internalType": "address", + "name": "_tbtcVault", + "type": "address" + }, + { + "internalType": "address", + "name": "_wormhole", + "type": "address" + }, + { + "internalType": "address", + "name": "_wormholeRelayer", + "type": "address" + }, + { + "internalType": "address", + "name": "_wormholeTokenBridge", + "type": "address" + }, + { + "internalType": "address", + "name": "_l2WormholeGateway", + "type": "address" + }, + { + "internalType": "uint16", + "name": "_l2ChainId", + "type": "uint16" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes4", + "name": "version", + "type": "bytes4" + }, + { + "internalType": "bytes", + "name": "inputVector", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "outputVector", + "type": "bytes" + }, + { + "internalType": "bytes4", + "name": "locktime", + "type": "bytes4" + } + ], + "internalType": "struct IBridgeTypes.BitcoinTxInfo", + "name": "fundingTx", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "fundingOutputIndex", + "type": "uint32" + }, + { + "internalType": "bytes8", + "name": "blindingFactor", + "type": "bytes8" + }, + { + "internalType": "bytes20", + "name": "walletPubKeyHash", + "type": "bytes20" + }, + { + "internalType": "bytes20", + "name": "refundPubKeyHash", + "type": "bytes20" + }, + { + "internalType": "bytes4", + "name": "refundLocktime", + "type": "bytes4" + }, + { + "internalType": "address", + "name": "vault", + "type": "address" + } + ], + "internalType": "struct IBridgeTypes.DepositRevealInfo", + "name": "reveal", + "type": "tuple" + }, + { + "internalType": "address", + "name": "l2DepositOwner", + "type": "address" + } + ], + "name": "initializeDeposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "l2BitcoinDepositor", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l2ChainId", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l2FinalizeDepositGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l2WormholeGateway", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "quoteFinalizeDeposit", + "outputs": [ + { + "internalType": "uint256", + "name": "cost", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "payload", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "", + "type": "bytes[]" + }, + { + "internalType": "bytes32", + "name": "sourceAddress", + "type": "bytes32" + }, + { + "internalType": "uint16", + "name": "sourceChain", + "type": "uint16" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "receiveWormholeMessages", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "tbtcToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tbtcVault", + "outputs": [ + { + "internalType": "contract ITBTCVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_l2FinalizeDepositGasLimit", + "type": "uint256" + } + ], + "name": "updateL2FinalizeDepositGasLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "wormhole", + "outputs": [ + { + "internalType": "contract IWormhole", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "wormholeRelayer", + "outputs": [ + { + "internalType": "contract IWormholeRelayer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "wormholeTokenBridge", + "outputs": [ + { + "internalType": "contract IWormholeTokenBridge", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "receipt": { + "to": null, + "from": "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", + "contractAddress": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "transactionIndex": 195, + "gasUsed": "843614", + "logsBloom": "0x00000000000020000000000000000000400004000000000000800000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000804000000000000000000000400000000200000000000000000000000000000000000080000000000000800000000000000000000000000000000400000000000000000000000000000000000000000020000080000000000020040000000000000400000000000000000020000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d", + "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "logs": [ + { + "transactionIndex": 195, + "blockNumber": 5389241, + "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x000000000000000000000000c3bd9ce767534ee4932a4c6fad4b911094e41cf0" + ], + "data": "0x", + "logIndex": 315, + "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + }, + { + "transactionIndex": 195, + "blockNumber": 5389241, + "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "topics": [ + "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x00000000000000000000000068ad60cc5e8f3b7cc53beab321cf0e6036962dbc" + ], + "data": "0x", + "logIndex": 316, + "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + }, + { + "transactionIndex": 195, + "blockNumber": 5389241, + "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "topics": [ + "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "logIndex": 317, + "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + }, + { + "transactionIndex": 195, + "blockNumber": 5389241, + "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "topics": [ + "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" + ], + "data": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000059dcf5b9f5c5f00c6edacd80d88fc1a2f8536787", + "logIndex": 318, + "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + } + ], + "blockNumber": 5389241, + "cumulativeGasUsed": "21487798", + "status": 1, + "byzantium": true + }, + "numDeployments": 1, + "implementation": "0xc3BD9ce767534eE4932A4C6FaD4B911094E41CF0", + "devdoc": "Contract deployed as upgradable proxy" +} \ No newline at end of file diff --git a/cross-chain/base/external/baseSepolia/BaseWormholeRelayer.json b/cross-chain/base/external/baseSepolia/BaseWormholeRelayer.json new file mode 100644 index 000000000..7b945deb8 --- /dev/null +++ b/cross-chain/base/external/baseSepolia/BaseWormholeRelayer.json @@ -0,0 +1,3 @@ +{ + "address": "0x93BAD53DDfB6132b0aC8E37f6029163E63372cEE" +} \ No newline at end of file diff --git a/cross-chain/base/external/sepolia/Bridge.json b/cross-chain/base/external/sepolia/Bridge.json new file mode 100644 index 000000000..4a4dba6e1 --- /dev/null +++ b/cross-chain/base/external/sepolia/Bridge.json @@ -0,0 +1,3 @@ +{ + "address": "0x9b1a7fE5a16A15F2f9475C5B231750598b113403" +} \ No newline at end of file diff --git a/cross-chain/base/external/sepolia/TBTCVault.json b/cross-chain/base/external/sepolia/TBTCVault.json new file mode 100644 index 000000000..25914993d --- /dev/null +++ b/cross-chain/base/external/sepolia/TBTCVault.json @@ -0,0 +1,3 @@ +{ + "address": "0xB5679dE944A79732A75CE556191DF11F489448d5" +} \ No newline at end of file diff --git a/cross-chain/base/external/sepolia/Wormhole.json b/cross-chain/base/external/sepolia/Wormhole.json new file mode 100644 index 000000000..f83c46ca5 --- /dev/null +++ b/cross-chain/base/external/sepolia/Wormhole.json @@ -0,0 +1,3 @@ +{ + "address": "0x4a8bc80Ed5a4067f1CCf107057b8270E0cC11A78" +} \ No newline at end of file diff --git a/cross-chain/base/external/sepolia/WormholeRelayer.json b/cross-chain/base/external/sepolia/WormholeRelayer.json new file mode 100644 index 000000000..677a1e21d --- /dev/null +++ b/cross-chain/base/external/sepolia/WormholeRelayer.json @@ -0,0 +1,3 @@ +{ + "address": "0x7B1bD7a6b4E61c2a123AC6BC2cbfC614437D0470" +} \ No newline at end of file From 4adf5537ff68ca4ee3538ffc90bc4b92628ef935 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 4 Mar 2024 13:13:34 +0100 Subject: [PATCH 09/24] Do not use Wormhole Relayer for L2 -> L1 deposit reveal Such a deposit reveal was tested for Base -> Ethereum and turned out to be super expensive. It takes ~0,045 ETH so ~150 USD to transfer deposit data to Ethereum. This is an unacceptable cost. To overcome that problem, we are abandoning the idea of using Wormhole Relay for that in favor of our own relay bot implementation. This also makes sense as eventually, we are aiming towards using gas-less reveals on Base. --- solidity/contracts/l2/L1BitcoinDepositor.sol | 55 ++----------- solidity/contracts/l2/L2BitcoinDepositor.sol | 87 ++------------------ solidity/contracts/l2/Wormhole.sol | 11 --- 3 files changed, 13 insertions(+), 140 deletions(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index f84ec3ab9..47bb63023 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -27,7 +27,6 @@ import "./Wormhole.sol"; // TODO: Document this contract. contract L1BitcoinDepositor is AbstractTBTCDepositor, - IWormholeReceiver, OwnableUpgradeable { using SafeERC20 for IERC20; @@ -36,9 +35,9 @@ contract L1BitcoinDepositor is /// - Unknown deposit has not been initialized yet. /// - Initialized deposit has been initialized with a call to /// `initializeDeposit` function and is known to this contract. - /// - Finalized deposit led to tBTC ERC20 minting and was finalized - /// with a call to `finalizeDeposit` function that deposited tBTC - /// to the Portal contract. + /// - Finalized deposit led to TBTC ERC20 minting and was finalized + /// with a call to `finalizeDeposit` function that transferred + /// TBTC ERC20 to the L2 deposit owner. enum DepositState { Unknown, Initialized, @@ -127,50 +126,10 @@ contract L1BitcoinDepositor is emit L2FinalizeDepositGasLimitUpdated(_l2FinalizeDepositGasLimit); } - // TODO: Document this function. - function receiveWormholeMessages( - bytes memory payload, - bytes[] memory, - bytes32 sourceAddress, - uint16 sourceChain, - bytes32 - ) external payable { - require( - msg.sender == address(wormholeRelayer), - "Caller is not Wormhole Relayer" - ); - - require( - sourceChain == l2ChainId, - "Source chain is not the expected L2 chain" - ); - - require( - WormholeUtils.fromWormholeAddress(sourceAddress) == - l2BitcoinDepositor, - "Source address is not the expected L2 Bitcoin depositor" - ); - - ( - IBridgeTypes.BitcoinTxInfo memory fundingTx, - IBridgeTypes.DepositRevealInfo memory reveal, - address l2DepositOwner - ) = abi.decode( - payload, - ( - IBridgeTypes.BitcoinTxInfo, - IBridgeTypes.DepositRevealInfo, - address - ) - ); - - initializeDeposit(fundingTx, reveal, l2DepositOwner); - } - // TODO: Document this function. function initializeDeposit( - IBridgeTypes.BitcoinTxInfo memory fundingTx, - IBridgeTypes.DepositRevealInfo memory reveal, + IBridgeTypes.BitcoinTxInfo calldata fundingTx, + IBridgeTypes.DepositRevealInfo calldata reveal, address l2DepositOwner ) public { require( @@ -200,7 +159,7 @@ contract L1BitcoinDepositor is } // TODO: Document this function. - function finalizeDeposit(uint256 depositKey) external payable { + function finalizeDeposit(uint256 depositKey) public payable { require( deposits[depositKey] == DepositState.Initialized, "Wrong deposit state" @@ -228,7 +187,7 @@ contract L1BitcoinDepositor is } // TODO: Document this function. - function quoteFinalizeDeposit() public view returns (uint256 cost) { + function quoteFinalizeDeposit() external view returns (uint256 cost) { cost = _quoteFinalizeDeposit(wormhole.messageFee()); } diff --git a/solidity/contracts/l2/L2BitcoinDepositor.sol b/solidity/contracts/l2/L2BitcoinDepositor.sol index f4727f59f..3b00313d6 100644 --- a/solidity/contracts/l2/L2BitcoinDepositor.sol +++ b/solidity/contracts/l2/L2BitcoinDepositor.sol @@ -15,7 +15,6 @@ pragma solidity ^0.8.17; -import {BTCUtils} from "@keep-network/bitcoin-spv-sol/contracts/BTCUtils.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../integrator/IBridge.sol"; @@ -29,24 +28,17 @@ interface IL2WormholeGateway { // TODO: Document this contract. contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { - using BTCUtils for bytes; - // TODO: Document state variables. IWormholeRelayer public wormholeRelayer; IL2WormholeGateway public l2WormholeGateway; uint16 public l1ChainId; address public l1BitcoinDepositor; - uint256 public l1InitializeDepositGasLimit; event DepositInitialized( - uint256 indexed depositKey, + IBridgeTypes.BitcoinTxInfo fundingTx, + IBridgeTypes.DepositRevealInfo reveal, address indexed l2DepositOwner, - address indexed l2Sender, - uint64 wormholeMessageSequence - ); - - event L1InitializeDepositGasLimitUpdated( - uint256 l1InitializeDepositGasLimit + address indexed l2Sender ); /// @custom:oz-upgrades-unsafe-allow constructor @@ -64,7 +56,6 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { wormholeRelayer = IWormholeRelayer(_wormholeRelayer); l2WormholeGateway = IL2WormholeGateway(_l2WormholeGateway); l1ChainId = _l1ChainId; - l1InitializeDepositGasLimit = 200_000; } // TODO: Document this function. @@ -83,74 +74,13 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { l1BitcoinDepositor = _l1BitcoinDepositor; } - // TODO: Document this function. - function updateL1InitializeDepositGasLimit( - uint256 _l1InitializeDepositGasLimit - ) external onlyOwner { - l1InitializeDepositGasLimit = _l1InitializeDepositGasLimit; - emit L1InitializeDepositGasLimitUpdated(_l1InitializeDepositGasLimit); - } - // TODO: Document this function. function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, address l2DepositOwner - ) external payable { - require( - l2DepositOwner != address(0), - "L2 deposit owner must not be 0x0" - ); - - // Cost of requesting a `initializeDeposit` message to be sent to - // `l1ChainId` with a gasLimit of `l1InitializeDepositGasLimit`. - uint256 cost = quoteInitializeDeposit(); - - require(msg.value == cost, "Payment for Wormhole Relayer is too low"); - - uint64 wormholeMessageSequence = wormholeRelayer.sendPayloadToEvm{ - value: cost - }( - l1ChainId, - l1BitcoinDepositor, - abi.encode(fundingTx, reveal, l2DepositOwner), // Message payload. - 0, // No receiver value needed. - l1InitializeDepositGasLimit, - l1ChainId, // Set the L1 chain as the refund chain to avoid cross-chain refunds. - msg.sender // Set the caller as the refund receiver. - ); - - uint256 depositKey = uint256( - keccak256( - abi.encodePacked( - abi - .encodePacked( - fundingTx.version, - fundingTx.inputVector, - fundingTx.outputVector, - fundingTx.locktime - ) - .hash256View(), - reveal.fundingOutputIndex - ) - ) - ); - - emit DepositInitialized( - depositKey, - l2DepositOwner, - msg.sender, - wormholeMessageSequence - ); - } - - // TODO: Document this function. - function quoteInitializeDeposit() public view returns (uint256 cost) { - (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( - l1ChainId, - 0, // No receiver value needed. - l1InitializeDepositGasLimit - ); + ) external { + emit DepositInitialized(fundingTx, reveal, l2DepositOwner, msg.sender); } // TODO: Document this function. @@ -182,11 +112,6 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { "Expected 1 additional VAA key for token transfer" ); - finalizeDeposit(additionalVaas[0]); - } - - // TODO: Document this function. - function finalizeDeposit(bytes memory vaa) internal { - l2WormholeGateway.receiveTbtc(vaa); + l2WormholeGateway.receiveTbtc(additionalVaas[0]); } } diff --git a/solidity/contracts/l2/Wormhole.sol b/solidity/contracts/l2/Wormhole.sol index 975643cc6..2721d6a44 100644 --- a/solidity/contracts/l2/Wormhole.sol +++ b/solidity/contracts/l2/Wormhole.sol @@ -41,17 +41,6 @@ interface IWormhole { /// @notice Wormhole Relayer interface. /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L74 interface IWormholeRelayer { - /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L122 - function sendPayloadToEvm( - uint16 targetChain, - address targetAddress, - bytes memory payload, - uint256 receiverValue, - uint256 gasLimit, - uint16 refundChain, - address refundAddress - ) external payable returns (uint64 sequence); - /// @dev See: https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/2b7db51f99b49eda99b44f4a044e751cb0b2e8ea/src/interfaces/IWormholeRelayer.sol#L442 function quoteEVMDeliveryPrice( uint16 targetChain, From 59a337fa11ed6a70e8292551650c8b51b2f89c54 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Mon, 4 Mar 2024 17:11:41 +0100 Subject: [PATCH 10/24] Reimbursements for deposit initialization and finalization --- solidity/contracts/l2/L1BitcoinDepositor.sol | 133 ++++++++++++++++++- 1 file changed, 128 insertions(+), 5 deletions(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index 47bb63023..7a90636e6 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -15,6 +15,7 @@ pragma solidity ^0.8.17; +import "@keep-network/random-beacon/contracts/Reimbursable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -27,7 +28,8 @@ import "./Wormhole.sol"; // TODO: Document this contract. contract L1BitcoinDepositor is AbstractTBTCDepositor, - OwnableUpgradeable + OwnableUpgradeable, + Reimbursable { using SafeERC20 for IERC20; @@ -44,11 +46,18 @@ contract L1BitcoinDepositor is Finalized } + /// @notice Holds information about a deferred gas reimbursement. + struct GasReimbursement { + /// @notice Receiver that is supposed to receive the reimbursement. + address receiver; + /// @notice Gas expenditure that is meant to be reimbursed. + uint256 gasSpent; + } + /// @notice Holds the deposit state, keyed by the deposit key calculated for /// the individual deposit during the call to `initializeDeposit` /// function. mapping(uint256 => DepositState) public deposits; - // TODO: Document other state variables. IERC20 public tbtcToken; IWormhole public wormhole; @@ -58,6 +67,21 @@ contract L1BitcoinDepositor is uint16 public l2ChainId; address public l2BitcoinDepositor; 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 + /// because the tBTC Bridge accepts all (even invalid) deposits but + /// mints ERC20 TBTC only for the valid ones. Paying out the + /// reimbursement directly upon initialization would make the + /// reimbursement pool vulnerable to malicious actors that could + /// drain it by initializing invalid deposits. + mapping(uint256 => GasReimbursement) public gasReimbursements; + /// @notice Gas that is meant to balance the overall cost of deposit initialization. + /// Can be updated by the owner based on the current market conditions. + uint256 public initializeDepositGasOffset; + /// @notice Gas that is meant to balance the overall cost of deposit finalization. + /// Can be updated by the owner based on the current market conditions. + uint256 public finalizeDepositGasOffset; event DepositInitialized( uint256 indexed depositKey, @@ -75,6 +99,18 @@ contract L1BitcoinDepositor is event L2FinalizeDepositGasLimitUpdated(uint256 l2FinalizeDepositGasLimit); + event GasOffsetParametersUpdated( + uint256 initializeDepositGasOffset, + uint256 finalizeDepositGasOffset + ); + + /// @dev This modifier comes from the `Reimbursable` base contract and + /// must be overridden to protect the `updateReimbursementPool` call. + modifier onlyReimbursableAdmin() override { + require(msg.sender == owner(), "Caller is not the owner"); + _; + } + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -99,6 +135,8 @@ contract L1BitcoinDepositor is l2WormholeGateway = _l2WormholeGateway; l2ChainId = _l2ChainId; l2FinalizeDepositGasLimit = 500_000; + initializeDepositGasOffset = 20_000; + finalizeDepositGasOffset = 20_000; } // TODO: Document this function. @@ -126,12 +164,32 @@ contract L1BitcoinDepositor is 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. + /// @param newInitializeDepositGasOffset New initialize deposit gas offset. + /// @param newFinalizeDepositGasOffset New finalize deposit gas offset. + function updateGasOffsetParameters( + uint256 newInitializeDepositGasOffset, + uint256 newFinalizeDepositGasOffset + ) external onlyOwner { + initializeDepositGasOffset = newInitializeDepositGasOffset; + finalizeDepositGasOffset = newFinalizeDepositGasOffset; + + emit GasOffsetParametersUpdated( + newInitializeDepositGasOffset, + newFinalizeDepositGasOffset + ); + } + // TODO: Document this function. function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, address l2DepositOwner - ) public { + ) external { + uint256 gasStart = gasleft(); + require( l2DepositOwner != address(0), "L2 deposit owner must not be 0x0" @@ -156,10 +214,26 @@ contract L1BitcoinDepositor is deposits[depositKey] = DepositState.Initialized; emit DepositInitialized(depositKey, l2DepositOwner, msg.sender); + + if (address(reimbursementPool) != address(0)) { + // Do not issue a reimbursement immediately. Record + // a deferred reimbursement that will be paid out upon deposit + // finalization. This is because the tBTC Bridge accepts all + // (even invalid) deposits but mints ERC20 TBTC only for the valid + // ones. Paying out the reimbursement directly upon initialization + // would make the reimbursement pool vulnerable to malicious actors + // that could drain it by initializing invalid deposits. + gasReimbursements[depositKey] = GasReimbursement({ + receiver: msg.sender, + gasSpent: (gasStart - gasleft()) + initializeDepositGasOffset + }); + } } // TODO: Document this function. function finalizeDeposit(uint256 depositKey) public payable { + uint256 gasStart = gasleft(); + require( deposits[depositKey] == DepositState.Initialized, "Wrong deposit state" @@ -183,7 +257,56 @@ contract L1BitcoinDepositor is tbtcAmount ); - transferTbtc(tbtcAmount, l2DepositOwner); + _transferTbtc(tbtcAmount, l2DepositOwner); + + if (address(reimbursementPool) != address(0)) { + // If there is a deferred reimbursement for this deposit + // initialization, pay it out now. + GasReimbursement memory reimbursement = gasReimbursements[ + depositKey + ]; + if (reimbursement.receiver != address(0)) { + delete gasReimbursements[depositKey]; + + reimbursementPool.refund( + reimbursement.gasSpent, + reimbursement.receiver + ); + } + + // Pay out the reimbursement for deposit finalization. As this + // call is payable and this transaction carries out a msg.value + // that covers Wormhole 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. + uint256 msgValueOffset = _refundToGasSpent(msg.value); + reimbursementPool.refund( + (gasStart - gasleft()) + + msgValueOffset + + finalizeDepositGasOffset, + msg.sender + ); + } + } + + /// @notice The `ReimbursementPool` contract issues refunds based on + /// gas spent. If there is a need to get a specific refund based + /// on WEI value, such a value must be first converted to gas spent. + /// This function does such a conversion. + /// @param refund Refund value in WEI. + /// @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) { + uint256 maxGasPrice = reimbursementPool.maxGasPrice(); + uint256 staticGas = reimbursementPool.staticGas(); + + uint256 gasPrice = tx.gasprice < maxGasPrice + ? tx.gasprice + : maxGasPrice; + + return (refund / gasPrice) - staticGas; } // TODO: Document this function. @@ -210,7 +333,7 @@ contract L1BitcoinDepositor is } // TODO: Document this function. - function transferTbtc(uint256 amount, bytes32 l2Receiver) internal { + 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); From 4aa0886696defb15e29d29d6d70f5ab44b820e91 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 5 Mar 2024 09:16:27 +0100 Subject: [PATCH 11/24] Add note about `ReimbursementPool` reentrancy risk --- solidity/contracts/l2/L1BitcoinDepositor.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index 7a90636e6..63e3037d0 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -259,6 +259,10 @@ contract L1BitcoinDepositor is _transferTbtc(tbtcAmount, l2DepositOwner); + // `ReimbursementPool` calls the untrusted receiver address using a + // low-level call. Reentrancy risk is mitigated by making sure that + // `ReimbursementPool.refund` is a non-reentrant function and executing + // reimbursements as the last step of the deposit finalization. if (address(reimbursementPool) != address(0)) { // If there is a deferred reimbursement for this deposit // initialization, pay it out now. From 49d2bb9caff530a71f21011d04ef2ae08d3f60e6 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 5 Mar 2024 13:02:31 +0100 Subject: [PATCH 12/24] Remaining documentation for the `L1BitcoinDepositor` contract --- solidity/contracts/l2/L1BitcoinDepositor.sol | 226 +++++++++++++++++-- 1 file changed, 204 insertions(+), 22 deletions(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index 63e3037d0..fd6f504cf 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -25,7 +25,49 @@ import "../integrator/IBridge.sol"; import "../integrator/ITBTCVault.sol"; import "./Wormhole.sol"; -// TODO: Document this contract. +/// @title L1BitcoinDepositor +/// @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. +/// +/// `L1BitcoinDepositor` is deployed on the L1 chain and interacts with +/// their L2 counterpart, the `L2BitcoinDepositor`, deployed on the given +/// L2 chain. Each `L1BitcoinDepositor` & `L2BitcoinDepositor` pair is +/// responsible for a specific L2 chain. +/// +/// The outline of the direct bridging mechanism is as follows: +/// 1. An L2 user issues a Bitcoin funding transaction to a P2(W)SH +/// deposit address that embeds the `L1BitcoinDepositor` contract +/// and L2 user addresses. The `L1BitcoinDepositor` contract serves +/// as the actual depositor on the L1 chain while the L2 user +/// address is set as the deposit owner who will receive the +/// minted ERC20 TBTC. +/// 2. The data about the Bitcoin funding transaction and deposit +/// address are passed to the relayer. In the first iteration of +/// the direct bridging mechanism, this is achieved using an +/// 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 +/// address. contract L1BitcoinDepositor is AbstractTBTCDepositor, OwnableUpgradeable, @@ -58,14 +100,25 @@ contract L1BitcoinDepositor is /// the individual deposit during the call to `initializeDeposit` /// function. mapping(uint256 => DepositState) public deposits; - // TODO: Document other state variables. + /// @notice ERC20 L1 TBTC token contract. IERC20 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 @@ -139,7 +192,15 @@ contract L1BitcoinDepositor is finalizeDepositGasOffset = 20_000; } - // TODO: Document this function. + /// @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 @@ -155,7 +216,11 @@ contract L1BitcoinDepositor is l2BitcoinDepositor = _l2BitcoinDepositor; } - // TODO: Document this function. + /// @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 @@ -167,22 +232,87 @@ contract L1BitcoinDepositor is /// @notice Updates the values of gas offset parameters. /// @dev Can be called only by the contract owner. The caller is responsible /// for validating parameters. - /// @param newInitializeDepositGasOffset New initialize deposit gas offset. - /// @param newFinalizeDepositGasOffset New finalize deposit gas offset. + /// @param _initializeDepositGasOffset New initialize deposit gas offset. + /// @param _finalizeDepositGasOffset New finalize deposit gas offset. function updateGasOffsetParameters( - uint256 newInitializeDepositGasOffset, - uint256 newFinalizeDepositGasOffset + uint256 _initializeDepositGasOffset, + uint256 _finalizeDepositGasOffset ) external onlyOwner { - initializeDepositGasOffset = newInitializeDepositGasOffset; - finalizeDepositGasOffset = newFinalizeDepositGasOffset; + initializeDepositGasOffset = _initializeDepositGasOffset; + finalizeDepositGasOffset = _finalizeDepositGasOffset; emit GasOffsetParametersUpdated( - newInitializeDepositGasOffset, - newFinalizeDepositGasOffset + _initializeDepositGasOffset, + _finalizeDepositGasOffset ); } - // TODO: Document this function. + /// @notice Initializes the deposit process on L1 by revealing the deposit + /// 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 + /// that is paid out upon deposit finalization (only if the + /// reimbursement pool is attached). + /// + /// The Bitcoin funding transaction must transfer funds to a P2(W)SH + /// deposit address whose underlying script is built from the + /// following components: + /// + /// DROP + /// DROP + /// DROP + /// DUP HASH160 EQUAL + /// IF + /// CHECKSIG + /// ELSE + /// DUP HASH160 EQUALVERIFY + /// CHECKLOCKTIMEVERIFY DROP + /// CHECKSIG + /// ENDIF + /// + /// Where: + /// + /// 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. + /// + /// 8-byte deposit blinding factor, as used in the + /// tBTC bridge. + /// + /// The compressed Bitcoin public key (33 + /// bytes and 02 or 03 prefix) of the deposit's wallet hashed in the + /// HASH160 Bitcoin opcode style. This must point to the active tBTC + /// bridge wallet. + /// + /// The compressed Bitcoin public key (33 bytes + /// and 02 or 03 prefix) that can be used to make the deposit refund + /// after the tBTC bridge refund locktime passed. Hashed in the + /// HASH160 Bitcoin opcode style. This is needed only as a security + /// measure protecting the user in case tBTC bridge completely stops + /// functioning. + /// + /// The Bitcoin script refund locktime (4-byte LE), + /// according to tBTC bridge rules. + /// + /// Please consult tBTC `Bridge.revealDepositWithExtraData` function + /// documentation for more information. + /// @param fundingTx Bitcoin funding transaction data. + /// @param reveal Deposit reveal data. + /// @param l2DepositOwner Address of the L2 deposit owner. + /// @dev Requirements: + /// - The L2 deposit owner address must not be 0x0, + /// - The function can be called only one time for the given Bitcoin + /// funding transaction, + /// - The L2 deposit owner must be embedded in the Bitcoin P2(W)SH + /// deposit script as the field. The 20-byte + /// address must be expressed as a 32-byte value left-padded with 0. + /// If the value in the Bitcoin script and the value passed as + /// parameter do not match, the function will revert, + /// - All the requirements of tBTC Bridge.revealDepositWithExtraData + /// must be met. function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, @@ -199,7 +329,13 @@ contract L1BitcoinDepositor is // encode it as deposit extra data. bytes32 extraData = WormholeUtils.toWormholeAddress(l2DepositOwner); - // TODO: Document how the Bridge works and why we don't need to validate input parameters. + // Input parameters do not have to be validated in any way. + // The tBTC Bridge is responsible for validating whether the provided + // Bitcoin funding transaction transfers funds to the P2(W)SH deposit + // address built from the reveal data. Despite the tBTC Bridge accepts + // all transactions that meet the format requirements, it mints ERC20 + // L1 TBTC only for the ones that actually occurred on the Bitcoin + // network and gathered enough confirmations. (uint256 depositKey, ) = _initializeDeposit( fundingTx, reveal, @@ -230,7 +366,26 @@ contract L1BitcoinDepositor is } } - // TODO: Document this function. + /// @notice Finalizes the deposit process by transferring ERC20 L1 TBTC + /// to the L2 deposit owner. This function should be called after + /// the deposit was initialized with a call to `initializeDeposit` + /// function and after ERC20 L1 TBTC was minted by the tBTC Bridge + /// to the `L1BitcoinDepositor` contract. Please note several hours + /// may pass between `initializeDeposit`and `finalizeDeposit`. + /// If the reimbursement pool is attached, the function pays out + /// a gas and call's value refund to the caller as well as the + /// deferred gas refund to the caller of `initializeDeposit` + /// corresponding to the finalized deposit. + /// @param depositKey The deposit key, as emitted in the `DepositInitialized` + /// event emitted by the `initializeDeposit` function for the deposit. + /// @dev Requirements: + /// - `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 + /// 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. function finalizeDeposit(uint256 depositKey) public payable { uint256 gasStart = gasleft(); @@ -313,12 +468,21 @@ contract L1BitcoinDepositor is return (refund / gasPrice) - staticGas; } - // TODO: Document this function. + /// @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()); } - // TODO: Document this function. + /// @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 @@ -336,7 +500,25 @@ contract L1BitcoinDepositor is cost = deliveryCost + messageFee; } - // TODO: Document this function. + /// @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. @@ -344,7 +526,7 @@ contract L1BitcoinDepositor is require(amount > 0, "Amount too low to bridge"); - // Cost of requesting a `finalize` message to be sent to + // Cost of requesting a `finalizeDeposit` message to be sent to // `l2ChainId` with a gasLimit of `l2FinalizeDepositGasLimit`. uint256 wormholeMessageFee = wormhole.messageFee(); uint256 cost = _quoteFinalizeDeposit(wormholeMessageFee); @@ -356,7 +538,7 @@ contract L1BitcoinDepositor is tbtcToken.safeIncreaseAllowance(address(wormholeTokenBridge), amount); // Initiate a Wormhole token transfer that will lock L1 TBTC within - // the Wormhole Token Bridge contract and transfer Wormhole-wrapped + // the Wormhole Token Bridge contract and assign Wormhole-wrapped // L2 TBTC to the corresponding `L2WormholeGateway` contract. uint64 transferSequence = wormholeTokenBridge.transferTokensWithPayload{ value: wormholeMessageFee @@ -369,7 +551,7 @@ contract L1BitcoinDepositor is abi.encode(l2Receiver) // Set the L2 receiver address as the transfer payload. ); - // Construct VAA representing the above Wormhole token transfer. + // Construct the VAA key corresponding to the above Wormhole token transfer. WormholeTypes.VaaKey[] memory additionalVaas = new WormholeTypes.VaaKey[](1); additionalVaas[0] = WormholeTypes.VaaKey({ @@ -381,7 +563,7 @@ contract L1BitcoinDepositor is }); // The Wormhole token transfer initiated above must be finalized on - // the L2 chain. We achieve that by sending the transfer VAA to the + // 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 From 2ea028b9faf7c5bdaab19d4c82dfaeefa5b7691c Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 5 Mar 2024 17:33:49 +0100 Subject: [PATCH 13/24] Remaining documentation for the `L2BitcoinDepositor` contract --- solidity/contracts/l2/L2BitcoinDepositor.sol | 75 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/solidity/contracts/l2/L2BitcoinDepositor.sol b/solidity/contracts/l2/L2BitcoinDepositor.sol index 3b00313d6..777d44985 100644 --- a/solidity/contracts/l2/L2BitcoinDepositor.sol +++ b/solidity/contracts/l2/L2BitcoinDepositor.sol @@ -20,18 +20,33 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../integrator/IBridge.sol"; import "./Wormhole.sol"; -// TODO: Document this interface. +/// @title IL2WormholeGateway +/// @notice Interface to the `L2WormholeGateway` contract. interface IL2WormholeGateway { - // TODO: Document this function. + /// @dev See ./L2WormholeGateway.sol#receiveTbtc function receiveTbtc(bytes memory vaa) external; } -// TODO: Document this contract. +/// @title L2BitcoinDepositor +/// @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. +/// +/// `L2BitcoinDepositor` is deployed on the L2 chain and interacts with +/// their L1 counterpart, the `L1BitcoinDepositor`, deployed on the +/// L1 tBTC ledger chain. Each `L1BitcoinDepositor` & `L2BitcoinDepositor` +/// pair is responsible for a specific L2 chain. +/// +/// Please consult the `L1BitcoinDepositor` docstring for an +/// outline of the direct bridging mechanism contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { - // TODO: Document state variables. + /// @notice `WormholeRelayer` contract on L2. IWormholeRelayer public wormholeRelayer; + /// @notice tBTC `L2WormholeGateway` contract on L2. IL2WormholeGateway public l2WormholeGateway; + /// @notice Wormhole chain ID of the corresponding L1 chain. uint16 public l1ChainId; + /// @notice tBTC `L1BitcoinDepositor` contract on the corresponding L1 chain. address public l1BitcoinDepositor; event DepositInitialized( @@ -58,7 +73,15 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { l1ChainId = _l1ChainId; } - // TODO: Document this function. + /// @notice Sets the address of the `L1BitcoinDepositor` contract on the + /// corresponding L1 chain. This function solves the chicken-and-egg + /// problem of setting the `L1BitcoinDepositor` contract address + /// on the `L2BitcoinDepositor` contract and vice versa. + /// @param _l1BitcoinDepositor Address of the `L1BitcoinDepositor` 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 attachL1BitcoinDepositor(address _l1BitcoinDepositor) external onlyOwner @@ -74,7 +97,26 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { l1BitcoinDepositor = _l1BitcoinDepositor; } - // TODO: Document this function. + /// @notice Initializes the deposit process on L2 by emitting an event + /// containing the deposit data (funding transaction and + /// components of the P2(W)SH deposit address). The event is + /// supposed to be picked up by the relayer and used to initialize + /// the deposit on L1 through the `L1BitcoinDepositor` contract. + /// @param fundingTx Bitcoin funding transaction data. + /// @param reveal Deposit reveal data. + /// @param l2DepositOwner Address of the L2 deposit owner. + /// @dev The alternative approach of using Wormhole Relayer to send the + /// deposit data to L1 was considered. However, it turned out to be + /// too expensive. For example, relying deposit data from Base L2 to + /// Ethereum L1 costs around ~0.045 ETH (~170 USD at the moment of writing). + /// Moreover, the next iteration of the direct bridging mechanism + /// assumes that no L2 transaction will be required to initialize the + /// deposit and the relayer should obtain the deposit data off-chain. + /// There is a high chance this function will be removed then. + /// That said, there was no sense to explore another cross-chain + /// messaging solutions. Relying on simple on-chain event and custom + /// off-chain relayer seems to be the most reasonable way to go. It + /// also aligns with the future direction of the direct bridging mechanism. function initializeDeposit( IBridgeTypes.BitcoinTxInfo calldata fundingTx, IBridgeTypes.DepositRevealInfo calldata reveal, @@ -83,7 +125,26 @@ contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { emit DepositInitialized(fundingTx, reveal, l2DepositOwner, msg.sender); } - // TODO: Document this function. + /// @notice Receives Wormhole messages originating from the corresponding + /// `L1BitcoinDepositor` contract that lives on the L1 chain. + /// Messages are issued upon deposit finalization on L1 and + /// are supposed to carry the VAA of the Wormhole token transfer of + /// ERC20 L1 TBTC to the L2 chain. This contract performs some basic + /// checks and forwards the VAA to the `L2WormholeGateway` contract + /// that is authorized to withdraw the Wormhole-wrapped L2 TBTC + /// from the Wormhole Token Bridge (representing the ERC20 TBTC + /// locked on L1) and use it to mint the canonical L2 TBTC for the + /// deposit owner. + /// @param additionalVaas Additional VAAs that are part of the Wormhole message. + /// @param sourceAddress Address of the source of the message (in Wormhole format). + /// @param sourceChain Wormhole chain ID of the source chain. + /// @dev Requirements: + /// - Can be called only by the Wormhole Relayer contract, + /// - The source chain must be the expected L1 chain, + /// - The source address must be the corresponding + /// `L1BitcoinDepositor` contract, + /// - The message must carry exactly 1 additional VAA key representing + /// the token transfer. function receiveWormholeMessages( bytes memory, bytes[] memory additionalVaas, From 9a3555cfcb4bfb00828f60b263d6e1f87f8121a6 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 5 Mar 2024 18:15:52 +0100 Subject: [PATCH 14/24] Suppress Slither false positives --- solidity/contracts/l2/L1BitcoinDepositor.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index fd6f504cf..e34e2319e 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -540,6 +540,7 @@ contract L1BitcoinDepositor is // 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 }( @@ -568,6 +569,7 @@ contract L1BitcoinDepositor is // 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 wormholeRelayer.sendVaasToEvm{value: cost - wormholeMessageFee}( l2ChainId, l2BitcoinDepositor, From fa89a6219f724685202c36e647f5c950c6a4f6c5 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 5 Mar 2024 18:46:55 +0100 Subject: [PATCH 15/24] Re-deploy contracts on Ethereum and Base Sepolia --- cross-chain/base/.openzeppelin/sepolia.json | 146 ++++++++---- .../base/.openzeppelin/unknown-84532.json | 34 ++- .../baseSepolia/BaseL2BitcoinDepositor.json | 192 +++++++-------- .../sepolia/BaseL1BitcoinDepositor.json | 219 +++++++++++++----- 4 files changed, 372 insertions(+), 219 deletions(-) diff --git a/cross-chain/base/.openzeppelin/sepolia.json b/cross-chain/base/.openzeppelin/sepolia.json index 1a5db310f..16d589e45 100644 --- a/cross-chain/base/.openzeppelin/sepolia.json +++ b/cross-chain/base/.openzeppelin/sepolia.json @@ -1,20 +1,20 @@ { "manifestVersion": "3.2", "admin": { - "address": "0x59dCF5B9f5C5F00C6EdaCD80D88fc1A2F8536787", - "txHash": "0x692393d9edd20bea6eb7179f3c23ac1037c41342597cbb016d16562b3be00255" + "address": "0x33D7816A1C356F2D36c8cBA5D449f9c46306f818", + "txHash": "0x6289dd2828a8879386e2065836adeb66c7f60ea1c5c3bec36df33803071a0ace" }, "proxies": [ { - "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", - "txHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "txHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", "kind": "transparent" } ], "impls": { - "d1bd959487df6a914f345d6db574fc494c26bc8818086a7af9b2a0dd9a48e771": { - "address": "0xc3BD9ce767534eE4932A4C6FaD4B911094E41CF0", - "txHash": "0xe2d7aa67f89bf83afa4272a412cf4be9136399128ed92c4ec94cf8689704ab2b", + "397d1cce625c7ec2463bd078e8cf930aa844e1a9684804f18f36aa9b60b6fa7c": { + "address": "0x193096824F7618b49E8742651D9656e977c756d1", + "txHash": "0x91b2e2ff923eb25960e9d827106c688f48931d17a849e04cbb0c83e0bf11ca18", "layout": { "solcVersion": "0.8.17", "storage": [ @@ -22,7 +22,7 @@ "label": "bridge", "offset": 0, "slot": "0", - "type": "t_contract(IBridge)3078", + "type": "t_contract(IBridge)3414", "contract": "AbstractTBTCDepositor", "src": "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol:95" }, @@ -30,7 +30,7 @@ "label": "tbtcVault", "offset": 0, "slot": "1", - "type": "t_contract(ITBTCVault)3104", + "type": "t_contract(ITBTCVault)3440", "contract": "AbstractTBTCDepositor", "src": "@keep-network/tbtc-v2/contracts/integrator/AbstractTBTCDepositor.sol:96" }, @@ -84,76 +84,116 @@ "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" }, { - "label": "deposits", + "label": "reimbursementPool", "offset": 0, "slot": "150", - "type": "t_mapping(t_uint256,t_enum(DepositState)3127)", + "type": "t_contract(ReimbursementPool)2999", + "contract": "Reimbursable", + "src": "@keep-network/random-beacon/contracts/Reimbursable.sol:51" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)49_storage", + "contract": "Reimbursable", + "src": "@keep-network/random-beacon/contracts/Reimbursable.sol:51" + }, + { + "label": "deposits", + "offset": 0, + "slot": "200", + "type": "t_mapping(t_uint256,t_enum(DepositState)3465)", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:64" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:111" }, { "label": "tbtcToken", "offset": 0, - "slot": "151", - "type": "t_contract(IERC20)4902", + "slot": "201", + "type": "t_contract(IERC20)5428", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:69" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:113" }, { "label": "wormhole", "offset": 0, - "slot": "152", - "type": "t_contract(IWormhole)3987", + "slot": "202", + "type": "t_contract(IWormhole)4366", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:70" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:115" }, { "label": "wormholeRelayer", "offset": 0, - "slot": "153", - "type": "t_contract(IWormholeRelayer)4047", + "slot": "203", + "type": "t_contract(IWormholeRelayer)4406", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:71" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:116" }, { "label": "wormholeTokenBridge", "offset": 0, - "slot": "154", - "type": "t_contract(IWormholeTokenBridge)4132", + "slot": "204", + "type": "t_contract(IWormholeTokenBridge)4491", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:72" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:118" }, { "label": "l2WormholeGateway", "offset": 0, - "slot": "155", + "slot": "205", "type": "t_address", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:73" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:119" }, { "label": "l2ChainId", "offset": 20, - "slot": "155", + "slot": "205", "type": "t_uint16", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:77" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:121" }, { "label": "l2BitcoinDepositor", "offset": 0, - "slot": "156", + "slot": "206", "type": "t_address", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:77" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:123" }, { "label": "l2FinalizeDepositGasLimit", "offset": 0, - "slot": "157", + "slot": "207", "type": "t_uint256", "contract": "L1BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:77" + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:128" + }, + { + "label": "gasReimbursements", + "offset": 0, + "slot": "208", + "type": "t_mapping(t_uint256,t_struct(GasReimbursement)3472_storage)", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:137" + }, + { + "label": "initializeDepositGasOffset", + "offset": 0, + "slot": "209", + "type": "t_uint256", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:146" + }, + { + "label": "finalizeDepositGasOffset", + "offset": 0, + "slot": "210", + "type": "t_uint256", + "contract": "L1BitcoinDepositor", + "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:153" } ], "types": { @@ -177,31 +217,35 @@ "label": "bool", "numberOfBytes": "1" }, - "t_contract(IBridge)3078": { + "t_contract(IBridge)3414": { "label": "contract IBridge", "numberOfBytes": "20" }, - "t_contract(IERC20)4902": { + "t_contract(IERC20)5428": { "label": "contract IERC20", "numberOfBytes": "20" }, - "t_contract(ITBTCVault)3104": { + "t_contract(ITBTCVault)3440": { "label": "contract ITBTCVault", "numberOfBytes": "20" }, - "t_contract(IWormhole)3987": { + "t_contract(IWormhole)4366": { "label": "contract IWormhole", "numberOfBytes": "20" }, - "t_contract(IWormholeRelayer)4047": { + "t_contract(IWormholeRelayer)4406": { "label": "contract IWormholeRelayer", "numberOfBytes": "20" }, - "t_contract(IWormholeTokenBridge)4132": { + "t_contract(IWormholeTokenBridge)4491": { "label": "contract IWormholeTokenBridge", "numberOfBytes": "20" }, - "t_enum(DepositState)3127": { + "t_contract(ReimbursementPool)2999": { + "label": "contract ReimbursementPool", + "numberOfBytes": "20" + }, + "t_enum(DepositState)3465": { "label": "enum L1BitcoinDepositor.DepositState", "members": [ "Unknown", @@ -210,10 +254,32 @@ ], "numberOfBytes": "1" }, - "t_mapping(t_uint256,t_enum(DepositState)3127)": { + "t_mapping(t_uint256,t_enum(DepositState)3465)": { "label": "mapping(uint256 => enum L1BitcoinDepositor.DepositState)", "numberOfBytes": "32" }, + "t_mapping(t_uint256,t_struct(GasReimbursement)3472_storage)": { + "label": "mapping(uint256 => struct L1BitcoinDepositor.GasReimbursement)", + "numberOfBytes": "32" + }, + "t_struct(GasReimbursement)3472_storage": { + "label": "struct L1BitcoinDepositor.GasReimbursement", + "members": [ + { + "label": "receiver", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "gasSpent", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, "t_uint16": { "label": "uint16", "numberOfBytes": "2" diff --git a/cross-chain/base/.openzeppelin/unknown-84532.json b/cross-chain/base/.openzeppelin/unknown-84532.json index ddcd1f325..a91417980 100644 --- a/cross-chain/base/.openzeppelin/unknown-84532.json +++ b/cross-chain/base/.openzeppelin/unknown-84532.json @@ -16,8 +16,8 @@ "kind": "transparent" }, { - "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", - "txHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "txHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", "kind": "transparent" } ], @@ -459,9 +459,9 @@ } } }, - "158e85b6f8306c53687c3df734c1d1b840db0b13301465860bf98e15ff8b1b13": { - "address": "0x1a53759DE2eADf73bd0b05c07a4F1F5B7912dA3d", - "txHash": "0xe1026e1340ff20ac460112ac2de7718f92f87897d00f6ad15cd59530f8179e4a", + "3369e76dd2db12ab8efac7048dbdc97f3c608ed52d148e172acb7764f92977e1": { + "address": "0x1Ecd87C8D510A7390a561AE0Ac54FBe7e5125BcF", + "txHash": "0xf53c89cdde65e96dd9d040a21421c54b21414ac552d5201c82cc50f794efd9c8", "layout": { "solcVersion": "0.8.17", "storage": [ @@ -510,17 +510,17 @@ "label": "wormholeRelayer", "offset": 0, "slot": "101", - "type": "t_contract(IWormholeRelayer)4047", + "type": "t_contract(IWormholeRelayer)338", "contract": "L2BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:49" + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:54" }, { "label": "l2WormholeGateway", "offset": 0, "slot": "102", - "type": "t_contract(IL2WormholeGateway)3662", + "type": "t_contract(IL2WormholeGateway)88", "contract": "L2BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:52" + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:58" }, { "label": "l1ChainId", @@ -528,7 +528,7 @@ "slot": "102", "type": "t_uint16", "contract": "L2BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:53" + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:64" }, { "label": "l1BitcoinDepositor", @@ -536,15 +536,7 @@ "slot": "103", "type": "t_address", "contract": "L2BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:54" - }, - { - "label": "l1InitializeDepositGasLimit", - "offset": 0, - "slot": "104", - "type": "t_uint256", - "contract": "L2BitcoinDepositor", - "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:57" + "src": "@keep-network/tbtc-v2/contracts/l2/L2BitcoinDepositor.sol:67" } ], "types": { @@ -564,11 +556,11 @@ "label": "bool", "numberOfBytes": "1" }, - "t_contract(IL2WormholeGateway)3662": { + "t_contract(IL2WormholeGateway)88": { "label": "contract IL2WormholeGateway", "numberOfBytes": "20" }, - "t_contract(IWormholeRelayer)4047": { + "t_contract(IWormholeRelayer)338": { "label": "contract IWormholeRelayer", "numberOfBytes": "20" }, diff --git a/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json b/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json index fe2c88640..49fca47c9 100644 --- a/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json +++ b/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json @@ -1,5 +1,5 @@ { - "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", "abi": [ { "inputs": [], @@ -10,10 +10,70 @@ "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "uint256", - "name": "depositKey", - "type": "uint256" + "components": [ + { + "internalType": "bytes4", + "name": "version", + "type": "bytes4" + }, + { + "internalType": "bytes", + "name": "inputVector", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "outputVector", + "type": "bytes" + }, + { + "internalType": "bytes4", + "name": "locktime", + "type": "bytes4" + } + ], + "indexed": false, + "internalType": "struct IBridgeTypes.BitcoinTxInfo", + "name": "fundingTx", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "fundingOutputIndex", + "type": "uint32" + }, + { + "internalType": "bytes8", + "name": "blindingFactor", + "type": "bytes8" + }, + { + "internalType": "bytes20", + "name": "walletPubKeyHash", + "type": "bytes20" + }, + { + "internalType": "bytes20", + "name": "refundPubKeyHash", + "type": "bytes20" + }, + { + "internalType": "bytes4", + "name": "refundLocktime", + "type": "bytes4" + }, + { + "internalType": "address", + "name": "vault", + "type": "address" + } + ], + "indexed": false, + "internalType": "struct IBridgeTypes.DepositRevealInfo", + "name": "reveal", + "type": "tuple" }, { "indexed": true, @@ -26,12 +86,6 @@ "internalType": "address", "name": "l2Sender", "type": "address" - }, - { - "indexed": false, - "internalType": "uint64", - "name": "wormholeMessageSequence", - "type": "uint64" } ], "name": "DepositInitialized", @@ -50,19 +104,6 @@ "name": "Initialized", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "l1InitializeDepositGasLimit", - "type": "uint256" - } - ], - "name": "L1InitializeDepositGasLimitUpdated", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -192,7 +233,7 @@ ], "name": "initializeDeposit", "outputs": [], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -221,19 +262,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "l1InitializeDepositGasLimit", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "l2WormholeGateway", @@ -260,19 +288,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "quoteInitializeDeposit", - "outputs": [ - { - "internalType": "uint256", - "name": "cost", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -326,19 +341,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_l1InitializeDepositGasLimit", - "type": "uint256" - } - ], - "name": "updateL1InitializeDepositGasLimit", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [], "name": "wormholeRelayer", @@ -353,35 +355,35 @@ "type": "function" } ], - "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", "receipt": { "to": null, "from": "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", - "contractAddress": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", - "transactionIndex": 4, - "gasUsed": "726908", - "logsBloom": "0x00000000000000000000000000000000400000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080202000001000000000000002000000000000000000000020000000000000000000800000000800080000000000000000000400000000200000000000000000000000000000000000080000000000000800020000000000000000000000000000400000000000000000000000000000001000000000020000000008000000020040000000000000400000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a", - "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", + "contractAddress": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "transactionIndex": 2, + "gasUsed": "704802", + "logsBloom": "0x00000000000000000000000000000000400000000000000000800000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000800000000000000000000002400000000200000000000000000000000000000000000080000000000000800080000000000000000000000000000400000000000000000000000000000000000000000020000000000000000020040000000008000400000000000000000020000000000000000000000000000000000000000000000000000004000000400000", + "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed", + "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", "logs": [ { - "transactionIndex": 4, - "blockNumber": 6731912, - "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", - "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "transactionIndex": 2, + "blockNumber": 6946054, + "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", "topics": [ "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", - "0x0000000000000000000000001a53759de2eadf73bd0b05c07a4f1f5b7912da3d" + "0x0000000000000000000000001ecd87c8d510a7390a561ae0ac54fbe7e5125bcf" ], "data": "0x", "logIndex": 0, - "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" }, { - "transactionIndex": 4, - "blockNumber": 6731912, - "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", - "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "transactionIndex": 2, + "blockNumber": 6946054, + "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", "topics": [ "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", "0x0000000000000000000000000000000000000000000000000000000000000000", @@ -389,39 +391,39 @@ ], "data": "0x", "logIndex": 1, - "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" }, { - "transactionIndex": 4, - "blockNumber": 6731912, - "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", - "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "transactionIndex": 2, + "blockNumber": 6946054, + "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", "topics": [ "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000001", "logIndex": 2, - "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" }, { - "transactionIndex": 4, - "blockNumber": 6731912, - "transactionHash": "0x1c530f7679fd72c5a372d5dfac619588b8e6b28643f3a89be6a32edecc4b8b39", - "address": "0x73A63e2Be2D911dc7eFAc189Bfdf48FbB6532B5b", + "transactionIndex": 2, + "blockNumber": 6946054, + "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", "topics": [ "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2f6c5b73239c39360ee0ea95047565dab13e3c7", "logIndex": 3, - "blockHash": "0x1d393452b60bc66f1c7dc498d2e387f08cc8396a6aa41baee9bcefe75f196d4a" + "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" } ], - "blockNumber": 6731912, - "cumulativeGasUsed": "834783", + "blockNumber": 6946054, + "cumulativeGasUsed": "769653", "status": 1, "byzantium": true }, "numDeployments": 1, - "implementation": "0x1a53759DE2eADf73bd0b05c07a4F1F5B7912dA3d", + "implementation": "0x1Ecd87C8D510A7390a561AE0Ac54FBe7e5125BcF", "devdoc": "Contract deployed as upgradable proxy" } \ No newline at end of file diff --git a/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json b/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json index 198538443..c07a1ff17 100644 --- a/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json +++ b/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json @@ -1,5 +1,5 @@ { - "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", "abi": [ { "inputs": [], @@ -68,6 +68,25 @@ "name": "DepositInitialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "initializeDepositGasOffset", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "finalizeDepositGasOffset", + "type": "uint256" + } + ], + "name": "GasOffsetParametersUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -113,6 +132,19 @@ "name": "OwnershipTransferred", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newReimbursementPool", + "type": "address" + } + ], + "name": "ReimbursementPoolUpdated", + "type": "event" + }, { "inputs": [], "name": "SATOSHI_MULTIPLIER", @@ -184,6 +216,43 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "finalizeDepositGasOffset", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "gasReimbursements", + "outputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "gasSpent", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -304,6 +373,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "initializeDepositGasOffset", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l2BitcoinDepositor", @@ -383,36 +465,16 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "bytes", - "name": "payload", - "type": "bytes" - }, - { - "internalType": "bytes[]", - "name": "", - "type": "bytes[]" - }, - { - "internalType": "bytes32", - "name": "sourceAddress", - "type": "bytes32" - }, - { - "internalType": "uint16", - "name": "sourceChain", - "type": "uint16" - }, + "inputs": [], + "name": "reimbursementPool", + "outputs": [ { - "internalType": "bytes32", + "internalType": "contract ReimbursementPool", "name": "", - "type": "bytes32" + "type": "address" } ], - "name": "receiveWormholeMessages", - "outputs": [], - "stateMutability": "payable", + "stateMutability": "view", "type": "function" }, { @@ -461,6 +523,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_initializeDepositGasOffset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_finalizeDepositGasOffset", + "type": "uint256" + } + ], + "name": "updateGasOffsetParameters", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -474,6 +554,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "contract ReimbursementPool", + "name": "_reimbursementPool", + "type": "address" + } + ], + "name": "updateReimbursementPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "wormhole", @@ -514,75 +607,75 @@ "type": "function" } ], - "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", "receipt": { "to": null, "from": "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", - "contractAddress": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", - "transactionIndex": 195, - "gasUsed": "843614", - "logsBloom": "0x00000000000020000000000000000000400004000000000000800000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000804000000000000000000000400000000200000000000000000000000000000000000080000000000000800000000000000000000000000000000400000000000000000000000000000000000000000020000080000000000020040000000000000400000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d", - "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", + "contractAddress": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "transactionIndex": 85, + "gasUsed": "887866", + "logsBloom": "0x00000000000000000000200000000000400000000000000000800000800000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000800000000000000000000000400000000200000000000000000000000000000000000080000000000000800000000000000000000000000000000440000000000000000000000000000000000000000020000000000000000020040000000000000400000000000000000020000000000400000000000000000000000000000000000000008000000000000000", + "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4", + "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", "logs": [ { - "transactionIndex": 195, - "blockNumber": 5389241, - "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", - "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "transactionIndex": 85, + "blockNumber": 5422966, + "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", "topics": [ "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", - "0x000000000000000000000000c3bd9ce767534ee4932a4c6fad4b911094e41cf0" + "0x000000000000000000000000193096824f7618b49e8742651d9656e977c756d1" ], "data": "0x", - "logIndex": 315, - "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + "logIndex": 2090, + "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" }, { - "transactionIndex": 195, - "blockNumber": 5389241, - "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", - "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "transactionIndex": 85, + "blockNumber": 5422966, + "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", "topics": [ "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000068ad60cc5e8f3b7cc53beab321cf0e6036962dbc" ], "data": "0x", - "logIndex": 316, - "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + "logIndex": 2091, + "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" }, { - "transactionIndex": 195, - "blockNumber": 5389241, - "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", - "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "transactionIndex": 85, + "blockNumber": 5422966, + "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", "topics": [ "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000001", - "logIndex": 317, - "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + "logIndex": 2092, + "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" }, { - "transactionIndex": 195, - "blockNumber": 5389241, - "transactionHash": "0xd0436e0eb5ff655fe1eac53a24e04d4c1741783d35f28fb24700db4dbc2d64c7", - "address": "0xFdF0F1C4dF891732b586A85F062B7e8eBF65c428", + "transactionIndex": 85, + "blockNumber": 5422966, + "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", "topics": [ "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" ], - "data": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000059dcf5b9f5c5f00c6edacd80d88fc1a2f8536787", - "logIndex": 318, - "blockHash": "0xfe3767404a2aaefd885ac2278f95ef4501739e3a3d5b6fc5855f2a9e28ed205d" + "data": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033d7816a1c356f2d36c8cba5d449f9c46306f818", + "logIndex": 2093, + "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" } ], - "blockNumber": 5389241, - "cumulativeGasUsed": "21487798", + "blockNumber": 5422966, + "cumulativeGasUsed": "24824577", "status": 1, "byzantium": true }, "numDeployments": 1, - "implementation": "0xc3BD9ce767534eE4932A4C6FaD4B911094E41CF0", + "implementation": "0x193096824F7618b49E8742651D9656e977c756d1", "devdoc": "Contract deployed as upgradable proxy" } \ No newline at end of file From fac595d4fb0284f16f882d04ceaef073cf0f816f Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 5 Mar 2024 19:14:38 +0100 Subject: [PATCH 16/24] Another attempt to suppress Slither false positives --- .github/workflows/contracts.yml | 2 +- solidity/contracts/l2/L1BitcoinDepositor.sol | 7 ++++++- solidity/contracts/l2/L2BitcoinDepositor.sol | 1 + solidity/contracts/l2/L2WormholeGateway.sol | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 19c8360ed..501a57084 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -419,7 +419,7 @@ jobs: - name: Install Slither env: - SLITHER_VERSION: 0.8.3 + SLITHER_VERSION: 0.9.0 run: pip3 install slither-analyzer==$SLITHER_VERSION # We need this step because the `@keep-network/tbtc` which we update in diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index e34e2319e..e6e053f89 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -185,6 +185,7 @@ contract L1BitcoinDepositor is wormhole = IWormhole(_wormhole); wormholeRelayer = IWormholeRelayer(_wormholeRelayer); wormholeTokenBridge = IWormholeTokenBridge(_wormholeTokenBridge); + // slither-disable-next-line missing-zero-check l2WormholeGateway = _l2WormholeGateway; l2ChainId = _l2ChainId; l2FinalizeDepositGasLimit = 500_000; @@ -347,8 +348,10 @@ contract L1BitcoinDepositor is "Wrong deposit state" ); + // slither-disable-next-line reentrancy-benign deposits[depositKey] = DepositState.Initialized; + // slither-disable-next-line reentrancy-events emit DepositInitialized(depositKey, l2DepositOwner, msg.sender); if (address(reimbursementPool) != address(0)) { @@ -359,6 +362,7 @@ contract L1BitcoinDepositor is // ones. Paying out the reimbursement directly upon initialization // would make the reimbursement pool vulnerable to malicious actors // that could drain it by initializing invalid deposits. + // slither-disable-next-line reentrancy-benign gasReimbursements[depositKey] = GasReimbursement({ receiver: msg.sender, gasSpent: (gasStart - gasleft()) + initializeDepositGasOffset @@ -404,6 +408,7 @@ contract L1BitcoinDepositor is bytes32 l2DepositOwner ) = _finalizeDeposit(depositKey); + // slither-disable-next-line reentrancy-events emit DepositFinalized( depositKey, WormholeUtils.fromWormholeAddress(l2DepositOwner), @@ -569,7 +574,7 @@ contract L1BitcoinDepositor is // 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 + // slither-disable-next-line arbitrary-send-eth,unused-return wormholeRelayer.sendVaasToEvm{value: cost - wormholeMessageFee}( l2ChainId, l2BitcoinDepositor, diff --git a/solidity/contracts/l2/L2BitcoinDepositor.sol b/solidity/contracts/l2/L2BitcoinDepositor.sol index 777d44985..f7a8f4007 100644 --- a/solidity/contracts/l2/L2BitcoinDepositor.sol +++ b/solidity/contracts/l2/L2BitcoinDepositor.sol @@ -39,6 +39,7 @@ interface IL2WormholeGateway { /// /// Please consult the `L1BitcoinDepositor` docstring for an /// outline of the direct bridging mechanism +// slither-disable-next-line locked-ether contract L2BitcoinDepositor is IWormholeReceiver, OwnableUpgradeable { /// @notice `WormholeRelayer` contract on L2. IWormholeRelayer public wormholeRelayer; diff --git a/solidity/contracts/l2/L2WormholeGateway.sol b/solidity/contracts/l2/L2WormholeGateway.sol index 8b2dfa2a4..148ddbc74 100644 --- a/solidity/contracts/l2/L2WormholeGateway.sol +++ b/solidity/contracts/l2/L2WormholeGateway.sol @@ -58,6 +58,7 @@ import "./L2TBTC.sol"; /// Wormhole tBTC representation through the bridge in an equal amount. /// @dev This contract is supposed to be deployed behind a transparent /// upgradeable proxy. +// slither-disable-next-line missing-inheritance contract L2WormholeGateway is Initializable, OwnableUpgradeable, From 105a52d884b08a28716e5a72dff11ad57e2dccd8 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Wed, 6 Mar 2024 14:39:32 +0100 Subject: [PATCH 17/24] Unit tests for `L2BitcoinDepositor` contract --- solidity/test/l2/L2BitcoinDepositor.test.ts | 374 ++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 solidity/test/l2/L2BitcoinDepositor.test.ts diff --git a/solidity/test/l2/L2BitcoinDepositor.test.ts b/solidity/test/l2/L2BitcoinDepositor.test.ts new file mode 100644 index 000000000..51f5a39b2 --- /dev/null +++ b/solidity/test/l2/L2BitcoinDepositor.test.ts @@ -0,0 +1,374 @@ +import { ethers, getUnnamedAccounts, helpers, waffle } from "hardhat" +import { randomBytes } from "crypto" +import chai, { expect } from "chai" +import { FakeContract, smock } from "@defi-wonderland/smock" +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" +import { ContractTransaction } from "ethers" +import { + IL2WormholeGateway, + IWormholeRelayer, + L2BitcoinDepositor, +} from "../../typechain" +import type { + DepositRevealInfoStruct, + BitcoinTxInfoStruct, +} from "../../typechain/L2BitcoinDepositor" + +chai.use(smock.matchers) + +const { impersonateAccount } = helpers.account +const { createSnapshot, restoreSnapshot } = helpers.snapshot + +describe("L2BitcoinDepositor", () => { + const contractsFixture = async () => { + const { deployer, governance } = await helpers.signers.getNamedSigners() + + const accounts = await getUnnamedAccounts() + const relayer = await ethers.getSigner(accounts[1]) + + const wormholeRelayer = await smock.fake( + "IWormholeRelayer" + ) + const l2WormholeGateway = await smock.fake( + "IL2WormholeGateway" + ) + // Just an arbitrary chain ID. + const l1ChainId = 2 + // Just an arbitrary L1BitcoinDepositor address. + const l1BitcoinDepositor = "0xeE6F5f69860f310114185677D017576aed0dEC83" + + const deployment = await helpers.upgrades.deployProxy( + // Hacky workaround allowing to deploy proxy contract any number of times + // without clearing `deployments/hardhat` directory. + // See: https://github.com/keep-network/hardhat-helpers/issues/38 + `L2BitcoinDepositor_${randomBytes(8).toString("hex")}`, + { + contractName: "L2BitcoinDepositor", + initializerArgs: [ + wormholeRelayer.address, + l2WormholeGateway.address, + l1ChainId, + ], + factoryOpts: { signer: deployer }, + proxyOpts: { + kind: "transparent", + }, + } + ) + const l2BitcoinDepositor = deployment[0] as L2BitcoinDepositor + + await l2BitcoinDepositor + .connect(deployer) + .transferOwnership(governance.address) + + return { + governance, + relayer, + wormholeRelayer, + l2WormholeGateway, + l1BitcoinDepositor, + l2BitcoinDepositor, + } + } + + type InitializeDepositFixture = { + fundingTx: BitcoinTxInfoStruct + reveal: DepositRevealInfoStruct + l2DepositOwner: string + } + + // Fixture used for initializeDeposit test scenario. + const initializeDepositFixture: InitializeDepositFixture = { + fundingTx: { + version: "0x01000000", + inputVector: + "0x01dfe39760a5edabdab013114053d789ada21e356b59fea41d980396" + + "c1a4474fad0100000023220020e57edf10136b0434e46bc08c5ac5a1e4" + + "5f64f778a96f984d0051873c7a8240f2ffffffff", + outputVector: + "0x02804f1200000000002200202f601522e7bb1f7de5c56bdbd45590b3" + + "499bad09190581dcaa17e152d8f0c2a9b7e837000000000017a9148688" + + "4e6be1525dab5ae0b451bd2c72cee67dcf4187", + locktime: "0x00000000", + }, + reveal: { + fundingOutputIndex: 0, + blindingFactor: "0xba863847d2d0fee3", + walletPubKeyHash: "0xf997563fee8610ca28f99ac05bd8a29506800d4d", + refundPubKeyHash: "0x7ac2d9378a1c47e589dfb8095ca95ed2140d2726", + refundLocktime: "0xde2b4c67", + vault: "0xB5679dE944A79732A75CE556191DF11F489448d5", + }, + l2DepositOwner: "0x23b82a7108F9CEb34C3CDC44268be21D151d4124", + } + + let governance: SignerWithAddress + let relayer: SignerWithAddress + + let wormholeRelayer: FakeContract + let l2WormholeGateway: FakeContract + let l1BitcoinDepositor: string + let l2BitcoinDepositor: L2BitcoinDepositor + + before(async () => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;({ + governance, + relayer, + wormholeRelayer, + l2WormholeGateway, + l1BitcoinDepositor, + l2BitcoinDepositor, + } = await waffle.loadFixture(contractsFixture)) + }) + + describe("attachL1BitcoinDepositor", () => { + context("when the L1BitcoinDepositor is already attached", () => { + before(async () => { + await createSnapshot() + + await l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(l1BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(l1BitcoinDepositor) + ).to.be.revertedWith("L1 Bitcoin Depositor already set") + }) + }) + + context("when the L1BitcoinDepositor is not attached", () => { + context("when new L1BitcoinDepositor is zero", () => { + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(ethers.constants.AddressZero) + ).to.be.revertedWith("L1 Bitcoin Depositor must not be 0x0") + }) + }) + + context("when new L1BitcoinDepositor is non-zero", () => { + before(async () => { + await createSnapshot() + + await l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(l1BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should set the l1BitcoinDepositor address properly", async () => { + expect(await l2BitcoinDepositor.l1BitcoinDepositor()).to.equal( + l1BitcoinDepositor + ) + }) + }) + }) + }) + + describe("initializeDeposit", () => { + let tx: ContractTransaction + + before(async () => { + await createSnapshot() + + tx = await l2BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should emit DepositInitialized event", async () => { + const { fundingTx, reveal, l2DepositOwner } = initializeDepositFixture + + // The `expect.to.emit.withArgs` assertion has troubles with + // matching complex event arguments as it uses strict equality + // underneath. To overcome that problem, we manually get event's + // arguments and check it against the expected ones using deep + // equality assertion (eql). + const receipt = await ethers.provider.getTransactionReceipt(tx.hash) + expect(receipt.logs.length).to.be.equal(1) + expect( + l2BitcoinDepositor.interface.parseLog(receipt.logs[0]).args + ).to.be.eql([ + [ + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime, + ], + [ + reveal.fundingOutputIndex, + reveal.blindingFactor, + reveal.walletPubKeyHash, + reveal.refundPubKeyHash, + reveal.refundLocktime, + reveal.vault, + ], + l2DepositOwner, + relayer.address, + ]) + }) + }) + + describe("receiveWormholeMessages", () => { + before(async () => { + await createSnapshot() + + await l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(l1BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + context("when the caller is not the WormholeRelayer", () => { + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(relayer) + // Parameters don't matter as the call should revert before. + .receiveWormholeMessages( + ethers.constants.HashZero, + [], + ethers.constants.HashZero, + 0, + ethers.constants.HashZero + ) + ).to.be.revertedWith("Caller is not Wormhole Relayer") + }) + }) + + context("when the caller is the WormholeRelayer", () => { + let wormholeRelayerSigner: SignerWithAddress + + before(async () => { + await createSnapshot() + + wormholeRelayerSigner = await impersonateAccount( + wormholeRelayer.address, + { + from: governance, + value: 10, + } + ) + }) + + after(async () => { + await restoreSnapshot() + }) + + context("when the source chain is not the expected L1", () => { + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(wormholeRelayerSigner) + .receiveWormholeMessages( + ethers.constants.HashZero, + [], + ethers.constants.HashZero, + 0, + ethers.constants.HashZero + ) + ).to.be.revertedWith("Source chain is not the expected L1 chain") + }) + }) + + context("when the source chain is the expected L1", () => { + context("when the source address is not the L1BitcoinDepositor", () => { + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(wormholeRelayerSigner) + .receiveWormholeMessages( + ethers.constants.HashZero, + [], + toWormholeAddress(relayer.address), + await l2BitcoinDepositor.l1ChainId(), + ethers.constants.HashZero + ) + ).to.be.revertedWith( + "Source address is not the expected L1 Bitcoin depositor" + ) + }) + }) + + context("when the source address is the L1BitcoinDepositor", () => { + context("when the number of additional VAAs is not 1", () => { + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(wormholeRelayerSigner) + .receiveWormholeMessages( + ethers.constants.HashZero, + [], + toWormholeAddress(l1BitcoinDepositor), + await l2BitcoinDepositor.l1ChainId(), + ethers.constants.HashZero + ) + ).to.be.revertedWith( + "Expected 1 additional VAA key for token transfer" + ) + }) + }) + + context("when the number of additional VAAs is 1", () => { + before(async () => { + await createSnapshot() + + l2WormholeGateway.receiveTbtc.returns() + + await l2BitcoinDepositor + .connect(wormholeRelayerSigner) + .receiveWormholeMessages( + ethers.constants.HashZero, + ["0x1234"], + toWormholeAddress(l1BitcoinDepositor), + await l2BitcoinDepositor.l1ChainId(), + ethers.constants.HashZero + ) + }) + + after(async () => { + l2WormholeGateway.receiveTbtc.reset() + + await restoreSnapshot() + }) + + it("should pass the VAA to the L2WormholeGateway", async () => { + expect(l2WormholeGateway.receiveTbtc).to.have.been.calledOnceWith( + "0x1234" + ) + }) + }) + }) + }) + }) + }) +}) + +// eslint-disable-next-line import/prefer-default-export +export function toWormholeAddress(address: string): string { + return `0x000000000000000000000000${address.slice(2)}` +} From 3fc19adbb6722f8fa9a708abb17ab42ad563bca2 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 7 Mar 2024 09:30:49 +0100 Subject: [PATCH 18/24] Make `finalizeDeposit` external --- solidity/contracts/l2/L1BitcoinDepositor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index e6e053f89..55bfb78fe 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -390,7 +390,7 @@ contract L1BitcoinDepositor is /// 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. - function finalizeDeposit(uint256 depositKey) public payable { + function finalizeDeposit(uint256 depositKey) external payable { uint256 gasStart = gasleft(); require( From ce5f8499cfdb932f7f9007607912d58791d589af Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 7 Mar 2024 10:12:30 +0100 Subject: [PATCH 19/24] Use upgradable OZ token types The OZ upgrade plugin complains about unsafe upgrade if standard versions of `IERC20` and `SafeERC20` are used. --- solidity/contracts/l2/L1BitcoinDepositor.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index 55bfb78fe..d1053b57c 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -16,8 +16,8 @@ pragma solidity ^0.8.17; import "@keep-network/random-beacon/contracts/Reimbursable.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.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"; import "../integrator/AbstractTBTCDepositor.sol"; @@ -73,7 +73,7 @@ contract L1BitcoinDepositor is OwnableUpgradeable, Reimbursable { - using SafeERC20 for IERC20; + using SafeERC20Upgradeable for IERC20Upgradeable; /// @notice Reflects the deposit state: /// - Unknown deposit has not been initialized yet. @@ -101,7 +101,7 @@ contract L1BitcoinDepositor is /// function. mapping(uint256 => DepositState) public deposits; /// @notice ERC20 L1 TBTC token contract. - IERC20 public tbtcToken; + IERC20Upgradeable public tbtcToken; /// @notice `Wormhole` core contract on L1. IWormhole public wormhole; /// @notice `WormholeRelayer` contract on L1. @@ -181,7 +181,7 @@ contract L1BitcoinDepositor is __AbstractTBTCDepositor_initialize(_tbtcBridge, _tbtcVault); __Ownable_init(); - tbtcToken = IERC20(ITBTCVault(_tbtcVault).tbtcToken()); + tbtcToken = IERC20Upgradeable(ITBTCVault(_tbtcVault).tbtcToken()); wormhole = IWormhole(_wormhole); wormholeRelayer = IWormholeRelayer(_wormholeRelayer); wormholeTokenBridge = IWormholeTokenBridge(_wormholeTokenBridge); From e902f81e96a0a29ecbc18c8a67eaad327036212e Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 7 Mar 2024 10:14:19 +0100 Subject: [PATCH 20/24] Add missing test scenario for `attachL1BitcoinDepositor` function We need to make sure only owner can call this function. --- solidity/test/l2/L2BitcoinDepositor.test.ts | 74 ++++++++++++--------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/solidity/test/l2/L2BitcoinDepositor.test.ts b/solidity/test/l2/L2BitcoinDepositor.test.ts index 51f5a39b2..8813bf610 100644 --- a/solidity/test/l2/L2BitcoinDepositor.test.ts +++ b/solidity/test/l2/L2BitcoinDepositor.test.ts @@ -123,40 +123,18 @@ describe("L2BitcoinDepositor", () => { }) describe("attachL1BitcoinDepositor", () => { - context("when the L1BitcoinDepositor is already attached", () => { - before(async () => { - await createSnapshot() - - await l2BitcoinDepositor - .connect(governance) - .attachL1BitcoinDepositor(l1BitcoinDepositor) - }) - - after(async () => { - await restoreSnapshot() - }) - + context("when the caller is not the owner", () => { it("should revert", async () => { await expect( l2BitcoinDepositor - .connect(governance) + .connect(relayer) .attachL1BitcoinDepositor(l1BitcoinDepositor) - ).to.be.revertedWith("L1 Bitcoin Depositor already set") + ).to.be.revertedWith("Ownable: caller is not the owner") }) }) - context("when the L1BitcoinDepositor is not attached", () => { - context("when new L1BitcoinDepositor is zero", () => { - it("should revert", async () => { - await expect( - l2BitcoinDepositor - .connect(governance) - .attachL1BitcoinDepositor(ethers.constants.AddressZero) - ).to.be.revertedWith("L1 Bitcoin Depositor must not be 0x0") - }) - }) - - context("when new L1BitcoinDepositor is non-zero", () => { + context("when the caller is the owner", () => { + context("when the L1BitcoinDepositor is already attached", () => { before(async () => { await createSnapshot() @@ -169,10 +147,44 @@ describe("L2BitcoinDepositor", () => { await restoreSnapshot() }) - it("should set the l1BitcoinDepositor address properly", async () => { - expect(await l2BitcoinDepositor.l1BitcoinDepositor()).to.equal( - l1BitcoinDepositor - ) + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(l1BitcoinDepositor) + ).to.be.revertedWith("L1 Bitcoin Depositor already set") + }) + }) + + context("when the L1BitcoinDepositor is not attached", () => { + context("when new L1BitcoinDepositor is zero", () => { + it("should revert", async () => { + await expect( + l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(ethers.constants.AddressZero) + ).to.be.revertedWith("L1 Bitcoin Depositor must not be 0x0") + }) + }) + + context("when new L1BitcoinDepositor is non-zero", () => { + before(async () => { + await createSnapshot() + + await l2BitcoinDepositor + .connect(governance) + .attachL1BitcoinDepositor(l1BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should set the l1BitcoinDepositor address properly", async () => { + expect(await l2BitcoinDepositor.l1BitcoinDepositor()).to.equal( + l1BitcoinDepositor + ) + }) }) }) }) From 4616e6dfb447cbbfa10df75d27488afa4c7dcc3e Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 8 Mar 2024 11:09:50 +0100 Subject: [PATCH 21/24] Unit tests for `L1BitcoinDepositor` contract --- solidity/contracts/l2/L1BitcoinDepositor.sol | 16 +- solidity/test/l2/L1BitcoinDepositor.test.ts | 1466 ++++++++++++++++++ solidity/test/l2/L2BitcoinDepositor.test.ts | 44 +- 3 files changed, 1485 insertions(+), 41 deletions(-) create mode 100644 solidity/test/l2/L1BitcoinDepositor.test.ts diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index d1053b57c..5edaea7f3 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -470,7 +470,21 @@ contract L1BitcoinDepositor is ? tx.gasprice : maxGasPrice; - return (refund / gasPrice) - staticGas; + // Should not happen but check just in case of weird ReimbursementPool + // configuration. + if (gasPrice == 0) { + return 0; + } + + uint256 gasSpent = (refund / gasPrice); + + // Should not happen but check just in case of weird ReimbursementPool + // configuration. + if (staticGas > gasSpent) { + return 0; + } + + return gasSpent - staticGas; } /// @notice Quotes the payment that must be attached to the `finalizeDeposit` diff --git a/solidity/test/l2/L1BitcoinDepositor.test.ts b/solidity/test/l2/L1BitcoinDepositor.test.ts new file mode 100644 index 000000000..5ca5ac208 --- /dev/null +++ b/solidity/test/l2/L1BitcoinDepositor.test.ts @@ -0,0 +1,1466 @@ +import { ethers, getUnnamedAccounts, helpers, waffle } from "hardhat" +import { randomBytes } from "crypto" +import chai, { expect } from "chai" +import { FakeContract, smock } from "@defi-wonderland/smock" +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" +import { BigNumber, ContractTransaction } from "ethers" +import { + IBridge, + IERC20, + IL2WormholeGateway, + ITBTCVault, + IWormhole, + IWormholeRelayer, + IWormholeTokenBridge, + L1BitcoinDepositor, + ReimbursementPool, + TestERC20, +} from "../../typechain" +import type { + BitcoinTxInfoStruct, + DepositRevealInfoStruct, +} from "../../typechain/L2BitcoinDepositor" +import { to1ePrecision } from "../helpers/contract-test-helpers" + +chai.use(smock.matchers) + +const { createSnapshot, restoreSnapshot } = helpers.snapshot +const { lastBlockTime } = helpers.time +// Just arbitrary values. +const l1ChainId = 10 +const l2ChainId = 20 + +describe("L1BitcoinDepositor", () => { + const contractsFixture = async () => { + const { deployer, governance } = await helpers.signers.getNamedSigners() + + const accounts = await getUnnamedAccounts() + const relayer = await ethers.getSigner(accounts[1]) + + const bridge = await smock.fake("IBridge") + const tbtcToken = await ( + await ethers.getContractFactory("TestERC20") + ).deploy() + const tbtcVault = await smock.fake("ITBTCVault", { + // The TBTCVault contract address must be known in advance and match + // the one used in initializeDeposit fixture. This is necessary to + // pass the vault address check in the initializeDeposit function. + address: tbtcVaultAddress, + }) + // Attack the tbtcToken mock to the tbtcVault mock. + tbtcVault.tbtcToken.returns(tbtcToken.address) + + const wormhole = await smock.fake("IWormhole") + wormhole.chainId.returns(l1ChainId) + + const wormholeRelayer = await smock.fake( + "IWormholeRelayer" + ) + const wormholeTokenBridge = await smock.fake( + "IWormholeTokenBridge" + ) + const l2WormholeGateway = await smock.fake( + "IL2WormholeGateway" + ) + // Just an arbitrary L2BitcoinDepositor address. + const l2BitcoinDepositor = "0xeE6F5f69860f310114185677D017576aed0dEC83" + const reimbursementPool = await smock.fake( + "ReimbursementPool" + ) + + const deployment = await helpers.upgrades.deployProxy( + // Hacky workaround allowing to deploy proxy contract any number of times + // without clearing `deployments/hardhat` directory. + // See: https://github.com/keep-network/hardhat-helpers/issues/38 + `L1BitcoinDepositor_${randomBytes(8).toString("hex")}`, + { + contractName: "L1BitcoinDepositor", + initializerArgs: [ + bridge.address, + tbtcVault.address, + wormhole.address, + wormholeRelayer.address, + wormholeTokenBridge.address, + l2WormholeGateway.address, + l2ChainId, + ], + factoryOpts: { signer: deployer }, + proxyOpts: { + kind: "transparent", + }, + } + ) + const l1BitcoinDepositor = deployment[0] as L1BitcoinDepositor + + await l1BitcoinDepositor + .connect(deployer) + .transferOwnership(governance.address) + + return { + governance, + relayer, + bridge, + tbtcToken, + tbtcVault, + wormhole, + wormholeRelayer, + wormholeTokenBridge, + l2WormholeGateway, + l2BitcoinDepositor, + reimbursementPool, + l1BitcoinDepositor, + } + } + + let governance: SignerWithAddress + let relayer: SignerWithAddress + + let bridge: FakeContract + let tbtcToken: TestERC20 + let tbtcVault: FakeContract + let wormhole: FakeContract + let wormholeRelayer: FakeContract + let wormholeTokenBridge: FakeContract + let l2WormholeGateway: FakeContract + let l2BitcoinDepositor: string + let reimbursementPool: FakeContract + let l1BitcoinDepositor: L1BitcoinDepositor + + before(async () => { + // eslint-disable-next-line @typescript-eslint/no-extra-semi + ;({ + governance, + relayer, + bridge, + tbtcToken, + tbtcVault, + wormhole, + wormholeRelayer, + wormholeTokenBridge, + l2WormholeGateway, + l1BitcoinDepositor, + reimbursementPool, + l2BitcoinDepositor, + } = await waffle.loadFixture(contractsFixture)) + }) + + describe("attachL2BitcoinDepositor", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .attachL2BitcoinDepositor(l2BitcoinDepositor) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + context("when the L2BitcoinDepositor is already attached", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(governance) + .attachL2BitcoinDepositor(l2BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(governance) + .attachL2BitcoinDepositor(l2BitcoinDepositor) + ).to.be.revertedWith("L2 Bitcoin Depositor already set") + }) + }) + + context("when the L2BitcoinDepositor is not attached", () => { + context("when new L2BitcoinDepositor is zero", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(governance) + .attachL2BitcoinDepositor(ethers.constants.AddressZero) + ).to.be.revertedWith("L2 Bitcoin Depositor must not be 0x0") + }) + }) + + context("when new L2BitcoinDepositor is non-zero", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(governance) + .attachL2BitcoinDepositor(l2BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should set the l2BitcoinDepositor address properly", async () => { + expect(await l1BitcoinDepositor.l2BitcoinDepositor()).to.equal( + l2BitcoinDepositor + ) + }) + }) + }) + }) + }) + + describe("updateReimbursementPool", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .updateReimbursementPool(reimbursementPool.address) + ).to.be.revertedWith("'Caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(governance) + .updateReimbursementPool(reimbursementPool.address) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should set the reimbursementPool address properly", async () => { + expect(await l1BitcoinDepositor.reimbursementPool()).to.equal( + reimbursementPool.address + ) + }) + + it("should emit ReimbursementPoolUpdated event", async () => { + await expect( + l1BitcoinDepositor + .connect(governance) + .updateReimbursementPool(reimbursementPool.address) + ) + .to.emit(l1BitcoinDepositor, "ReimbursementPoolUpdated") + .withArgs(reimbursementPool.address) + }) + }) + }) + + describe("updateL2FinalizeDepositGasLimit", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .updateL2FinalizeDepositGasLimit(100) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(governance) + .updateL2FinalizeDepositGasLimit(100) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should set the gas limit properly", async () => { + expect(await l1BitcoinDepositor.l2FinalizeDepositGasLimit()).to.equal( + 100 + ) + }) + + it("should emit L2FinalizeDepositGasLimitUpdated event", async () => { + await expect( + l1BitcoinDepositor + .connect(governance) + .updateL2FinalizeDepositGasLimit(100) + ) + .to.emit(l1BitcoinDepositor, "L2FinalizeDepositGasLimitUpdated") + .withArgs(100) + }) + }) + }) + + describe("updateGasOffsetParameters", () => { + context("when the caller is not the owner", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .updateGasOffsetParameters(1000, 2000) + ).to.be.revertedWith("Ownable: caller is not the owner") + }) + }) + + context("when the caller is the owner", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(governance) + .updateGasOffsetParameters(1000, 2000) + }) + + after(async () => { + await restoreSnapshot() + }) + + it("should set the gas offset params properly", async () => { + expect( + await l1BitcoinDepositor.initializeDepositGasOffset() + ).to.be.equal(1000) + + expect(await l1BitcoinDepositor.finalizeDepositGasOffset()).to.be.equal( + 2000 + ) + }) + + it("should emit GasOffsetParametersUpdated event", async () => { + await expect( + l1BitcoinDepositor + .connect(governance) + .updateGasOffsetParameters(1000, 2000) + ) + .to.emit(l1BitcoinDepositor, "GasOffsetParametersUpdated") + .withArgs(1000, 2000) + }) + }) + }) + + describe("initializeDeposit", () => { + context("when the L2 deposit owner is zero", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + ethers.constants.AddressZero + ) + ).to.be.revertedWith("L2 deposit owner must not be 0x0") + }) + }) + + context("when the L2 deposit owner is non-zero", () => { + context("when the requested vault is not TBTCVault", () => { + it("should revert", async () => { + const corruptedReveal = JSON.parse( + JSON.stringify(initializeDepositFixture.reveal) + ) + + // Set another vault address deliberately. This value must be + // different from the tbtcVaultAddress constant used in the fixture. + corruptedReveal.vault = ethers.constants.AddressZero + + await expect( + l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + corruptedReveal, + initializeDepositFixture.l2DepositOwner + ) + ).to.be.revertedWith("Vault address mismatch") + }) + }) + + context("when the requested vault is TBTCVault", () => { + context("when the deposit state is wrong", () => { + context("when the deposit state is Initialized", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + ).to.be.revertedWith("Wrong deposit state") + }) + }) + + context("when the deposit state is Finalized", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Set the Bridge mock to return a deposit state that allows + // to finalize the deposit. Set only relevant fields. + const revealedAt = (await lastBlockTime()) - 7200 + const finalizedAt = await lastBlockTime() + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: ethers.constants.AddressZero, + amount: BigNumber.from(100000), + revealedAt, + vault: ethers.constants.AddressZero, + treasuryFee: BigNumber.from(0), + sweptAt: finalizedAt, + extraData: ethers.constants.HashZero, + }) + + // Set the TBTCVault mock to return a deposit state + // that allows to finalize the deposit. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, finalizedAt]) + + // Set Wormhole mocks to allow deposit finalization. + const messageFee = 1000 + const deliveryCost = 5000 + wormhole.messageFee.returns(messageFee) + wormholeRelayer.quoteEVMDeliveryPrice.returns({ + nativePriceQuote: BigNumber.from(deliveryCost), + targetChainRefundPerGasUnused: BigNumber.from(0), + }) + wormholeTokenBridge.transferTokensWithPayload.returns(0) + wormholeRelayer.sendVaasToEvm.returns(0) + + await l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey, { + value: messageFee + deliveryCost, + }) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + wormhole.messageFee.reset() + wormholeRelayer.quoteEVMDeliveryPrice.reset() + wormholeTokenBridge.transferTokensWithPayload.reset() + wormholeRelayer.sendVaasToEvm.reset() + + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + ).to.be.revertedWith("Wrong deposit state") + }) + }) + }) + + context("when the deposit state is Unknown", () => { + context("when the reimbursement pool is not set", () => { + let tx: ContractTransaction + + before(async () => { + await createSnapshot() + + bridge.revealDepositWithExtraData + .whenCalledWith( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + toWormholeAddress(initializeDepositFixture.l2DepositOwner) + ) + .returns() + + tx = await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + + await restoreSnapshot() + }) + + it("should reveal the deposit to the Bridge", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(bridge.revealDepositWithExtraData).to.have.been.calledOnce + + const { fundingTx, reveal, l2DepositOwner } = + initializeDepositFixture + + // The `calledOnceWith` assertion is not used here because + // it doesn't use deep equality comparison and returns false + // despite comparing equal objects. We use a workaround + // to compare the arguments manually. + const call = bridge.revealDepositWithExtraData.getCall(0) + expect(call.args[0]).to.eql([ + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime, + ]) + expect(call.args[1]).to.eql([ + reveal.fundingOutputIndex, + reveal.blindingFactor, + reveal.walletPubKeyHash, + reveal.refundPubKeyHash, + reveal.refundLocktime, + reveal.vault, + ]) + expect(call.args[2]).to.eql( + toWormholeAddress(l2DepositOwner.toLowerCase()) + ) + }) + + it("should set the deposit state to Initialized", async () => { + expect( + await l1BitcoinDepositor.deposits( + initializeDepositFixture.depositKey + ) + ).to.equal(1) + }) + + it("should emit DepositInitialized event", async () => { + await expect(tx) + .to.emit(l1BitcoinDepositor, "DepositInitialized") + .withArgs( + initializeDepositFixture.depositKey, + initializeDepositFixture.l2DepositOwner, + relayer.address + ) + }) + + it("should not store the deferred gas reimbursement", async () => { + expect( + await l1BitcoinDepositor.gasReimbursements( + initializeDepositFixture.depositKey + ) + ).to.eql([ethers.constants.AddressZero, BigNumber.from(0)]) + }) + }) + + context("when the reimbursement pool is set", () => { + let tx: ContractTransaction + + before(async () => { + await createSnapshot() + + bridge.revealDepositWithExtraData + .whenCalledWith( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + toWormholeAddress(initializeDepositFixture.l2DepositOwner) + ) + .returns() + + await l1BitcoinDepositor + .connect(governance) + .updateReimbursementPool(reimbursementPool.address) + + tx = await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + + await restoreSnapshot() + }) + + it("should reveal the deposit to the Bridge", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(bridge.revealDepositWithExtraData).to.have.been.calledOnce + + const { fundingTx, reveal, l2DepositOwner } = + initializeDepositFixture + + // The `calledOnceWith` assertion is not used here because + // it doesn't use deep equality comparison and returns false + // despite comparing equal objects. We use a workaround + // to compare the arguments manually. + const call = bridge.revealDepositWithExtraData.getCall(0) + expect(call.args[0]).to.eql([ + fundingTx.version, + fundingTx.inputVector, + fundingTx.outputVector, + fundingTx.locktime, + ]) + expect(call.args[1]).to.eql([ + reveal.fundingOutputIndex, + reveal.blindingFactor, + reveal.walletPubKeyHash, + reveal.refundPubKeyHash, + reveal.refundLocktime, + reveal.vault, + ]) + expect(call.args[2]).to.eql( + toWormholeAddress(l2DepositOwner.toLowerCase()) + ) + }) + + it("should set the deposit state to Initialized", async () => { + expect( + await l1BitcoinDepositor.deposits( + initializeDepositFixture.depositKey + ) + ).to.equal(1) + }) + + it("should emit DepositInitialized event", async () => { + await expect(tx) + .to.emit(l1BitcoinDepositor, "DepositInitialized") + .withArgs( + initializeDepositFixture.depositKey, + initializeDepositFixture.l2DepositOwner, + relayer.address + ) + }) + + it("should store the deferred gas reimbursement", async () => { + const gasReimbursement = + await l1BitcoinDepositor.gasReimbursements( + initializeDepositFixture.depositKey + ) + + expect(gasReimbursement.receiver).to.equal(relayer.address) + // It doesn't make much sense to check the exact gas spent value + // here because a Bridge mock is used in for testing and + // the resulting value won't be realistic. We only check that + // the gas spent is greater than zero which means the deferred + // reimbursement has been recorded properly. + expect(gasReimbursement.gasSpent.toNumber()).to.be.greaterThan(0) + }) + }) + }) + }) + }) + }) + + describe("finalizeDeposit", () => { + before(async () => { + await createSnapshot() + + // The L2BitcoinDepositor contract must be attached to the L1BitcoinDepositor + // contract before the finalizeDeposit function is called. + await l1BitcoinDepositor + .connect(governance) + .attachL2BitcoinDepositor(l2BitcoinDepositor) + }) + + after(async () => { + await restoreSnapshot() + }) + + context("when the deposit state is wrong", () => { + context("when the deposit state is Unknown", () => { + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey) + ).to.be.revertedWith("Wrong deposit state") + }) + }) + + context("when the deposit state is Finalized", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Set the Bridge mock to return a deposit state that allows + // to finalize the deposit. Set only relevant fields. + const revealedAt = (await lastBlockTime()) - 7200 + const finalizedAt = await lastBlockTime() + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: ethers.constants.AddressZero, + amount: BigNumber.from(100000), + revealedAt, + vault: ethers.constants.AddressZero, + treasuryFee: BigNumber.from(0), + sweptAt: finalizedAt, + extraData: ethers.constants.HashZero, + }) + + // Set the TBTCVault mock to return a deposit state + // that allows to finalize the deposit. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, finalizedAt]) + + // Set Wormhole mocks to allow deposit finalization. + const messageFee = 1000 + const deliveryCost = 5000 + wormhole.messageFee.returns(messageFee) + wormholeRelayer.quoteEVMDeliveryPrice.returns({ + nativePriceQuote: BigNumber.from(deliveryCost), + targetChainRefundPerGasUnused: BigNumber.from(0), + }) + wormholeTokenBridge.transferTokensWithPayload.returns(0) + wormholeRelayer.sendVaasToEvm.returns(0) + + await l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey, { + value: messageFee + deliveryCost, + }) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + wormhole.messageFee.reset() + wormholeRelayer.quoteEVMDeliveryPrice.reset() + wormholeTokenBridge.transferTokensWithPayload.reset() + wormholeRelayer.sendVaasToEvm.reset() + + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey) + ).to.be.revertedWith("Wrong deposit state") + }) + }) + }) + + context("when the deposit state is Initialized", () => { + context("when the deposit is not finalized by the Bridge", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Set the Bridge mock to return a deposit state that does not allow + // to finalize the deposit. Set only relevant fields. + const revealedAt = (await lastBlockTime()) - 7200 + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: ethers.constants.AddressZero, + amount: BigNumber.from(100000), + revealedAt, + vault: ethers.constants.AddressZero, + treasuryFee: BigNumber.from(0), + sweptAt: 0, + extraData: ethers.constants.HashZero, + }) + + // Set the TBTCVault mock to return a deposit state + // that does not allow to finalize the deposit. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, 0]) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey) + ).to.be.revertedWith("Deposit not finalized by the bridge") + }) + }) + + context("when the deposit is finalized by the Bridge", () => { + context("when normalized amount is too low to bridge", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Set the Bridge mock to return a deposit state that pass the + // finalization check but fails the normalized amount check. + // Set only relevant fields. + const revealedAt = (await lastBlockTime()) - 7200 + const finalizedAt = await lastBlockTime() + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: ethers.constants.AddressZero, + amount: BigNumber.from(0), + revealedAt, + vault: ethers.constants.AddressZero, + treasuryFee: BigNumber.from(0), + sweptAt: finalizedAt, + extraData: ethers.constants.HashZero, + }) + + // Set the TBTCVault mock to return a deposit state that pass the + // finalization check and move to the normalized amount check. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, finalizedAt]) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey) + ).to.be.revertedWith("Amount too low to bridge") + }) + }) + + context("when normalized amount is not too low to bridge", () => { + context("when payment for Wormhole Relayer is too low", () => { + const messageFee = 1000 + const deliveryCost = 5000 + + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Set the Bridge mock to return a deposit state that allows + // to finalize the deposit. Set only relevant fields. + const revealedAt = (await lastBlockTime()) - 7200 + const finalizedAt = await lastBlockTime() + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: ethers.constants.AddressZero, + amount: BigNumber.from(100000), + revealedAt, + vault: ethers.constants.AddressZero, + treasuryFee: BigNumber.from(0), + sweptAt: finalizedAt, + extraData: ethers.constants.HashZero, + }) + + // Set the TBTCVault mock to return a deposit state + // that allows to finalize the deposit. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, finalizedAt]) + + // Set Wormhole mocks to allow deposit finalization. + wormhole.messageFee.returns(messageFee) + wormholeRelayer.quoteEVMDeliveryPrice.returns({ + nativePriceQuote: BigNumber.from(deliveryCost), + targetChainRefundPerGasUnused: BigNumber.from(0), + }) + wormholeTokenBridge.transferTokensWithPayload.returns(0) + wormholeRelayer.sendVaasToEvm.returns(0) + }) + + after(async () => { + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + wormhole.messageFee.reset() + wormholeRelayer.quoteEVMDeliveryPrice.reset() + wormholeTokenBridge.transferTokensWithPayload.reset() + wormholeRelayer.sendVaasToEvm.reset() + + await restoreSnapshot() + }) + + it("should revert", async () => { + await expect( + l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey, { + // Use a value by 1 WEI less than required. + value: messageFee + deliveryCost - 1, + }) + ).to.be.revertedWith("Payment for Wormhole Relayer is too low") + }) + }) + + context("when payment for Wormhole Relayer is not too low", () => { + const satoshiMultiplier = to1ePrecision(1, 10) + const messageFee = 1000 + const deliveryCost = 5000 + const transferSequence = 10 // Just an arbitrary value. + const depositAmount = BigNumber.from(100000) + const treasuryFee = BigNumber.from(500) + const optimisticMintingFeeDivisor = 20 // 5% + const depositTxMaxFee = BigNumber.from(1000) + + // amountSubTreasury = (depositAmount - treasuryFee) * satoshiMultiplier = 99500 * 1e10 + // omFee = amountSubTreasury / optimisticMintingFeeDivisor = 4975 * 1e10 + // txMaxFee = depositTxMaxFee * satoshiMultiplier = 1000 * 1e10 + // tbtcAmount = amountSubTreasury - omFee - txMaxFee = 93525 * 1e10 + const expectedTbtcAmount = to1ePrecision(93525, 10) + + let tx: ContractTransaction + + context("when the reimbursement pool is not set", () => { + before(async () => { + await createSnapshot() + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Set Bridge fees. Set only relevant fields. + bridge.depositParameters.returns({ + depositDustThreshold: 0, + depositTreasuryFeeDivisor: 0, + depositTxMaxFee, + depositRevealAheadPeriod: 0, + }) + tbtcVault.optimisticMintingFeeDivisor.returns( + optimisticMintingFeeDivisor + ) + + // Set the Bridge mock to return a deposit state that allows + // to finalize the deposit. + const revealedAt = (await lastBlockTime()) - 7200 + const finalizedAt = await lastBlockTime() + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: l1BitcoinDepositor.address, + amount: depositAmount, + revealedAt, + vault: initializeDepositFixture.reveal.vault, + treasuryFee, + sweptAt: finalizedAt, + extraData: toWormholeAddress( + initializeDepositFixture.l2DepositOwner + ), + }) + + // Set the TBTCVault mock to return a deposit state + // that allows to finalize the deposit. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, finalizedAt]) + + // Set Wormhole mocks to allow deposit finalization. + wormhole.messageFee.returns(messageFee) + wormholeRelayer.quoteEVMDeliveryPrice.returns({ + nativePriceQuote: BigNumber.from(deliveryCost), + targetChainRefundPerGasUnused: BigNumber.from(0), + }) + wormholeTokenBridge.transferTokensWithPayload.returns( + transferSequence + ) + // Return arbitrary sent value. + wormholeRelayer.sendVaasToEvm.returns(100) + + tx = await l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey, { + value: messageFee + deliveryCost, + }) + }) + + after(async () => { + bridge.depositParameters.reset() + tbtcVault.optimisticMintingFeeDivisor.reset() + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + wormhole.messageFee.reset() + wormholeRelayer.quoteEVMDeliveryPrice.reset() + wormholeTokenBridge.transferTokensWithPayload.reset() + wormholeRelayer.sendVaasToEvm.reset() + + await restoreSnapshot() + }) + + it("should set the deposit state to Finalized", async () => { + expect( + await l1BitcoinDepositor.deposits( + initializeDepositFixture.depositKey + ) + ).to.equal(2) + }) + + it("should emit DepositFinalized event", async () => { + await expect(tx) + .to.emit(l1BitcoinDepositor, "DepositFinalized") + .withArgs( + initializeDepositFixture.depositKey, + initializeDepositFixture.l2DepositOwner, + relayer.address, + depositAmount.mul(satoshiMultiplier), + expectedTbtcAmount + ) + }) + + it("should increase TBTC allowance for Wormhole Token Bridge", async () => { + expect( + await tbtcToken.allowance( + l1BitcoinDepositor.address, + wormholeTokenBridge.address + ) + ).to.equal(expectedTbtcAmount) + }) + + it("should create a proper Wormhole token transfer", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(wormholeTokenBridge.transferTokensWithPayload).to.have + .been.calledOnce + + // The `calledOnceWith` assertion is not used here because + // it doesn't use deep equality comparison and returns false + // despite comparing equal objects. We use a workaround + // to compare the arguments manually. + const call = + wormholeTokenBridge.transferTokensWithPayload.getCall(0) + expect(call.value).to.equal(messageFee) + expect(call.args[0]).to.equal(tbtcToken.address) + expect(call.args[1]).to.equal(expectedTbtcAmount) + expect(call.args[2]).to.equal( + await l1BitcoinDepositor.l2ChainId() + ) + expect(call.args[3]).to.equal( + toWormholeAddress(l2WormholeGateway.address.toLowerCase()) + ) + expect(call.args[4]).to.equal(0) + expect(call.args[5]).to.equal( + ethers.utils.defaultAbiCoder.encode( + ["address"], + [initializeDepositFixture.l2DepositOwner] + ) + ) + }) + + it("should send transfer VAA to L2", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(wormholeRelayer.sendVaasToEvm).to.have.been.calledOnce + + // The `calledOnceWith` assertion is not used here because + // it doesn't use deep equality comparison and returns false + // despite comparing equal objects. We use a workaround + // to compare the arguments manually. + const call = wormholeRelayer.sendVaasToEvm.getCall(0) + expect(call.value).to.equal(deliveryCost) + expect(call.args[0]).to.equal( + await l1BitcoinDepositor.l2ChainId() + ) + expect(call.args[1]).to.equal(l2BitcoinDepositor) + expect(call.args[2]).to.equal("0x") + expect(call.args[3]).to.equal(0) + expect(call.args[4]).to.equal( + await l1BitcoinDepositor.l2FinalizeDepositGasLimit() + ) + expect(call.args[5]).to.eql([ + [ + l1ChainId, + toWormholeAddress( + wormholeTokenBridge.address.toLowerCase() + ), + BigNumber.from(transferSequence), + ], + ]) + expect(call.args[6]).to.equal( + await l1BitcoinDepositor.l2ChainId() + ) + expect(call.args[7]).to.equal(relayer.address) + }) + + it("should not call the reimbursement pool", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(reimbursementPool.refund).to.not.have.been.called + }) + }) + + context("when the reimbursement pool is set", () => { + // Use 1Gwei to make sure it's smaller than default gas price + // used by Hardhat (200 Gwei) and this value will be used + // for msgValueOffset calculation. + const reimbursementPoolMaxGasPrice = BigNumber.from(1000000000) + const reimbursementPoolStaticGas = 10000 // Just an arbitrary value. + + let initializeDepositGasSpent: BigNumber + + before(async () => { + await createSnapshot() + + reimbursementPool.maxGasPrice.returns( + reimbursementPoolMaxGasPrice + ) + reimbursementPool.staticGas.returns(reimbursementPoolStaticGas) + + await l1BitcoinDepositor + .connect(governance) + .updateReimbursementPool(reimbursementPool.address) + + await l1BitcoinDepositor + .connect(relayer) + .initializeDeposit( + initializeDepositFixture.fundingTx, + initializeDepositFixture.reveal, + initializeDepositFixture.l2DepositOwner + ) + + // Capture the gas spent for the initializeDeposit call + // for post-finalization comparison. + initializeDepositGasSpent = ( + await l1BitcoinDepositor.gasReimbursements( + initializeDepositFixture.depositKey + ) + ).gasSpent + + // Set Bridge fees. Set only relevant fields. + bridge.depositParameters.returns({ + depositDustThreshold: 0, + depositTreasuryFeeDivisor: 0, + depositTxMaxFee, + depositRevealAheadPeriod: 0, + }) + tbtcVault.optimisticMintingFeeDivisor.returns( + optimisticMintingFeeDivisor + ) + + // Set the Bridge mock to return a deposit state that allows + // to finalize the deposit. + const revealedAt = (await lastBlockTime()) - 7200 + const finalizedAt = await lastBlockTime() + bridge.deposits + .whenCalledWith(initializeDepositFixture.depositKey) + .returns({ + depositor: l1BitcoinDepositor.address, + amount: depositAmount, + revealedAt, + vault: initializeDepositFixture.reveal.vault, + treasuryFee, + sweptAt: finalizedAt, + extraData: toWormholeAddress( + initializeDepositFixture.l2DepositOwner + ), + }) + + // Set the TBTCVault mock to return a deposit state + // that allows to finalize the deposit. + tbtcVault.optimisticMintingRequests + .whenCalledWith(initializeDepositFixture.depositKey) + .returns([revealedAt, finalizedAt]) + + // Set Wormhole mocks to allow deposit finalization. + wormhole.messageFee.returns(messageFee) + wormholeRelayer.quoteEVMDeliveryPrice.returns({ + nativePriceQuote: BigNumber.from(deliveryCost), + targetChainRefundPerGasUnused: BigNumber.from(0), + }) + wormholeTokenBridge.transferTokensWithPayload.returns( + transferSequence + ) + // Return arbitrary sent value. + wormholeRelayer.sendVaasToEvm.returns(100) + + tx = await l1BitcoinDepositor + .connect(relayer) + .finalizeDeposit(initializeDepositFixture.depositKey, { + value: messageFee + deliveryCost, + }) + }) + + after(async () => { + reimbursementPool.maxGasPrice.reset() + reimbursementPool.staticGas.reset() + bridge.depositParameters.reset() + tbtcVault.optimisticMintingFeeDivisor.reset() + bridge.revealDepositWithExtraData.reset() + bridge.deposits.reset() + tbtcVault.optimisticMintingRequests.reset() + wormhole.messageFee.reset() + wormholeRelayer.quoteEVMDeliveryPrice.reset() + wormholeTokenBridge.transferTokensWithPayload.reset() + wormholeRelayer.sendVaasToEvm.reset() + + await restoreSnapshot() + }) + + it("should set the deposit state to Finalized", async () => { + expect( + await l1BitcoinDepositor.deposits( + initializeDepositFixture.depositKey + ) + ).to.equal(2) + }) + + it("should emit DepositFinalized event", async () => { + await expect(tx) + .to.emit(l1BitcoinDepositor, "DepositFinalized") + .withArgs( + initializeDepositFixture.depositKey, + initializeDepositFixture.l2DepositOwner, + relayer.address, + depositAmount.mul(satoshiMultiplier), + expectedTbtcAmount + ) + }) + + it("should increase TBTC allowance for Wormhole Token Bridge", async () => { + expect( + await tbtcToken.allowance( + l1BitcoinDepositor.address, + wormholeTokenBridge.address + ) + ).to.equal(expectedTbtcAmount) + }) + + it("should create a proper Wormhole token transfer", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(wormholeTokenBridge.transferTokensWithPayload).to.have + .been.calledOnce + + // The `calledOnceWith` assertion is not used here because + // it doesn't use deep equality comparison and returns false + // despite comparing equal objects. We use a workaround + // to compare the arguments manually. + const call = + wormholeTokenBridge.transferTokensWithPayload.getCall(0) + expect(call.value).to.equal(messageFee) + expect(call.args[0]).to.equal(tbtcToken.address) + expect(call.args[1]).to.equal(expectedTbtcAmount) + expect(call.args[2]).to.equal( + await l1BitcoinDepositor.l2ChainId() + ) + expect(call.args[3]).to.equal( + toWormholeAddress(l2WormholeGateway.address.toLowerCase()) + ) + expect(call.args[4]).to.equal(0) + expect(call.args[5]).to.equal( + ethers.utils.defaultAbiCoder.encode( + ["address"], + [initializeDepositFixture.l2DepositOwner] + ) + ) + }) + + it("should send transfer VAA to L2", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(wormholeRelayer.sendVaasToEvm).to.have.been.calledOnce + + // The `calledOnceWith` assertion is not used here because + // it doesn't use deep equality comparison and returns false + // despite comparing equal objects. We use a workaround + // to compare the arguments manually. + const call = wormholeRelayer.sendVaasToEvm.getCall(0) + expect(call.value).to.equal(deliveryCost) + expect(call.args[0]).to.equal( + await l1BitcoinDepositor.l2ChainId() + ) + expect(call.args[1]).to.equal(l2BitcoinDepositor) + expect(call.args[2]).to.equal("0x") + expect(call.args[3]).to.equal(0) + expect(call.args[4]).to.equal( + await l1BitcoinDepositor.l2FinalizeDepositGasLimit() + ) + expect(call.args[5]).to.eql([ + [ + l1ChainId, + toWormholeAddress( + wormholeTokenBridge.address.toLowerCase() + ), + BigNumber.from(transferSequence), + ], + ]) + expect(call.args[6]).to.equal( + await l1BitcoinDepositor.l2ChainId() + ) + expect(call.args[7]).to.equal(relayer.address) + }) + + it("should pay out proper reimbursements", async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(reimbursementPool.refund).to.have.been.calledTwice + + // First call is the deferred gas reimbursement for deposit + // initialization. + const call1 = reimbursementPool.refund.getCall(0) + // Should reimburse the exact value stored upon deposit initialization. + expect(call1.args[0]).to.equal(initializeDepositGasSpent) + expect(call1.args[1]).to.equal(relayer.address) + + // Second call is the refund for deposit finalization. + const call2 = reimbursementPool.refund.getCall(1) + // It doesn't make much sense to check the exact gas spent + // value here because Wormhole contracts mocks are used for + // testing and the resulting value won't be realistic. + // We only check that the reimbursement is greater than the + // message value attached to the finalizeDeposit call which + // is a good indicator that the reimbursement has been + // calculated properly. + const msgValueOffset = BigNumber.from(messageFee + deliveryCost) + .div(reimbursementPoolMaxGasPrice) + .sub(reimbursementPoolStaticGas) + expect( + BigNumber.from(call2.args[0]).toNumber() + ).to.be.greaterThan(msgValueOffset.toNumber()) + expect(call2.args[1]).to.equal(relayer.address) + }) + }) + }) + }) + }) + }) + }) + + describe("quoteFinalizeDeposit", () => { + before(async () => { + await createSnapshot() + + wormhole.messageFee.returns(1000) + + wormholeRelayer.quoteEVMDeliveryPrice + .whenCalledWith( + await l1BitcoinDepositor.l2ChainId(), + 0, + await l1BitcoinDepositor.l2FinalizeDepositGasLimit() + ) + .returns({ + nativePriceQuote: BigNumber.from(5000), + targetChainRefundPerGasUnused: BigNumber.from(0), + }) + }) + + after(async () => { + wormhole.messageFee.reset() + wormholeRelayer.quoteEVMDeliveryPrice.reset() + + await restoreSnapshot() + }) + + it("should return the correct cost", async () => { + const cost = await l1BitcoinDepositor.quoteFinalizeDeposit() + expect(cost).to.be.equal(6000) // delivery cost + message fee + }) + }) +}) + +// Just an arbitrary TBTCVault address. +const tbtcVaultAddress = "0xB5679dE944A79732A75CE556191DF11F489448d5" + +export type InitializeDepositFixture = { + // Deposit key built as keccak256(fundingTxHash, reveal.fundingOutputIndex) + depositKey: string + fundingTx: BitcoinTxInfoStruct + reveal: DepositRevealInfoStruct + l2DepositOwner: string +} + +// Fixture used for initializeDeposit test scenario. +export const initializeDepositFixture: InitializeDepositFixture = { + depositKey: + "0x97a4104f4114ba56dde79d02c4e8296596c3259da60d0e53fa97170f7cf7258d", + fundingTx: { + version: "0x01000000", + inputVector: + "0x01dfe39760a5edabdab013114053d789ada21e356b59fea41d980396" + + "c1a4474fad0100000023220020e57edf10136b0434e46bc08c5ac5a1e4" + + "5f64f778a96f984d0051873c7a8240f2ffffffff", + outputVector: + "0x02804f1200000000002200202f601522e7bb1f7de5c56bdbd45590b3" + + "499bad09190581dcaa17e152d8f0c2a9b7e837000000000017a9148688" + + "4e6be1525dab5ae0b451bd2c72cee67dcf4187", + locktime: "0x00000000", + }, + reveal: { + fundingOutputIndex: 0, + blindingFactor: "0xba863847d2d0fee3", + walletPubKeyHash: "0xf997563fee8610ca28f99ac05bd8a29506800d4d", + refundPubKeyHash: "0x7ac2d9378a1c47e589dfb8095ca95ed2140d2726", + refundLocktime: "0xde2b4c67", + vault: tbtcVaultAddress, + }, + l2DepositOwner: "0x23b82a7108F9CEb34C3CDC44268be21D151d4124", +} + +// eslint-disable-next-line import/prefer-default-export +export function toWormholeAddress(address: string): string { + return `0x000000000000000000000000${address.slice(2)}` +} diff --git a/solidity/test/l2/L2BitcoinDepositor.test.ts b/solidity/test/l2/L2BitcoinDepositor.test.ts index 8813bf610..4b3747151 100644 --- a/solidity/test/l2/L2BitcoinDepositor.test.ts +++ b/solidity/test/l2/L2BitcoinDepositor.test.ts @@ -9,10 +9,10 @@ import { IWormholeRelayer, L2BitcoinDepositor, } from "../../typechain" -import type { - DepositRevealInfoStruct, - BitcoinTxInfoStruct, -} from "../../typechain/L2BitcoinDepositor" +import { + initializeDepositFixture, + toWormholeAddress, +} from "./L1BitcoinDepositor.test" chai.use(smock.matchers) @@ -71,37 +71,6 @@ describe("L2BitcoinDepositor", () => { } } - type InitializeDepositFixture = { - fundingTx: BitcoinTxInfoStruct - reveal: DepositRevealInfoStruct - l2DepositOwner: string - } - - // Fixture used for initializeDeposit test scenario. - const initializeDepositFixture: InitializeDepositFixture = { - fundingTx: { - version: "0x01000000", - inputVector: - "0x01dfe39760a5edabdab013114053d789ada21e356b59fea41d980396" + - "c1a4474fad0100000023220020e57edf10136b0434e46bc08c5ac5a1e4" + - "5f64f778a96f984d0051873c7a8240f2ffffffff", - outputVector: - "0x02804f1200000000002200202f601522e7bb1f7de5c56bdbd45590b3" + - "499bad09190581dcaa17e152d8f0c2a9b7e837000000000017a9148688" + - "4e6be1525dab5ae0b451bd2c72cee67dcf4187", - locktime: "0x00000000", - }, - reveal: { - fundingOutputIndex: 0, - blindingFactor: "0xba863847d2d0fee3", - walletPubKeyHash: "0xf997563fee8610ca28f99ac05bd8a29506800d4d", - refundPubKeyHash: "0x7ac2d9378a1c47e589dfb8095ca95ed2140d2726", - refundLocktime: "0xde2b4c67", - vault: "0xB5679dE944A79732A75CE556191DF11F489448d5", - }, - l2DepositOwner: "0x23b82a7108F9CEb34C3CDC44268be21D151d4124", - } - let governance: SignerWithAddress let relayer: SignerWithAddress @@ -379,8 +348,3 @@ describe("L2BitcoinDepositor", () => { }) }) }) - -// eslint-disable-next-line import/prefer-default-export -export function toWormholeAddress(address: string): string { - return `0x000000000000000000000000${address.slice(2)}` -} From d6cabdb351a6440a1ad4f6088325215c92a88bf2 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 8 Mar 2024 11:43:35 +0100 Subject: [PATCH 22/24] Optimize the `GasReimbursement` structure It's enough to store the `gasSpent` as `uint96` which allows us to save one storage slot. Moreover, we are increasing the default value of `initializeDepositGasOffset` to adhere to real-world gas consumption. --- solidity/contracts/l2/L1BitcoinDepositor.sol | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index 5edaea7f3..09f7a0b0d 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -93,7 +93,7 @@ contract L1BitcoinDepositor is /// @notice Receiver that is supposed to receive the reimbursement. address receiver; /// @notice Gas expenditure that is meant to be reimbursed. - uint256 gasSpent; + uint96 gasSpent; } /// @notice Holds the deposit state, keyed by the deposit key calculated for @@ -189,7 +189,7 @@ contract L1BitcoinDepositor is l2WormholeGateway = _l2WormholeGateway; l2ChainId = _l2ChainId; l2FinalizeDepositGasLimit = 500_000; - initializeDepositGasOffset = 20_000; + initializeDepositGasOffset = 40_000; finalizeDepositGasOffset = 20_000; } @@ -355,6 +355,16 @@ contract L1BitcoinDepositor is emit DepositInitialized(depositKey, l2DepositOwner, msg.sender); if (address(reimbursementPool) != address(0)) { + uint256 gasSpent = (gasStart - gasleft()) + + initializeDepositGasOffset; + + // Should not happen as long as initializeDepositGasOffset is + // set to a reasonable value. If it happens, it's better to + // omit the reimbursement than to revert the transaction. + if (gasSpent > type(uint96).max) { + return; + } + // Do not issue a reimbursement immediately. Record // a deferred reimbursement that will be paid out upon deposit // finalization. This is because the tBTC Bridge accepts all @@ -365,7 +375,7 @@ contract L1BitcoinDepositor is // slither-disable-next-line reentrancy-benign gasReimbursements[depositKey] = GasReimbursement({ receiver: msg.sender, - gasSpent: (gasStart - gasleft()) + initializeDepositGasOffset + gasSpent: uint96(gasSpent) }); } } From f754ebcceda08718bf34b50c709ae0b85b0cda5b Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 8 Mar 2024 14:05:47 +0100 Subject: [PATCH 23/24] Re-deploy contracts on Ethereum on Base Sepolia --- cross-chain/base/.openzeppelin/sepolia.json | 44 +++++----- .../base/.openzeppelin/unknown-84532.json | 4 +- .../baseSepolia/BaseL2BitcoinDepositor.json | 56 ++++++------- .../sepolia/BaseL1BitcoinDepositor.json | 80 +++++++++---------- 4 files changed, 94 insertions(+), 90 deletions(-) diff --git a/cross-chain/base/.openzeppelin/sepolia.json b/cross-chain/base/.openzeppelin/sepolia.json index 16d589e45..a088e7559 100644 --- a/cross-chain/base/.openzeppelin/sepolia.json +++ b/cross-chain/base/.openzeppelin/sepolia.json @@ -1,20 +1,20 @@ { "manifestVersion": "3.2", "admin": { - "address": "0x33D7816A1C356F2D36c8cBA5D449f9c46306f818", - "txHash": "0x6289dd2828a8879386e2065836adeb66c7f60ea1c5c3bec36df33803071a0ace" + "address": "0xDd0007713CB99564B7835FD628A1718e8F9f9785", + "txHash": "0x078036699e010480a0d2a4c443352cf0f6311f8882dddcf0b4c6c5136bcc69b4" }, "proxies": [ { - "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", - "txHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "address": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", + "txHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", "kind": "transparent" } ], "impls": { - "397d1cce625c7ec2463bd078e8cf930aa844e1a9684804f18f36aa9b60b6fa7c": { - "address": "0x193096824F7618b49E8742651D9656e977c756d1", - "txHash": "0x91b2e2ff923eb25960e9d827106c688f48931d17a849e04cbb0c83e0bf11ca18", + "e1501c59bd3fc7501392b17ad132c0ef733008f7128caa90d854edd898c505ec": { + "address": "0x720Cb49A8b3c03E199075544F7f1F4d772Dd6d06", + "txHash": "0xf055a10c164376e93258e15ff257e76fc8eff5a737820237271a8fb3e3506fe4", "layout": { "solcVersion": "0.8.17", "storage": [ @@ -111,7 +111,7 @@ "label": "tbtcToken", "offset": 0, "slot": "201", - "type": "t_contract(IERC20)5428", + "type": "t_contract(IERC20Upgradeable)6699", "contract": "L1BitcoinDepositor", "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:113" }, @@ -119,7 +119,7 @@ "label": "wormhole", "offset": 0, "slot": "202", - "type": "t_contract(IWormhole)4366", + "type": "t_contract(IWormhole)5318", "contract": "L1BitcoinDepositor", "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:115" }, @@ -127,7 +127,7 @@ "label": "wormholeRelayer", "offset": 0, "slot": "203", - "type": "t_contract(IWormholeRelayer)4406", + "type": "t_contract(IWormholeRelayer)5358", "contract": "L1BitcoinDepositor", "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:116" }, @@ -135,7 +135,7 @@ "label": "wormholeTokenBridge", "offset": 0, "slot": "204", - "type": "t_contract(IWormholeTokenBridge)4491", + "type": "t_contract(IWormholeTokenBridge)5443", "contract": "L1BitcoinDepositor", "src": "@keep-network/tbtc-v2/contracts/l2/L1BitcoinDepositor.sol:118" }, @@ -221,23 +221,23 @@ "label": "contract IBridge", "numberOfBytes": "20" }, - "t_contract(IERC20)5428": { - "label": "contract IERC20", + "t_contract(IERC20Upgradeable)6699": { + "label": "contract IERC20Upgradeable", "numberOfBytes": "20" }, "t_contract(ITBTCVault)3440": { "label": "contract ITBTCVault", "numberOfBytes": "20" }, - "t_contract(IWormhole)4366": { + "t_contract(IWormhole)5318": { "label": "contract IWormhole", "numberOfBytes": "20" }, - "t_contract(IWormholeRelayer)4406": { + "t_contract(IWormholeRelayer)5358": { "label": "contract IWormholeRelayer", "numberOfBytes": "20" }, - "t_contract(IWormholeTokenBridge)4491": { + "t_contract(IWormholeTokenBridge)5443": { "label": "contract IWormholeTokenBridge", "numberOfBytes": "20" }, @@ -273,12 +273,12 @@ }, { "label": "gasSpent", - "type": "t_uint256", - "offset": 0, - "slot": "1" + "type": "t_uint96", + "offset": 20, + "slot": "0" } ], - "numberOfBytes": "64" + "numberOfBytes": "32" }, "t_uint16": { "label": "uint16", @@ -291,6 +291,10 @@ "t_uint8": { "label": "uint8", "numberOfBytes": "1" + }, + "t_uint96": { + "label": "uint96", + "numberOfBytes": "12" } } } diff --git a/cross-chain/base/.openzeppelin/unknown-84532.json b/cross-chain/base/.openzeppelin/unknown-84532.json index a91417980..0bb616427 100644 --- a/cross-chain/base/.openzeppelin/unknown-84532.json +++ b/cross-chain/base/.openzeppelin/unknown-84532.json @@ -16,8 +16,8 @@ "kind": "transparent" }, { - "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", - "txHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "address": "0x04BE8F183572ec802aD26756F3E9398098700E76", + "txHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", "kind": "transparent" } ], diff --git a/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json b/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json index 49fca47c9..a4f4e4337 100644 --- a/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json +++ b/cross-chain/base/deployments/baseSepolia/BaseL2BitcoinDepositor.json @@ -1,5 +1,5 @@ { - "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "address": "0x04BE8F183572ec802aD26756F3E9398098700E76", "abi": [ { "inputs": [], @@ -355,71 +355,71 @@ "type": "function" } ], - "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "transactionHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", "receipt": { "to": null, "from": "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", - "contractAddress": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "contractAddress": "0x04BE8F183572ec802aD26756F3E9398098700E76", "transactionIndex": 2, "gasUsed": "704802", - "logsBloom": "0x00000000000000000000000000000000400000000000000000800000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000800000000000000000000002400000000200000000000000000000000000000000000080000000000000800080000000000000000000000000000400000000000000000000000000000000000000000020000000000000000020040000000008000400000000000000000020000000000000000000000000000000000000000000000000000004000000400000", - "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed", - "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", + "logsBloom": "0x00000000000000000000000000000000400000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000800000000000000000000000400002000200000000000000000000000000000000000080000000000000800080000000000000000000000000000400000000000100000000000000000000000000000020000000000000000020040000000008000400000000000000000020000000000000000000080000000000000000000000000000000000000000400000", + "blockHash": "0xcb1f5aa73f2f63d359224b51e7abf66a431e3420daa6940b72a3a58dd5a00c85", + "transactionHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", "logs": [ { "transactionIndex": 2, - "blockNumber": 6946054, - "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", - "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "blockNumber": 7063584, + "transactionHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", + "address": "0x04BE8F183572ec802aD26756F3E9398098700E76", "topics": [ "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", "0x0000000000000000000000001ecd87c8d510a7390a561ae0ac54fbe7e5125bcf" ], "data": "0x", - "logIndex": 0, - "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" + "logIndex": 1, + "blockHash": "0xcb1f5aa73f2f63d359224b51e7abf66a431e3420daa6940b72a3a58dd5a00c85" }, { "transactionIndex": 2, - "blockNumber": 6946054, - "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", - "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "blockNumber": 7063584, + "transactionHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", + "address": "0x04BE8F183572ec802aD26756F3E9398098700E76", "topics": [ "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000068ad60cc5e8f3b7cc53beab321cf0e6036962dbc" ], "data": "0x", - "logIndex": 1, - "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" + "logIndex": 2, + "blockHash": "0xcb1f5aa73f2f63d359224b51e7abf66a431e3420daa6940b72a3a58dd5a00c85" }, { "transactionIndex": 2, - "blockNumber": 6946054, - "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", - "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "blockNumber": 7063584, + "transactionHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", + "address": "0x04BE8F183572ec802aD26756F3E9398098700E76", "topics": [ "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000001", - "logIndex": 2, - "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" + "logIndex": 3, + "blockHash": "0xcb1f5aa73f2f63d359224b51e7abf66a431e3420daa6940b72a3a58dd5a00c85" }, { "transactionIndex": 2, - "blockNumber": 6946054, - "transactionHash": "0x2709623a9f64d8e28a2ab1c3642ee1635a7637a9758f9cd15e9c29640c5d0418", - "address": "0x5fA3D34F6c1537A0393098F0e124064B66cE8E24", + "blockNumber": 7063584, + "transactionHash": "0xaeb9ee6679e0f96108788abffa9ed78c943c09565686dfdb826378e9bd1487df", + "address": "0x04BE8F183572ec802aD26756F3E9398098700E76", "topics": [ "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b2f6c5b73239c39360ee0ea95047565dab13e3c7", - "logIndex": 3, - "blockHash": "0x77164fc82fcb80121eeb90ca789d5ab13ac7c7d5ceeea1b45255888f08acdeed" + "logIndex": 4, + "blockHash": "0xcb1f5aa73f2f63d359224b51e7abf66a431e3420daa6940b72a3a58dd5a00c85" } ], - "blockNumber": 6946054, - "cumulativeGasUsed": "769653", + "blockNumber": 7063584, + "cumulativeGasUsed": "804788", "status": 1, "byzantium": true }, diff --git a/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json b/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json index c07a1ff17..1826e6b36 100644 --- a/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json +++ b/cross-chain/base/deployments/sepolia/BaseL1BitcoinDepositor.json @@ -1,5 +1,5 @@ { - "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "address": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", "abi": [ { "inputs": [], @@ -245,9 +245,9 @@ "type": "address" }, { - "internalType": "uint256", + "internalType": "uint96", "name": "gasSpent", - "type": "uint256" + "type": "uint96" } ], "stateMutability": "view", @@ -489,7 +489,7 @@ "name": "tbtcToken", "outputs": [ { - "internalType": "contract IERC20", + "internalType": "contract IERC20Upgradeable", "name": "", "type": "address" } @@ -607,75 +607,75 @@ "type": "function" } ], - "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "transactionHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", "receipt": { "to": null, "from": "0x68ad60CC5e8f3B7cC53beaB321cf0e6036962dBc", - "contractAddress": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", - "transactionIndex": 85, - "gasUsed": "887866", - "logsBloom": "0x00000000000000000000200000000000400000000000000000800000800000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000800000000000000000000000400000000200000000000000000000000000000000000080000000000000800000000000000000000000000000000440000000000000000000000000000000000000000020000000000000000020040000000000000400000000000000000020000000000400000000000000000000000000000000000000008000000000000000", - "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4", - "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", + "contractAddress": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", + "transactionIndex": 94, + "gasUsed": "887851", + "logsBloom": "0x00000000000000000000000000000000400000000000000400800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202000001000000000000000000000000000000000000020000000000000000000800000000800000000000000008000000400000000200000000000000000000000000000040000080000000000000800000000000000000000000000000000400000000000000000000000000000010000000000020000000000000200020040000000000000400000000000000000020000000000000000000000000000000100000000000000000000000000000000000", + "blockHash": "0x9d4a08d2b6fd26df76447aa1b5e0054686b1d462dc12be4d271dca1299060a2c", + "transactionHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", "logs": [ { - "transactionIndex": 85, - "blockNumber": 5422966, - "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", - "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "transactionIndex": 94, + "blockNumber": 5441536, + "transactionHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", + "address": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", "topics": [ "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", - "0x000000000000000000000000193096824f7618b49e8742651d9656e977c756d1" + "0x000000000000000000000000720cb49a8b3c03e199075544f7f1f4d772dd6d06" ], "data": "0x", - "logIndex": 2090, - "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" + "logIndex": 75, + "blockHash": "0x9d4a08d2b6fd26df76447aa1b5e0054686b1d462dc12be4d271dca1299060a2c" }, { - "transactionIndex": 85, - "blockNumber": 5422966, - "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", - "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "transactionIndex": 94, + "blockNumber": 5441536, + "transactionHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", + "address": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", "topics": [ "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", "0x0000000000000000000000000000000000000000000000000000000000000000", "0x00000000000000000000000068ad60cc5e8f3b7cc53beab321cf0e6036962dbc" ], "data": "0x", - "logIndex": 2091, - "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" + "logIndex": 76, + "blockHash": "0x9d4a08d2b6fd26df76447aa1b5e0054686b1d462dc12be4d271dca1299060a2c" }, { - "transactionIndex": 85, - "blockNumber": 5422966, - "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", - "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "transactionIndex": 94, + "blockNumber": 5441536, + "transactionHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", + "address": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", "topics": [ "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" ], "data": "0x0000000000000000000000000000000000000000000000000000000000000001", - "logIndex": 2092, - "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" + "logIndex": 77, + "blockHash": "0x9d4a08d2b6fd26df76447aa1b5e0054686b1d462dc12be4d271dca1299060a2c" }, { - "transactionIndex": 85, - "blockNumber": 5422966, - "transactionHash": "0xcbbc13b83d1e8108251c2fbcf61c321f97dc346ade968cc074837f1c3fe0e704", - "address": "0x65935daB5d33D48f6E3712CDD602793aD38c8B75", + "transactionIndex": 94, + "blockNumber": 5441536, + "transactionHash": "0x5a405183332f623649fcf19f8506cf2582882d5dc2b05582e0066388ef122229", + "address": "0x0c5e36731008f4AFC1AF5Da2C4D5E07eE4a3EB69", "topics": [ "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" ], - "data": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000033d7816a1c356f2d36c8cba5d449f9c46306f818", - "logIndex": 2093, - "blockHash": "0x4a6195872ab8b2a475371b9cb081f1e093eaa2d8a836c20754ab763629e5dfd4" + "data": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dd0007713cb99564b7835fd628a1718e8f9f9785", + "logIndex": 78, + "blockHash": "0x9d4a08d2b6fd26df76447aa1b5e0054686b1d462dc12be4d271dca1299060a2c" } ], - "blockNumber": 5422966, - "cumulativeGasUsed": "24824577", + "blockNumber": 5441536, + "cumulativeGasUsed": "8903785", "status": 1, "byzantium": true }, "numDeployments": 1, - "implementation": "0x193096824F7618b49E8742651D9656e977c756d1", + "implementation": "0x720Cb49A8b3c03E199075544F7f1F4d772Dd6d06", "devdoc": "Contract deployed as upgradable proxy" } \ No newline at end of file From a438e1a20a1f1c43630bd9c24acda6328a3b06cf Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Fri, 8 Mar 2024 14:17:49 +0100 Subject: [PATCH 24/24] Adjust default value of `initializeDepositGasOffset` again --- solidity/contracts/l2/L1BitcoinDepositor.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/contracts/l2/L1BitcoinDepositor.sol b/solidity/contracts/l2/L1BitcoinDepositor.sol index 09f7a0b0d..fccd99f91 100644 --- a/solidity/contracts/l2/L1BitcoinDepositor.sol +++ b/solidity/contracts/l2/L1BitcoinDepositor.sol @@ -189,7 +189,7 @@ contract L1BitcoinDepositor is l2WormholeGateway = _l2WormholeGateway; l2ChainId = _l2ChainId; l2FinalizeDepositGasLimit = 500_000; - initializeDepositGasOffset = 40_000; + initializeDepositGasOffset = 60_000; finalizeDepositGasOffset = 20_000; }