status-go/services/wallet/bridge/hop.go

575 lines
18 KiB
Go

package bridge
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
netUrl "net/url"
"strings"
"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/common/hexutil"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/contracts"
"github.com/status-im/status-go/contracts/hop"
hopL1CctpImplementation "github.com/status-im/status-go/contracts/hop/l1Contracts/l1CctpImplementation"
hopL1Erc20Bridge "github.com/status-im/status-go/contracts/hop/l1Contracts/l1Erc20Bridge"
hopL1EthBridge "github.com/status-im/status-go/contracts/hop/l1Contracts/l1EthBridge"
hopL1HopBridge "github.com/status-im/status-go/contracts/hop/l1Contracts/l1HopBridge"
hopL2AmmWrapper "github.com/status-im/status-go/contracts/hop/l2Contracts/l2AmmWrapper"
hopL2ArbitrumBridge "github.com/status-im/status-go/contracts/hop/l2Contracts/l2ArbitrumBridge"
hopL2CctpImplementation "github.com/status-im/status-go/contracts/hop/l2Contracts/l2CctpImplementation"
hopL2OptimismBridge "github.com/status-im/status-go/contracts/hop/l2Contracts/l2OptimismBridge"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/rpc/chain"
"github.com/status-im/status-go/services/wallet/bigint"
walletCommon "github.com/status-im/status-go/services/wallet/common"
"github.com/status-im/status-go/services/wallet/thirdparty"
"github.com/status-im/status-go/services/wallet/token"
"github.com/status-im/status-go/transactions"
)
const (
SevenDaysInSeconds = 604800
hopSymbol = "HOP"
)
type HopTxArgs struct {
transactions.SendTxArgs
ChainID uint64 `json:"chainId"`
Symbol string `json:"symbol"`
Recipient common.Address `json:"recipient"`
Amount *hexutil.Big `json:"amount"`
BonderFee *hexutil.Big `json:"bonderFee"`
}
type BonderFee struct {
AmountIn *bigint.BigInt `json:"amountIn"`
Slippage float32 `json:"slippage"`
AmountOutMin *bigint.BigInt `json:"amountOutMin"`
DestinationAmountOutMin *bigint.BigInt `json:"destinationAmountOutMin"`
BonderFee *bigint.BigInt `json:"bonderFee"`
EstimatedRecieved *bigint.BigInt `json:"estimatedRecieved"`
Deadline int64 `json:"deadline"`
DestinationDeadline int64 `json:"destinationDeadline"`
}
func (bf *BonderFee) UnmarshalJSON(data []byte) error {
type Alias BonderFee
aux := &struct {
AmountIn string `json:"amountIn"`
Slippage float32 `json:"slippage"`
AmountOutMin string `json:"amountOutMin"`
DestinationAmountOutMin string `json:"destinationAmountOutMin"`
BonderFee string `json:"bonderFee"`
EstimatedRecieved string `json:"estimatedRecieved"`
Deadline int64 `json:"deadline"`
DestinationDeadline *int64 `json:"destinationDeadline"`
*Alias
}{
Alias: (*Alias)(bf),
}
if err := json.Unmarshal(data, aux); err != nil {
return err
}
bf.AmountIn = &bigint.BigInt{Int: new(big.Int)}
bf.AmountIn.SetString(aux.AmountIn, 10)
bf.AmountOutMin = &bigint.BigInt{Int: new(big.Int)}
bf.AmountOutMin.SetString(aux.AmountOutMin, 10)
bf.DestinationAmountOutMin = &bigint.BigInt{Int: new(big.Int)}
bf.DestinationAmountOutMin.SetString(aux.DestinationAmountOutMin, 10)
bf.BonderFee = &bigint.BigInt{Int: new(big.Int)}
bf.BonderFee.SetString(aux.BonderFee, 10)
bf.EstimatedRecieved = &bigint.BigInt{Int: new(big.Int)}
bf.EstimatedRecieved.SetString(aux.EstimatedRecieved, 10)
bf.Deadline = aux.Deadline
if aux.DestinationDeadline != nil {
bf.DestinationDeadline = *aux.DestinationDeadline
}
return nil
}
type HopBridge struct {
transactor transactions.TransactorIface
httpClient *thirdparty.HTTPClient
tokenManager *token.Manager
contractMaker *contracts.ContractMaker
bonderFee *BonderFee
}
func NewHopBridge(rpcClient *rpc.Client, transactor transactions.TransactorIface, tokenManager *token.Manager) *HopBridge {
return &HopBridge{
contractMaker: &contracts.ContractMaker{RPCClient: rpcClient},
httpClient: thirdparty.NewHTTPClient(),
transactor: transactor,
tokenManager: tokenManager,
}
}
func (h *HopBridge) Name() string {
return HopName
}
func (h *HopBridge) AvailableFor(params BridgeParams) (bool, error) {
// We chcek if the contract is available on the network for the token
_, err := h.GetContractAddress(params)
// toToken is not nil only if the send type is Swap
return err == nil && params.ToToken == nil, nil
}
func (c *HopBridge) getAppropriateABI(contractType string, chainID uint64, token *token.Token) (abi.ABI, error) {
switch contractType {
case hop.CctpL1Bridge:
return abi.JSON(strings.NewReader(hopL1CctpImplementation.HopL1CctpImplementationABI))
case hop.L1Bridge:
if token.IsNative() {
return abi.JSON(strings.NewReader(hopL1EthBridge.HopL1EthBridgeABI))
}
if token.Symbol == hopSymbol {
return abi.JSON(strings.NewReader(hopL1HopBridge.HopL1HopBridgeABI))
}
return abi.JSON(strings.NewReader(hopL1Erc20Bridge.HopL1Erc20BridgeABI))
case hop.L2AmmWrapper:
return abi.JSON(strings.NewReader(hopL2AmmWrapper.HopL2AmmWrapperABI))
case hop.CctpL2Bridge:
return abi.JSON(strings.NewReader(hopL2CctpImplementation.HopL2CctpImplementationABI))
case hop.L2Bridge:
if chainID == walletCommon.OptimismMainnet ||
chainID == walletCommon.OptimismSepolia {
return abi.JSON(strings.NewReader(hopL2OptimismBridge.HopL2OptimismBridgeABI))
}
if chainID == walletCommon.ArbitrumMainnet ||
chainID == walletCommon.ArbitrumSepolia {
return abi.JSON(strings.NewReader(hopL2ArbitrumBridge.HopL2ArbitrumBridgeABI))
}
}
return abi.ABI{}, errors.New("not available for contract type")
}
func (h *HopBridge) PackTxInputData(params BridgeParams, contractType string) ([]byte, error) {
abi, err := h.getAppropriateABI(contractType, params.FromChain.ChainID, params.FromToken)
if err != nil {
return []byte{}, err
}
switch contractType {
case hop.CctpL1Bridge:
return h.packCctpL1BridgeTx(abi, params.ToChain.ChainID, params.ToAddr)
case hop.L1Bridge:
return h.packL1BridgeTx(abi, params.ToChain.ChainID, params.ToAddr)
case hop.L2AmmWrapper:
return h.packL2AmmWrapperTx(abi, params.ToChain.ChainID, params.ToAddr)
case hop.CctpL2Bridge:
return h.packCctpL2BridgeTx(abi, params.ToChain.ChainID, params.ToAddr)
case hop.L2Bridge:
return h.packL2BridgeTx(abi, params.ToChain.ChainID, params.ToAddr)
}
return []byte{}, errors.New("contract type not supported yet")
}
func (h *HopBridge) EstimateGas(params BridgeParams) (uint64, error) {
value := big.NewInt(0)
if params.FromToken.IsNative() {
value = params.AmountIn
}
contractAddress, contractType, err := hop.GetContractAddress(params.FromChain.ChainID, params.FromToken.Symbol)
if err != nil {
return 0, err
}
input, err := h.PackTxInputData(params, contractType)
if err != nil {
return 0, err
}
ethClient, err := h.contractMaker.RPCClient.EthClient(params.FromChain.ChainID)
if err != nil {
return 0, err
}
msg := ethereum.CallMsg{
From: params.FromAddr,
To: &contractAddress,
Value: value,
Data: input,
}
estimation, err := ethClient.EstimateGas(context.Background(), msg)
if err != nil {
if !params.FromToken.IsNative() {
// TODO: this is a temporary solution until we find a better way to estimate the gas
// hardcoding the estimation for other than ETH, cause we cannot get a proper estimation without having an approval placed first
// this is an error we're facing otherwise: `execution reverted: ERC20: transfer amount exceeds allowance`
estimation = 350000
} else {
return 0, err
}
}
increasedEstimation := float64(estimation) * IncreaseEstimatedGasFactor
return uint64(increasedEstimation), nil
}
func (h *HopBridge) BuildTx(params BridgeParams) (*ethTypes.Transaction, error) {
toAddr := types.Address(params.ToAddr)
sendArgs := &TransactionBridge{
HopTx: &HopTxArgs{
SendTxArgs: transactions.SendTxArgs{
From: types.Address(params.FromAddr),
To: &toAddr,
Value: (*hexutil.Big)(params.AmountIn),
Data: types.HexBytes("0x0"),
},
Symbol: params.FromToken.Symbol,
Recipient: params.ToAddr,
Amount: (*hexutil.Big)(params.AmountIn),
BonderFee: (*hexutil.Big)(params.BonderFee),
ChainID: params.ToChain.ChainID,
},
ChainID: params.FromChain.ChainID,
}
return h.BuildTransaction(sendArgs)
}
func (h *HopBridge) GetContractAddress(params BridgeParams) (common.Address, error) {
address, _, err := hop.GetContractAddress(params.FromChain.ChainID, params.FromToken.Symbol)
return address, err
}
func (h *HopBridge) sendOrBuild(sendArgs *TransactionBridge, signerFn bind.SignerFn) (tx *ethTypes.Transaction, err error) {
fromChain := h.contractMaker.RPCClient.NetworkManager.Find(sendArgs.ChainID)
if fromChain == nil {
return tx, fmt.Errorf("ChainID not supported %d", sendArgs.ChainID)
}
token := h.tokenManager.FindToken(fromChain, sendArgs.HopTx.Symbol)
nonce, err := h.transactor.NextNonce(h.contractMaker.RPCClient, fromChain.ChainID, sendArgs.HopTx.From)
if err != nil {
return tx, err
}
argNonce := hexutil.Uint64(nonce)
sendArgs.HopTx.Nonce = &argNonce
txOpts := sendArgs.HopTx.ToTransactOpts(signerFn)
if token.IsNative() {
txOpts.Value = (*big.Int)(sendArgs.HopTx.Amount)
}
ethClient, err := h.contractMaker.RPCClient.EthClient(fromChain.ChainID)
if err != nil {
return tx, err
}
contractAddress, contractType, err := hop.GetContractAddress(fromChain.ChainID, sendArgs.HopTx.Symbol)
if err != nil {
return tx, err
}
switch contractType {
case hop.CctpL1Bridge:
return h.sendCctpL1BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts)
case hop.L1Bridge:
return h.sendL1BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts, token)
case hop.L2AmmWrapper:
return h.sendL2AmmWrapperTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts)
case hop.CctpL2Bridge:
return h.sendCctpL2BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts)
case hop.L2Bridge:
return h.sendL2BridgeTx(contractAddress, ethClient, sendArgs.HopTx.ChainID, sendArgs.HopTx.Recipient, txOpts)
}
return tx, err
}
func (h *HopBridge) Send(sendArgs *TransactionBridge, verifiedAccount *account.SelectedExtKey) (hash types.Hash, err error) {
tx, err := h.sendOrBuild(sendArgs, getSigner(sendArgs.ChainID, sendArgs.HopTx.From, verifiedAccount))
if err != nil {
return types.Hash{}, err
}
return types.Hash(tx.Hash()), nil
}
func (h *HopBridge) BuildTransaction(sendArgs *TransactionBridge) (*ethTypes.Transaction, error) {
return h.sendOrBuild(sendArgs, nil)
}
func (h *HopBridge) CalculateFees(params BridgeParams) (*big.Int, *big.Int, error) {
hopChainsMap := map[uint64]string{
walletCommon.EthereumMainnet: "ethereum",
walletCommon.OptimismMainnet: "optimism",
walletCommon.ArbitrumMainnet: "arbitrum",
}
fromChainName, ok := hopChainsMap[params.FromChain.ChainID]
if !ok {
return nil, nil, errors.New("from chain not supported")
}
toChainName, ok := hopChainsMap[params.ToChain.ChainID]
if !ok {
return nil, nil, errors.New("to chain not supported")
}
reqParams := netUrl.Values{}
reqParams.Add("amount", params.AmountIn.String())
reqParams.Add("token", params.FromToken.Symbol)
reqParams.Add("fromChain", fromChainName)
reqParams.Add("toChain", toChainName)
reqParams.Add("slippage", "0.5") // menas 0.5%
url := "https://api.hop.exchange/v1/quote"
response, err := h.httpClient.DoGetRequest(context.Background(), url, reqParams)
if err != nil {
return nil, nil, err
}
h.bonderFee = &BonderFee{}
err = json.Unmarshal(response, h.bonderFee)
if err != nil {
return nil, nil, err
}
// Remove token fee from bonder fee as said here:
// https://docs.hop.exchange/v/developer-docs/api/api#get-v1-quote
// `bonderFee` - The suggested bonder fee for the amount in. The bonder fee also includes the cost of the destination transaction fee.
tokenFee := big.NewInt(0) //new(big.Int).Sub(h.bonderFee.AmountIn.Int, h.bonderFee.EstimatedRecieved.Int)
return h.bonderFee.BonderFee.Int, tokenFee, nil
}
func (h *HopBridge) CalculateAmountOut(params BridgeParams) (*big.Int, error) {
return h.bonderFee.EstimatedRecieved.Int, nil
}
func (h *HopBridge) packCctpL1BridgeTx(abi abi.ABI, toChainID uint64, to common.Address) ([]byte, error) {
return abi.Pack("send",
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int)
}
func (h *HopBridge) sendCctpL1BridgeTx(contractAddress common.Address, ethClient chain.ClientInterface, toChainID uint64, to common.Address, txOpts *bind.TransactOpts) (tx *ethTypes.Transaction, err error) {
contractInstance, err := hopL1CctpImplementation.NewHopL1CctpImplementation(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.Send(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int)
}
func (h *HopBridge) packL1BridgeTx(abi abi.ABI, toChainID uint64, to common.Address) ([]byte, error) {
return abi.Pack("sendToL2",
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline),
common.Address{},
big.NewInt(0))
}
func (h *HopBridge) sendL1BridgeTx(contractAddress common.Address, ethClient chain.ClientInterface, toChainID uint64, to common.Address, txOpts *bind.TransactOpts, token *token.Token) (tx *ethTypes.Transaction, err error) {
if token.IsNative() {
contractInstance, err := hopL1EthBridge.NewHopL1EthBridge(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.SendToL2(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline),
common.Address{},
big.NewInt(0))
}
if token.Symbol == hopSymbol {
contractInstance, err := hopL1HopBridge.NewHopL1HopBridge(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.SendToL2(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline),
common.Address{},
big.NewInt(0))
}
contractInstance, err := hopL1Erc20Bridge.NewHopL1Erc20Bridge(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.SendToL2(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline),
common.Address{},
big.NewInt(0))
}
func (h *HopBridge) packCctpL2BridgeTx(abi abi.ABI, toChainID uint64, to common.Address) ([]byte, error) {
return abi.Pack("send",
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int)
}
func (h *HopBridge) sendCctpL2BridgeTx(contractAddress common.Address, ethClient chain.ClientInterface, toChainID uint64, to common.Address, txOpts *bind.TransactOpts) (tx *ethTypes.Transaction, err error) {
contractInstance, err := hopL2CctpImplementation.NewHopL2CctpImplementation(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.Send(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int,
)
}
func (h *HopBridge) packL2AmmWrapperTx(abi abi.ABI, toChainID uint64, to common.Address) ([]byte, error) {
return abi.Pack("swapAndSend",
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline),
h.bonderFee.DestinationAmountOutMin.Int,
big.NewInt(h.bonderFee.DestinationDeadline))
}
func (h *HopBridge) sendL2AmmWrapperTx(contractAddress common.Address, ethClient chain.ClientInterface, toChainID uint64, to common.Address, txOpts *bind.TransactOpts) (tx *ethTypes.Transaction, err error) {
contractInstance, err := hopL2AmmWrapper.NewHopL2AmmWrapper(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.SwapAndSend(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline),
h.bonderFee.DestinationAmountOutMin.Int,
big.NewInt(h.bonderFee.DestinationDeadline))
}
func (h *HopBridge) packL2BridgeTx(abi abi.ABI, toChainID uint64, to common.Address) ([]byte, error) {
return abi.Pack("send",
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline))
}
func (h *HopBridge) sendL2BridgeTx(contractAddress common.Address, ethClient chain.ClientInterface, toChainID uint64, to common.Address, txOpts *bind.TransactOpts) (tx *ethTypes.Transaction, err error) {
fromChainID := ethClient.NetworkID()
if fromChainID == walletCommon.OptimismMainnet ||
fromChainID == walletCommon.OptimismSepolia {
contractInstance, err := hopL2OptimismBridge.NewHopL2OptimismBridge(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.Send(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline))
}
if fromChainID == walletCommon.ArbitrumMainnet ||
fromChainID == walletCommon.ArbitrumSepolia {
contractInstance, err := hopL2ArbitrumBridge.NewHopL2ArbitrumBridge(
contractAddress,
ethClient,
)
if err != nil {
return tx, err
}
return contractInstance.Send(
txOpts,
big.NewInt(int64(toChainID)),
to,
h.bonderFee.AmountIn.Int,
h.bonderFee.BonderFee.Int,
h.bonderFee.AmountOutMin.Int,
big.NewInt(h.bonderFee.Deadline))
}
return tx, errors.New("tx for chain not supported yet")
}