From 3211e831f69ab9d6ad327040d9826f5dc006ac59 Mon Sep 17 00:00:00 2001 From: Oliver Townsend <133903322+ogtownsend@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:08:00 -0700 Subject: [PATCH] Add LM Optimism L1 to L2 bridge interface (#840) This PR adds the Optimism bridge interface for L1 to L2 transfers. Some important things to note: - The Optimism L1 bridge adapter increments a nonce with each transfer and each time `sendERC20` is called. This nonce is included in multiple events throughout the transfer's lifecycle and is how we "match" and link events together to confirm they've reached the various states. - The `GetTransfers()` function makes use of this nonce and implements the matching logic to link these 3 events together and partition transfers into categories: - L1's LiquidityManager `LiquidityTransferred.bridgeReturnData`, which should equal - OP's StandardBridge event `ERC20BridgeFinalized.extraData` which should equal - L2's LiquidityManager `LiquidityTransferred.bridgeSpecificData` - See the `TODO: Applying the 1.2x gas...` comment on how we should think about adjusting the gas limit appropriately to account for how OP's native bridge dynamically burns gas --- .../native_solc_compile_all_liquiditymanager | 2 + .../IOptimismCrossDomainMessenger.sol | 18 + .../optimism/IOptimismL1StandardBridge.sol | 18 + .../optimism/IOptimismStandardBridge.sol | 40 ++ .../optimism_cross_domain_messenger.go | 16 +- .../optimism_l1_standard_bridge.go | 173 ++++++ .../optimism_standard_bridge.go | 345 +++++++++++ ...rapper-dependency-versions-do-not-edit.txt | 4 +- .../liquiditymanager/go_generate.go | 2 + core/scripts/ccip/liquiditymanager/main.go | 28 +- .../ccip/liquiditymanager/opstack/deploy.go | 8 +- .../opstack/prove_withdrawal.go | 42 +- .../liquiditymanager/opstack/send_to_l2.go | 6 +- core/scripts/ccip/liquiditymanager/util.go | 8 +- .../liquiditymanager/bridge/arb/common.go | 55 -- .../bridge/arb/common_test.go | 5 +- .../liquiditymanager/bridge/arb/l1_to_l2.go | 69 ++- .../bridge/arb/l1_to_l2_test.go | 57 +- .../liquiditymanager/bridge/arb/l2_to_l1.go | 69 ++- .../plugins/liquiditymanager/bridge/bridge.go | 132 +++-- .../liquiditymanager/bridge/common/chains.go | 48 ++ .../bridge/common/chains_test.go | 101 ++++ .../liquiditymanager/bridge/common/common.go | 101 ++++ .../liquiditymanager/bridge/opstack/common.go | 96 ++++ .../bridge/opstack/common_test.go | 279 +++++++++ .../bridge/opstack/contracts.go | 30 + .../bridge/opstack/l1_to_l2.go | 536 ++++++++++++++++++ .../bridge/opstack/l1_to_l2_test.go | 452 +++++++++++++++ .../bridge/opstack/l2_to_l1.go | 532 +++++++++++++++++ .../bridge/opstack/l2_to_l1_test.go | 371 ++++++++++++ .../bridge/opstack/test_helper.go | 52 ++ .../bridge/opstack/withdrawprover/helpers.go | 129 +++++ .../bridge/opstack/withdrawprover/prover.go | 36 +- .../opstack/withdrawprover/prover_test.go | 4 +- .../ocr2/plugins/liquiditymanager/plugin.go | 8 +- 35 files changed, 3646 insertions(+), 226 deletions(-) create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol create mode 100644 contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol create mode 100644 core/gethwrappers/liquiditymanager/generated/optimism_l1_standard_bridge/optimism_l1_standard_bridge.go create mode 100644 core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge/optimism_standard_bridge.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/common/chains.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/common/chains_test.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/common/common.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common_test.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/contracts.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2_test.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1_test.go create mode 100644 core/services/ocr2/plugins/liquiditymanager/bridge/opstack/test_helper.go diff --git a/contracts/scripts/native_solc_compile_all_liquiditymanager b/contracts/scripts/native_solc_compile_all_liquiditymanager index 5bafeb92f3..a29f041c77 100755 --- a/contracts/scripts/native_solc_compile_all_liquiditymanager +++ b/contracts/scripts/native_solc_compile_all_liquiditymanager @@ -62,4 +62,6 @@ compileContract liquiditymanager/interfaces/optimism/IOptimismL2ToL1MessagePasse compileContract liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol compileContract liquiditymanager/interfaces/optimism/IOptimismPortal2.sol compileContract liquiditymanager/interfaces/optimism/IOptimismDisputeGameFactory.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol +compileContract liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol compileContract liquiditymanager/encoders/OptimismL1BridgeAdapterEncoder.sol diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol index 95c2e48876..2b5cc65072 100644 --- a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismCrossDomainMessenger.sol @@ -10,4 +10,22 @@ interface IOptimismCrossDomainMessenger { /// @param messageNonce Unique nonce attached to the message. /// @param gasLimit Minimum gas limit that the message can be executed with. event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); + + /// @notice Relays a message that was sent by the other CrossDomainMessenger contract. Can only + /// be executed via cross-chain call from the other messenger OR if the message was + /// already received once and is currently being replayed. + /// @param _nonce Nonce of the message being relayed. + /// @param _sender Address of the user who sent the message. + /// @param _target Address that the message is targeted at. + /// @param _value ETH value to send with the message. + /// @param _minGasLimit Minimum amount of gas that the message can be executed with. + /// @param _message Message to send to the target. + function relayMessage( + uint256 _nonce, + address _sender, + address _target, + uint256 _value, + uint256 _minGasLimit, + bytes calldata _message + ) external payable; } diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol new file mode 100644 index 0000000000..3a518fcf79 --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismL1StandardBridge.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/L1/L1StandardBridge.sol +pragma solidity ^0.8.0; + +interface IOptimismL1StandardBridge { + /// @custom:legacy + /// @notice Deposits some amount of ETH into a target account on L2. + /// Note that if ETH is sent to a contract on L2 and the call fails, then that ETH will + /// be locked in the L2StandardBridge. ETH may be recoverable if the call can be + /// successfully replayed by increasing the amount of gas supplied to the call. If the + /// call will fail for any amount of gas, then the ETH will be locked permanently. + /// @param _to Address of the recipient on L2. + /// @param _minGasLimit Minimum gas limit for the deposit message on L2. + /// @param _extraData Optional data to forward to L2. + /// Data supplied here will not be used to execute any code on L2 and is + /// only emitted as extra data for the convenience of off-chain tooling. + function depositETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable; +} diff --git a/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol new file mode 100644 index 0000000000..2f9ef91d7c --- /dev/null +++ b/contracts/src/v0.8/liquiditymanager/interfaces/optimism/IOptimismStandardBridge.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// Copied from https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/universal/StandardBridge.sol#L88 +pragma solidity ^0.8.0; + +interface IOptimismStandardBridge { + /// @notice Emitted when an ERC20 bridge is finalized on this chain. + /// @param localToken Address of the ERC20 on this chain. + /// @param remoteToken Address of the ERC20 on the remote chain. + /// @param from Address of the sender. + /// @param to Address of the receiver. + /// @param amount Amount of the ERC20 sent. + /// @param extraData Extra data sent with the transaction. + event ERC20BridgeFinalized( + address indexed localToken, + address indexed remoteToken, + address indexed from, + address to, + uint256 amount, + bytes extraData + ); + + /// @notice Finalizes an ERC20 bridge on this chain. Can only be triggered by the other + /// StandardBridge contract on the remote chain. + /// @param _localToken Address of the ERC20 on this chain. + /// @param _remoteToken Address of the corresponding token on the remote chain. + /// @param _from Address of the sender. + /// @param _to Address of the receiver. + /// @param _amount Amount of the ERC20 being bridged. + /// @param _extraData Extra data to be sent with the transaction. Note that the recipient will + /// not be triggered with this data, but it will be emitted and can be used + /// to identify the transaction. + function finalizeBridgeERC20( + address _localToken, + address _remoteToken, + address _from, + address _to, + uint256 _amount, + bytes calldata _extraData + ) external; +} diff --git a/core/gethwrappers/liquiditymanager/generated/optimism_cross_domain_messenger/optimism_cross_domain_messenger.go b/core/gethwrappers/liquiditymanager/generated/optimism_cross_domain_messenger/optimism_cross_domain_messenger.go index e8bda1bf86..599d2e3739 100644 --- a/core/gethwrappers/liquiditymanager/generated/optimism_cross_domain_messenger/optimism_cross_domain_messenger.go +++ b/core/gethwrappers/liquiditymanager/generated/optimism_cross_domain_messenger/optimism_cross_domain_messenger.go @@ -31,7 +31,7 @@ var ( ) var OptimismCrossDomainMessengerMetaData = &bind.MetaData{ - ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"messageNonce\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"gasLimit\",\"type\":\"uint256\"}],\"name\":\"SentMessage\",\"type\":\"event\"}]", + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"messageNonce\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"gasLimit\",\"type\":\"uint256\"}],\"name\":\"SentMessage\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_nonce\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_sender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_minGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"_message\",\"type\":\"bytes\"}],\"name\":\"relayMessage\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]", } var OptimismCrossDomainMessengerABI = OptimismCrossDomainMessengerMetaData.ABI @@ -152,6 +152,18 @@ func (_OptimismCrossDomainMessenger *OptimismCrossDomainMessengerTransactorRaw) return _OptimismCrossDomainMessenger.Contract.contract.Transact(opts, method, params...) } +func (_OptimismCrossDomainMessenger *OptimismCrossDomainMessengerTransactor) RelayMessage(opts *bind.TransactOpts, _nonce *big.Int, _sender common.Address, _target common.Address, _value *big.Int, _minGasLimit *big.Int, _message []byte) (*types.Transaction, error) { + return _OptimismCrossDomainMessenger.contract.Transact(opts, "relayMessage", _nonce, _sender, _target, _value, _minGasLimit, _message) +} + +func (_OptimismCrossDomainMessenger *OptimismCrossDomainMessengerSession) RelayMessage(_nonce *big.Int, _sender common.Address, _target common.Address, _value *big.Int, _minGasLimit *big.Int, _message []byte) (*types.Transaction, error) { + return _OptimismCrossDomainMessenger.Contract.RelayMessage(&_OptimismCrossDomainMessenger.TransactOpts, _nonce, _sender, _target, _value, _minGasLimit, _message) +} + +func (_OptimismCrossDomainMessenger *OptimismCrossDomainMessengerTransactorSession) RelayMessage(_nonce *big.Int, _sender common.Address, _target common.Address, _value *big.Int, _minGasLimit *big.Int, _message []byte) (*types.Transaction, error) { + return _OptimismCrossDomainMessenger.Contract.RelayMessage(&_OptimismCrossDomainMessenger.TransactOpts, _nonce, _sender, _target, _value, _minGasLimit, _message) +} + type OptimismCrossDomainMessengerSentMessageIterator struct { Event *OptimismCrossDomainMessengerSentMessage @@ -302,6 +314,8 @@ func (_OptimismCrossDomainMessenger *OptimismCrossDomainMessenger) Address() com } type OptimismCrossDomainMessengerInterface interface { + RelayMessage(opts *bind.TransactOpts, _nonce *big.Int, _sender common.Address, _target common.Address, _value *big.Int, _minGasLimit *big.Int, _message []byte) (*types.Transaction, error) + FilterSentMessage(opts *bind.FilterOpts, target []common.Address) (*OptimismCrossDomainMessengerSentMessageIterator, error) WatchSentMessage(opts *bind.WatchOpts, sink chan<- *OptimismCrossDomainMessengerSentMessage, target []common.Address) (event.Subscription, error) diff --git a/core/gethwrappers/liquiditymanager/generated/optimism_l1_standard_bridge/optimism_l1_standard_bridge.go b/core/gethwrappers/liquiditymanager/generated/optimism_l1_standard_bridge/optimism_l1_standard_bridge.go new file mode 100644 index 0000000000..b376a33435 --- /dev/null +++ b/core/gethwrappers/liquiditymanager/generated/optimism_l1_standard_bridge/optimism_l1_standard_bridge.go @@ -0,0 +1,173 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package optimism_l1_standard_bridge + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +var OptimismL1StandardBridgeMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"uint32\",\"name\":\"_minGasLimit\",\"type\":\"uint32\"},{\"internalType\":\"bytes\",\"name\":\"_extraData\",\"type\":\"bytes\"}],\"name\":\"depositETHTo\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]", +} + +var OptimismL1StandardBridgeABI = OptimismL1StandardBridgeMetaData.ABI + +type OptimismL1StandardBridge struct { + address common.Address + abi abi.ABI + OptimismL1StandardBridgeCaller + OptimismL1StandardBridgeTransactor + OptimismL1StandardBridgeFilterer +} + +type OptimismL1StandardBridgeCaller struct { + contract *bind.BoundContract +} + +type OptimismL1StandardBridgeTransactor struct { + contract *bind.BoundContract +} + +type OptimismL1StandardBridgeFilterer struct { + contract *bind.BoundContract +} + +type OptimismL1StandardBridgeSession struct { + Contract *OptimismL1StandardBridge + CallOpts bind.CallOpts + TransactOpts bind.TransactOpts +} + +type OptimismL1StandardBridgeCallerSession struct { + Contract *OptimismL1StandardBridgeCaller + CallOpts bind.CallOpts +} + +type OptimismL1StandardBridgeTransactorSession struct { + Contract *OptimismL1StandardBridgeTransactor + TransactOpts bind.TransactOpts +} + +type OptimismL1StandardBridgeRaw struct { + Contract *OptimismL1StandardBridge +} + +type OptimismL1StandardBridgeCallerRaw struct { + Contract *OptimismL1StandardBridgeCaller +} + +type OptimismL1StandardBridgeTransactorRaw struct { + Contract *OptimismL1StandardBridgeTransactor +} + +func NewOptimismL1StandardBridge(address common.Address, backend bind.ContractBackend) (*OptimismL1StandardBridge, error) { + abi, err := abi.JSON(strings.NewReader(OptimismL1StandardBridgeABI)) + if err != nil { + return nil, err + } + contract, err := bindOptimismL1StandardBridge(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &OptimismL1StandardBridge{address: address, abi: abi, OptimismL1StandardBridgeCaller: OptimismL1StandardBridgeCaller{contract: contract}, OptimismL1StandardBridgeTransactor: OptimismL1StandardBridgeTransactor{contract: contract}, OptimismL1StandardBridgeFilterer: OptimismL1StandardBridgeFilterer{contract: contract}}, nil +} + +func NewOptimismL1StandardBridgeCaller(address common.Address, caller bind.ContractCaller) (*OptimismL1StandardBridgeCaller, error) { + contract, err := bindOptimismL1StandardBridge(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &OptimismL1StandardBridgeCaller{contract: contract}, nil +} + +func NewOptimismL1StandardBridgeTransactor(address common.Address, transactor bind.ContractTransactor) (*OptimismL1StandardBridgeTransactor, error) { + contract, err := bindOptimismL1StandardBridge(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &OptimismL1StandardBridgeTransactor{contract: contract}, nil +} + +func NewOptimismL1StandardBridgeFilterer(address common.Address, filterer bind.ContractFilterer) (*OptimismL1StandardBridgeFilterer, error) { + contract, err := bindOptimismL1StandardBridge(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &OptimismL1StandardBridgeFilterer{contract: contract}, nil +} + +func bindOptimismL1StandardBridge(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := OptimismL1StandardBridgeMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _OptimismL1StandardBridge.Contract.OptimismL1StandardBridgeCaller.contract.Call(opts, result, method, params...) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OptimismL1StandardBridge.Contract.OptimismL1StandardBridgeTransactor.contract.Transfer(opts) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _OptimismL1StandardBridge.Contract.OptimismL1StandardBridgeTransactor.contract.Transact(opts, method, params...) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _OptimismL1StandardBridge.Contract.contract.Call(opts, result, method, params...) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OptimismL1StandardBridge.Contract.contract.Transfer(opts) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _OptimismL1StandardBridge.Contract.contract.Transact(opts, method, params...) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeTransactor) DepositETHTo(opts *bind.TransactOpts, _to common.Address, _minGasLimit uint32, _extraData []byte) (*types.Transaction, error) { + return _OptimismL1StandardBridge.contract.Transact(opts, "depositETHTo", _to, _minGasLimit, _extraData) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeSession) DepositETHTo(_to common.Address, _minGasLimit uint32, _extraData []byte) (*types.Transaction, error) { + return _OptimismL1StandardBridge.Contract.DepositETHTo(&_OptimismL1StandardBridge.TransactOpts, _to, _minGasLimit, _extraData) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridgeTransactorSession) DepositETHTo(_to common.Address, _minGasLimit uint32, _extraData []byte) (*types.Transaction, error) { + return _OptimismL1StandardBridge.Contract.DepositETHTo(&_OptimismL1StandardBridge.TransactOpts, _to, _minGasLimit, _extraData) +} + +func (_OptimismL1StandardBridge *OptimismL1StandardBridge) Address() common.Address { + return _OptimismL1StandardBridge.address +} + +type OptimismL1StandardBridgeInterface interface { + DepositETHTo(opts *bind.TransactOpts, _to common.Address, _minGasLimit uint32, _extraData []byte) (*types.Transaction, error) + + Address() common.Address +} diff --git a/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge/optimism_standard_bridge.go b/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge/optimism_standard_bridge.go new file mode 100644 index 0000000000..b6a7d45143 --- /dev/null +++ b/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge/optimism_standard_bridge.go @@ -0,0 +1,345 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package optimism_standard_bridge + +import ( + "errors" + "fmt" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated" +) + +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +var OptimismStandardBridgeMetaData = &bind.MetaData{ + ABI: "[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"localToken\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"remoteToken\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"extraData\",\"type\":\"bytes\"}],\"name\":\"ERC20BridgeFinalized\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_localToken\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_remoteToken\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_from\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"_extraData\",\"type\":\"bytes\"}],\"name\":\"finalizeBridgeERC20\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +var OptimismStandardBridgeABI = OptimismStandardBridgeMetaData.ABI + +type OptimismStandardBridge struct { + address common.Address + abi abi.ABI + OptimismStandardBridgeCaller + OptimismStandardBridgeTransactor + OptimismStandardBridgeFilterer +} + +type OptimismStandardBridgeCaller struct { + contract *bind.BoundContract +} + +type OptimismStandardBridgeTransactor struct { + contract *bind.BoundContract +} + +type OptimismStandardBridgeFilterer struct { + contract *bind.BoundContract +} + +type OptimismStandardBridgeSession struct { + Contract *OptimismStandardBridge + CallOpts bind.CallOpts + TransactOpts bind.TransactOpts +} + +type OptimismStandardBridgeCallerSession struct { + Contract *OptimismStandardBridgeCaller + CallOpts bind.CallOpts +} + +type OptimismStandardBridgeTransactorSession struct { + Contract *OptimismStandardBridgeTransactor + TransactOpts bind.TransactOpts +} + +type OptimismStandardBridgeRaw struct { + Contract *OptimismStandardBridge +} + +type OptimismStandardBridgeCallerRaw struct { + Contract *OptimismStandardBridgeCaller +} + +type OptimismStandardBridgeTransactorRaw struct { + Contract *OptimismStandardBridgeTransactor +} + +func NewOptimismStandardBridge(address common.Address, backend bind.ContractBackend) (*OptimismStandardBridge, error) { + abi, err := abi.JSON(strings.NewReader(OptimismStandardBridgeABI)) + if err != nil { + return nil, err + } + contract, err := bindOptimismStandardBridge(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &OptimismStandardBridge{address: address, abi: abi, OptimismStandardBridgeCaller: OptimismStandardBridgeCaller{contract: contract}, OptimismStandardBridgeTransactor: OptimismStandardBridgeTransactor{contract: contract}, OptimismStandardBridgeFilterer: OptimismStandardBridgeFilterer{contract: contract}}, nil +} + +func NewOptimismStandardBridgeCaller(address common.Address, caller bind.ContractCaller) (*OptimismStandardBridgeCaller, error) { + contract, err := bindOptimismStandardBridge(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &OptimismStandardBridgeCaller{contract: contract}, nil +} + +func NewOptimismStandardBridgeTransactor(address common.Address, transactor bind.ContractTransactor) (*OptimismStandardBridgeTransactor, error) { + contract, err := bindOptimismStandardBridge(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &OptimismStandardBridgeTransactor{contract: contract}, nil +} + +func NewOptimismStandardBridgeFilterer(address common.Address, filterer bind.ContractFilterer) (*OptimismStandardBridgeFilterer, error) { + contract, err := bindOptimismStandardBridge(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &OptimismStandardBridgeFilterer{contract: contract}, nil +} + +func bindOptimismStandardBridge(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := OptimismStandardBridgeMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +func (_OptimismStandardBridge *OptimismStandardBridgeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _OptimismStandardBridge.Contract.OptimismStandardBridgeCaller.contract.Call(opts, result, method, params...) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OptimismStandardBridge.Contract.OptimismStandardBridgeTransactor.contract.Transfer(opts) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _OptimismStandardBridge.Contract.OptimismStandardBridgeTransactor.contract.Transact(opts, method, params...) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _OptimismStandardBridge.Contract.contract.Call(opts, result, method, params...) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _OptimismStandardBridge.Contract.contract.Transfer(opts) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _OptimismStandardBridge.Contract.contract.Transact(opts, method, params...) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeTransactor) FinalizeBridgeERC20(opts *bind.TransactOpts, _localToken common.Address, _remoteToken common.Address, _from common.Address, _to common.Address, _amount *big.Int, _extraData []byte) (*types.Transaction, error) { + return _OptimismStandardBridge.contract.Transact(opts, "finalizeBridgeERC20", _localToken, _remoteToken, _from, _to, _amount, _extraData) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeSession) FinalizeBridgeERC20(_localToken common.Address, _remoteToken common.Address, _from common.Address, _to common.Address, _amount *big.Int, _extraData []byte) (*types.Transaction, error) { + return _OptimismStandardBridge.Contract.FinalizeBridgeERC20(&_OptimismStandardBridge.TransactOpts, _localToken, _remoteToken, _from, _to, _amount, _extraData) +} + +func (_OptimismStandardBridge *OptimismStandardBridgeTransactorSession) FinalizeBridgeERC20(_localToken common.Address, _remoteToken common.Address, _from common.Address, _to common.Address, _amount *big.Int, _extraData []byte) (*types.Transaction, error) { + return _OptimismStandardBridge.Contract.FinalizeBridgeERC20(&_OptimismStandardBridge.TransactOpts, _localToken, _remoteToken, _from, _to, _amount, _extraData) +} + +type OptimismStandardBridgeERC20BridgeFinalizedIterator struct { + Event *OptimismStandardBridgeERC20BridgeFinalized + + contract *bind.BoundContract + event string + + logs chan types.Log + sub ethereum.Subscription + done bool + fail error +} + +func (it *OptimismStandardBridgeERC20BridgeFinalizedIterator) Next() bool { + + if it.fail != nil { + return false + } + + if it.done { + select { + case log := <-it.logs: + it.Event = new(OptimismStandardBridgeERC20BridgeFinalized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + + select { + case log := <-it.logs: + it.Event = new(OptimismStandardBridgeERC20BridgeFinalized) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +func (it *OptimismStandardBridgeERC20BridgeFinalizedIterator) Error() error { + return it.fail +} + +func (it *OptimismStandardBridgeERC20BridgeFinalizedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +type OptimismStandardBridgeERC20BridgeFinalized struct { + LocalToken common.Address + RemoteToken common.Address + From common.Address + To common.Address + Amount *big.Int + ExtraData []byte + Raw types.Log +} + +func (_OptimismStandardBridge *OptimismStandardBridgeFilterer) FilterERC20BridgeFinalized(opts *bind.FilterOpts, localToken []common.Address, remoteToken []common.Address, from []common.Address) (*OptimismStandardBridgeERC20BridgeFinalizedIterator, error) { + + var localTokenRule []interface{} + for _, localTokenItem := range localToken { + localTokenRule = append(localTokenRule, localTokenItem) + } + var remoteTokenRule []interface{} + for _, remoteTokenItem := range remoteToken { + remoteTokenRule = append(remoteTokenRule, remoteTokenItem) + } + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + + logs, sub, err := _OptimismStandardBridge.contract.FilterLogs(opts, "ERC20BridgeFinalized", localTokenRule, remoteTokenRule, fromRule) + if err != nil { + return nil, err + } + return &OptimismStandardBridgeERC20BridgeFinalizedIterator{contract: _OptimismStandardBridge.contract, event: "ERC20BridgeFinalized", logs: logs, sub: sub}, nil +} + +func (_OptimismStandardBridge *OptimismStandardBridgeFilterer) WatchERC20BridgeFinalized(opts *bind.WatchOpts, sink chan<- *OptimismStandardBridgeERC20BridgeFinalized, localToken []common.Address, remoteToken []common.Address, from []common.Address) (event.Subscription, error) { + + var localTokenRule []interface{} + for _, localTokenItem := range localToken { + localTokenRule = append(localTokenRule, localTokenItem) + } + var remoteTokenRule []interface{} + for _, remoteTokenItem := range remoteToken { + remoteTokenRule = append(remoteTokenRule, remoteTokenItem) + } + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + + logs, sub, err := _OptimismStandardBridge.contract.WatchLogs(opts, "ERC20BridgeFinalized", localTokenRule, remoteTokenRule, fromRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + + event := new(OptimismStandardBridgeERC20BridgeFinalized) + if err := _OptimismStandardBridge.contract.UnpackLog(event, "ERC20BridgeFinalized", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +func (_OptimismStandardBridge *OptimismStandardBridgeFilterer) ParseERC20BridgeFinalized(log types.Log) (*OptimismStandardBridgeERC20BridgeFinalized, error) { + event := new(OptimismStandardBridgeERC20BridgeFinalized) + if err := _OptimismStandardBridge.contract.UnpackLog(event, "ERC20BridgeFinalized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +func (_OptimismStandardBridge *OptimismStandardBridge) ParseLog(log types.Log) (generated.AbigenLog, error) { + switch log.Topics[0] { + case _OptimismStandardBridge.abi.Events["ERC20BridgeFinalized"].ID: + return _OptimismStandardBridge.ParseERC20BridgeFinalized(log) + + default: + return nil, fmt.Errorf("abigen wrapper received unknown log topic: %v", log.Topics[0]) + } +} + +func (OptimismStandardBridgeERC20BridgeFinalized) Topic() common.Hash { + return common.HexToHash("0xd59c65b35445225835c83f50b6ede06a7be047d22e357073e250d9af537518cd") +} + +func (_OptimismStandardBridge *OptimismStandardBridge) Address() common.Address { + return _OptimismStandardBridge.address +} + +type OptimismStandardBridgeInterface interface { + FinalizeBridgeERC20(opts *bind.TransactOpts, _localToken common.Address, _remoteToken common.Address, _from common.Address, _to common.Address, _amount *big.Int, _extraData []byte) (*types.Transaction, error) + + FilterERC20BridgeFinalized(opts *bind.FilterOpts, localToken []common.Address, remoteToken []common.Address, from []common.Address) (*OptimismStandardBridgeERC20BridgeFinalizedIterator, error) + + WatchERC20BridgeFinalized(opts *bind.WatchOpts, sink chan<- *OptimismStandardBridgeERC20BridgeFinalized, localToken []common.Address, remoteToken []common.Address, from []common.Address) (event.Subscription, error) + + ParseERC20BridgeFinalized(log types.Log) (*OptimismStandardBridgeERC20BridgeFinalized, error) + + ParseLog(log types.Log) (generated.AbigenLog, error) + + Address() common.Address +} diff --git a/core/gethwrappers/liquiditymanager/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/liquiditymanager/generation/generated-wrapper-dependency-versions-do-not-edit.txt index 8b9f5dad3c..59a00556aa 100644 --- a/core/gethwrappers/liquiditymanager/generation/generated-wrapper-dependency-versions-do-not-edit.txt +++ b/core/gethwrappers/liquiditymanager/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -15,13 +15,15 @@ liquiditymanager: ../../../contracts/solc/v0.8.24/LiquidityManager/LiquidityMana mock_l1_bridge_adapter: ../../../contracts/solc/v0.8.24/MockBridgeAdapter/MockL1BridgeAdapter.abi ../../../contracts/solc/v0.8.24/MockBridgeAdapter/MockL1BridgeAdapter.bin 538d2e3855031bcb4ef28ab8f0c54c8249e90936a588cde81b965d1dd2d08ad4 mock_l2_bridge_adapter: ../../../contracts/solc/v0.8.24/MockBridgeAdapter/MockL2BridgeAdapter.abi ../../../contracts/solc/v0.8.24/MockBridgeAdapter/MockL2BridgeAdapter.bin 8ff182e2ac6aac98e1fe85c37d6d92a0b0570de695ed1292127ae25babe96bda no_op_ocr3: ../../../contracts/solc/v0.8.24/NoOpOCR3/NoOpOCR3.abi ../../../contracts/solc/v0.8.24/NoOpOCR3/NoOpOCR3.bin 964010f2f2c2b1f78d1149c19e6656fb50c8b76ff160b7d2b7e93e5446d19e3d -optimism_cross_domain_messenger: ../../../contracts/solc/v0.8.24/IOptimismCrossDomainMessenger/IOptimismCrossDomainMessenger.abi ../../../contracts/solc/v0.8.24/IOptimismCrossDomainMessenger/IOptimismCrossDomainMessenger.bin cfc9724d20c87eb0c567efb6f7cad7d0039a929e518c9de18afdaaea6160be4d +optimism_cross_domain_messenger: ../../../contracts/solc/v0.8.24/IOptimismCrossDomainMessenger/IOptimismCrossDomainMessenger.abi ../../../contracts/solc/v0.8.24/IOptimismCrossDomainMessenger/IOptimismCrossDomainMessenger.bin e6d54a344ca1cf29e3b2d320bad4ab4b5aa6c197705d7a65586d4d215d751fca optimism_dispute_game_factory: ../../../contracts/solc/v0.8.24/IOptimismDisputeGameFactory/IOptimismDisputeGameFactory.abi ../../../contracts/solc/v0.8.24/IOptimismDisputeGameFactory/IOptimismDisputeGameFactory.bin d4bcd96a87fdc6316b3788bd33eb8d96140002f7716b123a03ba4196a5aeeb72 optimism_l1_bridge_adapter: ../../../contracts/solc/v0.8.24/OptimismL1BridgeAdapter/OptimismL1BridgeAdapter.abi ../../../contracts/solc/v0.8.24/OptimismL1BridgeAdapter/OptimismL1BridgeAdapter.bin f05678747b99fa7bc4255e7c11a44e5e49f51f749a97f5b41ac9badae0592ac1 optimism_l1_bridge_adapter_encoder: ../../../contracts/solc/v0.8.24/OptimismL1BridgeAdapterEncoder/OptimismL1BridgeAdapterEncoder.abi ../../../contracts/solc/v0.8.24/OptimismL1BridgeAdapterEncoder/OptimismL1BridgeAdapterEncoder.bin 449f12408130b7d0a17aa1da14b5bbae7f22d90307422992ebf26c7fe93b85f2 +optimism_l1_standard_bridge: ../../../contracts/solc/v0.8.24/IOptimismL1StandardBridge/IOptimismL1StandardBridge.abi ../../../contracts/solc/v0.8.24/IOptimismL1StandardBridge/IOptimismL1StandardBridge.bin ba8676c979f072983617d55b51ea3bc482abe06356830da57816c28f5397eb2e optimism_l2_bridge_adapter: ../../../contracts/solc/v0.8.24/OptimismL2BridgeAdapter/OptimismL2BridgeAdapter.abi ../../../contracts/solc/v0.8.24/OptimismL2BridgeAdapter/OptimismL2BridgeAdapter.bin ac707b967a62f8a70c8d1b1d02d28d1d8474f0511c279f709b68bbb3e08067bd optimism_l2_output_oracle: ../../../contracts/solc/v0.8.24/IOptimismL2OutputOracle/IOptimismL2OutputOracle.abi ../../../contracts/solc/v0.8.24/IOptimismL2OutputOracle/IOptimismL2OutputOracle.bin c89386866c41c4b31fed4b8b945ba27aa8258ad472968da98a81448a8a95e43c optimism_l2_to_l1_message_passer: ../../../contracts/solc/v0.8.24/IOptimismL2ToL1MessagePasser/IOptimismL2ToL1MessagePasser.abi ../../../contracts/solc/v0.8.24/IOptimismL2ToL1MessagePasser/IOptimismL2ToL1MessagePasser.bin 51f4568aa734c564a9aa82169f06e974e30650aeccbd07b20b0c8c60d48459fd optimism_portal: ../../../contracts/solc/v0.8.24/IOptimismPortal/IOptimismPortal.abi ../../../contracts/solc/v0.8.24/IOptimismPortal/IOptimismPortal.bin a644f108c9267f16bcea1648c8935e0e3741484b9b9ba7e87e0c2cb02bd0839f optimism_portal_2: ../../../contracts/solc/v0.8.24/IOptimismPortal2/IOptimismPortal2.abi ../../../contracts/solc/v0.8.24/IOptimismPortal2/IOptimismPortal2.bin a205fe314abb9056a23ee1ed609e182d012e3809d886c68c96c7b13da9513ab4 +optimism_standard_bridge: ../../../contracts/solc/v0.8.24/IOptimismStandardBridge/IOptimismStandardBridge.abi ../../../contracts/solc/v0.8.24/IOptimismStandardBridge/IOptimismStandardBridge.bin aaa354f8d9a45484aacb896eb148d315ac58587fad0e607adcd468723e653a94 report_encoder: ../../../contracts/solc/v0.8.24/ReportEncoder/ReportEncoder.abi ../../../contracts/solc/v0.8.24/ReportEncoder/ReportEncoder.bin 43c10d4541b687ce08e754e07ccaa8ac6e5a4f2973d359ece4a56a02b68149d1 diff --git a/core/gethwrappers/liquiditymanager/go_generate.go b/core/gethwrappers/liquiditymanager/go_generate.go index a8f4752ce1..5fb8174465 100644 --- a/core/gethwrappers/liquiditymanager/go_generate.go +++ b/core/gethwrappers/liquiditymanager/go_generate.go @@ -33,6 +33,8 @@ package liquiditymanager //go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.24/IOptimismCrossDomainMessenger/IOptimismCrossDomainMessenger.abi ../../../contracts/solc/v0.8.24/IOptimismCrossDomainMessenger/IOptimismCrossDomainMessenger.bin OptimismCrossDomainMessenger optimism_cross_domain_messenger //go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.24/IOptimismPortal2/IOptimismPortal2.abi ../../../contracts/solc/v0.8.24/IOptimismPortal2/IOptimismPortal2.bin OptimismPortal2 optimism_portal_2 //go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.24/IOptimismDisputeGameFactory/IOptimismDisputeGameFactory.abi ../../../contracts/solc/v0.8.24/IOptimismDisputeGameFactory/IOptimismDisputeGameFactory.bin OptimismDisputeGameFactory optimism_dispute_game_factory +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.24/IOptimismStandardBridge/IOptimismStandardBridge.abi ../../../contracts/solc/v0.8.24/IOptimismStandardBridge/IOptimismStandardBridge.bin OptimismStandardBridge optimism_standard_bridge +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.24/IOptimismL1StandardBridge/IOptimismL1StandardBridge.abi ../../../contracts/solc/v0.8.24/IOptimismL1StandardBridge/IOptimismL1StandardBridge.bin OptimismL1StandardBridge optimism_l1_standard_bridge // Generate mocks for tests //go:generate mockery --quiet --dir ./generated/arbitrum_l1_bridge_adapter/ --name ArbitrumL1BridgeAdapterInterface --output ./mocks/mock_arbitrum_l1_bridge_adapter --outpkg mock_arbitrum_l1_bridge_adapter --case=underscore diff --git a/core/scripts/ccip/liquiditymanager/main.go b/core/scripts/ccip/liquiditymanager/main.go index 9497ac0ffc..c97e237784 100644 --- a/core/scripts/ccip/liquiditymanager/main.go +++ b/core/scripts/ccip/liquiditymanager/main.go @@ -341,9 +341,9 @@ func main() { _, tx, _, err := optimism_l1_bridge_adapter.DeployOptimismL1BridgeAdapter( env.Transactors[*l1ChainID], env.Clients[*l1ChainID], - opstack.OptimismContracts[*l1ChainID]["L1StandardBridge"], - opstack.OptimismContracts[*l1ChainID]["WETH"], - opstack.OptimismContracts[*l1ChainID]["OptimismPortal"], + opstack.OptimismContractsByChainID[*l1ChainID]["L1StandardBridge"], + opstack.OptimismContractsByChainID[*l1ChainID]["WETH"], + opstack.OptimismContractsByChainID[*l1ChainID]["OptimismPortalProxy"], ) helpers.PanicErr(err) helpers.ConfirmContractDeployed(context.Background(), env.Clients[*l1ChainID], tx, int64(*l1ChainID)) @@ -353,7 +353,7 @@ func main() { helpers.ParseArgs(cmd, os.Args[2:], "l2-chain-id") env := multienv.New(false, false) - _, tx, _, err := optimism_l2_bridge_adapter.DeployOptimismL2BridgeAdapter(env.Transactors[*l2ChainID], env.Clients[*l2ChainID], opstack.OptimismContracts[*l2ChainID]["WETH"]) + _, tx, _, err := optimism_l2_bridge_adapter.DeployOptimismL2BridgeAdapter(env.Transactors[*l2ChainID], env.Clients[*l2ChainID], opstack.OptimismContractsByChainID[*l2ChainID]["WETH"]) helpers.PanicErr(err) helpers.ConfirmContractDeployed(context.Background(), env.Clients[*l2ChainID], tx, int64(*l2ChainID)) case "op-send-to-l2": @@ -415,8 +415,8 @@ func main() { *l1ChainID, *l2ChainID, common.HexToAddress(*l1BridgeAdapterAddress), - opstack.OptimismContracts[*l1ChainID]["L2OutputOracle"], - opstack.OptimismContracts[*l1ChainID]["OptimismPortal"], + opstack.OptimismContractsByChainID[*l1ChainID]["OptimismPortalProxy"], + opstack.OptimismContractsByChainID[*l1ChainID]["L2OutputOracle"], common.HexToHash(*l2TxHash)) case "op-finalize-l1": cmd := flag.NewFlagSet("op-finalize-l1", flag.ExitOnError) @@ -460,6 +460,7 @@ func main() { remoteChainID := cmd.Uint64("remote-chain-id", 0, "Remote Chain ID") amount := cmd.String("amount", "1", "Amount") shouldWrapNative := cmd.Bool("should-wrap-native", false, "Should wrap native") + bridgeSpecificPayloadStr := cmd.String("bridge-specific-payload", "", "Bridge specific payload in hex format") helpers.ParseArgs(cmd, os.Args[2:], "l2-chain-id", "l2-liquiditymanager-address", "remote-chain-id", "amount") env := multienv.New(false, false) @@ -471,7 +472,9 @@ func main() { mustGetChainByEvmID(*remoteChainID).Selector, decimal.RequireFromString(*amount).BigInt(), *shouldWrapNative, - []byte{}, // no bridge specific payload for receiving liquidity on OP L2 + // No bridge specific payload required for receiving liquidity on OP L2, though we can optionally encode + // information here if needed. For example: the nonce used for matching bridge events in the bridge interface. + common.FromHex(*bridgeSpecificPayloadStr), ) helpers.PanicErr(err) helpers.ConfirmTXMined(context.Background(), env.Clients[*l2ChainID], tx, int64(*l2ChainID), @@ -510,8 +513,8 @@ func main() { *remoteChainID, decimal.RequireFromString(*amount).BigInt(), common.HexToAddress(*l1LiquidityManagerAddress), - opstack.OptimismContracts[*l1ChainID]["L2OutputOracle"], - opstack.OptimismContracts[*l1ChainID]["OptimismPortal"], + opstack.OptimismContractsByChainID[*l1ChainID]["OptimismPortalProxy"], + opstack.OptimismContractsByChainID[*l1ChainID]["L2OutputOracle"], common.HexToHash(*l2TxHash), ) case "op-finalize-withdrawal-on-l1-via-rebalancer": @@ -534,6 +537,13 @@ func main() { common.HexToAddress(*l1LiquidityManagerAddress), common.HexToHash(*l2TxHash), ) + case "op-get-fpac-enabled": + cmd := flag.NewFlagSet("op-get-fpac-enabled", flag.ExitOnError) + l1ChainID := cmd.Uint64("l1-chain-id", 0, "L1 Chain ID") + l2ChainID := cmd.Uint64("l2-chain-id", 0, "L2 Chain ID") + helpers.ParseArgs(cmd, os.Args[2:], "l1-chain-id", "l2-chain-id") + env := multienv.New(false, false) + opstack.CallGetFPACEnabled(env, *l1ChainID, *l2ChainID) } } diff --git a/core/scripts/ccip/liquiditymanager/opstack/deploy.go b/core/scripts/ccip/liquiditymanager/opstack/deploy.go index c706a4154e..f05abec930 100644 --- a/core/scripts/ccip/liquiditymanager/opstack/deploy.go +++ b/core/scripts/ccip/liquiditymanager/opstack/deploy.go @@ -8,18 +8,18 @@ import ( var ( // Optimism Contracts // See https://docs.optimism.io/chain/addresses - OptimismContracts map[uint64]map[string]common.Address + OptimismContractsByChainID map[uint64]map[string]common.Address ) func init() { - OptimismContracts = map[uint64]map[string]common.Address{ + OptimismContractsByChainID = map[uint64]map[string]common.Address{ chainsel.ETHEREUM_TESTNET_SEPOLIA.EvmChainID: { "L1StandardBridge": common.HexToAddress("0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1"), "L1CrossDomainMessenger": common.HexToAddress("0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef"), "WETH": common.HexToAddress("0x7b79995e5f793a07bc00c21412e50ecae098e7f9"), "FaucetTestingToken": common.HexToAddress("0x5589BB8228C07c4e15558875fAf2B859f678d129"), - "OptimismPortal": common.HexToAddress("0x16Fc5058F25648194471939df75CF27A2fdC48BC"), - "L2OutputOracle": common.HexToAddress("0x90E9c4f8a994a250F6aEfd61CAFb4F2e895D458F"), + "OptimismPortalProxy": common.HexToAddress("0x16Fc5058F25648194471939df75CF27A2fdC48BC"), + "L2OutputOracle": common.HexToAddress("0x90E9c4f8a994a250F6aEfd61CAFb4F2e895D458F"), // Removed after FPAC upgrade }, chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.EvmChainID: { "WETH": common.HexToAddress("0x4200000000000000000000000000000000000006"), diff --git a/core/scripts/ccip/liquiditymanager/opstack/prove_withdrawal.go b/core/scripts/ccip/liquiditymanager/opstack/prove_withdrawal.go index c7e721d016..3eed3cba70 100644 --- a/core/scripts/ccip/liquiditymanager/opstack/prove_withdrawal.go +++ b/core/scripts/ccip/liquiditymanager/opstack/prove_withdrawal.go @@ -30,8 +30,8 @@ func ProveWithdrawal( l1ChainID, l2ChainID uint64, l1BridgeAdapterAddress, - l2OutputOracleAddress, - optimismPortalAddress common.Address, + optimismPortalAddress, + l2OutputOracleAddress common.Address, l2TxHash common.Hash, ) { l2Client, ok := env.Clients[l2ChainID] @@ -63,9 +63,9 @@ func ProveWithdrawalViaRebalancer( l2ChainID, remoteChainID uint64, amount *big.Int, - l1RebalancerAddress, - l2OutputOracleAddress, - optimismPortalAddress common.Address, + l1LiquidityManagerAddress, + optimismPortalAddress, + l2OutputOracleAddress common.Address, l2TxHash common.Hash, ) { remoteChain, ok := chainsel.ChainByEvmChainID(remoteChainID) @@ -84,10 +84,10 @@ func ProveWithdrawalViaRebalancer( encodedPayload := proveMessagePayload(l1Client, l2Client, l2TxHash, optimismPortalAddress, l2OutputOracleAddress) - l1Rebalancer, err := liquiditymanager.NewLiquidityManager(l1RebalancerAddress, l1Client) + l1LiquidityManager, err := liquiditymanager.NewLiquidityManager(l1LiquidityManagerAddress, l1Client) helpers.PanicErr(err) - tx, err := l1Rebalancer.ReceiveLiquidity( + tx, err := l1LiquidityManager.ReceiveLiquidity( env.Transactors[l1ChainID], remoteChain.Selector, amount, @@ -99,6 +99,34 @@ func ProveWithdrawalViaRebalancer( "ProveWithdrawal", amount.String(), "from", remoteChain.Name) } +func CallGetFPACEnabled( + env multienv.Env, + l1ChainID, + l2ChainID uint64, +) { + l1Client, ok := env.Clients[l1ChainID] + if !ok { + panic(fmt.Sprintf("No L1 client found for chain ID %d", l1ChainID)) + } + + l2Client, ok := env.Clients[l2ChainID] + if !ok { + panic(fmt.Sprintf("No L2 client found for chain ID %d", l2ChainID)) + } + + prover, err := withdrawprover.New( + ðClient{l1Client}, + ðClient{l2Client}, + OptimismContractsByChainID[l1ChainID]["OptimismPortalProxy"], + OptimismContractsByChainID[l1ChainID]["L2OutputOracle"], + ) + helpers.PanicErr(err) + + fpacEnabled, err := prover.GetFPAC(context.Background()) + helpers.PanicErr(err) + fmt.Println("FPAC enabled:", fpacEnabled) +} + func proveMessagePayload( l1Client, l2Client *ethclient.Client, l2TxHash common.Hash, diff --git a/core/scripts/ccip/liquiditymanager/opstack/send_to_l2.go b/core/scripts/ccip/liquiditymanager/opstack/send_to_l2.go index be2f62351d..22a280858c 100644 --- a/core/scripts/ccip/liquiditymanager/opstack/send_to_l2.go +++ b/core/scripts/ccip/liquiditymanager/opstack/send_to_l2.go @@ -82,7 +82,7 @@ func SendToL2( gasPrice, err := env.Clients[l1ChainID].SuggestGasPrice(context.Background()) helpers.PanicErr(err) - // Estimate gas of the bridging operation and multiply that by 1.6 since + // Estimate gas of the bridging operation and multiply that by 1.8 since // optimism bridging costs are paid for in gas. gasCost, err := env.Clients[l1ChainID].EstimateGas(context.Background(), ethereum.CallMsg{ From: env.Transactors[l1ChainID].From, @@ -137,7 +137,7 @@ func SendToL2ViaRebalancer( panic(fmt.Sprintf("Insufficient liquidity, add more tokens to the liquidity container or specify smaller amount: %s < %s", liquidity, amount)) } - // Estimate gas of the bridging operation and multiply that by 1.6 since + // Estimate gas of the bridging operation and multiply that by 1.8 since // optimism bridging costs are paid for in gas. calldata, err := rebalancerABI.Pack("rebalanceLiquidity", remoteChain.Selector, @@ -150,7 +150,7 @@ func SendToL2ViaRebalancer( gasPrice, err := env.Clients[l1ChainID].SuggestGasPrice(context.Background()) helpers.PanicErr(err) - // Estimate gas of the bridging operation and multiply that by 1.6 since + // Estimate gas of the bridging operation and multiply that by 1.8 since // optimism bridging costs are paid for in gas. gasCost, err := env.Clients[l1ChainID].EstimateGas(context.Background(), ethereum.CallMsg{ From: env.Transactors[l1ChainID].From, diff --git a/core/scripts/ccip/liquiditymanager/util.go b/core/scripts/ccip/liquiditymanager/util.go index aa2111fb5c..26165b580d 100644 --- a/core/scripts/ccip/liquiditymanager/util.go +++ b/core/scripts/ccip/liquiditymanager/util.go @@ -196,9 +196,9 @@ func deployL1BridgeAdapter( _, tx, _, err := optimism_l1_bridge_adapter.DeployOptimismL1BridgeAdapter( l1Transactor, l1Client, - opstack.OptimismContracts[l1ChainID]["L1StandardBridge"], - opstack.OptimismContracts[l1ChainID]["WETH"], - opstack.OptimismContracts[l1ChainID]["OptimismPortal"], + opstack.OptimismContractsByChainID[l1ChainID]["L1StandardBridge"], + opstack.OptimismContractsByChainID[l1ChainID]["WETH"], + opstack.OptimismContractsByChainID[l1ChainID]["OptimismPortalProxy"], ) helpers.PanicErr(err) return helpers.ConfirmContractDeployed(context.Background(), l1Client, tx, int64(l1ChainID)) @@ -223,7 +223,7 @@ func deployL2BridgeAdapter( _, tx, _, err := optimism_l2_bridge_adapter.DeployOptimismL2BridgeAdapter( l2Transactor, l2Client, - opstack.OptimismContracts[l2ChainID]["WETH"], + opstack.OptimismContractsByChainID[l2ChainID]["WETH"], ) helpers.PanicErr(err) return helpers.ConfirmContractDeployed(context.Background(), l2Client, tx, int64(l2ChainID)) diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common.go b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common.go index 2e1ae7525a..8e1f4a846e 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common.go @@ -1,47 +1,23 @@ package arb import ( - "fmt" "math/big" - "time" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - gethtypes "github.com/ethereum/go-ethereum/core/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/arb_node_interface" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/arbitrum_l1_bridge_adapter" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/arbitrum_rollup_core" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/arbsys" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/l2_arbitrum_gateway" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/l2_arbitrum_messenger" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" - "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" ) const ( - DurationMonth = 720 * time.Hour - - // LiquidityTransferredToChainSelectorTopicIndex is the index of the topic in the LiquidityTransferred event - // that contains the "to" chain selector. - LiquidityTransferredToChainSelectorTopicIndex = 3 - // LiquidityTransferredFromChainSelectorTopicIndex is the index of the topic in the LiquidityTransferred event - // that contains the "from" chain selector. - LiquidityTransferredFromChainSelectorTopicIndex = 2 // DepositFinalizedToAddressTopicIndex is the index of the topic in the DepositFinalized event // that contains the "to" address. DepositFinalizedToAddressTopicIndex = 3 - - // Arbitrum stages - // StageRebalanceConfirmed is set as the transfer stage when the rebalanceLiquidity tx is confirmed onchain. - StageRebalanceConfirmed = 1 - // StageFinalizeReady is set as the transfer stage when the finalization is ready to execute onchain. - StageFinalizeReady = 2 - // StageFinalizeConfirmed is set as the transfer stage when the finalization is confirmed onchain. - // This is a terminal stage. - StageFinalizeConfirmed = 3 ) var ( @@ -65,37 +41,6 @@ var ( l2BaseFeeMultiplier = big.NewInt(3) submissionFeeMultiplier = big.NewInt(4) - // liquidityManager event - emitted on both L1 and L2 - LiquidityTransferredTopic = liquiditymanager.LiquidityManagerLiquidityTransferred{}.Topic() - nodeInterfaceABI = abihelpers.MustParseABI(arb_node_interface.NodeInterfaceMetaData.ABI) l1AdapterABI = abihelpers.MustParseABI(arbitrum_l1_bridge_adapter.ArbitrumL1BridgeAdapterMetaData.ABI) ) - -type logKey struct { - txHash common.Hash - logIndex int64 -} - -func parseLiquidityTransferred(parseFunc func(gethtypes.Log) (*liquiditymanager.LiquidityManagerLiquidityTransferred, error), lgs []logpoller.Log) ([]*liquiditymanager.LiquidityManagerLiquidityTransferred, map[logKey]logpoller.Log, error) { - transferred := make([]*liquiditymanager.LiquidityManagerLiquidityTransferred, len(lgs)) - toLP := make(map[logKey]logpoller.Log) - for i, lg := range lgs { - parsed, err := parseFunc(lg.ToGethLog()) - if err != nil { - // should never happen - return nil, nil, fmt.Errorf("parse LiquidityTransferred log: %w", err) - } - transferred[i] = parsed - toLP[logKey{ - txHash: lg.TxHash, - logIndex: lg.LogIndex, - }] = lg - } - return transferred, toLP, nil -} - -func toHash(selector models.NetworkSelector) common.Hash { - encoded := hexutil.EncodeUint64(uint64(selector)) - return common.HexToHash(encoded) -} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common_test.go index 0bbefb5ff2..3d02687375 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common_test.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/common_test.go @@ -10,6 +10,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/arb" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" ) func Test_TopicIndexes(t *testing.T) { @@ -34,7 +35,7 @@ func Test_TopicIndexes(t *testing.T) { } require.True(t, toChainSelectorArg.Indexed) - require.Equal(t, arb.LiquidityTransferredToChainSelectorTopicIndex, topicIndex) + require.Equal(t, bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, topicIndex) }) t.Run("liquidity transferred from chain selector idx", func(t *testing.T) { @@ -54,7 +55,7 @@ func Test_TopicIndexes(t *testing.T) { } require.True(t, fromChainSelectorArg.Indexed) - require.Equal(t, arb.LiquidityTransferredFromChainSelectorTopicIndex, topicIndex) + require.Equal(t, bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, topicIndex) }) t.Run("deposit finalized to address idx", func(t *testing.T) { diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2.go b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2.go index 38998d5b2a..cebfb9de34 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2.go @@ -32,6 +32,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" ) @@ -86,17 +87,23 @@ func NewL1ToL2Bridge( return nil, fmt.Errorf("instantiate L1 inbox at %s: %w", l1InboxAddress, err) } - l1FilterName := fmt.Sprintf("ArbitrumL2ToL1Bridge-L1-LiquidityManager:%s-Local:%s-Remote:%s", - l1LiquidityManagerAddress.String(), localChain.Name, remoteChain.Name) + l1FilterName := bridgecommon.GetBridgeFilterName( + "ArbitrumL1ToL2Bridge", + "L1", + l1LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + "", + ) // FIXME Makram please pass the valid context ctx := context.Background() err = l1LogPoller.RegisterFilter(ctx, logpoller.Filter{ Addresses: []common.Address{l1LiquidityManagerAddress}, Name: l1FilterName, EventSigs: []common.Hash{ - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, }, - Retention: DurationMonth, + Retention: bridgecommon.DurationMonth, }) if err != nil { return nil, fmt.Errorf("register L1 log filter: %w", err) @@ -140,8 +147,14 @@ func NewL1ToL2Bridge( return nil, fmt.Errorf("get counterpart gateway for gateway %s: %w", l1TokenGateway, err) } - l2FilterName := fmt.Sprintf("ArbitrumL2ToL1Bridge-L2-L2Gateway:%s-LiquidityManager:%s-Local:%s-Remote:%s", - l2Gateway.Hex(), l2LiquidityManagerAddress.Hex(), localChain.Name, remoteChain.Name) + l2FilterName := bridgecommon.GetBridgeFilterName( + "ArbitrumL1ToL2Bridge", + "L2", + l2LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + fmt.Sprintf("L2Gateway:%s", l2Gateway.Hex()), + ) err = l2LogPoller.RegisterFilter(ctx, logpoller.Filter{ Addresses: []common.Address{ l2Gateway, // emits DepositFinalized @@ -149,10 +162,10 @@ func NewL1ToL2Bridge( }, Name: l2FilterName, EventSigs: []common.Hash{ - DepositFinalizedTopic, // emitted by the gateways - LiquidityTransferredTopic, // emitted by the liquidityManagers + DepositFinalizedTopic, // emitted by the gateways + bridgecommon.LiquidityTransferredTopic, // emitted by the liquidityManagers }, - Retention: DurationMonth, + Retention: bridgecommon.DurationMonth, }) if err != nil { return nil, fmt.Errorf("register L2 log filter: %w", err) @@ -245,7 +258,7 @@ func (l *l1ToL2Bridge) GetTransfers( "receiveLogs", len(receiveLogs), ) - parsedSent, parsedToLP, err := parseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, sendLogs) + parsedSent, parsedToLP, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, sendLogs) if err != nil { return nil, fmt.Errorf("parse L1 -> L2 transfers: %w", err) } @@ -255,7 +268,8 @@ func (l *l1ToL2Bridge) GetTransfers( return nil, fmt.Errorf("parse DepositFinalized logs: %w", err) } - parsedReceived, _, err := parseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, receiveLogs) + // Technically an L2 event, but the l1LiquidityManager ABI parsing should be the same + parsedReceived, _, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, receiveLogs) if err != nil { return nil, fmt.Errorf("parse LiquidityTransferred logs: %w", err) } @@ -293,11 +307,11 @@ func (l *l1ToL2Bridge) GetTransfers( func (l *l1ToL2Bridge) getLogs(ctx context.Context, fromTs time.Time) (sendLogs []logpoller.Log, depositFinalizedLogs []logpoller.Log, receiveLogs []logpoller.Log, err error) { sendLogs, err = l.l1LogPoller.IndexedLogsCreatedAfter( ctx, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l.l1LiquidityManager.Address(), - LiquidityTransferredToChainSelectorTopicIndex, + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, []common.Hash{ - toHash(l.remoteSelector), + bridgecommon.NetworkSelectorToHash(l.remoteSelector), }, fromTs, 1, @@ -323,11 +337,11 @@ func (l *l1ToL2Bridge) getLogs(ctx context.Context, fromTs time.Time) (sendLogs receiveLogs, err = l.l2LogPoller.IndexedLogsCreatedAfter( ctx, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l.l2LiquidityManagerAddress, - LiquidityTransferredFromChainSelectorTopicIndex, + bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, []common.Hash{ - toHash(l.localSelector), + bridgecommon.NetworkSelectorToHash(l.localSelector), }, fromTs, 1, @@ -344,7 +358,7 @@ func (l *l1ToL2Bridge) toPendingTransfers( notReady, ready []*liquiditymanager.LiquidityManagerLiquidityTransferred, readyData [][]byte, - parsedToLP map[logKey]logpoller.Log, + parsedToLP map[bridgecommon.LogKey]logpoller.Log, ) ([]models.PendingTransfer, error) { if len(ready) != len(readyData) { return nil, fmt.Errorf("length of ready and readyData should be the same: len(ready) = %d, len(readyData) = %d", @@ -361,12 +375,12 @@ func (l *l1ToL2Bridge) toPendingTransfers( LocalTokenAddress: localToken, RemoteTokenAddress: remoteToken, Amount: ubig.New(transfer.Amount), - Date: parsedToLP[logKey{ - txHash: transfer.Raw.TxHash, - logIndex: int64(transfer.Raw.Index), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), }].BlockTimestamp, BridgeData: []byte{}, // no finalization data, not ready - Stage: StageRebalanceConfirmed, + Stage: bridgecommon.StageRebalanceConfirmed, }, Status: models.TransferStatusNotReady, ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), @@ -382,12 +396,12 @@ func (l *l1ToL2Bridge) toPendingTransfers( LocalTokenAddress: localToken, RemoteTokenAddress: remoteToken, Amount: ubig.New(transfer.Amount), - Date: parsedToLP[logKey{ - txHash: transfer.Raw.TxHash, - logIndex: int64(transfer.Raw.Index), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), }].BlockTimestamp, BridgeData: readyData[i], // finalization data since its ready - Stage: StageFinalizeReady, + Stage: bridgecommon.StageFinalizeReady, }, Status: models.TransferStatusReady, // ready == finalized for L1 -> L2 transfers due to auto-finalization by the native bridge ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), @@ -424,6 +438,9 @@ func partitionTransfers( } } else if len(sentLogs) < len(effectiveDepositFinalized) { // more finalized than have been sent - should be impossible + // TODO: what if a rebalance is triggered at T=0, the sent log is emitted at t=1 + // and the DepositFinalized log is emitted at T=2, and our query goes back to T=2. There will be + // 1 DepositFinalized log and 0 sent logs. Maybe drop this condition and just do the matching return nil, nil, nil, fmt.Errorf("got more finalized logs than sent - should be impossible: len(sent) = %d, len(finalized) = %d", len(sentLogs), len(effectiveDepositFinalized)) } else { diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2_test.go index 9aded1ac07..64f85ba06e 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2_test.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l1_to_l2_test.go @@ -25,6 +25,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/mocks/mock_arbitrum_inbox" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" ) @@ -1008,7 +1009,7 @@ func Test_l1ToL2Bridge_toPendingTransfers(t *testing.T) { notReady []*liquiditymanager.LiquidityManagerLiquidityTransferred ready []*liquiditymanager.LiquidityManagerLiquidityTransferred readyData [][]byte - parsedToLP map[logKey]logpoller.Log + parsedToLP map[bridgecommon.LogKey]logpoller.Log } tests := []struct { name string @@ -1073,7 +1074,7 @@ func Test_l1ToL2Bridge_toPendingTransfers(t *testing.T) { readyData: [][]byte{ {1, 2, 3}, }, - parsedToLP: make(map[logKey]logpoller.Log), + parsedToLP: make(map[bridgecommon.LogKey]logpoller.Log), }, []models.PendingTransfer{ { @@ -1185,10 +1186,10 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { func(t *testing.T, f fields, a args) { f.l1LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l1LiquidityManager.Address(), - LiquidityTransferredToChainSelectorTopicIndex, - []common.Hash{toHash(remoteSelector)}, + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, + []common.Hash{bridgecommon.NetworkSelectorToHash(remoteSelector)}, a.fromTs, evmtypes.Confirmations(1), ).Return(nil, errors.New("error")) @@ -1219,10 +1220,10 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { func(t *testing.T, f fields, a args) { f.l1LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l1LiquidityManager.Address(), - LiquidityTransferredToChainSelectorTopicIndex, - []common.Hash{toHash(remoteSelector)}, + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, + []common.Hash{bridgecommon.NetworkSelectorToHash(remoteSelector)}, a.fromTs, evmtypes.Confirmations(1), ).Return([]logpoller.Log{{}, {}}, nil) @@ -1263,10 +1264,10 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { func(t *testing.T, f fields, a args) { f.l1LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l1LiquidityManager.Address(), - LiquidityTransferredToChainSelectorTopicIndex, - []common.Hash{toHash(remoteSelector)}, + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, + []common.Hash{bridgecommon.NetworkSelectorToHash(remoteSelector)}, a.fromTs, evmtypes.Confirmations(1), ).Return([]logpoller.Log{{}, {}}, nil) @@ -1281,10 +1282,10 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { ).Return([]logpoller.Log{{}, {}}, nil) f.l2LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l2LiquidityManagerAddress, - LiquidityTransferredFromChainSelectorTopicIndex, - []common.Hash{toHash(localSelector)}, + bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, + []common.Hash{bridgecommon.NetworkSelectorToHash(localSelector)}, a.fromTs, evmtypes.Confirmations(1), ).Return(nil, errors.New("error")) @@ -1316,15 +1317,15 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { func(t *testing.T, f fields, a args) { f.l1LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l1LiquidityManager.Address(), - LiquidityTransferredToChainSelectorTopicIndex, - []common.Hash{toHash(remoteSelector)}, + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, + []common.Hash{bridgecommon.NetworkSelectorToHash(remoteSelector)}, a.fromTs, evmtypes.Confirmations(1), ).Return([]logpoller.Log{ - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x1")}, - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x2")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x1")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x2")}, }, nil) f.l2LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, @@ -1340,15 +1341,15 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { }, nil) f.l2LogPoller.On("IndexedLogsCreatedAfter", mock.Anything, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l2LiquidityManagerAddress, - LiquidityTransferredFromChainSelectorTopicIndex, - []common.Hash{toHash(localSelector)}, + bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, + []common.Hash{bridgecommon.NetworkSelectorToHash(localSelector)}, a.fromTs, evmtypes.Confirmations(1), ).Return([]logpoller.Log{ - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x5")}, - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x6")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x5")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x6")}, }, nil) }, func(t *testing.T, f fields) { @@ -1356,16 +1357,16 @@ func Test_l1ToL2Bridge_getLogs(t *testing.T) { f.l2LogPoller.AssertExpectations(t) }, []logpoller.Log{ - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x1")}, - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x2")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x1")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x2")}, }, []logpoller.Log{ {EventSig: DepositFinalizedTopic, TxHash: common.HexToHash("0x3")}, {EventSig: DepositFinalizedTopic, TxHash: common.HexToHash("0x4")}, }, []logpoller.Log{ - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x5")}, - {EventSig: LiquidityTransferredTopic, TxHash: common.HexToHash("0x6")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x5")}, + {EventSig: bridgecommon.LiquidityTransferredTopic, TxHash: common.HexToHash("0x6")}, }, false, }, diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l2_to_l1.go b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l2_to_l1.go index 2175f5bd27..6263603436 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l2_to_l1.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/arb/l2_to_l1.go @@ -30,6 +30,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" ) @@ -72,8 +73,14 @@ func NewL2ToL1Bridge( if !ok { return nil, fmt.Errorf("unknown chain selector for remote chain: %d", remoteSelector) } - l2FilterName := fmt.Sprintf("ArbitrumL2ToL1Bridge-L2-LiquidityManager:%s-Local:%s-Remote:%s", - l2LiquidityManagerAddress.Hex(), localChain.Name, remoteChain.Name) + l2FilterName := bridgecommon.GetBridgeFilterName( + "ArbitrumL2ToL1Bridge", + "L2", + l2LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + "", + ) // FIXME Makram fix the context plax ctx := context.Background() err := l2LogPoller.RegisterFilter( @@ -81,30 +88,36 @@ func NewL2ToL1Bridge( logpoller.Filter{ Name: l2FilterName, EventSigs: []common.Hash{ - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, }, Addresses: []common.Address{l2LiquidityManagerAddress}, - Retention: DurationMonth, + Retention: bridgecommon.DurationMonth, }) if err != nil { return nil, fmt.Errorf("register filter for Arbitrum L2 to L1 bridge: %w", err) } - l1FilterName := fmt.Sprintf("ArbitrumL2ToL1Bridge-L1-Rollup:%s-LiquidityManager:%s-Local:%s-Remote:%s", - l1RollupAddress.Hex(), l1LiquidityManagerAddress.Hex(), localChain.Name, remoteChain.Name) + l1FilterName := bridgecommon.GetBridgeFilterName( + "ArbitrumL2ToL1Bridge", + "L1", + l1LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + fmt.Sprintf("Rollup:%s", l1RollupAddress.Hex()), + ) err = l1LogPoller.RegisterFilter( ctx, logpoller.Filter{ Name: l1FilterName, EventSigs: []common.Hash{ - NodeConfirmedTopic, // emitted by rollup - LiquidityTransferredTopic, // emitted by rebalancer + NodeConfirmedTopic, // emitted by rollup + bridgecommon.LiquidityTransferredTopic, // emitted by rebalancer }, Addresses: []common.Address{ l1RollupAddress, // to get node confirmed logs l1LiquidityManagerAddress, // to get LiquidityTransferred logs }, - Retention: DurationMonth, + Retention: bridgecommon.DurationMonth, }) if err != nil { return nil, fmt.Errorf("register filter for Arbitrum L1 to L2 bridge: %w", err) @@ -244,15 +257,15 @@ func (l *l2ToL1Bridge) GetTransfers(ctx context.Context, localToken models.Addre // TODO: make more performant. Perhaps filter on more than just one topic here to avoid doing in-memory filtering. sendLogs, err := l.l2LogPoller.IndexedLogsCreatedAfter( ctx, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l.l2LiquidityManager.Address(), - LiquidityTransferredToChainSelectorTopicIndex, + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, []common.Hash{ - toHash(l.remoteSelector), + bridgecommon.NetworkSelectorToHash(l.remoteSelector), }, // todo: this should not be hardcoded // todo: heavy query warning - time.Now().Add(-DurationMonth/2), + time.Now().Add(-bridgecommon.DurationMonth/2), evmtypes.Finalized, ) if err != nil && !errors.Is(err, sql.ErrNoRows) { @@ -264,13 +277,13 @@ func (l *l2ToL1Bridge) GetTransfers(ctx context.Context, localToken models.Addre // ready to finalize more than once, since that will cause reverts onchain. receiveLogs, err := l.l1LogPoller.IndexedLogsCreatedAfter( ctx, - LiquidityTransferredTopic, + bridgecommon.LiquidityTransferredTopic, l.l1LiquidityManager.Address(), - LiquidityTransferredFromChainSelectorTopicIndex, + bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, []common.Hash{ - toHash(l.localSelector), + bridgecommon.NetworkSelectorToHash(l.localSelector), }, - time.Now().Add(-DurationMonth/2), + time.Now().Add(-bridgecommon.DurationMonth/2), 1, ) if err != nil && !errors.Is(err, sql.ErrNoRows) { @@ -282,12 +295,12 @@ func (l *l2ToL1Bridge) GetTransfers(ctx context.Context, localToken models.Addre "l2ToL1Finalizations", receiveLogs, ) - parsedSent, parsedToLP, err := parseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, sendLogs) + parsedSent, parsedToLP, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, sendLogs) if err != nil { return nil, fmt.Errorf("parse L2 -> L1 transfers: %w", err) } - parsedReceived, _, err := parseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, receiveLogs) + parsedReceived, _, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, receiveLogs) if err != nil { return nil, fmt.Errorf("parse L2 -> L1 finalizations: %w", err) } @@ -305,7 +318,7 @@ func (l *l2ToL1Bridge) toPendingTransfers( ready []*liquiditymanager.LiquidityManagerLiquidityTransferred, readyData [][]byte, notReady []*liquiditymanager.LiquidityManagerLiquidityTransferred, - parsedToLP map[logKey]logpoller.Log, + parsedToLP map[bridgecommon.LogKey]logpoller.Log, ) ([]models.PendingTransfer, error) { if len(ready) != len(readyData) { return nil, fmt.Errorf("length of ready and readyData should be the same: len(ready) = %d, len(readyData) = %d", @@ -322,12 +335,12 @@ func (l *l2ToL1Bridge) toPendingTransfers( LocalTokenAddress: localToken, RemoteTokenAddress: remoteToken, Amount: ubig.New(transfer.Amount), - Date: parsedToLP[logKey{ - txHash: transfer.Raw.TxHash, - logIndex: int64(transfer.Raw.Index), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), }].BlockTimestamp, BridgeData: readyData[i], // finalization data for withdrawals that are ready - Stage: StageFinalizeReady, + Stage: bridgecommon.StageFinalizeReady, }, Status: models.TransferStatusReady, ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), @@ -343,12 +356,12 @@ func (l *l2ToL1Bridge) toPendingTransfers( LocalTokenAddress: localToken, RemoteTokenAddress: remoteToken, Amount: ubig.New(transfer.Amount), - Date: parsedToLP[logKey{ - txHash: transfer.Raw.TxHash, - logIndex: int64(transfer.Raw.Index), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), }].BlockTimestamp, BridgeData: []byte{}, // No data since its not ready - Stage: StageRebalanceConfirmed, + Stage: bridgecommon.StageRebalanceConfirmed, }, Status: models.TransferStatusNotReady, ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/bridge.go b/core/services/ocr2/plugins/liquiditymanager/bridge/bridge.go index f4469c7fab..c6fb25d3d1 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/bridge.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/bridge.go @@ -14,6 +14,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/arb" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/opstack" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/testonlybridge" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" ) @@ -124,18 +126,11 @@ func (f *factory) initBridge(source, dest models.NetworkSelector) (Bridge, error var err error switch source { + // Arbitrum L2 --> Ethereum L1 bridge case models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector), models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector): - // source: arbitrum l2 -> dest: ethereum l1 - // only dest that is supported is eth mainnet if source == arb mainnet - // only dest that is supported is eth sepolia if source == arb sepolia - if source == models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector) && - dest != models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector) { - return nil, fmt.Errorf("unsupported destination for arbitrum mainnet l1 -> l2 bridge: %d, must be eth mainnet", dest) - } - if source == models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector) && - dest != models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector) { - return nil, fmt.Errorf("unsupported destination for arbitrum sepolia l1 -> l2 bridge: %d, must be eth sepolia", dest) + if !bridgecommon.Supports(source, dest) { + return nil, fmt.Errorf("unsupported destination for arbitrum l2 -> l1 bridge: %d", dest) } l2Deps, ok := f.evmDeps[source] if !ok { @@ -171,51 +166,122 @@ func (f *factory) initBridge(source, dest models.NetworkSelector) (Bridge, error l2Deps.ethClient, // l2 eth client l1Deps.ethClient, // l1 eth client ) - case models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), - models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector): - // source: Ethereum L1 -> dest: Arbitrum L2 - // only dest that is supported is arbitrum mainnet if source == eth mainnet - // only dest that is supported is arbitrum sepolia if source == eth sepolia - if source == models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector) && - dest != models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector) { - return nil, fmt.Errorf("unsupported destination for eth mainnet l1 -> l2 bridge: %d, must be arb mainnet", dest) - } - if source == models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector) && - dest != models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector) { - return nil, fmt.Errorf("unsupported destination for eth sepolia l1 -> l2 bridge: %d, must be arb sepolia", dest) + + // Optimism L2 --> Ethereum L1 bridge + case models.NetworkSelector(chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector), + models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector): + if !bridgecommon.Supports(source, dest) { + return nil, fmt.Errorf("unsupported destination for optimism l2 -> l1 bridge: %d", dest) } - l1Deps, ok := f.evmDeps[source] + l2Deps, ok := f.evmDeps[source] if !ok { return nil, fmt.Errorf("evm dependencies not found for source selector %d", source) } - l2Deps, ok := f.evmDeps[dest] + l1Deps, ok := f.evmDeps[dest] if !ok { return nil, fmt.Errorf("evm dependencies not found for dest selector %d", dest) } - l1BridgeAdapter, ok := l1Deps.bridgeAdapters[dest] + l1BridgeAdapter, ok := l1Deps.bridgeAdapters[source] if !ok { - return nil, fmt.Errorf("bridge adapter not found for source selector %d in deps for selector %d", source, dest) + return nil, fmt.Errorf("bridge adapter not found for source selector %d in deps for dest selector %d", dest, source) + } + l2BridgeAdapter, ok := l2Deps.bridgeAdapters[dest] + if !ok { + return nil, fmt.Errorf("bridge adapter not found for dest selector %d in deps for source selector %d", source, dest) } f.lggr.Infow("addresses check", - "l1GatewayRouterAddress", arb.AllContracts[uint64(source)]["L1GatewayRouter"], - "inboxAddress", arb.AllContracts[uint64(source)]["L1Inbox"], + "l1StandardBridgeProxyAddress", opstack.OptimismContractsByChainSelector[uint64(dest)]["L1StandardBridgeProxy"], + "l2StandardBridgeAddress", opstack.OptimismContractsByChainSelector[uint64(source)]["L2StandardBridge"], "l1liquidityManagerAddress", l1Deps.liquidityManagerAddress, "l2liquidityManagerAddress", l2Deps.liquidityManagerAddress, "l1BridgeAdapter", l1BridgeAdapter, + "l2BridgeAdapter", l2BridgeAdapter, ) - bridge, err = arb.NewL1ToL2Bridge( + bridge, err = opstack.NewL2ToL1Bridge( f.lggr, source, dest, - common.Address(l1Deps.liquidityManagerAddress), // l1 liquidityManager address - common.Address(l2Deps.liquidityManagerAddress), // l2 liquidityManager address - arb.AllContracts[uint64(source)]["L1GatewayRouter"], // l1 gateway router address - arb.AllContracts[uint64(source)]["L1Inbox"], // l1 inbox address + common.Address(l1Deps.liquidityManagerAddress), // l1 liquidityManager address + common.Address(l2Deps.liquidityManagerAddress), // l2 liquidityManager address l1Deps.ethClient, // l1 eth client l2Deps.ethClient, // l2 eth client l1Deps.lp, // l1 log poller l2Deps.lp, // l2 log poller ) + // Ethereum L1 --> Arbitrum L2 bridge OR + // Ethereum L1 --> Optimism L2 bridge + case models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector): + if !bridgecommon.Supports(source, dest) { + return nil, fmt.Errorf("unsupported destination for eth l1 -> l2 bridge: %d", dest) + } + l1Deps, ok := f.evmDeps[source] + if !ok { + return nil, fmt.Errorf("evm dependencies not found for source selector %d", source) + } + l2Deps, ok := f.evmDeps[dest] + if !ok { + return nil, fmt.Errorf("evm dependencies not found for dest selector %d", dest) + } + l1BridgeAdapter, ok := l1Deps.bridgeAdapters[dest] + if !ok { + return nil, fmt.Errorf("bridge adapter not found for source selector %d in deps for selector %d", source, dest) + } + f.lggr.Infow("addresses check", + "l1GatewayRouterAddress", arb.AllContracts[uint64(source)]["L1GatewayRouter"], + "inboxAddress", arb.AllContracts[uint64(source)]["L1Inbox"], + "l1liquidityManagerAddress", l1Deps.liquidityManagerAddress, + "l2liquidityManagerAddress", l2Deps.liquidityManagerAddress, + "l1BridgeAdapter", l1BridgeAdapter, + ) + switch dest { + case models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector), + models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector): + f.lggr.Infow("dest arb addresses check", + "l1GatewayRouterAddress", arb.AllContracts[uint64(source)]["L1GatewayRouter"], + "inboxAddress", arb.AllContracts[uint64(source)]["L1Inbox"], + "l1liquidityManagerAddress", l1Deps.liquidityManagerAddress, + "l2liquidityManagerAddress", l2Deps.liquidityManagerAddress, + "l1BridgeAdapter", l1BridgeAdapter, + ) + bridge, err = arb.NewL1ToL2Bridge( + f.lggr, + source, + dest, + common.Address(l1Deps.liquidityManagerAddress), // l1 liquidityManager address + common.Address(l2Deps.liquidityManagerAddress), // l2 liquidityManager address + arb.AllContracts[uint64(source)]["L1GatewayRouter"], // l1 gateway router address + arb.AllContracts[uint64(source)]["L1Inbox"], // l1 inbox address + l1Deps.ethClient, // l1 eth client + l2Deps.ethClient, // l2 eth client + l1Deps.lp, // l1 log poller + l2Deps.lp, // l2 log poller + ) + case models.NetworkSelector(chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector), + models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector): + f.lggr.Infow("dest OP addresses check", + "L1StandardBridgeProxyAddress", opstack.OptimismContractsByChainSelector[uint64(source)]["L1StandardBridgeProxy"], + "L2StandardBridgeAddress", opstack.OptimismContractsByChainSelector[uint64(dest)]["L2StandardBridge"], + "l1liquidityManagerAddress", l1Deps.liquidityManagerAddress, + "l2liquidityManagerAddress", l2Deps.liquidityManagerAddress, + "l1BridgeAdapter", l1BridgeAdapter, + ) + bridge, err = opstack.NewL1ToL2Bridge( + f.lggr, + source, + dest, + common.Address(l1Deps.liquidityManagerAddress), // l1 liquidityManager address + common.Address(l2Deps.liquidityManagerAddress), // l2 liquidityManager address + opstack.OptimismContractsByChainSelector[uint64(source)]["L1StandardBridgeProxy"], // l1 standard bridge proxy address + opstack.OptimismContractsByChainSelector[uint64(dest)]["L2StandardBridge"], // l2 standard bridge address + l1Deps.ethClient, // l1 eth client + l2Deps.ethClient, // l2 eth client + l1Deps.lp, // l1 log poller + l2Deps.lp, // l2 log poller + ) + default: + return nil, fmt.Errorf("unsupported destination for eth l1 -> l2 bridge: %d", dest) + } case models.NetworkSelector(chainsel.GETH_TESTNET.Selector), models.NetworkSelector(chainsel.TEST_90000001.Selector), models.NetworkSelector(chainsel.TEST_90000002.Selector), diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/common/chains.go b/core/services/ocr2/plugins/liquiditymanager/bridge/common/chains.go new file mode 100644 index 0000000000..f39b97c64b --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/common/chains.go @@ -0,0 +1,48 @@ +package common + +import ( + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" +) + +type Chains map[uint64][]uint64 + +func Supports(src, dest models.NetworkSelector) bool { + if chains[uint64(src)] == nil { + return false + } + for _, d := range chains[uint64(src)] { + if d == uint64(dest) { + return true + } + } + return false +} + +// Supported source chain -> destination chains for bridge transfers +var chains = Chains{ + // Source = Ethereum + chainsel.ETHEREUM_MAINNET.Selector: []uint64{ + chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector, + chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector, + }, + chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector: []uint64{ + chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector, + chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector, + }, + // Source = Arbitrum + chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector: []uint64{ + chainsel.ETHEREUM_MAINNET.Selector, + }, + chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector: []uint64{ + chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector, + }, + // Source = Optimism + chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector: []uint64{ + chainsel.ETHEREUM_MAINNET.Selector, + }, + chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector: []uint64{ + chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector, + }, +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/common/chains_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/common/chains_test.go new file mode 100644 index 0000000000..95acca6d30 --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/common/chains_test.go @@ -0,0 +1,101 @@ +package common + +import ( + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" +) + +func TestSupports(t *testing.T) { + tests := []struct { + src, dest models.NetworkSelector + expected bool + }{ + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector), + expected: true, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector), + expected: false, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + expected: false, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector), + expected: false, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_ARBITRUM_1.Selector), + expected: false, + }, + { + src: models.NetworkSelector(chainsel.ETHEREUM_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector), + expected: false, + }, + { + src: models.NetworkSelector(chainsel.AREON_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.ETHEREUM_MAINNET_ARBITRUM_1.Selector), + expected: false, + }, + { + src: models.NetworkSelector(chainsel.AREON_MAINNET.Selector), + dest: models.NetworkSelector(chainsel.AVALANCHE_MAINNET.Selector), + expected: false, + }, + } + + for _, tc := range tests { + t.Run("Test", func(t *testing.T) { + result := Supports(tc.src, tc.dest) + if result != tc.expected { + t.Errorf("Supports(%v, %v) = %v, want %v", tc.src, tc.dest, result, tc.expected) + } + }) + } +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/common/common.go b/core/services/ocr2/plugins/liquiditymanager/bridge/common/common.go new file mode 100644 index 0000000000..8d9d17fc68 --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/common/common.go @@ -0,0 +1,101 @@ +package common + +import ( + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + gethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" +) + +const ( + DurationMonth = 720 * time.Hour + + // TODO: these index values might need to be updated when Ryan makes changes to the LM contract and event fields + // LiquidityTransferredToChainSelectorTopicIndex is the index of the topic in the LiquidityTransferred event + // that contains the "to" chain selector. + LiquidityTransferredToChainSelectorTopicIndex = 3 + // LiquidityTransferredFromChainSelectorTopicIndex is the index of the topic in the LiquidityTransferred event + // that contains the "from" chain selector. + LiquidityTransferredFromChainSelectorTopicIndex = 2 + // DepositFinalizedToAddressTopicIndex is the index of the topic in the DepositFinalized event + // that contains the "to" address. + DepositFinalizedToAddressTopicIndex = 3 + // FinalizationStepCompletedRemoteChainSelectorTopicIndex is the index of the topic in the FinalizationStepCompleted + // event that contains the "remote" chain selector. + FinalizationStepCompletedRemoteChainSelectorTopicIndex = 2 + + // StageRebalanceConfirmed is set as the transfer stage when the rebalanceLiquidity tx is confirmed onchain, but + // when it has not yet been finalized. + StageRebalanceConfirmed = 1 + // StageFinalizeReady is set as the transfer stage when the finalization is ready to execute onchain. + StageFinalizeReady = 2 + // StageFinalizeConfirmed is set as the transfer stage when the finalization is confirmed onchain. + // This is a terminal stage. + StageFinalizeConfirmed = 3 +) + +var ( + // LiquidityManager event - emitted on both L1 and L2 + LiquidityTransferredTopic = liquiditymanager.LiquidityManagerLiquidityTransferred{}.Topic() + FinalizationStepCompletedTopic = liquiditymanager.LiquidityManagerFinalizationStepCompleted{}.Topic() +) + +func NetworkSelectorToHash(selector models.NetworkSelector) common.Hash { + encoded := hexutil.EncodeUint64(uint64(selector)) + return common.HexToHash(encoded) +} + +type LogKey struct { + TxHash common.Hash + LogIndex int64 +} + +func ParseLiquidityTransferred(parseFunc func(gethtypes.Log) (*liquiditymanager.LiquidityManagerLiquidityTransferred, error), lgs []logpoller.Log) ([]*liquiditymanager.LiquidityManagerLiquidityTransferred, map[LogKey]logpoller.Log, error) { + transferred := make([]*liquiditymanager.LiquidityManagerLiquidityTransferred, len(lgs)) + toLP := make(map[LogKey]logpoller.Log) + for i, lg := range lgs { + parsed, err := parseFunc(lg.ToGethLog()) + if err != nil { + // should never happen + return nil, nil, fmt.Errorf("parse LiquidityTransferred log: %w", err) + } + transferred[i] = parsed + toLP[LogKey{ + TxHash: lg.TxHash, + LogIndex: lg.LogIndex, + }] = lg + } + return transferred, toLP, nil +} + +func ParseFinalizationStepCompleted(parseFunc func(gethtypes.Log) (*liquiditymanager.LiquidityManagerFinalizationStepCompleted, error), lgs []logpoller.Log) ([]*liquiditymanager.LiquidityManagerFinalizationStepCompleted, error) { + completed := make([]*liquiditymanager.LiquidityManagerFinalizationStepCompleted, len(lgs)) + for i, lg := range lgs { + parsed, err := parseFunc(lg.ToGethLog()) + if err != nil { + return nil, fmt.Errorf("parse FinalizationStepCompleted log: %w", err) + } + completed[i] = parsed + } + return completed, nil +} + +func GetBridgeFilterName(bridgeName, filterLayer string, liquidityManagerAddress common.Address, localChain, remoteChain, extra string) string { + filterName := fmt.Sprintf("%s-%s_LiquidityManager:%s_LocalChain:%s_RemoteChain:%s", + filterLayer, + bridgeName, + liquidityManagerAddress.Hex(), + localChain, + remoteChain, + ) + if extra != "" { + filterName = fmt.Sprintf("%s_%s", filterName, extra) + } + return filterName +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common.go new file mode 100644 index 0000000000..f1080f26ae --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common.go @@ -0,0 +1,96 @@ +package opstack + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_cross_domain_messenger" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_l1_bridge_adapter_encoder" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_l1_standard_bridge" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" +) + +const ( + // ERC20BridgeFinalizedFromAddressTopicIndex is the index of the topic in the ERC20BridgeFinalized event + // that contains the "from" address. In the case of an L1 to L2 transfer, this event will be emitted by the OP + // StandardBridge on L2 and the "from" address should be the L1 bridge adapter contract address. + ERC20BridgeFinalizedFromAddressTopicIndex = 3 + + // Optimism stages + // StageRebalanceConfirmed is set as the transfer stage when the rebalanceLiquidity tx is confirmed onchain, but + // when it has not yet been finalized. + StageRebalanceConfirmed = 1 + // StageFinalizeReady is set as the transfer stage when the finalization is ready to execute onchain. + StageFinalizeReady = 2 + // StageFinalizeConfirmed is set as the transfer stage when the finalization is confirmed onchain. + // This is a terminal stage. + StageFinalizeConfirmed = 3 + + // Function calls + DepositETHToFunction = "depositETHTo" +) + +var ( + // Optimism events emitted on L2 + ERC20BridgeFinalizedTopic = optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized{}.Topic() + + // ABIs + l1standardBridgeABI = abihelpers.MustParseABI(optimism_l1_standard_bridge.OptimismL1StandardBridgeMetaData.ABI) + l1OPBridgeAdapterEncoderABI = abihelpers.MustParseABI(optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterEncoderMetaData.ABI) + opCrossDomainMessengerABI = abihelpers.MustParseABI(optimism_cross_domain_messenger.OptimismCrossDomainMessengerMetaData.ABI) + opStandardBridgeABI = abihelpers.MustParseABI(optimism_standard_bridge.OptimismStandardBridgeMetaData.ABI) +) + +/** + * filterExecuted filters out the transfers that have already been executed on the destination chain. + * @param readyCandidates The initiating transfer logs that are emitted on the source chain when a transfer is issued + * @param receivedLogs The logs emitted on the destination chain when a transfer is received + */ +func filterExecuted( + readyCandidates []*liquiditymanager.LiquidityManagerLiquidityTransferred, + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred, +) ( + ready []*liquiditymanager.LiquidityManagerLiquidityTransferred, + err error, +) { + for _, readyCandidate := range readyCandidates { + exists, err := matchingExecutionExists(readyCandidate, receivedLogs) + if err != nil { + return nil, fmt.Errorf("error checking if ready candidate has been executed: %w", err) + } + if !exists { + ready = append(ready, readyCandidate) + } + } + return +} + +// We encode the nonce (which is used as a unique ID for identifying a given transfer) in the bridgeSpecificData field +// of the receiving LiquidityTransferred log. We can use this to match the sent and received logs. +func matchingExecutionExists( + readyCandidate *liquiditymanager.LiquidityManagerLiquidityTransferred, + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred, +) (bool, error) { + // The nonce is included in the bridgeReturnData when it is emitted as a sent LiquidityTransferred event. + sendLogNonceID, err := abiutils.UnpackUint256(readyCandidate.BridgeReturnData) + if err != nil { + return false, fmt.Errorf("unpack sendLogNonceID from send LiquidityTransferred log (%s): %w, BridgeReturnData: %s", + readyCandidate.Raw.TxHash, err, hexutil.Encode(readyCandidate.BridgeReturnData)) + } + // On the receiving side, the nonce is stored in the BridgeSpecificData field instead + for _, receivedLog := range receivedLogs { + receiveLogNonceID, err := abiutils.UnpackUint256(receivedLog.BridgeSpecificData) + if err != nil { + return false, fmt.Errorf("unpack receiveLogNonceID from receive LiquidityTransferred log (%s): %w, BridgeSpecificData: %s", + receivedLog.Raw.TxHash, err, hexutil.Encode(receivedLog.BridgeSpecificData)) + } + if sendLogNonceID.Cmp(receiveLogNonceID) == 0 { + return true, nil + } + } + return false, nil +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common_test.go new file mode 100644 index 0000000000..38cd734ccb --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/common_test.go @@ -0,0 +1,279 @@ +package opstack + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" +) + +func Test_TopicIndexes(t *testing.T) { + var ( + rebalancerABI = abihelpers.MustParseABI(liquiditymanager.LiquidityManagerMetaData.ABI) + standardBridgeABI = abihelpers.MustParseABI(optimism_standard_bridge.OptimismStandardBridgeMetaData.ABI) + ) + t.Run("liquidity transferred to chain selector idx", func(t *testing.T) { + ltEvent, ok := rebalancerABI.Events["LiquidityTransferred"] + require.True(t, ok) + + var toChainSelectorArg abi.Argument + var topicIndex = 0 + for _, arg := range ltEvent.Inputs { + if arg.Indexed { + topicIndex++ + } + if arg.Name == "toChainSelector" { + toChainSelectorArg = arg + break + } + } + + require.True(t, toChainSelectorArg.Indexed) + require.Equal(t, bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, topicIndex) + }) + + t.Run("liquidity transferred from chain selector idx", func(t *testing.T) { + ltEvent, ok := rebalancerABI.Events["LiquidityTransferred"] + require.True(t, ok) + + var fromChainSelectorArg abi.Argument + var topicIndex = 0 + for _, arg := range ltEvent.Inputs { + if arg.Indexed { + topicIndex++ + } + if arg.Name == "fromChainSelector" { + fromChainSelectorArg = arg + break + } + } + + require.True(t, fromChainSelectorArg.Indexed) + require.Equal(t, bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, topicIndex) + }) + + t.Run("ERC20 bridge finalized to address idx", func(t *testing.T) { + bfEvent, ok := standardBridgeABI.Events["ERC20BridgeFinalized"] + require.True(t, ok) + + var fromAddressArg abi.Argument + var topicIndex = 0 + for _, arg := range bfEvent.Inputs { + if arg.Indexed { + topicIndex++ + } + if arg.Name == "from" { + fromAddressArg = arg + break + } + } + + require.True(t, fromAddressArg.Indexed) + require.Equal(t, ERC20BridgeFinalizedFromAddressTopicIndex, topicIndex) + }) +} + +func Test_filterExecuted(t *testing.T) { + l2LiquidityManagerAddress := common.HexToAddress("0xabc") + l1ChainSelector := chainsel.ETHEREUM_MAINNET.Selector + l2ChainSelector := chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector + type args struct { + readyCandidates []*liquiditymanager.LiquidityManagerLiquidityTransferred + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred + } + tests := []struct { + name string + args args + wantReady []*liquiditymanager.LiquidityManagerLiquidityTransferred + wantErr bool + }{ + { + name: "no logs", + args: args{ + readyCandidates: []*liquiditymanager.LiquidityManagerLiquidityTransferred{}, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{}, + }, + wantReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{}, + wantErr: false, + }, + { + name: "no received logs", + args: args{ + readyCandidates: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{}, + }, + wantReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + }, + wantErr: false, + }, + { + name: "mismatched nonces", + args: args{ + readyCandidates: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 1 + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: []byte{}, + // nonce = 2 + BridgeSpecificData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + }}, + }, + wantReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 1 + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + }, + wantErr: false, + }, + { + name: "matching nonces", + args: args{ + readyCandidates: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 1 + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: []byte{}, + // nonce = 1 + BridgeSpecificData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + }}, + }, + wantReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{}, + wantErr: false, + }, + { + name: "multiple logs, some matching, some not", + args: args{ + readyCandidates: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 1, is executed + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 2, not executed + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + BridgeSpecificData: []byte{}, + }, + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 3, is executed + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + BridgeSpecificData: []byte{}, + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: []byte{}, + // nonce = 1 + BridgeSpecificData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + }, + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: []byte{}, + // nonce = 3 + BridgeSpecificData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + }, + }, + }, + wantReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + // nonce = 2 + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + BridgeSpecificData: []byte{}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotReady, err := filterExecuted(tt.args.readyCandidates, tt.args.receivedLogs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assertLiquidityTransferredEventSlicesEqual(t, tt.wantReady, gotReady, sortByBridgeReturnData) + } + }) + } +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/contracts.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/contracts.go new file mode 100644 index 0000000000..60663c51cb --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/contracts.go @@ -0,0 +1,30 @@ +package opstack + +import ( + "github.com/ethereum/go-ethereum/common" + chainsel "github.com/smartcontractkit/chain-selectors" +) + +var ( + // Optimism contract addresses: https://docs.optimism.io/chain/addresses + OptimismContractsByChainSelector map[uint64]map[string]common.Address +) + +func init() { + OptimismContractsByChainSelector = map[uint64]map[string]common.Address{ + chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector: { + "L1StandardBridgeProxy": common.HexToAddress("0xFBb0621E0B23b5478B630BD55a5f21f67730B0F1"), + "L1CrossDomainMessenger": common.HexToAddress("0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef"), + "WETH": common.HexToAddress("0x7b79995e5f793a07bc00c21412e50ecae098e7f9"), + "FaucetTestingToken": common.HexToAddress("0x5589BB8228C07c4e15558875fAf2B859f678d129"), + "OptimismPortalProxy": common.HexToAddress("0x16Fc5058F25648194471939df75CF27A2fdC48BC"), + "L2OutputOracle": common.HexToAddress("0x90E9c4f8a994a250F6aEfd61CAFb4F2e895D458F"), // Removed after FPAC upgrade + }, + chainsel.ETHEREUM_TESTNET_SEPOLIA_OPTIMISM_1.Selector: { + "L2StandardBridge": common.HexToAddress("0x4200000000000000000000000000000000000010"), + "WETH": common.HexToAddress("0x4200000000000000000000000000000000000006"), + "FaucetTestingToken": common.HexToAddress("0xD08a2917653d4E460893203471f0000826fb4034"), + "L2ToL1MessagePasser": common.HexToAddress("0x4200000000000000000000000000000000000016"), + }, + } +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2.go new file mode 100644 index 0000000000..cb8c2b8a4e --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2.go @@ -0,0 +1,536 @@ +package opstack + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + chainsel "github.com/smartcontractkit/chain-selectors" + "go.uber.org/multierr" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_l1_bridge_adapter" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" +) + +type l1ToL2Bridge struct { + localSelector models.NetworkSelector + remoteSelector models.NetworkSelector + l1LiquidityManager liquiditymanager.LiquidityManagerInterface + l2LiquidityManager liquiditymanager.LiquidityManagerInterface + l1BridgeAdapter optimism_l1_bridge_adapter.OptimismL1BridgeAdapterInterface + l1StandardBridge optimism_standard_bridge.OptimismStandardBridgeInterface + l2StandardBridge optimism_standard_bridge.OptimismStandardBridgeInterface + l1Client client.Client + l2Client client.Client + l1LogPoller logpoller.LogPoller + l2LogPoller logpoller.LogPoller + l1FilterName string + l2FilterName string + l1Token, l2Token common.Address + lggr logger.Logger +} + +func NewL1ToL2Bridge( + lggr logger.Logger, + localSelector, + remoteSelector models.NetworkSelector, + l1LiquidityManagerAddress, + l2LiquidityManagerAddress, + l1StandardBridgeProxyAddress, + l2StandardBridgeAddress common.Address, + l1Client, + l2Client client.Client, + l1LogPoller, + l2LogPoller logpoller.LogPoller, +) (*l1ToL2Bridge, error) { + localChain, ok := chainsel.ChainBySelector(uint64(localSelector)) + if !ok { + return nil, fmt.Errorf("unknown chain selector for local chain: %d", localSelector) + } + remoteChain, ok := chainsel.ChainBySelector(uint64(remoteSelector)) + if !ok { + return nil, fmt.Errorf("unknown chain selector for remote chain: %d", remoteSelector) + } + + l1FilterName := bridgecommon.GetBridgeFilterName( + "OptimismL1ToL2Bridge", + "L1", + l1LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + "", + ) + + // TODO: FIXME pass valid context + ctx := context.Background() + err := l1LogPoller.RegisterFilter(ctx, logpoller.Filter{ + Addresses: []common.Address{l1LiquidityManagerAddress}, // emits LiquidityTransferred + Name: l1FilterName, + EventSigs: []common.Hash{ + bridgecommon.LiquidityTransferredTopic, + }, + Retention: bridgecommon.DurationMonth, + }) + if err != nil { + return nil, fmt.Errorf("failed to register L1 log filter: %w", err) + } + + // TODO: confirm that we're able to use these L1 proxy addresses for listening to emitted events + l1StandardBridge, err := optimism_standard_bridge.NewOptimismStandardBridge(l1StandardBridgeProxyAddress, l1Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate L1StandardBridge at %s: %w", l1StandardBridgeProxyAddress, err) + } + + l1LiquidityManager, err := liquiditymanager.NewLiquidityManager(l1LiquidityManagerAddress, l1Client) + if err != nil { + return nil, fmt.Errorf("instantiate L1 liquidityManager at %s: %w", l1LiquidityManagerAddress, err) + } + + xchainRebal, err := l1LiquidityManager.GetCrossChainRebalancer(nil, uint64(remoteSelector)) + if err != nil { + return nil, fmt.Errorf("get cross chain liquidityManager for remote chain %s: %w", remoteChain.Name, err) + } + + l1BridgeAdapter, err := optimism_l1_bridge_adapter.NewOptimismL1BridgeAdapter(xchainRebal.LocalBridge, l1Client) + if err != nil { + return nil, fmt.Errorf("instantiate L1 bridge adapter at %s: %w", xchainRebal.LocalBridge, err) + } + + l1Token, err := l1LiquidityManager.ILocalToken(nil) + if err != nil { + return nil, fmt.Errorf("get local token from L1 LiquidityManager: %w", err) + } + + l2StandardBridge, err := optimism_standard_bridge.NewOptimismStandardBridge(l2StandardBridgeAddress, l2Client) + if err != nil { + return nil, fmt.Errorf("failed to instantiate L2StandardBridge at %s: %w", l2StandardBridgeAddress, err) + } + + l2LiquidityManager, err := liquiditymanager.NewLiquidityManager(l2LiquidityManagerAddress, l2Client) + if err != nil { + return nil, fmt.Errorf("instantiate L2 liquidityManager at %s: %w", l2LiquidityManagerAddress, err) + } + + l2Token, err := l2LiquidityManager.ILocalToken(nil) + if err != nil { + return nil, fmt.Errorf("get local token from L2 LiquidityManager: %w", err) + } + + l2FilterName := bridgecommon.GetBridgeFilterName( + "OptimismL1ToL2Bridge", + "L2", + l2LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + "", + ) + err = l2LogPoller.RegisterFilter(ctx, logpoller.Filter{ + Addresses: []common.Address{ + l2StandardBridgeAddress, // emits ERC20BridgeFinalized + l2LiquidityManagerAddress, // emits LiquidityTransferred + }, + Name: l2FilterName, + EventSigs: []common.Hash{ + ERC20BridgeFinalizedTopic, // emitted by the L2 StandardBridge + bridgecommon.LiquidityTransferredTopic, // emitted by the L2 LiquidityManager + }, + Retention: bridgecommon.DurationMonth, + }) + if err != nil { + return nil, fmt.Errorf("failed to register L2 log filter: %w", err) + } + + return &l1ToL2Bridge{ + localSelector: localSelector, + remoteSelector: remoteSelector, + l1LiquidityManager: l1LiquidityManager, + l2LiquidityManager: l2LiquidityManager, + l1BridgeAdapter: l1BridgeAdapter, + l1StandardBridge: l1StandardBridge, + l2StandardBridge: l2StandardBridge, + l1Client: l1Client, + l2Client: l2Client, + l1LogPoller: l1LogPoller, + l2LogPoller: l2LogPoller, + l1FilterName: l1FilterName, + l2FilterName: l2FilterName, + l1Token: l1Token, + l2Token: l2Token, + lggr: lggr, + }, nil +} + +func (l *l1ToL2Bridge) GetTransfers( + ctx context.Context, + localToken, + remoteToken models.Address, +) ([]models.PendingTransfer, error) { + lggr := l.lggr.With( + "localToken", localToken, + "remoteToken", remoteToken, + ) + lggr.Info("getting transfers from L1 -> L2") + + if l.l1Token.Cmp(common.Address(localToken)) != 0 { + return nil, fmt.Errorf("local token mismatch: expected %s, got %s", l.l1Token, localToken) + } + if l.l2Token.Cmp(common.Address(remoteToken)) != 0 { + return nil, fmt.Errorf("remote token mismatch: expected %s, got %s", l.l2Token, remoteToken) + } + + // TODO: heavy query warning + fromTs := time.Now().Add(-24 * time.Hour) // last day + sendLogs, erc20BridgeFinalizedLogs, receiveLogs, err := l.getLogs(ctx, fromTs) + if err != nil { + return nil, err + } + + lggr.Infow("got sorted logs", + "sendLogs", sendLogs, + "erc20BridgeFinalizedLogs", erc20BridgeFinalizedLogs, + "receiveLogs", receiveLogs, + ) + + parsedSent, parsedToLP, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, sendLogs) + if err != nil { + return nil, fmt.Errorf("parse L1 -> L2 LiquidityTransferred sent logs: %w", err) + } + + parsedERC20BridgeFinalized, err := l.parseERC20BridgeFinalized(erc20BridgeFinalizedLogs) + if err != nil { + return nil, fmt.Errorf("parse ERC20BridgeFinalized logs: %w", err) + } + + parseReceived, _, err := bridgecommon.ParseLiquidityTransferred(l.l2LiquidityManager.ParseLiquidityTransferred, receiveLogs) + if err != nil { + return nil, fmt.Errorf("parse L1 -> L2 LiquidityTransferred received logs: %w", err) + } + + lggr.Infow("parsed logs", + "parsedSent", len(parsedSent), + "parsedERC20BridgeFinalized", len(parsedERC20BridgeFinalized), + "parseReceived", len(parseReceived), + ) + + notReady, ready, missingSent, err := partitionTransfers( + localToken, + l.l1BridgeAdapter.Address(), + l.l2StandardBridge.Address(), + parsedSent, + parsedERC20BridgeFinalized, + parseReceived) + if err != nil { + return nil, fmt.Errorf("partition transfers: %w", err) + } + if len(missingSent) > 0 { + lggr.Warnw("found L2 bridge finalization logs with no corresponding L1 LiquidityTransferred log", "missingSent", missingSent) + } + + return l.toPendingTransfers(localToken, remoteToken, notReady, ready, parsedToLP) +} + +func (l *l1ToL2Bridge) GetBridgePayloadAndFee( + ctx context.Context, + transfer models.Transfer, +) ([]byte, *big.Int, error) { + // TODO: maybe add check if this is a native transfer or ERC20 transfer + calldata, err := l1standardBridgeABI.Pack( + // If we're sending WETH, the bridge adapter unwraps it and calls depositETHTo on the native bridge + DepositETHToFunction, + transfer.Receiver, // 'to' + 0, // 'l2Gas': hardcoded to 0 in the OptimismL1BridgeAdapter contract + transfer.BridgeData, // 'data' + ) + if err != nil { + log.Fatalf("Failed to pack depositETHTo function call: %v", err) + } + + // Estimate gas needed for the depositETHTo call issued from the L1 Bridge Adapter + l1StandardBridgeAddress := l.l1StandardBridge.Address() + gasPrice, err := l.l1Client.SuggestGasPrice(ctx) + if err != nil { + log.Fatalf("Failed to get suggested gas price: %v", err) + } + gasLimit, err := l.l1Client.EstimateGas(ctx, ethereum.CallMsg{ + From: l.l1BridgeAdapter.Address(), + To: &l1StandardBridgeAddress, + Data: calldata, + GasPrice: gasPrice, + }) + if err != nil { + log.Fatalf("Failed to estimate gas: %v", err) + } + + // Scale gas limit by recommended 20% buffer to account for gas burned for L2 txn: + // https://docs.optimism.io/builders/app-developers/bridging/messaging#fees-for-sending-data-between-l1-and-l2 + // TODO: Applying the 1.2x gas limit bump here to the fee won't really have an effect on the actual gas burned by + // the OP bridge since the gas units are hardcoded to 1e6 in services/relay/evm/liquidity_manager.go. We should + // instead consider a better way to dynamically bump the gas used by the transmitter, or just hardcode an even + // higher gas limit in the transmitter. The OP team has confirmed that only gas up to the "market rate" will be + // burned, not all gas remaining in the limit. + gasLimitBigInt := new(big.Int).SetUint64(gasLimit) + gasLimitWithL2Buffer := new(big.Int).Mul(gasLimitBigInt, big.NewInt(120)) + gasLimitWithL2Buffer = new(big.Int).Div(gasLimitWithL2Buffer, big.NewInt(100)) + + finalGasFee := new(big.Int).Mul(gasPrice, gasLimitWithL2Buffer) + return transfer.BridgeData, finalGasFee, nil +} + +func (l *l1ToL2Bridge) QuorumizedBridgePayload(payloads [][]byte, f int) ([]byte, error) { + if len(payloads) <= f { + return nil, fmt.Errorf("not enough payloads to reach quorum, need at least f+1=%d, got len(payloads)=%d", f+1, len(payloads)) + } + + var transferNonces []*big.Int + for _, payload := range payloads { + decodedTransferNonce, err := abiutils.UnpackUint256(payload) + if err != nil { + return nil, fmt.Errorf("unpack transfer nonce from bridge payload: %w", err) + } + transferNonces = append(transferNonces, decodedTransferNonce) + } + if len(transferNonces) == 0 { + return nil, errors.New("no transfer nonces found in bridge payloads") + } + + // TODO: reconfirm that all nonces should be the same across all payloads in a given round for OP + for _, nonce := range transferNonces { + if nonce.Cmp(transferNonces[0]) != 0 { + return nil, fmt.Errorf("nonces in payloads do not match: %v", transferNonces) + } + } + + return payloads[0], nil +} + +func (l *l1ToL2Bridge) getLogs(ctx context.Context, fromTs time.Time) (sendLogs []logpoller.Log, erc20BridgeFinalizedLogs []logpoller.Log, receiveLogs []logpoller.Log, err error) { + // LiquidityTransferred events emitted by the L1 LiquidityManager. Represents transfers that have been initiated + // from L1 to L2. + sendLogs, err = l.l1LogPoller.IndexedLogsCreatedAfter( + ctx, + bridgecommon.LiquidityTransferredTopic, + l.l1LiquidityManager.Address(), + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, + []common.Hash{ + bridgecommon.NetworkSelectorToHash(l.remoteSelector), + }, + fromTs, + 1, + ) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, nil, nil, fmt.Errorf("get LiquidityTransferred events from L1 LiquidityManager: %w", err) + } + + // ERC20BridgeFinalized events emitted by Optimism's L2StandardBridge. Represents L1 to L2 transfers that have been + // finalized on L2, but potentially not yet "received" by the L2 LiquidityManager. + erc20BridgeFinalizedLogs, err = l.l2LogPoller.IndexedLogsCreatedAfter( + ctx, + ERC20BridgeFinalizedTopic, + l.l2StandardBridge.Address(), + // We register the filter on the "from" address in OP whereas Arb registers it on the "to" address. In OP only + // the "to" address is indexed for this event. To be safe, we check the "to" address below in the partitioning + // step anyway. + ERC20BridgeFinalizedFromAddressTopicIndex, + []common.Hash{ + common.HexToHash(l.l1BridgeAdapter.Address().Hex()), + }, + fromTs, + evmtypes.Finalized, + ) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, nil, nil, fmt.Errorf("get DepositFinalized events from L2 gateway: %w", err) + } + + // LiquidityTransferred events emitted by the L2 LiquidityManager. Represents transfers that have been received on L2. + receiveLogs, err = l.l2LogPoller.IndexedLogsCreatedAfter( + ctx, + bridgecommon.LiquidityTransferredTopic, + l.l2LiquidityManager.Address(), + bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, + []common.Hash{ + bridgecommon.NetworkSelectorToHash(l.localSelector), + }, + fromTs, + 1, + ) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, nil, nil, fmt.Errorf("get LiquidityTransferred events from L2 LiquidityManager: %w", err) + } + + return sendLogs, erc20BridgeFinalizedLogs, receiveLogs, nil +} + +func (l *l1ToL2Bridge) parseERC20BridgeFinalized(erc20BridgeFinalizedLogs []logpoller.Log) ([]*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized, error) { + finalized := make([]*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized, len(erc20BridgeFinalizedLogs)) + for i, lg := range erc20BridgeFinalizedLogs { + parsed, err := l.l2StandardBridge.ParseERC20BridgeFinalized(lg.ToGethLog()) + if err != nil { + return nil, fmt.Errorf("parse ERC20BridgeFinalized log: %w", err) + } + finalized[i] = parsed + } + return finalized, nil +} + +/** + * This function partitions the transfer events into four categories: + * - notReady: sent (L1 LiquidityTransferred), but not finalized (L2 ERC20BridgeFinalized) + * - ready: sent (L1 LiquidityTransferred), and finalized (L2 ERC20BridgeFinalized), but not received (L2 LiquidityTransferred) + * - done: sent (L1 LiquidityTransferred), finalized (L2 ERC20BridgeFinalized), and received (L2 LiquidityTransferred) + * - missingSent: finalized (L2 ERC20BridgeFinalized), but no corresponding sent (L1 LiquidityTransferred) + * + * Since we only care about the pending transfers, this function only returns 'notReady', 'ready', and 'missingSent'. + * The matching logic is performed based on the fact that a nonce is piped through all events: + * sent_LiquidityTransferred.bridgeReturnData == ERC20BridgeFinalized.extraData == received_LiquidityTransferred.bridgeSpecificData + */ +func partitionTransfers( + localToken models.Address, + l1BridgeAdapterAddress common.Address, + l2LiquidityManagerAddress common.Address, + sentLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred, + erc20BridgeFinalizedLogs []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized, + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred, +) ( + notReady, + ready []*liquiditymanager.LiquidityManagerLiquidityTransferred, + missingSent []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized, + err error, +) { + transferNonceToSentLogMap := make(map[string]*liquiditymanager.LiquidityManagerLiquidityTransferred) + foundMatchingFinalizedLogMap := make(map[string]bool) + for _, sentLog := range sentLogs { + if sentLog.To != l2LiquidityManagerAddress { + continue + } + var transferNonce *big.Int + transferNonce, err = abiutils.UnpackUint256(sentLog.BridgeReturnData) + if err != nil { + return nil, nil, nil, fmt.Errorf("unpack transfer nonce from L1 LiquidityTransferred log (%s): %w, data: %s", + sentLog.Raw.TxHash, err, hexutil.Encode(sentLog.BridgeReturnData)) + } + transferNonceToSentLogMap[transferNonce.String()] = sentLog + foundMatchingFinalizedLogMap[transferNonce.String()] = false + } + + // For each finalized log, check if it has a corresponding sent log. If there is no corresponding sent log, add it + // to 'missingSent' + for _, finalized := range erc20BridgeFinalizedLogs { + if finalized.RemoteToken != common.Address(localToken) { + continue + } + if finalized.From != l1BridgeAdapterAddress { + continue + } + if finalized.To != l2LiquidityManagerAddress { + continue + } + var transferNonce *big.Int + transferNonce, err = abiutils.UnpackUint256(finalized.ExtraData) + if err != nil { + return nil, nil, nil, fmt.Errorf("unpack transfer nonce from L2 ERC20BridgeFinalized log (%s): %w, data: %s", + finalized.Raw.TxHash, err, hexutil.Encode(finalized.ExtraData)) + } + if sentLog, exists := transferNonceToSentLogMap[transferNonce.String()]; exists { + // If a corresponding sentLog exists for this finalized log, add it to ready + ready = append(ready, sentLog) + foundMatchingFinalizedLogMap[transferNonce.String()] = true + } else if !exists { + // Else, if a corresponding sentLog does not exist, add it to missingSent + missingSent = append(missingSent, finalized) + } + } + + // Any entries in foundMatchingFinalizedLogMap that are 'false' were not found to have a matching finalized log + // and are therefore not ready to be "received" yet. + for transferID, found := range foundMatchingFinalizedLogMap { + if !found { + if sentLog, exists := transferNonceToSentLogMap[transferID]; exists { + notReady = append(notReady, sentLog) + } + } + } + + // Filter out from 'ready' any logs from transfers that have already been received by the L2 LM + ready, err = filterExecuted(ready, receivedLogs) + if err != nil { + return nil, nil, nil, fmt.Errorf("filter executed: %w", err) + } + return +} + +func (l *l1ToL2Bridge) toPendingTransfers( + localToken, + remoteToken models.Address, + notReady, + ready []*liquiditymanager.LiquidityManagerLiquidityTransferred, + parsedToLP map[bridgecommon.LogKey]logpoller.Log, +) ([]models.PendingTransfer, error) { + var transfers []models.PendingTransfer + for _, transfer := range notReady { + transfers = append(transfers, models.PendingTransfer{ + Transfer: models.Transfer{ + From: l.localSelector, + To: l.remoteSelector, + Sender: models.Address(l.l1LiquidityManager.Address()), + Receiver: models.Address(l.l2LiquidityManager.Address()), + LocalTokenAddress: localToken, + RemoteTokenAddress: remoteToken, + Amount: ubig.New(transfer.Amount), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), + }].BlockTimestamp, + BridgeData: transfer.BridgeReturnData, // unique nonce from the OP Bridge Adapter + Stage: bridgecommon.StageRebalanceConfirmed, + }, + Status: models.TransferStatusNotReady, + ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), + }) + } + for _, transfer := range ready { + transfers = append(transfers, models.PendingTransfer{ + Transfer: models.Transfer{ + From: l.localSelector, + To: l.remoteSelector, + Sender: models.Address(l.l1LiquidityManager.Address()), + Receiver: models.Address(l.l2LiquidityManager.Address()), + LocalTokenAddress: localToken, + RemoteTokenAddress: remoteToken, + Amount: ubig.New(transfer.Amount), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), + }].BlockTimestamp, + BridgeData: transfer.BridgeReturnData, // unique nonce from the OP Bridge Adapter + Stage: bridgecommon.StageFinalizeReady, + }, + Status: models.TransferStatusReady, // ready == finalized for L1 -> L2 transfers due to auto-finalization by the native bridge + ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), + }) + } + return transfers, nil +} + +func (l *l1ToL2Bridge) Close(ctx context.Context) error { + return multierr.Combine( + l.l1LogPoller.UnregisterFilter(ctx, l.l1FilterName), + l.l2LogPoller.UnregisterFilter(ctx, l.l2FilterName), + ) +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2_test.go new file mode 100644 index 0000000000..be4c6419cc --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l1_to_l2_test.go @@ -0,0 +1,452 @@ +package opstack + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + lpmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_standard_bridge" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" +) + +func Test_l1ToL2Bridge_QuorumizedBridgePayload(t *testing.T) { + type args struct { + payloads [][]byte + f int + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + "not enough payloads", + args{ + [][]byte{}, + 1, + }, + nil, + true, + }, + { + "non-matching nonces/payloads", + args{ + [][]byte{ + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + }, + 1, + }, + nil, + true, + }, + { + "happy path", + args{ + [][]byte{ + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + }, + 1, + }, + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + false, + }, + { + "happy path, fewer payloads", + args{ + [][]byte{ + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + }, + 1, + }, + mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &l1ToL2Bridge{} + got, err := l.QuorumizedBridgePayload(tt.args.payloads, tt.args.f) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + unpackedGot, err := abiutils.UnpackUint256(got) + require.NoError(t, err) + unpackedExpected, err := abiutils.UnpackUint256(tt.want) + require.NoError(t, err) + require.Equal(t, unpackedExpected, unpackedGot) + } + }) + } +} + +func Test_partitionTransfers(t *testing.T) { + l1LocalToken := models.Address(common.HexToAddress("0x123")) + l2LocalToken := models.Address(common.HexToAddress("0x456")) + l1BridgeAdapterAddress := common.HexToAddress("0x789") + l2LiquidityManagerAddress := common.HexToAddress("0xabc") + l1ChainSelector := chainsel.ETHEREUM_MAINNET.Selector + l2ChainSelector := chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector + + type args struct { + localToken models.Address + l1BridgeAdapterAddress common.Address + l2LiquidityManagerAddress common.Address + sentLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred + erc20BridgeFinalizedLogs []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred + } + tests := []struct { + name string + args args + wantNotReady []*liquiditymanager.LiquidityManagerLiquidityTransferred + wantReady []*liquiditymanager.LiquidityManagerLiquidityTransferred + wantMissingSent []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized + wantErr bool + }{ + { + name: "empty", + args: args{ + localToken: models.Address{}, + l1BridgeAdapterAddress: common.Address{}, + l2LiquidityManagerAddress: common.Address{}, + sentLogs: nil, + erc20BridgeFinalizedLogs: nil, + receivedLogs: nil, + }, + wantNotReady: nil, + wantReady: nil, + wantMissingSent: nil, + wantErr: false, + }, + { + name: "happy path - one ready, one not ready, one missing, one done", + args: args{ + localToken: l1LocalToken, + l1BridgeAdapterAddress: l1BridgeAdapterAddress, + l2LiquidityManagerAddress: l2LiquidityManagerAddress, + sentLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // This one is not ready (only present in sentLogs) + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + // This one is ready (present in sentLogs and finalized logs) + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + BridgeSpecificData: []byte{}, + }, + // This one is done/already received (present in sentLogs, finalized logs, and receivedLogs), should not be included in any output slices + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + BridgeSpecificData: []byte{}, + }, + }, + erc20BridgeFinalizedLogs: []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized{ + // This one is ready (present in sentLogs and finalized logs) + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.Address(l1LocalToken), + From: l1BridgeAdapterAddress, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + }, + // This one is already done (present in sentLogs, finalized logs, and receivedLogs) + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.Address(l1LocalToken), + From: l1BridgeAdapterAddress, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + }, + // This one is missing (not present in sentLogs) + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.Address(l1LocalToken), + From: l1BridgeAdapterAddress, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000065"), + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: []byte{}, + BridgeSpecificData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + }, + }, + }, + wantNotReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // This one is not ready (only present in sentLogs) + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + }, + wantReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // This one is ready (present in sentLogs and finalized logs) + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + BridgeSpecificData: []byte{}, + }, + }, + wantMissingSent: []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized{ + // This one is missing (not present in sentLogs) + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.Address(l1LocalToken), + From: l1BridgeAdapterAddress, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000065"), + }, + }, + wantErr: false, + }, + { + name: "L2 standard bridge finalized event - mismatched from, to, remote token fields", + args: args{ + localToken: l1LocalToken, + l1BridgeAdapterAddress: l1BridgeAdapterAddress, + l2LiquidityManagerAddress: l2LiquidityManagerAddress, + sentLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // Mismatched finalized event 'from' field + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + // Mismatched finalized event 'to' field + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + BridgeSpecificData: []byte{}, + }, + // Mismatched finalized event 'remote_token' field + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + BridgeSpecificData: []byte{}, + }, + }, + erc20BridgeFinalizedLogs: []*optimism_standard_bridge.OptimismStandardBridgeERC20BridgeFinalized{ + // Mismatched finalized event 'from' field + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.Address(l1LocalToken), + From: common.HexToAddress("0x123"), + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + }, + // Mismatched finalized event 'to' field + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.Address(l1LocalToken), + From: l1BridgeAdapterAddress, + To: common.HexToAddress("0x456"), + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + }, + // Mismatched finalized event 'remote_token' field + { + LocalToken: common.Address(l2LocalToken), + RemoteToken: common.HexToAddress("0xabcd"), + From: l1BridgeAdapterAddress, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + ExtraData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{}, + }, + wantNotReady: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // Mismatched finalized event 'from' field + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000001"), + BridgeSpecificData: []byte{}, + }, + // Mismatched finalized event 'to' field + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000002"), + BridgeSpecificData: []byte{}, + }, + // Mismatched finalized event 'remote_token' field + { + FromChainSelector: l1ChainSelector, + ToChainSelector: l2ChainSelector, + To: l2LiquidityManagerAddress, + Amount: big.NewInt(1), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000003"), + BridgeSpecificData: []byte{}, + }}, + wantReady: nil, + wantMissingSent: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotNotReady, gotReady, gotMissingSent, err := partitionTransfers(tt.args.localToken, tt.args.l1BridgeAdapterAddress, tt.args.l2LiquidityManagerAddress, tt.args.sentLogs, tt.args.erc20BridgeFinalizedLogs, tt.args.receivedLogs) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assertLiquidityTransferredEventSlicesEqual(t, tt.wantNotReady, gotNotReady, sortByBridgeReturnData) + assertLiquidityTransferredEventSlicesEqual(t, tt.wantReady, gotReady, sortByBridgeReturnData) + assert.Equal(t, tt.wantMissingSent, gotMissingSent) + } + }) + } +} + +func Test_l1ToL2Bridge_Close(t *testing.T) { + type fields struct { + l1LogPoller *lpmocks.LogPoller + l2LogPoller *lpmocks.LogPoller + l1FilterName string + l2FilterName string + } + tests := []struct { + name string + fields fields + wantErr bool + before func(*testing.T, fields) + assertions func(*testing.T, fields) + }{ + { + "happy path", + fields{ + l1LogPoller: lpmocks.NewLogPoller(t), + l2LogPoller: lpmocks.NewLogPoller(t), + l1FilterName: "l1FilterName", + l2FilterName: "l2FilterName", + }, + false, + func(t *testing.T, f fields) { + f.l1LogPoller.On("UnregisterFilter", mock.Anything, f.l1FilterName).Return(nil) + f.l2LogPoller.On("UnregisterFilter", mock.Anything, f.l2FilterName).Return(nil) + }, + func(t *testing.T, f fields) { + f.l1LogPoller.AssertExpectations(t) + f.l2LogPoller.AssertExpectations(t) + }, + }, + { + "l1 unregister error", + fields{ + l1LogPoller: lpmocks.NewLogPoller(t), + l2LogPoller: lpmocks.NewLogPoller(t), + l1FilterName: "l1FilterName", + l2FilterName: "l2FilterName", + }, + true, + func(t *testing.T, f fields) { + f.l1LogPoller.On("UnregisterFilter", mock.Anything, f.l1FilterName).Return(errors.New("unregister error")) + f.l2LogPoller.On("UnregisterFilter", mock.Anything, f.l2FilterName).Return(nil) + }, + func(t *testing.T, f fields) { + f.l1LogPoller.AssertExpectations(t) + f.l2LogPoller.AssertExpectations(t) + }, + }, + { + "l2 unregister error", + fields{ + l1LogPoller: lpmocks.NewLogPoller(t), + l2LogPoller: lpmocks.NewLogPoller(t), + l1FilterName: "l1FilterName", + l2FilterName: "l2FilterName", + }, + true, + func(t *testing.T, f fields) { + f.l1LogPoller.On("UnregisterFilter", mock.Anything, f.l1FilterName).Return(nil) + f.l2LogPoller.On("UnregisterFilter", mock.Anything, f.l2FilterName).Return(errors.New("unregister error")) + }, + func(t *testing.T, f fields) { + f.l1LogPoller.AssertExpectations(t) + f.l2LogPoller.AssertExpectations(t) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &l1ToL2Bridge{ + l1LogPoller: tt.fields.l1LogPoller, + l2LogPoller: tt.fields.l2LogPoller, + l1FilterName: tt.fields.l1FilterName, + l2FilterName: tt.fields.l2FilterName, + } + if tt.before != nil { + tt.before(t, tt.fields) + defer tt.assertions(t, tt.fields) + } + + err := l.Close(testutils.Context(t)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1.go new file mode 100644 index 0000000000..5a70619e66 --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1.go @@ -0,0 +1,532 @@ +package opstack + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + chainsel "github.com/smartcontractkit/chain-selectors" + "go.uber.org/multierr" + + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + ubig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" + bridgecommon "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/common" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" +) + +type l2ToL1Bridge struct { + localSelector models.NetworkSelector + remoteSelector models.NetworkSelector + l1LiquidityManager liquiditymanager.LiquidityManagerInterface + l2LiquidityManager liquiditymanager.LiquidityManagerInterface + l1Client client.Client + l2Client client.Client + l1LogPoller logpoller.LogPoller + l2LogPoller logpoller.LogPoller + l1FilterName string + l2FilterName string + l1Token, l2Token common.Address + lggr logger.Logger +} + +func NewL2ToL1Bridge( + lggr logger.Logger, + localSelector, + remoteSelector models.NetworkSelector, + l1LiquidityManagerAddress, + l2LiquidityManagerAddress common.Address, + l1Client, + l2Client client.Client, + l1LogPoller, + l2LogPoller logpoller.LogPoller, +) (*l2ToL1Bridge, error) { + localChain, ok := chainsel.ChainBySelector(uint64(localSelector)) + if !ok { + return nil, fmt.Errorf("unknown chain selector for local chain: %d", localSelector) + } + remoteChain, ok := chainsel.ChainBySelector(uint64(remoteSelector)) + if !ok { + return nil, fmt.Errorf("unknown chain selector for remote chain: %d", remoteSelector) + } + + l2FilterName := bridgecommon.GetBridgeFilterName( + "OptimismL2ToL1Bridge", + "L2", + l2LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + "", + ) + // TODO (ogtownsend): pass context from above + ctx := context.Background() + err := l2LogPoller.RegisterFilter( + ctx, + logpoller.Filter{ + Name: l2FilterName, + EventSigs: []common.Hash{ + bridgecommon.LiquidityTransferredTopic, + }, + Addresses: []common.Address{l2LiquidityManagerAddress}, + Retention: bridgecommon.DurationMonth, + }) + if err != nil { + return nil, fmt.Errorf("register L2 LM filter for Optimism L2 to L1 bridge: %w", err) + } + + l1FilterName := bridgecommon.GetBridgeFilterName( + "OptimismL2ToL1Bridge", + "L1", + l1LiquidityManagerAddress, + localChain.Name, + remoteChain.Name, + "", + ) + + err = l1LogPoller.RegisterFilter( + ctx, + logpoller.Filter{ + Name: l1FilterName, + EventSigs: []common.Hash{ + bridgecommon.FinalizationStepCompletedTopic, // emitted by LiquidityManager + bridgecommon.LiquidityTransferredTopic, // emitted by LiquidityManager + }, + Addresses: []common.Address{ + l1LiquidityManagerAddress, // to get LiquidityTransferred and FinalizationStepCompleted logs + }, + Retention: bridgecommon.DurationMonth, + }) + if err != nil { + return nil, fmt.Errorf("register L1 LM filter for Optimism L2 to L1 bridge: %w", err) + } + + l1LiquidityManager, err := liquiditymanager.NewLiquidityManager(l1LiquidityManagerAddress, l1Client) + if err != nil { + return nil, fmt.Errorf("instantiate L1 LiquidityManager: %w", err) + } + + l2LiquidityManager, err := liquiditymanager.NewLiquidityManager(l2LiquidityManagerAddress, l2Client) + if err != nil { + return nil, fmt.Errorf("instantiate L2 LiquidityManager: %w", err) + } + + l2Token, err := l2LiquidityManager.ILocalToken(nil) + if err != nil { + return nil, fmt.Errorf("get L2 local token address: %w", err) + } + l1Token, err := l1LiquidityManager.ILocalToken(nil) + if err != nil { + return nil, fmt.Errorf("get L1 local token address: %w", err) + } + + lggr = lggr.Named("OptimismL2ToL1Bridge").With( + "localSelector", localSelector, + "remoteSelector", remoteSelector, + "l1LiquidityManager", l1LiquidityManagerAddress.Hex(), + "l2LiquidityManager", l2LiquidityManagerAddress.Hex(), + "l1Token", l1Token.Hex(), + "l2Token", l2Token.Hex(), + ) + lggr.Infow("Initialized Optimism L2 to L1 bridge") + + return &l2ToL1Bridge{ + localSelector: localSelector, + remoteSelector: remoteSelector, + l1LiquidityManager: l1LiquidityManager, + l2LiquidityManager: l2LiquidityManager, + l1Client: l1Client, + l2Client: l2Client, + l1LogPoller: l1LogPoller, + l2LogPoller: l2LogPoller, + l1FilterName: l1FilterName, + l2FilterName: l2FilterName, + l1Token: l1Token, + l2Token: l2Token, + lggr: lggr, + }, nil +} + +func (l *l2ToL1Bridge) GetTransfers( + ctx context.Context, + localToken, + remoteToken models.Address, +) ([]models.PendingTransfer, error) { + lggr := l.lggr.With("l2Token", localToken, "l1Token", remoteToken) + if l.l2Token.Cmp(common.Address(localToken)) != 0 { + return nil, fmt.Errorf("local token mismatch: expected %s, got %s", l.l2Token, localToken) + } + if l.l1Token.Cmp(common.Address(remoteToken)) != 0 { + return nil, fmt.Errorf("remote token mismatch: expected %s, got %s", l.l1Token, remoteToken) + } + + sendLogs, proveFinalizationStepLogs, receivedLogs, err := l.getLogs(ctx) + if err != nil { + return nil, fmt.Errorf("get logs: %w", err) + } + + lggr.Infow("Got L2 -> L1 transfer and finalization step logs", + "sendLogs", len(sendLogs), + "proveFinalizedLogs", len(proveFinalizationStepLogs), + "receivedLogs", len(receivedLogs), + ) + + parsedSent, parsedToLp, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, sendLogs) + if err != nil { + return nil, fmt.Errorf("parse L2 -> L1 transfer sent logs: %w", err) + } + + parsedProveFinalizationSteps, err := bridgecommon.ParseFinalizationStepCompleted(l.l1LiquidityManager.ParseFinalizationStepCompleted, proveFinalizationStepLogs) + if err != nil { + return nil, fmt.Errorf("parse L2 -> L1 transfer prove finalization step logs: %w", err) + } + + parsedReceived, _, err := bridgecommon.ParseLiquidityTransferred(l.l1LiquidityManager.ParseLiquidityTransferred, receivedLogs) + if err != nil { + return nil, fmt.Errorf("parse L2 -> L1 transfer received logs: %w", err) + } + + lggr.Infow("parsed logs", + "parsedSent", len(parsedSent), + "parsedProveFinalizationSteps", len(parsedProveFinalizationSteps), + "parsedReceived", len(parsedReceived), + ) + + needsToBeProven, needsToBeFinalized, missingSent, err := partitionWithdrawalTransfers( + l.localSelector, + l.l1LiquidityManager.Address(), + parsedSent, + parsedProveFinalizationSteps, + parsedReceived, + lggr, + ) + if err != nil { + return nil, fmt.Errorf("partition transfers: %w", err) + } + if len(missingSent) > 0 { + l.lggr.Errorw("missing sent logs", "missingSent", missingSent) + } + + return l.toPendingTransfers(ctx, lggr, localToken, remoteToken, needsToBeProven, needsToBeFinalized, parsedToLp) +} + +/** + * partitionWithdrawalTransfers matches and divides in-progress and completed transfers into three groups: + * 1) needsToBeProven: transfers that have been started by the L2 LM but are not yet proven on L1 + * 2) needsToBeFinalized: transfers that have been proven on L1 but are not yet finalized (received) on L1 + * 3) missingSent: transfers that have a prove finalization step log but no matching sent log + * + * It does this by matching the transfer's unique nonce emitted in certain events' fields. These events and fields are: + * - L2 LiquidityTransferred.bridgeReturnData: emitted by the L2 LM when a transfer is initiated + * - L1 FinalizationStepCompleted.bridgeSpecificData: emitted by the L1 LM when a L2 to L1 withdrawal is proven + * - L1 LiquidityTransferred.bridgeSpecificData: emitted by the L1 LM when a L2 to L1 withdrawal is finalized + */ +func partitionWithdrawalTransfers( + localSelector models.NetworkSelector, + l1LiquidityManagerAddress common.Address, + sentLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred, + proveFinalizationStepLogs []*liquiditymanager.LiquidityManagerFinalizationStepCompleted, + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred, + lggr logger.Logger, +) ( + needsToBeProven, + needsToBeFinalized []*liquiditymanager.LiquidityManagerLiquidityTransferred, + missingSent []*liquiditymanager.LiquidityManagerFinalizationStepCompleted, + err error, +) { + transferNonceToSentLogMap := make(map[string]*liquiditymanager.LiquidityManagerLiquidityTransferred) + foundMatchingProveFinalizationStepMap := make(map[string]bool) + for _, sentLog := range sentLogs { + if sentLog.To != l1LiquidityManagerAddress { + lggr.Warnw("skipping sent log with mismatched 'To' address", "sentLog", sentLog) + continue + } + if sentLog.FromChainSelector != uint64(localSelector) { + lggr.Warnw("skipping sent log with mismatched 'FromChainSelector'", "sentLog", sentLog) + continue + } + var transferNonce *big.Int + transferNonce, err = abiutils.UnpackUint256(sentLog.BridgeReturnData) + if err != nil { + return nil, nil, nil, fmt.Errorf("unpack transfer nonce from L2 LiquidityTransferred log. Log tx: %s. Err: %w, log bridgeReturnData: %s", + sentLog.Raw.TxHash, err, hexutil.Encode(sentLog.BridgeReturnData)) + } + transferNonceToSentLogMap[transferNonce.String()] = sentLog + foundMatchingProveFinalizationStepMap[transferNonce.String()] = false + } + + // For each proveFinalizationStep, check if it matches a sentLogs log + for _, proveStep := range proveFinalizationStepLogs { + // L1's prove finalization step log's remote chain selector should be L2 + if proveStep.RemoteChainSelector != uint64(localSelector) { + lggr.Warnw("skipping prove finalization step log with mismatched 'RemoteChainSelector'", "proveStep", proveStep) + continue + } + var transferNonce *big.Int + transferNonce, err = withdrawprover.UnpackNonceFromFinalizationStepBridgeSpecificData(proveStep, l1OPBridgeAdapterEncoderABI, opCrossDomainMessengerABI, opStandardBridgeABI) + if err != nil { + return nil, nil, nil, fmt.Errorf("get transfer nonce from L1 FinalizationStepCompleted log. Log tx: %s. Err: %w", + proveStep.Raw.TxHash, err) + } + lggr.Infow("Unpacked transfer nonce from finalization step", "transferNonce", transferNonce.String()) + if sentLog, exists := transferNonceToSentLogMap[transferNonce.String()]; exists { + // If a corresponding sentLog exists for this proveFinalizationStep, append to needsToBeFinalized and + // mark it as found + needsToBeFinalized = append(needsToBeFinalized, sentLog) + foundMatchingProveFinalizationStepMap[transferNonce.String()] = true + } else { + // If no corresponding sentLog exists for this proveFinalizationStep, append to missingSent + missingSent = append(missingSent, proveStep) + } + } + + // Any entries in foundMatchingProveFinalizationStepMap that are still false are transfers that need to be proven + // TODO (ogtownsend / amirylm): is the plugin able to handle the case where we've already instructed the plugin to prove() a transfer, but + // the prove log hasn't been emitted or ingested by the log poller yet? We could potentially send two prove() txs + for transferNonce, found := range foundMatchingProveFinalizationStepMap { + if !found { + if sentLog, exists := transferNonceToSentLogMap[transferNonce]; exists { + needsToBeProven = append(needsToBeProven, sentLog) + } + } + } + + // Filter out from needsToBeFinalized any entries that have already been receivedLogs by the L1 LM + needsToBeFinalized, err = filterExecuted(needsToBeFinalized, receivedLogs) + return +} + +func (l *l2ToL1Bridge) getLogs(ctx context.Context) (sendLogs, proveFinalizationStepLogs, receivedLogs []logpoller.Log, err error) { + // Get all L2 -> L1 transfers that have been sent from the L2 LM in the past 14 days + sendLogs, err = l.l2LogPoller.IndexedLogsCreatedAfter( + ctx, + bridgecommon.LiquidityTransferredTopic, + l.l2LiquidityManager.Address(), + bridgecommon.LiquidityTransferredToChainSelectorTopicIndex, + []common.Hash{ + bridgecommon.NetworkSelectorToHash(l.remoteSelector), + }, + time.Now().Add(-bridgecommon.DurationMonth/2), + evmtypes.Finalized, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("get L2 -> L1 transfers from log poller on L2: %w", err) + } + + // Get all L2 -> L1 transfers that have been proven/finalized in the past 14 days + proveFinalizationStepLogs, err = l.l1LogPoller.IndexedLogsCreatedAfter( + ctx, + bridgecommon.FinalizationStepCompletedTopic, + l.l1LiquidityManager.Address(), + bridgecommon.FinalizationStepCompletedRemoteChainSelectorTopicIndex, + []common.Hash{ + bridgecommon.NetworkSelectorToHash(l.remoteSelector), + }, + time.Now().Add(-bridgecommon.DurationMonth/2), + evmtypes.Finalized, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("get L1 -> L2 transfers from log poller on L1: %w", err) + } + + receivedLogs, err = l.l1LogPoller.IndexedLogsCreatedAfter( + ctx, + bridgecommon.LiquidityTransferredTopic, + l.l1LiquidityManager.Address(), + bridgecommon.LiquidityTransferredFromChainSelectorTopicIndex, + []common.Hash{ + bridgecommon.NetworkSelectorToHash(l.localSelector), + }, + time.Now().Add(-bridgecommon.DurationMonth/2), + evmtypes.Finalized, + ) + if err != nil { + return nil, nil, nil, fmt.Errorf("get L1 -> L2 transfers from log poller on L1: %w", err) + } + + return sendLogs, proveFinalizationStepLogs, receivedLogs, nil +} + +func (l *l2ToL1Bridge) toPendingTransfers( + ctx context.Context, + lggr logger.Logger, + localToken, remoteToken models.Address, + needsToBeProven, needsToBeFinalized []*liquiditymanager.LiquidityManagerLiquidityTransferred, + parsedToLP map[bridgecommon.LogKey]logpoller.Log, +) ([]models.PendingTransfer, error) { + var transfers []models.PendingTransfer + for _, transfer := range needsToBeProven { + provePayload, err := l.generateTransferBridgeDataForProve(ctx, lggr, transfer) + if err != nil { + return nil, fmt.Errorf("generate transfer bridge data for prove: %w", err) + } + transfers = append(transfers, models.PendingTransfer{ + Transfer: models.Transfer{ + From: l.localSelector, + To: l.remoteSelector, + Sender: models.Address(l.l2LiquidityManager.Address()), + Receiver: models.Address(l.l1LiquidityManager.Address()), + LocalTokenAddress: localToken, + RemoteTokenAddress: remoteToken, + Amount: ubig.New(transfer.Amount), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), + }].BlockTimestamp, + BridgeData: provePayload, + Stage: bridgecommon.StageRebalanceConfirmed, + }, + // Both "prove" and "finalize" are handled by the "finalizeWithdrawalERC20" call in the + // OptimismL1BridgeAdapter, therefore we set the status to "Ready" + Status: models.TransferStatusReady, + ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), + }) + } + for _, transfer := range needsToBeFinalized { + finalizePayload, err := l.generateTransferBridgeDataForFinalize(ctx, transfer) + if err != nil { + return nil, fmt.Errorf("generate transfer bridge data for finalize: %w", err) + } + transfers = append(transfers, models.PendingTransfer{ + Transfer: models.Transfer{ + From: l.localSelector, + To: l.remoteSelector, + Sender: models.Address(l.l2LiquidityManager.Address()), + Receiver: models.Address(l.l1LiquidityManager.Address()), + LocalTokenAddress: localToken, + RemoteTokenAddress: remoteToken, + Amount: ubig.New(transfer.Amount), + Date: parsedToLP[bridgecommon.LogKey{ + TxHash: transfer.Raw.TxHash, + LogIndex: int64(transfer.Raw.Index), + }].BlockTimestamp, + BridgeData: finalizePayload, + Stage: bridgecommon.StageFinalizeReady, + }, + Status: models.TransferStatusReady, // Ready to be finalized + ID: fmt.Sprintf("%s-%d", transfer.Raw.TxHash.Hex(), transfer.Raw.Index), + }) + } + return transfers, nil +} + +func (l *l2ToL1Bridge) generateTransferBridgeDataForProve( + ctx context.Context, + lggr logger.Logger, + transfer *liquiditymanager.LiquidityManagerLiquidityTransferred, +) ([]byte, error) { + // Portal and Proxy addresses are kept on Eth L1 + optimismPortalProxyAddress := OptimismContractsByChainSelector[uint64(l.remoteSelector)]["OptimismPortalProxy"] + optimismL2OutputOracleAddress := OptimismContractsByChainSelector[uint64(l.remoteSelector)]["L2OutputOracle"] + lggr.Infow("Generating transfer bridge data for prove, address check", + "remoteSelector", uint64(l.remoteSelector), + "OptimismPortalProxy", optimismPortalProxyAddress, + "L2OutputOracle", optimismL2OutputOracleAddress, + ) + + prover, err := withdrawprover.New( + l.l1Client, + l.l2Client, + optimismPortalProxyAddress, + optimismL2OutputOracleAddress, + ) + if err != nil { + return nil, fmt.Errorf("instantiate withdraw prover: %w", err) + } + + messageProof, err := prover.Prove(ctx, transfer.Raw.TxHash) + if err != nil { + return nil, fmt.Errorf("prove message: %w", err) + } + lggr.Infow("Calling proveWithdrawalTransaction on bridge adapter", "nonce", messageProof.LowLevelMessage.Nonce, + "sender", messageProof.LowLevelMessage.Sender.String(), + "target", messageProof.LowLevelMessage.Target.String(), + "value", messageProof.LowLevelMessage.Value.String(), + "gasLimit", messageProof.LowLevelMessage.GasLimit.String(), + "data", hexutil.Encode(messageProof.LowLevelMessage.Data), + "l2OutputIndex", messageProof.L2OutputIndex, + "outputRootProof version", hexutil.Encode(messageProof.OutputRootProof.Version[:]), + "outputRootProof stateRoot", hexutil.Encode(messageProof.OutputRootProof.StateRoot[:]), + "outputRootProof messagePasserStorageRoot", hexutil.Encode(messageProof.OutputRootProof.MessagePasserStorageRoot[:]), + "outputRootProof latestBlockHash", hexutil.Encode(messageProof.OutputRootProof.LatestBlockHash[:]), + "withdrawalProof", formatWithdrawalProof(messageProof.WithdrawalProof)) + + encodedPayload, err := withdrawprover.EncodeProveWithdrawalPayload(l1OPBridgeAdapterEncoderABI, messageProof) + if err != nil { + return nil, fmt.Errorf("EncodeProveWithdrawalPayload: %w", err) + } + + return encodedPayload, nil +} + +func (l *l2ToL1Bridge) generateTransferBridgeDataForFinalize( + ctx context.Context, + transfer *liquiditymanager.LiquidityManagerLiquidityTransferred, +) ([]byte, error) { + receipt, err := l.l2Client.TransactionReceipt(ctx, transfer.Raw.TxHash) + if err != nil { + return nil, fmt.Errorf("get transaction receipt: %w", err) + } + + messagePassedLog := withdrawprover.GetMessagePassedLog(receipt.Logs) + if messagePassedLog == nil { + panic(fmt.Sprintf("No message passed log found in receipt %s", receipt.TxHash.String())) + } + + messagePassed, err := withdrawprover.ParseMessagePassedLog(messagePassedLog) + if err != nil { + return nil, fmt.Errorf("parse message passed log: %w", err) + } + + encodedFinalizeWithdrawal, err := withdrawprover.EncodeFinalizeWithdrawalPayload(l1OPBridgeAdapterEncoderABI, messagePassed) + if err != nil { + return nil, fmt.Errorf("EncodeFinalizeWithdrawalPayload: %w", err) + } + + return encodedFinalizeWithdrawal, nil +} + +// GetBridgePayloadAndFee implements bridge.Bridge. +func (l *l2ToL1Bridge) GetBridgePayloadAndFee( + _ context.Context, + _ models.Transfer, +) ([]byte, *big.Int, error) { + // Optimism L2 to L1 transfers require no bridge specific payload. + return []byte{}, big.NewInt(0), nil +} + +// QuorumizedBridgePayload implements bridge.Bridge. +func (l *l2ToL1Bridge) QuorumizedBridgePayload(_ [][]byte, _ int) ([]byte, error) { + // Optimism L2 to L1 transfers require no bridge specific payload. + return []byte{}, nil +} + +// Close implements bridge.Bridge. +func (l *l2ToL1Bridge) Close(ctx context.Context) error { + return multierr.Combine( + l.l2LogPoller.UnregisterFilter(ctx, l.l2FilterName), + l.l1LogPoller.UnregisterFilter(ctx, l.l1FilterName), + ) +} + +func formatWithdrawalProof(proof [][]byte) string { + var builder strings.Builder + builder.WriteString("{") + for i, p := range proof { + builder.WriteString(hexutil.Encode(p)) + if i < len(proof)-1 { + builder.WriteString(", ") + } + } + builder.WriteString("}") + return builder.String() +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1_test.go new file mode 100644 index 0000000000..d001eb2029 --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/l2_to_l1_test.go @@ -0,0 +1,371 @@ +package opstack + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + lpmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/models" + + "github.com/stretchr/testify/mock" +) + +func Test_L2ToL1Bridge_partitionTransfers(t *testing.T) { + l1LiquidityManagerAddress := common.HexToAddress("0xabc") + l1ChainSelector := chainsel.ETHEREUM_MAINNET.Selector + l2ChainSelector := chainsel.ETHEREUM_MAINNET_OPTIMISM_1.Selector + + type args struct { + localSelector models.NetworkSelector + l1LiquidityManagerAddress common.Address + sentLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred + proveFinalizationStepLogs []*liquiditymanager.LiquidityManagerFinalizationStepCompleted + receivedLogs []*liquiditymanager.LiquidityManagerLiquidityTransferred + } + tests := []struct { + name string + args args + wantNeedsToBeProven []*liquiditymanager.LiquidityManagerLiquidityTransferred + wantNeedsToBeFinalized []*liquiditymanager.LiquidityManagerLiquidityTransferred + wantMissingSent []*liquiditymanager.LiquidityManagerFinalizationStepCompleted + wantErr bool + }{ + { + name: "empty", + args: args{ + localSelector: models.NetworkSelector(uint64(0)), + sentLogs: nil, + proveFinalizationStepLogs: nil, + receivedLogs: nil, + }, + wantNeedsToBeProven: nil, + wantNeedsToBeFinalized: nil, + wantMissingSent: nil, + wantErr: false, + }, + { + name: "happy path - one to be proven, one to be finalized, one missing sent, one done", + args: args{ + localSelector: models.NetworkSelector(l2ChainSelector), + l1LiquidityManagerAddress: l1LiquidityManagerAddress, + sentLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // This one needs to be proven + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(4), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000004"), + BridgeSpecificData: []byte{}, + }, + // This one needs to be finalized + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(7), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000005"), + BridgeSpecificData: []byte{}, + }, + // This one is done/already received (present in sent logs, proven logs, and received logs), should not be included in any output + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(10), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000006"), + BridgeSpecificData: []byte{}, + }, + }, + proveFinalizationStepLogs: []*liquiditymanager.LiquidityManagerFinalizationStepCompleted{ + // This one is ready to be finalized + { + OcrSeqNum: uint64(1), + RemoteChainSelector: l2ChainSelector, + // prove logs from the 0x0...5 nonce (amount=7) + BridgeSpecificData: mustConvertHexBridgeSpecificDataToBytes(t, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001b8c0000000000000000000000000000000000000000000000000000000000000000debdfbead9da4e4e96744bb44a697fb9b5db11acd7bd6a2c78d58face684206970c451d3ddd756858ee282bcec7c3923065ba3b2988e91949025e6b4e77487de332e3e384bb4ce3a6fc7978d0dfc5945341c1473744ad3a41635fbd1fb4b9c6100000000000000000000000000000000000000000000000000000000000003e00001000000000000000000000000000000000000000000000000000000000dd7000000000000000000000000420000000000000000000000000000000000000700000000000000000000000058cc85b8d04ea49cc6dbd3cbffd00b4b8d6cb3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004698800000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000204d764ad0b0001000000000000000000000000000000000000000000000000000000000d590000000000000000000000004200000000000000000000000000000000000010000000000000000000000000fbb0621e0b23b5478b630bd55a5f21f67730b0f10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001040166a07a0000000000000000000000005589bb8228c07c4e15558875faf2b859f678d129000000000000000000000000d08a2917653d4e460893203471f0000826fb4034000000000000000000000000fb023f4edb2aa1ebbcc07970f9c0541071713445000000000000000000000000e87704d3f0dd040fd2a49c3606043dd6fc05bf33000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000007400000000000000000000000000000000000000000000000000000000000000214f90211a0c37d9a90a47f430d6d54dabcbd2f364fdec4bbefa75aa69eab8f3cf50026de73a0a399109fd13eb2ebb25f5382c7bbe1a3d4c6433968ca9f19b78a0871e0cbf712a025225c15784b8550f2fb4d8257db8a352453c05b81013dc4a8f5fdfde94b2043a0b1fdf531766da35c16cae24330ec921abe5f7f950396fec11deea83db82346a4a0117a1e2d227b113cddb855f2af8fbed60661d0cb71e2ac0474f405ad359232f8a08a05951056d4a4e2e4ca1109d9c345aeaaf28f9be4a2a810b5652e69518ca609a02900527859b85e1008d498194a583a3effad975638925b6a1c6a8ff38ae2e804a0d466461fe5b2004b72cc68f4013ea3d2e6fc49b0b5598ff091724ed3a86fc2bfa0863849da3e275da0a24c159f0ec0c2bb2c1024fd254492f6a7f3abefff96a77ea0ed4a2b060237d351dd851375674b6dfb3d7a864281b1105b6a602c8f989962fda07e250d776e741b434bffd936ba18f8ea9c9f359672e80d93d3128bf060605ec1a08ff7943dae97c26473ac8062323fe14a28b33a892c710ef769a7ddbd1d13b2d8a0bb62fef18ac02456958d40d12353c27db1aca9b90edfe8a4de910e0e8cf27d9ba088e3c60cbf0b6f0ad69730ad3bf23825d8f9f8037544069669dfc3ae3a39f826a02cfaadf653fc6cc9bc182e8ae4a6f7736ab1b01363444aa65e8c1182d4fd8e7ea08a6919cd1b827d7e346cd9f5aa269c01e5179e3a79e9e050fcb1c34028d236e8800000000000000000000000000000000000000000000000000000000000000000000000000000000000000214f90211a0d83a5b715cffa9ff815a9cbede60d80e380c0d8b3f8433c1cfd9f9332ca96fcfa08f3d6be3e11f4a8d26b7b95f526c5997db678b7ade30fc4476f3b62513f636a0a00bd2482ce34a4dea248ad239a79f04082a73549cf115a2f9ef8b59f3a0ab32bea00bb41ca6732f1a882e39fd10cd1e46dcc524ad968bc19d35c20efcbc4d53844aa0d6bb21815ed32081c3987cb8d7f4b82971e856f10cf016f0f200c7481bc8e72da05eb07b5cd5ac9db7b8b112f21b68c911bbc8267bfaf715687a3c38fa520e6dd5a0651a7362fdc70b0dc0482f18f06aeb22c8b0a6025415680989f6c1fe8abad4f7a0cd2f2124bdc93e740643df5e0fd6c44c8d2fbb160cb31ae1379124af6185b3f0a0188c44d5164eff07559f80590db1b7b71dd6bd2caa8535ebaa917beb51777016a0838fb57029d1dcd8e8bdffe2c415631f6117d512cd1218754c1176ee1cf9e234a077a2600394a2b1ff56a36442b788589a4ebf2f72754b70f21a137c3a8a9a4593a01314e5e6eebd982c60dcc670e1e33f99304aee763578785b10ec7633c2d17c84a0d9b11242125a529dc046bec59277320e1ef2f73b83d4189afe0ab2a5e1db95efa0b40124e9a39f63fcbd93f2f2143ff5b2b9f336d5653dc55debded322cb555c44a0fd4817897049284c3fb2395c57c53e6254820a7c466e37ec409f93b5391db355a05dcf85c54422dd72a6d74b4f730a156290b290720164bd81f516f2f1118392d4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000154f90151a01f1a153520d71c7a9dee923b4f1a30c09f49f57e930d62072b80aa99460d9440a070d2af65cf4b7972e7bf9443f0fe82ff29225a48071ac6ebe1fbc0ae49356642a0943b13b0b2608c1e772c599809292c4079c16d90f9f1e7d69f8925102d8c1feba0593648a0c1d2c819da4fc25f8c38beac96bc8b25533c7581e2117c911254d8f48080a0a45d531264251deba88eb85c03e1826daddbb0b0bce37165e71d9236af892fc08080a04e425050083943d27aa01fdb706b5715693c18ed9ff8ef050fcd0c2ea292687aa09b9a3d00ec6310bd2f5714e5c60c7c597128add8087e18986dc18d716c6d011780a0fcf815ea12a633987173946bf59a53d93c461c71e47b3eee7cb07885027288f4a093b3dc885d64ad3421e67bf396530d92268a6ea17fe536e75f3d45ff2d6f034b80a0d3f01d4772d6326d75fad188647163487f5c5df4b809e33fffe344262fcaa398800000000000000000000000000000000000000000000000000000000000000000000000000000000000000073f8718080808080808080a0bb626d1fc2d928384c17de477f7b7170c65267b016976bc21305ebf39fc1543ca0f3ea5fd76e00ddbf83625e203d99441c86c44557e86896aad4ec87a4683bdf618080a0afebeaa9674e4a195459974044cf4da821d72aff3654d6d70b14ce90075c13f980808080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e19f20aeeb5205bebd523c696a4b099033965d1bcb4af41ce5f3070145a8debd6601000000000000000000000000000000000000000000000000000000000000"), + }, + // This one is done/already received + { + OcrSeqNum: uint64(2), + RemoteChainSelector: l2ChainSelector, + // prove logs from the 0x0...6 nonce (amount=10) + BridgeSpecificData: mustConvertHexBridgeSpecificDataToBytes(t, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000b40000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001b8c0000000000000000000000000000000000000000000000000000000000000000debdfbead9da4e4e96744bb44a697fb9b5db11acd7bd6a2c78d58face684206970c451d3ddd756858ee282bcec7c3923065ba3b2988e91949025e6b4e77487de332e3e384bb4ce3a6fc7978d0dfc5945341c1473744ad3a41635fbd1fb4b9c6100000000000000000000000000000000000000000000000000000000000003e00001000000000000000000000000000000000000000000000000000000000dd8000000000000000000000000420000000000000000000000000000000000000700000000000000000000000058cc85b8d04ea49cc6dbd3cbffd00b4b8d6cb3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004698800000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000204d764ad0b0001000000000000000000000000000000000000000000000000000000000d5a0000000000000000000000004200000000000000000000000000000000000010000000000000000000000000fbb0621e0b23b5478b630bd55a5f21f67730b0f10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001040166a07a0000000000000000000000005589bb8228c07c4e15558875faf2b859f678d129000000000000000000000000d08a2917653d4e460893203471f0000826fb4034000000000000000000000000fb023f4edb2aa1ebbcc07970f9c0541071713445000000000000000000000000e87704d3f0dd040fd2a49c3606043dd6fc05bf33000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000000000000000000000000000000000000000000214f90211a0c37d9a90a47f430d6d54dabcbd2f364fdec4bbefa75aa69eab8f3cf50026de73a0a399109fd13eb2ebb25f5382c7bbe1a3d4c6433968ca9f19b78a0871e0cbf712a025225c15784b8550f2fb4d8257db8a352453c05b81013dc4a8f5fdfde94b2043a0b1fdf531766da35c16cae24330ec921abe5f7f950396fec11deea83db82346a4a0117a1e2d227b113cddb855f2af8fbed60661d0cb71e2ac0474f405ad359232f8a08a05951056d4a4e2e4ca1109d9c345aeaaf28f9be4a2a810b5652e69518ca609a02900527859b85e1008d498194a583a3effad975638925b6a1c6a8ff38ae2e804a0d466461fe5b2004b72cc68f4013ea3d2e6fc49b0b5598ff091724ed3a86fc2bfa0863849da3e275da0a24c159f0ec0c2bb2c1024fd254492f6a7f3abefff96a77ea0ed4a2b060237d351dd851375674b6dfb3d7a864281b1105b6a602c8f989962fda07e250d776e741b434bffd936ba18f8ea9c9f359672e80d93d3128bf060605ec1a08ff7943dae97c26473ac8062323fe14a28b33a892c710ef769a7ddbd1d13b2d8a0bb62fef18ac02456958d40d12353c27db1aca9b90edfe8a4de910e0e8cf27d9ba088e3c60cbf0b6f0ad69730ad3bf23825d8f9f8037544069669dfc3ae3a39f826a02cfaadf653fc6cc9bc182e8ae4a6f7736ab1b01363444aa65e8c1182d4fd8e7ea08a6919cd1b827d7e346cd9f5aa269c01e5179e3a79e9e050fcb1c34028d236e8800000000000000000000000000000000000000000000000000000000000000000000000000000000000000214f90211a050a1a407e76cf8932498e5c8a03f7d3ce9f7db1cb88489482b2eea389d3df548a0ab20926cf02651c8b90cb2b9b4b22804c7e8fe2d6c7be438b81ec442c21f0d31a0768e2e35089267f2334ecbb13f49e07e59a062c29696c674cdd4e32d5dcd7a17a00c662583b98d019310f01ca016aa2d352c803b2207ce01a48f4179e5ab72d6dba0c1373828efc2d5325b71fc2a194c038098cdeb9e59d6ebad7c20664a9e627061a0aa98badb806f96bb55ee46d01edb661b304913fb876a9708efdf7664a631cde8a00f05546016560ce9b05ee583b4d97b15fc5b7ab5ec56de37e299be381767c3aba005941b0ad5b606fdca4cae738efbf2daf029e63d3d4a5bfb81c550a99b073968a052db10435032fa673fd56943db628ce658dfd053ab4022b5d8f48d5e54994bf4a0ed2df687867bfb62f41273c9e0e317f0da20c6595eb16d0b9dea48c70e0b3e06a03bc54d1680436348eb3506d51dfb4cd0e9c9d39f1366530e111e96d369b2322aa06ec4829d91f0cc40a45ee77a963c3730dd3b42ce4d99beec108b97760bf1ead8a092f6bc5d2c4072d22f33a13b45a721cab92eb56909dcbd19f389fbcf189b6edba013731d52dfc35f4618b37b283cb56a09d35bcf2d89f84ef81460a406780e6308a0faeca47f31cb0ddcd7da6df3e12bfce63cb3187c40fd33a090d4866d6fb13e2ba0f0ac4d51c4c48907eb0d891b1c4f99e69107b76c5a051a089c08d2000b16d34f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000194f90191a06b2d6a0a36e2097af8ac6d861fae2fb81da5b1dc4553ba982d6a71f9210e8974a00e791535b1da1445dfa7de9981297de0e2639b5ca7dc13aabbb161a436e516d3a06b7af11ff178474bf69cd8e146038a51e75cada9fe34385b9ecfd33932e872bca0621657da81c2062813821315378490c32be9ef39515d0f4d55b6cdffb2cbb4bba0316b2a623c5088749c2bd529059478298a1679d9a8a41e01f7d9b0c24f5bdfd480a005fe24b494030bfedbdd124afe76081090e6e0775ea7266020ff3bae70969f8aa04f947b0b21110b7f4e53d62590787965fe43ef305af4d84d7965d3f669df8c3480a030e5de29740af91c19a24371f3a69fe37f9c7052a76698d64ff01f71561db388a0dced5f59e458fc0e2e0ff23ab31c430c69a0ba173cccd3618b4a520481c15818a06545ca38717113fa3435e643143d8a7043bdbfeb23a47c87218496f2bb03faeba054aa7606fea02cd36637ab236b286c7615c7658aa8d66fa119f3454bd8200fb680a00e7816c71cdddaf4ee29e022b778522a47e28944e1e8e3751a1663e613b00d6480800000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e19f3005eb0f046d71740f0d3530fc1e102351275c0840f962572e24c54ad9cd9c01000000000000000000000000000000000000000000000000000000000000"), + }, + // This one is missing (not present in sent logs) + { + OcrSeqNum: uint64(3), + RemoteChainSelector: l2ChainSelector, + BridgeSpecificData: mustConvertHexBridgeSpecificDataToBytes(t, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001b8c0000000000000000000000000000000000000000000000000000000000000000debdfbead9da4e4e96744bb44a697fb9b5db11acd7bd6a2c78d58face684206970c451d3ddd756858ee282bcec7c3923065ba3b2988e91949025e6b4e77487de332e3e384bb4ce3a6fc7978d0dfc5945341c1473744ad3a41635fbd1fb4b9c6100000000000000000000000000000000000000000000000000000000000003e00001000000000000000000000000000000000000000000000000000000000dd7000000000000000000000000420000000000000000000000000000000000000700000000000000000000000058cc85b8d04ea49cc6dbd3cbffd00b4b8d6cb3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004698800000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000204d764ad0b0001000000000000000000000000000000000000000000000000000000000d590000000000000000000000004200000000000000000000000000000000000010000000000000000000000000fbb0621e0b23b5478b630bd55a5f21f67730b0f10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001040166a07a0000000000000000000000005589bb8228c07c4e15558875faf2b859f678d129000000000000000000000000d08a2917653d4e460893203471f0000826fb4034000000000000000000000000fb023f4edb2aa1ebbcc07970f9c0541071713445000000000000000000000000e87704d3f0dd040fd2a49c3606043dd6fc05bf33000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000007400000000000000000000000000000000000000000000000000000000000000214f90211a0c37d9a90a47f430d6d54dabcbd2f364fdec4bbefa75aa69eab8f3cf50026de73a0a399109fd13eb2ebb25f5382c7bbe1a3d4c6433968ca9f19b78a0871e0cbf712a025225c15784b8550f2fb4d8257db8a352453c05b81013dc4a8f5fdfde94b2043a0b1fdf531766da35c16cae24330ec921abe5f7f950396fec11deea83db82346a4a0117a1e2d227b113cddb855f2af8fbed60661d0cb71e2ac0474f405ad359232f8a08a05951056d4a4e2e4ca1109d9c345aeaaf28f9be4a2a810b5652e69518ca609a02900527859b85e1008d498194a583a3effad975638925b6a1c6a8ff38ae2e804a0d466461fe5b2004b72cc68f4013ea3d2e6fc49b0b5598ff091724ed3a86fc2bfa0863849da3e275da0a24c159f0ec0c2bb2c1024fd254492f6a7f3abefff96a77ea0ed4a2b060237d351dd851375674b6dfb3d7a864281b1105b6a602c8f989962fda07e250d776e741b434bffd936ba18f8ea9c9f359672e80d93d3128bf060605ec1a08ff7943dae97c26473ac8062323fe14a28b33a892c710ef769a7ddbd1d13b2d8a0bb62fef18ac02456958d40d12353c27db1aca9b90edfe8a4de910e0e8cf27d9ba088e3c60cbf0b6f0ad69730ad3bf23825d8f9f8037544069669dfc3ae3a39f826a02cfaadf653fc6cc9bc182e8ae4a6f7736ab1b01363444aa65e8c1182d4fd8e7ea08a6919cd1b827d7e346cd9f5aa269c01e5179e3a79e9e050fcb1c34028d236e8800000000000000000000000000000000000000000000000000000000000000000000000000000000000000214f90211a0d83a5b715cffa9ff815a9cbede60d80e380c0d8b3f8433c1cfd9f9332ca96fcfa08f3d6be3e11f4a8d26b7b95f526c5997db678b7ade30fc4476f3b62513f636a0a00bd2482ce34a4dea248ad239a79f04082a73549cf115a2f9ef8b59f3a0ab32bea00bb41ca6732f1a882e39fd10cd1e46dcc524ad968bc19d35c20efcbc4d53844aa0d6bb21815ed32081c3987cb8d7f4b82971e856f10cf016f0f200c7481bc8e72da05eb07b5cd5ac9db7b8b112f21b68c911bbc8267bfaf715687a3c38fa520e6dd5a0651a7362fdc70b0dc0482f18f06aeb22c8b0a6025415680989f6c1fe8abad4f7a0cd2f2124bdc93e740643df5e0fd6c44c8d2fbb160cb31ae1379124af6185b3f0a0188c44d5164eff07559f80590db1b7b71dd6bd2caa8535ebaa917beb51777016a0838fb57029d1dcd8e8bdffe2c415631f6117d512cd1218754c1176ee1cf9e234a077a2600394a2b1ff56a36442b788589a4ebf2f72754b70f21a137c3a8a9a4593a01314e5e6eebd982c60dcc670e1e33f99304aee763578785b10ec7633c2d17c84a0d9b11242125a529dc046bec59277320e1ef2f73b83d4189afe0ab2a5e1db95efa0b40124e9a39f63fcbd93f2f2143ff5b2b9f336d5653dc55debded322cb555c44a0fd4817897049284c3fb2395c57c53e6254820a7c466e37ec409f93b5391db355a05dcf85c54422dd72a6d74b4f730a156290b290720164bd81f516f2f1118392d4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000154f90151a01f1a153520d71c7a9dee923b4f1a30c09f49f57e930d62072b80aa99460d9440a070d2af65cf4b7972e7bf9443f0fe82ff29225a48071ac6ebe1fbc0ae49356642a0943b13b0b2608c1e772c599809292c4079c16d90f9f1e7d69f8925102d8c1feba0593648a0c1d2c819da4fc25f8c38beac96bc8b25533c7581e2117c911254d8f48080a0a45d531264251deba88eb85c03e1826daddbb0b0bce37165e71d9236af892fc08080a04e425050083943d27aa01fdb706b5715693c18ed9ff8ef050fcd0c2ea292687aa09b9a3d00ec6310bd2f5714e5c60c7c597128add8087e18986dc18d716c6d011780a0fcf815ea12a633987173946bf59a53d93c461c71e47b3eee7cb07885027288f4a093b3dc885d64ad3421e67bf396530d92268a6ea17fe536e75f3d45ff2d6f034b80a0d3f01d4772d6326d75fad188647163487f5c5df4b809e33fffe344262fcaa398800000000000000000000000000000000000000000000000000000000000000000000000000000000000000073f8718080808080808080a0bb626d1fc2d928384c17de477f7b7170c65267b016976bc21305ebf39fc1543ca0f3ea5fd76e00ddbf83625e203d99441c86c44557e86896aad4ec87a4683bdf618080a0afebeaa9674e4a195459974044cf4da821d72aff3654d6d70b14ce90075c13f980808080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e19f20aeeb5205bebd523c696a4b099033965d1bcb4af41ce5f3070145a8debd6601000000000000000000000000000000000000000000000000000000000000"), + }, + }, + receivedLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(10), + BridgeReturnData: []byte{}, + // NOTE: remember, the nonce is in bridgeSpecificData in the received logs instead of bridgeReturnData + BridgeSpecificData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000006"), + }, + }, + }, + wantNeedsToBeProven: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(4), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000004"), + BridgeSpecificData: []byte{}, + }, + }, + wantNeedsToBeFinalized: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(7), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000005"), + BridgeSpecificData: []byte{}, + }, + }, + wantMissingSent: []*liquiditymanager.LiquidityManagerFinalizationStepCompleted{ + { + OcrSeqNum: uint64(3), + RemoteChainSelector: l2ChainSelector, + BridgeSpecificData: mustConvertHexBridgeSpecificDataToBytes(t, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001b8c0000000000000000000000000000000000000000000000000000000000000000debdfbead9da4e4e96744bb44a697fb9b5db11acd7bd6a2c78d58face684206970c451d3ddd756858ee282bcec7c3923065ba3b2988e91949025e6b4e77487de332e3e384bb4ce3a6fc7978d0dfc5945341c1473744ad3a41635fbd1fb4b9c6100000000000000000000000000000000000000000000000000000000000003e00001000000000000000000000000000000000000000000000000000000000dd7000000000000000000000000420000000000000000000000000000000000000700000000000000000000000058cc85b8d04ea49cc6dbd3cbffd00b4b8d6cb3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004698800000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000204d764ad0b0001000000000000000000000000000000000000000000000000000000000d590000000000000000000000004200000000000000000000000000000000000010000000000000000000000000fbb0621e0b23b5478b630bd55a5f21f67730b0f10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001040166a07a0000000000000000000000005589bb8228c07c4e15558875faf2b859f678d129000000000000000000000000d08a2917653d4e460893203471f0000826fb4034000000000000000000000000fb023f4edb2aa1ebbcc07970f9c0541071713445000000000000000000000000e87704d3f0dd040fd2a49c3606043dd6fc05bf33000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000007400000000000000000000000000000000000000000000000000000000000000214f90211a0c37d9a90a47f430d6d54dabcbd2f364fdec4bbefa75aa69eab8f3cf50026de73a0a399109fd13eb2ebb25f5382c7bbe1a3d4c6433968ca9f19b78a0871e0cbf712a025225c15784b8550f2fb4d8257db8a352453c05b81013dc4a8f5fdfde94b2043a0b1fdf531766da35c16cae24330ec921abe5f7f950396fec11deea83db82346a4a0117a1e2d227b113cddb855f2af8fbed60661d0cb71e2ac0474f405ad359232f8a08a05951056d4a4e2e4ca1109d9c345aeaaf28f9be4a2a810b5652e69518ca609a02900527859b85e1008d498194a583a3effad975638925b6a1c6a8ff38ae2e804a0d466461fe5b2004b72cc68f4013ea3d2e6fc49b0b5598ff091724ed3a86fc2bfa0863849da3e275da0a24c159f0ec0c2bb2c1024fd254492f6a7f3abefff96a77ea0ed4a2b060237d351dd851375674b6dfb3d7a864281b1105b6a602c8f989962fda07e250d776e741b434bffd936ba18f8ea9c9f359672e80d93d3128bf060605ec1a08ff7943dae97c26473ac8062323fe14a28b33a892c710ef769a7ddbd1d13b2d8a0bb62fef18ac02456958d40d12353c27db1aca9b90edfe8a4de910e0e8cf27d9ba088e3c60cbf0b6f0ad69730ad3bf23825d8f9f8037544069669dfc3ae3a39f826a02cfaadf653fc6cc9bc182e8ae4a6f7736ab1b01363444aa65e8c1182d4fd8e7ea08a6919cd1b827d7e346cd9f5aa269c01e5179e3a79e9e050fcb1c34028d236e8800000000000000000000000000000000000000000000000000000000000000000000000000000000000000214f90211a0d83a5b715cffa9ff815a9cbede60d80e380c0d8b3f8433c1cfd9f9332ca96fcfa08f3d6be3e11f4a8d26b7b95f526c5997db678b7ade30fc4476f3b62513f636a0a00bd2482ce34a4dea248ad239a79f04082a73549cf115a2f9ef8b59f3a0ab32bea00bb41ca6732f1a882e39fd10cd1e46dcc524ad968bc19d35c20efcbc4d53844aa0d6bb21815ed32081c3987cb8d7f4b82971e856f10cf016f0f200c7481bc8e72da05eb07b5cd5ac9db7b8b112f21b68c911bbc8267bfaf715687a3c38fa520e6dd5a0651a7362fdc70b0dc0482f18f06aeb22c8b0a6025415680989f6c1fe8abad4f7a0cd2f2124bdc93e740643df5e0fd6c44c8d2fbb160cb31ae1379124af6185b3f0a0188c44d5164eff07559f80590db1b7b71dd6bd2caa8535ebaa917beb51777016a0838fb57029d1dcd8e8bdffe2c415631f6117d512cd1218754c1176ee1cf9e234a077a2600394a2b1ff56a36442b788589a4ebf2f72754b70f21a137c3a8a9a4593a01314e5e6eebd982c60dcc670e1e33f99304aee763578785b10ec7633c2d17c84a0d9b11242125a529dc046bec59277320e1ef2f73b83d4189afe0ab2a5e1db95efa0b40124e9a39f63fcbd93f2f2143ff5b2b9f336d5653dc55debded322cb555c44a0fd4817897049284c3fb2395c57c53e6254820a7c466e37ec409f93b5391db355a05dcf85c54422dd72a6d74b4f730a156290b290720164bd81f516f2f1118392d4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000154f90151a01f1a153520d71c7a9dee923b4f1a30c09f49f57e930d62072b80aa99460d9440a070d2af65cf4b7972e7bf9443f0fe82ff29225a48071ac6ebe1fbc0ae49356642a0943b13b0b2608c1e772c599809292c4079c16d90f9f1e7d69f8925102d8c1feba0593648a0c1d2c819da4fc25f8c38beac96bc8b25533c7581e2117c911254d8f48080a0a45d531264251deba88eb85c03e1826daddbb0b0bce37165e71d9236af892fc08080a04e425050083943d27aa01fdb706b5715693c18ed9ff8ef050fcd0c2ea292687aa09b9a3d00ec6310bd2f5714e5c60c7c597128add8087e18986dc18d716c6d011780a0fcf815ea12a633987173946bf59a53d93c461c71e47b3eee7cb07885027288f4a093b3dc885d64ad3421e67bf396530d92268a6ea17fe536e75f3d45ff2d6f034b80a0d3f01d4772d6326d75fad188647163487f5c5df4b809e33fffe344262fcaa398800000000000000000000000000000000000000000000000000000000000000000000000000000000000000073f8718080808080808080a0bb626d1fc2d928384c17de477f7b7170c65267b016976bc21305ebf39fc1543ca0f3ea5fd76e00ddbf83625e203d99441c86c44557e86896aad4ec87a4683bdf618080a0afebeaa9674e4a195459974044cf4da821d72aff3654d6d70b14ce90075c13f980808080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e19f20aeeb5205bebd523c696a4b099033965d1bcb4af41ce5f3070145a8debd6601000000000000000000000000000000000000000000000000000000000000"), + }, + }, + wantErr: false, + }, + { + name: "sent logs - one valid log, two invalid logs with incorrect To and FromChainSelector fields", + args: args{ + localSelector: models.NetworkSelector(l2ChainSelector), + l1LiquidityManagerAddress: l1LiquidityManagerAddress, + sentLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + // This one is valid and needs to be proven + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(4), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000004"), + BridgeSpecificData: []byte{}, + }, + // This one has an invalid FromChainSelector + { + FromChainSelector: uint64(909090), // Non-existent chain selector + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(7), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000005"), + BridgeSpecificData: []byte{}, + }, + // This one has an invalid To field + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: common.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead"), + Amount: big.NewInt(10), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000006"), + BridgeSpecificData: []byte{}, + }, + }, + proveFinalizationStepLogs: nil, + receivedLogs: nil, + }, + wantNeedsToBeProven: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(4), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000004"), + BridgeSpecificData: []byte{}, + }, + }, + wantNeedsToBeFinalized: nil, + wantMissingSent: nil, + wantErr: false, + }, + { + name: "prove finalization step logs - invalid remote chain selector", + args: args{ + localSelector: models.NetworkSelector(l2ChainSelector), + l1LiquidityManagerAddress: l1LiquidityManagerAddress, + sentLogs: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(7), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000005"), + BridgeSpecificData: []byte{}, + }, + }, + proveFinalizationStepLogs: []*liquiditymanager.LiquidityManagerFinalizationStepCompleted{ + // This one would have been ready to be finalized, but it has an invalid remote chain selector + { + OcrSeqNum: uint64(1), + RemoteChainSelector: uint64(909090), // Non-existent chain selector + // prove logs from the 0x0...5 nonce (amount=7) + BridgeSpecificData: mustConvertHexBridgeSpecificDataToBytes(t, "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000bc0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000001b8c0000000000000000000000000000000000000000000000000000000000000000debdfbead9da4e4e96744bb44a697fb9b5db11acd7bd6a2c78d58face684206970c451d3ddd756858ee282bcec7c3923065ba3b2988e91949025e6b4e77487de332e3e384bb4ce3a6fc7978d0dfc5945341c1473744ad3a41635fbd1fb4b9c6100000000000000000000000000000000000000000000000000000000000003e00001000000000000000000000000000000000000000000000000000000000dd7000000000000000000000000420000000000000000000000000000000000000700000000000000000000000058cc85b8d04ea49cc6dbd3cbffd00b4b8d6cb3ef0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004698800000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000204d764ad0b0001000000000000000000000000000000000000000000000000000000000d590000000000000000000000004200000000000000000000000000000000000010000000000000000000000000fbb0621e0b23b5478b630bd55a5f21f67730b0f10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001040166a07a0000000000000000000000005589bb8228c07c4e15558875faf2b859f678d129000000000000000000000000d08a2917653d4e460893203471f0000826fb4034000000000000000000000000fb023f4edb2aa1ebbcc07970f9c0541071713445000000000000000000000000e87704d3f0dd040fd2a49c3606043dd6fc05bf33000000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000007400000000000000000000000000000000000000000000000000000000000000214f90211a0c37d9a90a47f430d6d54dabcbd2f364fdec4bbefa75aa69eab8f3cf50026de73a0a399109fd13eb2ebb25f5382c7bbe1a3d4c6433968ca9f19b78a0871e0cbf712a025225c15784b8550f2fb4d8257db8a352453c05b81013dc4a8f5fdfde94b2043a0b1fdf531766da35c16cae24330ec921abe5f7f950396fec11deea83db82346a4a0117a1e2d227b113cddb855f2af8fbed60661d0cb71e2ac0474f405ad359232f8a08a05951056d4a4e2e4ca1109d9c345aeaaf28f9be4a2a810b5652e69518ca609a02900527859b85e1008d498194a583a3effad975638925b6a1c6a8ff38ae2e804a0d466461fe5b2004b72cc68f4013ea3d2e6fc49b0b5598ff091724ed3a86fc2bfa0863849da3e275da0a24c159f0ec0c2bb2c1024fd254492f6a7f3abefff96a77ea0ed4a2b060237d351dd851375674b6dfb3d7a864281b1105b6a602c8f989962fda07e250d776e741b434bffd936ba18f8ea9c9f359672e80d93d3128bf060605ec1a08ff7943dae97c26473ac8062323fe14a28b33a892c710ef769a7ddbd1d13b2d8a0bb62fef18ac02456958d40d12353c27db1aca9b90edfe8a4de910e0e8cf27d9ba088e3c60cbf0b6f0ad69730ad3bf23825d8f9f8037544069669dfc3ae3a39f826a02cfaadf653fc6cc9bc182e8ae4a6f7736ab1b01363444aa65e8c1182d4fd8e7ea08a6919cd1b827d7e346cd9f5aa269c01e5179e3a79e9e050fcb1c34028d236e8800000000000000000000000000000000000000000000000000000000000000000000000000000000000000214f90211a0d83a5b715cffa9ff815a9cbede60d80e380c0d8b3f8433c1cfd9f9332ca96fcfa08f3d6be3e11f4a8d26b7b95f526c5997db678b7ade30fc4476f3b62513f636a0a00bd2482ce34a4dea248ad239a79f04082a73549cf115a2f9ef8b59f3a0ab32bea00bb41ca6732f1a882e39fd10cd1e46dcc524ad968bc19d35c20efcbc4d53844aa0d6bb21815ed32081c3987cb8d7f4b82971e856f10cf016f0f200c7481bc8e72da05eb07b5cd5ac9db7b8b112f21b68c911bbc8267bfaf715687a3c38fa520e6dd5a0651a7362fdc70b0dc0482f18f06aeb22c8b0a6025415680989f6c1fe8abad4f7a0cd2f2124bdc93e740643df5e0fd6c44c8d2fbb160cb31ae1379124af6185b3f0a0188c44d5164eff07559f80590db1b7b71dd6bd2caa8535ebaa917beb51777016a0838fb57029d1dcd8e8bdffe2c415631f6117d512cd1218754c1176ee1cf9e234a077a2600394a2b1ff56a36442b788589a4ebf2f72754b70f21a137c3a8a9a4593a01314e5e6eebd982c60dcc670e1e33f99304aee763578785b10ec7633c2d17c84a0d9b11242125a529dc046bec59277320e1ef2f73b83d4189afe0ab2a5e1db95efa0b40124e9a39f63fcbd93f2f2143ff5b2b9f336d5653dc55debded322cb555c44a0fd4817897049284c3fb2395c57c53e6254820a7c466e37ec409f93b5391db355a05dcf85c54422dd72a6d74b4f730a156290b290720164bd81f516f2f1118392d4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000154f90151a01f1a153520d71c7a9dee923b4f1a30c09f49f57e930d62072b80aa99460d9440a070d2af65cf4b7972e7bf9443f0fe82ff29225a48071ac6ebe1fbc0ae49356642a0943b13b0b2608c1e772c599809292c4079c16d90f9f1e7d69f8925102d8c1feba0593648a0c1d2c819da4fc25f8c38beac96bc8b25533c7581e2117c911254d8f48080a0a45d531264251deba88eb85c03e1826daddbb0b0bce37165e71d9236af892fc08080a04e425050083943d27aa01fdb706b5715693c18ed9ff8ef050fcd0c2ea292687aa09b9a3d00ec6310bd2f5714e5c60c7c597128add8087e18986dc18d716c6d011780a0fcf815ea12a633987173946bf59a53d93c461c71e47b3eee7cb07885027288f4a093b3dc885d64ad3421e67bf396530d92268a6ea17fe536e75f3d45ff2d6f034b80a0d3f01d4772d6326d75fad188647163487f5c5df4b809e33fffe344262fcaa398800000000000000000000000000000000000000000000000000000000000000000000000000000000000000073f8718080808080808080a0bb626d1fc2d928384c17de477f7b7170c65267b016976bc21305ebf39fc1543ca0f3ea5fd76e00ddbf83625e203d99441c86c44557e86896aad4ec87a4683bdf618080a0afebeaa9674e4a195459974044cf4da821d72aff3654d6d70b14ce90075c13f980808080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e19f20aeeb5205bebd523c696a4b099033965d1bcb4af41ce5f3070145a8debd6601000000000000000000000000000000000000000000000000000000000000"), + }, + }, + receivedLogs: nil, + }, + wantNeedsToBeProven: []*liquiditymanager.LiquidityManagerLiquidityTransferred{ + { + FromChainSelector: l2ChainSelector, + ToChainSelector: l1ChainSelector, + To: l1LiquidityManagerAddress, + Amount: big.NewInt(7), + BridgeReturnData: mustPackBridgeTransferNonce(t, "0x0000000000000000000000000000000000000000000000000000000000000005"), + BridgeSpecificData: []byte{}, + }, + }, + wantNeedsToBeFinalized: nil, + wantMissingSent: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotNeedsToBeProven, gotNeedsToBeFinalized, gotMissingSent, err := partitionWithdrawalTransfers( + tt.args.localSelector, + tt.args.l1LiquidityManagerAddress, + tt.args.sentLogs, + tt.args.proveFinalizationStepLogs, + tt.args.receivedLogs, + logger.TestLogger(t), + ) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + assertLiquidityTransferredEventSlicesEqual(t, tt.wantNeedsToBeProven, gotNeedsToBeProven, sortByBridgeReturnData) + assertLiquidityTransferredEventSlicesEqual(t, tt.wantNeedsToBeFinalized, gotNeedsToBeFinalized, sortByBridgeReturnData) + assert.Equal(t, tt.wantMissingSent, gotMissingSent) + } + }) + } +} + +func Test_L2ToL1Bridge_GetBridgePayloadAndFee(t *testing.T) { + bridge := &l2ToL1Bridge{} + payload, fee, err := bridge.GetBridgePayloadAndFee(testutils.Context(t), models.Transfer{}) + require.NoError(t, err) + require.Empty(t, payload) + require.Equal(t, big.NewInt(0), fee) +} + +func Test_L2ToL1Bridge_QuorumizedBridgePayload(t *testing.T) { + bridge := &l2ToL1Bridge{} + payload, err := bridge.QuorumizedBridgePayload(make([][]byte, 0), 0) + require.NoError(t, err) + require.Empty(t, payload) +} + +func Test_L21ToL1Bridge_Close(t *testing.T) { + type fields struct { + l1LogPoller *lpmocks.LogPoller + l2LogPoller *lpmocks.LogPoller + l1FilterName string + l2FilterName string + } + tests := []struct { + name string + fields fields + wantErr bool + before func(*testing.T, fields) + assertions func(*testing.T, fields) + }{ + { + "happy path", + fields{ + l1LogPoller: lpmocks.NewLogPoller(t), + l2LogPoller: lpmocks.NewLogPoller(t), + l1FilterName: "l1FilterName", + l2FilterName: "l2FilterName", + }, + false, + func(t *testing.T, f fields) { + f.l1LogPoller.On("UnregisterFilter", mock.Anything, f.l1FilterName).Return(nil) + f.l2LogPoller.On("UnregisterFilter", mock.Anything, f.l2FilterName).Return(nil) + }, + func(t *testing.T, f fields) { + f.l1LogPoller.AssertExpectations(t) + f.l2LogPoller.AssertExpectations(t) + }, + }, + { + "l1 unregister error", + fields{ + l1LogPoller: lpmocks.NewLogPoller(t), + l2LogPoller: lpmocks.NewLogPoller(t), + l1FilterName: "l1FilterName", + l2FilterName: "l2FilterName", + }, + true, + func(t *testing.T, f fields) { + f.l1LogPoller.On("UnregisterFilter", mock.Anything, f.l1FilterName).Return(errors.New("unregister error")) + f.l2LogPoller.On("UnregisterFilter", mock.Anything, f.l2FilterName).Return(nil) + }, + func(t *testing.T, f fields) { + f.l1LogPoller.AssertExpectations(t) + f.l2LogPoller.AssertExpectations(t) + }, + }, + { + "l2 unregister error", + fields{ + l1LogPoller: lpmocks.NewLogPoller(t), + l2LogPoller: lpmocks.NewLogPoller(t), + l1FilterName: "l1FilterName", + l2FilterName: "l2FilterName", + }, + true, + func(t *testing.T, f fields) { + f.l1LogPoller.On("UnregisterFilter", mock.Anything, f.l1FilterName).Return(nil) + f.l2LogPoller.On("UnregisterFilter", mock.Anything, f.l2FilterName).Return(errors.New("unregister error")) + }, + func(t *testing.T, f fields) { + f.l1LogPoller.AssertExpectations(t) + f.l2LogPoller.AssertExpectations(t) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &l1ToL2Bridge{ + l1LogPoller: tt.fields.l1LogPoller, + l2LogPoller: tt.fields.l2LogPoller, + l1FilterName: tt.fields.l1FilterName, + l2FilterName: tt.fields.l2FilterName, + } + if tt.before != nil { + tt.before(t, tt.fields) + defer tt.assertions(t, tt.fields) + } + + err := l.Close(testutils.Context(t)) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/test_helper.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/test_helper.go new file mode 100644 index 0000000000..965fb94fea --- /dev/null +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/test_helper.go @@ -0,0 +1,52 @@ +package opstack + +import ( + "encoding/hex" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" +) + +func assertLiquidityTransferredEventSlicesEqual( + t *testing.T, + expected, + actual []*liquiditymanager.LiquidityManagerLiquidityTransferred, + sortComparator func(a, b *liquiditymanager.LiquidityManagerLiquidityTransferred) bool, +) { + require.Equal(t, len(expected), len(actual)) + sort.Slice(expected, func(i, j int) bool { + return sortComparator(expected[i], expected[j]) + }) + sort.Slice(actual, func(i, j int) bool { + return sortComparator(actual[i], actual[j]) + }) + for i := range expected { + assert.Equal(t, expected[i].OcrSeqNum, actual[i].OcrSeqNum) + assert.Equal(t, expected[i].FromChainSelector, actual[i].FromChainSelector) + assert.Equal(t, expected[i].ToChainSelector, actual[i].ToChainSelector) + assert.Equal(t, expected[i].To, actual[i].To) + assert.Equal(t, expected[i].Amount, actual[i].Amount) + assert.Equal(t, expected[i].BridgeSpecificData, actual[i].BridgeSpecificData) + assert.Equal(t, expected[i].BridgeReturnData, actual[i].BridgeReturnData) + } +} + +func sortByBridgeReturnData(a, b *liquiditymanager.LiquidityManagerLiquidityTransferred) bool { + return hex.EncodeToString(a.BridgeReturnData) < hex.EncodeToString(b.BridgeReturnData) +} + +func mustPackBridgeTransferNonce(t *testing.T, bridgeDataHex string) []byte { + packed, err := hex.DecodeString(bridgeDataHex[2:]) + require.NoError(t, err) + return packed +} + +func mustConvertHexBridgeSpecificDataToBytes(t *testing.T, hexData string) []byte { + packed, err := hex.DecodeString(hexData[2:]) + require.NoError(t, err) + return packed +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/helpers.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/helpers.go index 9439fbb785..a95aa95afb 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/helpers.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/helpers.go @@ -2,6 +2,7 @@ package withdrawprover import ( "fmt" + "math/big" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" @@ -10,13 +11,22 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/liquiditymanager" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_l1_bridge_adapter_encoder" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/liquiditymanager/generated/optimism_l2_to_l1_message_passer" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/liquiditymanager/abiutils" ) var ( l2ToL1MessagePasserABI *abi.ABI ) +const ( + // L2 to L1 finalize withdrawal actions (used for generating the LM's finalization payload so the LM contract knows which action to take) + FinalizationActionProveWithdrawal uint8 = 0 + FinalizationActionFinalizeWithdrawal uint8 = 1 +) + func init() { abi, err := optimism_l2_to_l1_message_passer.OptimismL2ToL1MessagePasserMetaData.GetAbi() if err != nil { @@ -96,3 +106,122 @@ func toProofBytes(proof []hexutil.Bytes) [][]byte { } return proofBytes } + +func EncodeProveWithdrawalPayload(opBridgeAdapterEncoderABI abi.ABI, messageProof BedrockMessageProof) ([]byte, error) { + encodedProveWithdrawal, err := opBridgeAdapterEncoderABI.Methods["encodeOptimismProveWithdrawalPayload"].Inputs.Pack( + optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterOptimismProveWithdrawalPayload{ + WithdrawalTransaction: optimism_l1_bridge_adapter_encoder.TypesWithdrawalTransaction{ + Nonce: messageProof.LowLevelMessage.Nonce, + Sender: messageProof.LowLevelMessage.Sender, + Target: messageProof.LowLevelMessage.Target, + Value: messageProof.LowLevelMessage.Value, + GasLimit: messageProof.LowLevelMessage.GasLimit, + Data: messageProof.LowLevelMessage.Data, + }, + L2OutputIndex: messageProof.L2OutputIndex, + OutputRootProof: optimism_l1_bridge_adapter_encoder.TypesOutputRootProof{ + Version: messageProof.OutputRootProof.Version, + StateRoot: messageProof.OutputRootProof.StateRoot, + MessagePasserStorageRoot: messageProof.OutputRootProof.MessagePasserStorageRoot, + LatestBlockhash: messageProof.OutputRootProof.LatestBlockHash, + }, + WithdrawalProof: messageProof.WithdrawalProof, + }, + ) + if err != nil { + return nil, fmt.Errorf("encodeOptimismProveWithdrawalPayload: %w", err) + } + + // Then encode the finalize withdraw ERC 20 payload + encodedPayload, err := encodeFinalizeWithdrawalBridgeAdapterPayload( + opBridgeAdapterEncoderABI, + FinalizationActionProveWithdrawal, + encodedProveWithdrawal, + ) + if err != nil { + return nil, fmt.Errorf("encodeFinalizeWithdrawalERC20Payload: %w", err) + } + + return encodedPayload, nil +} + +func EncodeFinalizeWithdrawalPayload(opBridgeAdapterEncoderABI abi.ABI, messagePassed *optimism_l2_to_l1_message_passer.OptimismL2ToL1MessagePasserMessagePassed) ([]byte, error) { + encodedFinalizeWithdrawal, err := opBridgeAdapterEncoderABI.Methods["encodeOptimismFinalizationPayload"].Inputs.Pack( + optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterOptimismFinalizationPayload{ + WithdrawalTransaction: optimism_l1_bridge_adapter_encoder.TypesWithdrawalTransaction{ + Nonce: messagePassed.Nonce, + Sender: messagePassed.Sender, + Target: messagePassed.Target, + Value: messagePassed.Value, + GasLimit: messagePassed.GasLimit, + Data: messagePassed.Data, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("encodeOptimismFinalizationPayload: %w", err) + } + + // then encode the finalize withdraw erc20 payload next. + encodedPayload, err := encodeFinalizeWithdrawalBridgeAdapterPayload( + opBridgeAdapterEncoderABI, + FinalizationActionFinalizeWithdrawal, + encodedFinalizeWithdrawal, + ) + if err != nil { + return nil, fmt.Errorf("encodeFinalizeWithdrawalERC20Payload: %w", err) + } + return encodedPayload, nil +} + +func UnpackNonceFromFinalizationStepBridgeSpecificData( + proveStep *liquiditymanager.LiquidityManagerFinalizationStepCompleted, + l1OPBridgeAdapterEncoderABI abi.ABI, + opCrossDomainMessengerABI abi.ABI, + opStandardBridgeABI abi.ABI, +) (*big.Int, error) { + encodedPayload := proveStep.BridgeSpecificData + + // Unpack outermost finalize withdraw erc20 payload + unpackedFinalizeWithdrawERC20Payload, err := l1OPBridgeAdapterEncoderABI.Methods["encodeFinalizeWithdrawalERC20Payload"].Inputs.Unpack(encodedPayload) + if err != nil { + return nil, fmt.Errorf("unpack finalizeWithdrawalERC20Payload: %w", err) + } + outFinalizeWithdrawERC20Payload := *abi.ConvertType(unpackedFinalizeWithdrawERC20Payload[0], new(optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterFinalizeWithdrawERC20Payload)).(*optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterFinalizeWithdrawERC20Payload) + + // Unpack optimism prove withdrawal payload + unpackedOptimismProveWithdrawalPayload, err := l1OPBridgeAdapterEncoderABI.Methods["encodeOptimismProveWithdrawalPayload"].Inputs.Unpack(outFinalizeWithdrawERC20Payload.Data) + if err != nil { + return nil, fmt.Errorf("unpack optimismProveWithdrawalPayload: %w", err) + } + outOptimismProveWithdrawalPayload := *abi.ConvertType(unpackedOptimismProveWithdrawalPayload[0], new(optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterOptimismProveWithdrawalPayload)).(*optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterOptimismProveWithdrawalPayload) + + // Unpack withdrawal transaction's data from relayMessage data. Trim the first 4 bytes since this was encoded with + // the function selector: https://github.com/ethereum-optimism/optimism/blob/f707883038d527cbf1e9f8ea513fe33255deadbc/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol#L186 + decodedRelayMessage, err := opCrossDomainMessengerABI.Methods["relayMessage"].Inputs.Unpack(outOptimismProveWithdrawalPayload.WithdrawalTransaction.Data[4:]) + if err != nil { + return nil, fmt.Errorf("unpack relayMessage data: %w", err) + } + + // Unpack relay message Message field into StandardBridge's finalizeBridgeERC20 params. Trim the first 4 bytes since + // this was encoded with the function selector. The nonce is the 6th parameter. + unpackedFinalizeBridgeParams, err := opStandardBridgeABI.Methods["finalizeBridgeERC20"].Inputs.Unpack(decodedRelayMessage[5].([]byte)[4:]) + if err != nil { + return nil, fmt.Errorf("unpack finalizeBridgeERC20 params: %w", err) + } + + return abiutils.UnpackUint256(unpackedFinalizeBridgeParams[5].([]byte)) +} + +func encodeFinalizeWithdrawalBridgeAdapterPayload(opBridgeAdapterEncoderABI abi.ABI, action uint8, data []byte) ([]byte, error) { + encodedPayload, err := opBridgeAdapterEncoderABI.Methods["encodeFinalizeWithdrawalERC20Payload"].Inputs.Pack( + optimism_l1_bridge_adapter_encoder.OptimismL1BridgeAdapterFinalizeWithdrawERC20Payload{ + Action: action, + Data: data, + }, + ) + if err != nil { + return nil, fmt.Errorf("encodeFinalizeWithdrawalERC20Payload: %w", err) + } + return encodedPayload, nil +} diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover.go index 004af89b53..4e34b56012 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover.go @@ -68,17 +68,17 @@ func New( optimismPortal, err := optimism_portal.NewOptimismPortal(optimismPortalAddress, l1Client) if err != nil { - return nil, err + return nil, fmt.Errorf("new optimism portal: %w", err) } optimismPortal2, err := optimism_portal_2.NewOptimismPortal2(optimismPortalAddress, l1Client) if err != nil { - return nil, err + return nil, fmt.Errorf("new optimism portal 2: %w", err) } l2OutputOracle, err := optimism_l2_output_oracle.NewOptimismL2OutputOracle(l2OutputOracleAddress, l1Client) if err != nil { - return nil, err + return nil, fmt.Errorf("new l2 output oracle: %w", err) } return &prover{ @@ -137,8 +137,7 @@ func (p *prover) Prove(ctx context.Context, withdrawalTxHash common.Hash) ( return messageProof, fmt.Errorf("make state trie proof tx hash %s: %w", withdrawalTxHash.Hex(), err) } - header, err := p.l2Client.HeaderByNumber( - context.Background(), messageBedrockOutput.L2BlockNumber) + header, err := p.l2Client.HeaderByNumber(ctx, messageBedrockOutput.L2BlockNumber) if err != nil { return messageProof, fmt.Errorf("get header by number tx hash %s: %w", withdrawalTxHash.Hex(), err) } @@ -167,13 +166,13 @@ func (p *prover) makeStateTrieProof( err := p.l2Client.CallContext(ctx, &resp, "eth_getProof", address, []string{hexutil.Encode(slot[:])}, hexutil.EncodeBig(l2BlockNumber)) if err != nil { - return stateTrieProof{}, err + return stateTrieProof{}, fmt.Errorf("call eth_getProof with address %s, slot %s, l2BlockNumber %s: %w", address.String(), hexutil.Encode(slot[:]), l2BlockNumber.String(), err) } updatedProof, err := merkleutils.MaybeAddProofNode( crypto.Keccak256Hash(slot[:]), toProofBytes(resp.StorageProof[0].Proof)) if err != nil { - return stateTrieProof{}, err + return stateTrieProof{}, fmt.Errorf("maybe add proof node: %w", err) } return stateTrieProof{ @@ -188,14 +187,14 @@ func (p *prover) getMessageBedrockOutput( ctx context.Context, l2BlockNumber *big.Int, ) (bedrockOutput, error) { - fpacEnabled, err := p.getFPAC(ctx) + fpacEnabled, err := p.GetFPAC(ctx) if err != nil { - return bedrockOutput{}, err + return bedrockOutput{}, fmt.Errorf("get FPAC: %w", err) } if fpacEnabled { gameType, err2 := p.optimismPortal2.RespectedGameType(&bind.CallOpts{Context: ctx}) if err2 != nil { - return bedrockOutput{}, err2 + return bedrockOutput{}, fmt.Errorf("get respected game type from portal: %w", err2) } disputeGameFactoryAddress, err2 := p.optimismPortal2.DisputeGameFactory(&bind.CallOpts{Context: ctx}) @@ -210,7 +209,7 @@ func (p *prover) getMessageBedrockOutput( gameCount, err2 := disputeGameFactory.GameCount(&bind.CallOpts{Context: ctx}) if err2 != nil { - return bedrockOutput{}, err2 + return bedrockOutput{}, fmt.Errorf("get game count: %w", err2) } start := int64(0) @@ -228,13 +227,13 @@ func (p *prover) getMessageBedrockOutput( big.NewInt(start), big.NewInt(end)) if err2 != nil { - return bedrockOutput{}, err2 + return bedrockOutput{}, fmt.Errorf("find latest games: %w", err2) } for _, game := range latestGames { blockNumber, err2 := abiutils.UnpackUint256(game.ExtraData) if err2 != nil { - return bedrockOutput{}, err2 + return bedrockOutput{}, fmt.Errorf("unpack block number from dispute game: %w", err2) } if blockNumber.Cmp(l2BlockNumber) >= 0 { @@ -250,21 +249,20 @@ func (p *prover) getMessageBedrockOutput( // if there's no match then we can't prove the message to the portal. return bedrockOutput{}, fmt.Errorf("no game found for block number %s", l2BlockNumber.String()) } - // Try to find the output index that corresponds to the block number attached to the message. // We'll explicitly handle "cannot get output" errors as a null return value, but anything else // needs to get thrown. Might need to revisit this in the future to be a little more robust // when connected to RPCs that don't return nice error messages. l2OutputIndex, err := p.l2OutputOracle.GetL2OutputIndexAfter(&bind.CallOpts{Context: ctx}, l2BlockNumber) if err != nil { - return bedrockOutput{}, err + return bedrockOutput{}, fmt.Errorf("[FPAC not enabled] get l2 output index after block number %s: %w", l2BlockNumber.String(), err) } // Now pull the proposal out given the output index. Should always work as long as the above // codepath completed successfully. proposal, err := p.l2OutputOracle.GetL2Output(&bind.CallOpts{Context: ctx}, l2OutputIndex) if err != nil { - return bedrockOutput{}, err + return bedrockOutput{}, fmt.Errorf("[FPAC not enabled] get l2 output for index %s from oracle: %w", l2OutputIndex.String(), err) } return bedrockOutput{ @@ -275,11 +273,11 @@ func (p *prover) getMessageBedrockOutput( }, nil } -// getFPAC returns whether FPAC (fault proof upgrade) is enabled on the optimism portal. -func (p *prover) getFPAC(ctx context.Context) (bool, error) { +// GetFPAC returns whether FPAC (fault proof upgrade) is enabled on the optimism portal. +func (p *prover) GetFPAC(ctx context.Context) (bool, error) { semVer, err := p.optimismPortal.Version(&bind.CallOpts{Context: ctx}) if err != nil { - return false, err + return false, fmt.Errorf("get version from portal: %w", err) } version := semver.MustParse(semVer) diff --git a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover_test.go b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover_test.go index 2e7b96a27f..a1983807e7 100644 --- a/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover_test.go +++ b/core/services/ocr2/plugins/liquiditymanager/bridge/opstack/withdrawprover/prover_test.go @@ -18,7 +18,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" ) -func Test_prover_getFPAC(t *testing.T) { +func Test_prover_GetFPAC(t *testing.T) { type fields struct { optimismPortal *mock_optimism_portal.OptimismPortalInterface } @@ -93,7 +93,7 @@ func Test_prover_getFPAC(t *testing.T) { } tt.expect(t, tt.fields, tt.args) defer tt.assert(t, tt.fields) - got, err := p.getFPAC(tt.args.ctx) + got, err := p.GetFPAC(tt.args.ctx) if tt.wantErr { require.Error(t, err) } else { diff --git a/core/services/ocr2/plugins/liquiditymanager/plugin.go b/core/services/ocr2/plugins/liquiditymanager/plugin.go index 23a6f21136..2ebb0afeb3 100644 --- a/core/services/ocr2/plugins/liquiditymanager/plugin.go +++ b/core/services/ocr2/plugins/liquiditymanager/plugin.go @@ -218,16 +218,16 @@ func combinedUnexecutedTransfers( resolvedTransfersQuorum []models.Transfer, inflightTransfers []models.Transfer, ) []liquidityrebalancer.UnexecutedTransfer { - unexecuted := make([]liquidityrebalancer.UnexecutedTransfer, 0, len(pendingTransfers)+len(resolvedTransfersQuorum)+len(inflightTransfers)) - for _, pendingTransfer := range pendingTransfers { - unexecuted = append(unexecuted, pendingTransfer) - } + unexecuted := make([]liquidityrebalancer.UnexecutedTransfer, 0, len(resolvedTransfersQuorum)+len(inflightTransfers)+len(pendingTransfers)) for _, resolvedTransfer := range resolvedTransfersQuorum { unexecuted = append(unexecuted, resolvedTransfer) } for _, inflightTransfer := range inflightTransfers { unexecuted = append(unexecuted, inflightTransfer) } + for _, pendingTransfer := range pendingTransfers { + unexecuted = append(unexecuted, pendingTransfer) + } return unexecuted }