package e2e_test import ( "context" "fmt" "math/big" "net/http" "slices" "strconv" "sync" "time" "github.com/ethereum/go-ethereum/common" "github.com/Flaque/filet" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/params" "github.com/phayes/freeport" "github.com/synapsecns/sanguine/core" "github.com/synapsecns/sanguine/core/dbcommon" "github.com/synapsecns/sanguine/core/retry" "github.com/synapsecns/sanguine/core/testsuite" "github.com/synapsecns/sanguine/ethergo/backends" "github.com/synapsecns/sanguine/ethergo/backends/anvil" "github.com/synapsecns/sanguine/ethergo/backends/base" "github.com/synapsecns/sanguine/ethergo/contracts" signerConfig "github.com/synapsecns/sanguine/ethergo/signer/config" "github.com/synapsecns/sanguine/ethergo/signer/wallet" cctpTest "github.com/synapsecns/sanguine/services/cctp-relayer/testutil" omnirpcClient "github.com/synapsecns/sanguine/services/omnirpc/client" "github.com/synapsecns/sanguine/services/omnirpc/testhelper" apiConfig "github.com/synapsecns/sanguine/services/rfq/api/config" "github.com/synapsecns/sanguine/services/rfq/api/db/sql" "github.com/synapsecns/sanguine/services/rfq/api/rest" "github.com/synapsecns/sanguine/services/rfq/contracts/ierc20" "github.com/synapsecns/sanguine/services/rfq/guard/guardconfig" guardConnect "github.com/synapsecns/sanguine/services/rfq/guard/guarddb/connect" guardService "github.com/synapsecns/sanguine/services/rfq/guard/service" "github.com/synapsecns/sanguine/services/rfq/relayer/relconfig" "github.com/synapsecns/sanguine/services/rfq/relayer/reldb/connect" "github.com/synapsecns/sanguine/services/rfq/relayer/service" "github.com/synapsecns/sanguine/services/rfq/testutil" "github.com/synapsecns/sanguine/services/rfq/util" ) func (i *IntegrationSuite) setupQuoterAPI() { dbPath := filet.TmpDir(i.T(), "") apiPort, err := freeport.GetFreePort() i.NoError(err) apiStore, err := sql.Connect(i.GetTestContext(), dbcommon.Sqlite, dbPath, i.metrics) i.NoError(err) // make the api without bridges apiCfg := apiConfig.Config{ Database: apiConfig.DatabaseConfig{ Type: dbcommon.Sqlite.String(), DSN: dbPath, }, OmniRPCURL: i.omniServer, Bridges: map[uint32]string{ originBackendChainID: i.manager.Get(i.GetTestContext(), i.originBackend, testutil.FastBridgeType).Address().String(), destBackendChainID: i.manager.Get(i.GetTestContext(), i.destBackend, testutil.FastBridgeType).Address().String(), }, Port: strconv.Itoa(apiPort), } api, err := rest.NewAPI(i.GetTestContext(), apiCfg, i.metrics, i.omniClient, apiStore) i.NoError(err) i.apiServer = fmt.Sprintf("http://localhost:%d", apiPort) go func() { err = api.Run(i.GetTestContext()) i.NoError(err) }() // make sure api server hast started testsuite.Eventually(i.GetTestContext(), i.T(), func() bool { var req *http.Request req, err = http.NewRequestWithContext(i.GetTestContext(), http.MethodGet, i.apiServer, nil) i.NoError(err) //nolint: bodyclose _, err = http.DefaultClient.Do(req) return err == nil }) } // setupBackends sets up the ether backends and the omnirpc client/server func (i *IntegrationSuite) setupBackends() { var wg sync.WaitGroup // Note: we're intentionally not gonna give these guys any tokens to allow the test to do it. What we will do is give them some eth and store the keys. var err error i.relayerWallet, err = wallet.FromRandom() i.NoError(err) i.guardWallet, err = wallet.FromRandom() i.NoError(err) i.userWallet, err = wallet.FromRandom() i.NoError(err) // Technically, we can use anvil for origin and geth for destination since only origin needs to use a block wg.Add(2) go func() { defer wg.Done() options := anvil.NewAnvilOptionBuilder() options.SetChainID(1) err = retry.WithBackoff(i.GetTestContext(), func(ctx context.Context) error { i.originBackend, err = anvil.NewAnvilBackend(i.GetTestContext(), i.T(), options) if err != nil { return fmt.Errorf("failed to create anvil backend: %w", err) } return nil }, retry.WithMaxTotalTime(5*time.Minute)) i.Nil(err) i.setupBE(i.originBackend) }() go func() { defer wg.Done() options := anvil.NewAnvilOptionBuilder() options.SetChainID(destBackendChainID) err = retry.WithBackoff(i.GetTestContext(), func(ctx context.Context) error { i.destBackend, err = anvil.NewAnvilBackend(i.GetTestContext(), i.T(), options) if err != nil { return fmt.Errorf("failed to create anvil backend: %w", err) } return nil }, retry.WithMaxTotalTime(5*time.Minute)) i.Nil(err) i.setupBE(i.destBackend) }() wg.Wait() i.omniServer = testhelper.NewOmnirpcServer(i.GetTestContext(), i.T(), i.originBackend, i.destBackend) i.omniClient = omnirpcClient.NewOmnirpcClient(i.omniServer, i.metrics, omnirpcClient.WithCaptureReqRes()) i.setupCCTP() } // setupBe sets up one backend func (i *IntegrationSuite) setupBE(backend backends.SimulatedTestBackend) { // prdeploys are contracts we want to deploy before running the test to speed it up. Obviously, these can be deployed when we need them as well, // but this way we can do something while we're waiting for the other backend to startup. // no need to wait for these to deploy since they can happen in background as soon as the backend is up. predeployTokens := []contracts.ContractType{testutil.DAIType, testutil.USDTType, testutil.WETH9Type} predeploys := append(predeployTokens, testutil.FastBridgeType) slices.Reverse(predeploys) // return fast bridge first ethAmount := *new(big.Int).Mul(big.NewInt(params.Ether), big.NewInt(10)) // store the keys backend.Store(base.WalletToKey(i.T(), i.relayerWallet)) backend.Store(base.WalletToKey(i.T(), i.guardWallet)) backend.Store(base.WalletToKey(i.T(), i.userWallet)) // fund each of the wallets backend.FundAccount(i.GetTestContext(), i.relayerWallet.Address(), ethAmount) backend.FundAccount(i.GetTestContext(), i.guardWallet.Address(), ethAmount) backend.FundAccount(i.GetTestContext(), i.userWallet.Address(), ethAmount) var wg sync.WaitGroup // TODO: in the case of relayer this not finishing before the test starts can lead to race conditions since // nonce may be shared between submitter and relayer. Think about how to deal w/ this. for _, user := range []wallet.Wallet{i.relayerWallet, i.guardWallet, i.userWallet} { wg.Add(1) go func(userWallet wallet.Wallet) { defer wg.Done() for _, token := range predeployTokens { i.Approve(backend, i.manager.Get(i.GetTestContext(), backend, token), userWallet) } }(user) } wg.Wait() } func (i *IntegrationSuite) setupCCTP() { // deploy the contract to all backends testBackends := core.ToSlice(i.originBackend, i.destBackend) // register remote deployments and tokens for _, b := range testBackends { backend := b err := retry.WithBackoff(i.GetTestContext(), func(_ context.Context) (err error) { cctpContract, cctpHandle := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), backend) _, tokenMessengeHandle := i.cctpDeployManager.GetMockTokenMessengerType(i.GetTestContext(), backend) // on the above contract, set the remote for each backend for _, backendToSetFrom := range core.ToSlice(i.originBackend, i.destBackend) { // we don't need to set the backends own remote! if backendToSetFrom.GetChainID() == backend.GetChainID() { continue } remoteCCTP, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), backendToSetFrom) remoteMessenger, _ := i.cctpDeployManager.GetMockTokenMessengerType(i.GetTestContext(), backendToSetFrom) txOpts := backend.GetTxContext(i.GetTestContext(), cctpContract.OwnerPtr()) // set the remote cctp contract on this cctp contract // TODO: verify chainID / domain are correct remoteDomain := cctpTest.ChainIDDomainMap[uint32(remoteCCTP.ChainID().Int64())] tx, err := cctpHandle.SetRemoteDomainConfig(txOpts.TransactOpts, big.NewInt(remoteCCTP.ChainID().Int64()), remoteDomain, remoteCCTP.Address()) if err != nil { return fmt.Errorf("could not set remote domain config: %w", err) } backend.WaitForConfirmation(i.GetTestContext(), tx) // register the remote token messenger on the tokenMessenger contract _, err = tokenMessengeHandle.SetRemoteTokenMessenger(txOpts.TransactOpts, uint32(backendToSetFrom.GetChainID()), addressToBytes32(remoteMessenger.Address())) if err != nil { return fmt.Errorf("could not set remote token messenger: %w", err) } } return nil }, retry.WithMaxTotalTime(30*time.Second)) i.NoError(err) } } // addressToBytes32 converts an address to a bytes32. func addressToBytes32(addr common.Address) [32]byte { var buf [32]byte copy(buf[:], addr[:]) return buf } func (i *IntegrationSuite) waitForContractDeployment(ctx context.Context, backend backends.SimulatedTestBackend, address common.Address) error { // nolint: wrapcheck return retry.WithBackoff(ctx, func(_ context.Context) error { code, err := backend.CodeAt(ctx, address, nil) if err != nil { return fmt.Errorf("could not get code: %w", err) } if len(code) == 0 { return fmt.Errorf("contract not deployed at %s", address.Hex()) } return nil }, retry.WithMaxTotalTime(30*time.Second)) } func (i *IntegrationSuite) Approve(backend backends.SimulatedTestBackend, token contracts.DeployedContract, user wallet.Wallet) { err := i.waitForContractDeployment(i.GetTestContext(), backend, token.Address()) i.Require().NoError(err, "Failed to wait for contract deployment") erc20, err := ierc20.NewIERC20(token.Address(), backend) i.Require().NoError(err, "Failed to get erc20") _, fastBridge := i.manager.GetFastBridge(i.GetTestContext(), backend) allowance, err := erc20.Allowance(&bind.CallOpts{Context: i.GetTestContext()}, user.Address(), fastBridge.Address()) i.Require().NoError(err, "Failed to get allowance") if allowance.Cmp(big.NewInt(0)) == 0 { txOpts := backend.GetTxContext(i.GetTestContext(), user.AddressPtr()) tx, err := erc20.Approve(txOpts.TransactOpts, fastBridge.Address(), core.CopyBigInt(abi.MaxUint256)) i.Require().NoError(err, "Failed to approve") backend.WaitForConfirmation(i.GetTestContext(), tx) } } func (i *IntegrationSuite) getRelayerConfig() relconfig.Config { // construct the config relayerAPIPort, err := freeport.GetFreePort() i.NoError(err) dsn := filet.TmpDir(i.T(), "") cctpContractOrigin, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), i.originBackend) cctpContractDest, _ := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), i.destBackend) return relconfig.Config{ // generated ex-post facto Chains: map[int]relconfig.ChainConfig{ originBackendChainID: { RFQAddress: i.manager.Get(i.GetTestContext(), i.originBackend, testutil.FastBridgeType).Address().String(), RebalanceConfigs: relconfig.RebalanceConfigs{ Synapse: &relconfig.SynapseCCTPRebalanceConfig{ SynapseCCTPAddress: cctpContractOrigin.Address().Hex(), }, }, Confirmations: 0, Tokens: map[string]relconfig.TokenConfig{ "ETH": { Address: util.EthAddress.String(), PriceUSD: 2000, Decimals: 18, }, }, NativeToken: "ETH", }, destBackendChainID: { RFQAddress: i.manager.Get(i.GetTestContext(), i.destBackend, testutil.FastBridgeType).Address().String(), RebalanceConfigs: relconfig.RebalanceConfigs{ Synapse: &relconfig.SynapseCCTPRebalanceConfig{ SynapseCCTPAddress: cctpContractDest.Address().Hex(), }, }, Confirmations: 0, Tokens: map[string]relconfig.TokenConfig{ "ETH": { Address: util.EthAddress.String(), PriceUSD: 2000, Decimals: 18, }, }, NativeToken: "ETH", }, }, OmniRPCURL: i.omniServer, // TODO: need to stop hardcoding Database: relconfig.DatabaseConfig{ Type: dbcommon.Sqlite.String(), DSN: dsn, }, // generated ex-post facto QuotableTokens: map[string][]string{}, RFQAPIURL: i.apiServer, Signer: signerConfig.SignerConfig{ Type: signerConfig.FileType.String(), File: filet.TmpFile(i.T(), "", i.relayerWallet.PrivateKeyHex()).Name(), }, RelayerAPIPort: strconv.Itoa(relayerAPIPort), BaseChainConfig: relconfig.ChainConfig{ OriginGasEstimate: 500000, DestGasEstimate: 1000000, }, FeePricer: relconfig.FeePricerConfig{ GasPriceCacheTTLSeconds: 60, TokenPriceCacheTTLSeconds: 60, }, RebalanceInterval: 0, VolumeLimit: 10_000, } } //nolint:gocognit,cyclop func (i *IntegrationSuite) setupRelayer() { // add myself as a filler var wg sync.WaitGroup wg.Add(2) for _, backend := range core.ToSlice(i.originBackend, i.destBackend) { go func(backend backends.SimulatedTestBackend) { defer wg.Done() err := retry.WithBackoff(i.GetTestContext(), func(ctx context.Context) error { metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend) txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr()) relayerRole, err := rfqContract.RELAYERROLE(&bind.CallOpts{Context: i.GetTestContext()}) if err != nil { return fmt.Errorf("could not get relayer role: %w", err) } tx, err := rfqContract.GrantRole(txContext.TransactOpts, relayerRole, i.relayerWallet.Address()) if err != nil { return fmt.Errorf("could not grant role: %w", err) } backend.WaitForConfirmation(i.GetTestContext(), tx) return nil }, retry.WithMaxTotalTime(15*time.Second)) i.NoError(err) }(backend) } wg.Wait() cfg := i.getRelayerConfig() // in the first backend, we want to deploy a bunch of different tokens // TODO: functionalize me. for _, backend := range core.ToSlice(i.originBackend, i.destBackend) { tokenTypes := []contracts.ContractType{testutil.DAIType, testutil.USDTType, testutil.WETH9Type, cctpTest.MockMintBurnTokenType} for _, tokenType := range tokenTypes { useCCTP := tokenType == cctpTest.MockMintBurnTokenType var tokenAddress string if useCCTP { tokenAddress = i.cctpDeployManager.Get(i.GetTestContext(), backend, cctpTest.MockMintBurnTokenType).Address().String() } else { tokenAddress = i.manager.Get(i.GetTestContext(), backend, tokenType).Address().String() } quotableTokenID := fmt.Sprintf("%d-%s", backend.GetChainID(), tokenAddress) tokenCaller, err := ierc20.NewIerc20Ref(common.HexToAddress(tokenAddress), backend) i.NoError(err) decimals, err := tokenCaller.Decimals(&bind.CallOpts{Context: i.GetTestContext()}) i.NoError(err) rebalanceMethod := "" if useCCTP { rebalanceMethod = "synapsecctp" } // first the simple part, add the token to the token map cfg.Chains[int(backend.GetChainID())].Tokens[tokenType.Name()] = relconfig.TokenConfig{ Address: tokenAddress, Decimals: decimals, PriceUSD: 1, // TODO: this will break on non-stables RebalanceMethods: []string{rebalanceMethod}, MaintenanceBalancePct: 20, InitialBalancePct: 50, } compatibleTokens := []contracts.ContractType{tokenType} // DAI/USDC are fungible if tokenType == testutil.DAIType || tokenType == cctpTest.MockMintBurnTokenType { compatibleTokens = []contracts.ContractType{testutil.DAIType, cctpTest.MockMintBurnTokenType} } // now we need to add the token to the quotable tokens map for _, token := range compatibleTokens { otherBackend := i.getOtherBackend(backend) var otherToken string if token == cctpTest.MockMintBurnTokenType { otherToken = i.cctpDeployManager.Get(i.GetTestContext(), otherBackend, cctpTest.MockMintBurnTokenType).Address().String() } else { otherToken = i.manager.Get(i.GetTestContext(), otherBackend, token).Address().String() } cfg.QuotableTokens[quotableTokenID] = append(cfg.QuotableTokens[quotableTokenID], fmt.Sprintf("%d-%s", otherBackend.GetChainID(), otherToken)) } // register the token with cctp contract cctpContract, cctpHandle := i.cctpDeployManager.GetSynapseCCTP(i.GetTestContext(), backend) txOpts := backend.GetTxContext(i.GetTestContext(), cctpContract.OwnerPtr()) tokenName := fmt.Sprintf("CCTP.%s", tokenType.Name()) tx, err := cctpHandle.AddToken(txOpts.TransactOpts, tokenName, tokenCaller.Address(), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0)) i.Require().NoError(err) backend.WaitForConfirmation(i.GetTestContext(), tx) } } // Add ETH as quotable token from origin to destination cfg.QuotableTokens[fmt.Sprintf("%d-%s", originBackendChainID, util.EthAddress)] = []string{ fmt.Sprintf("%d-%s", destBackendChainID, util.EthAddress), } cfg.QuotableTokens[fmt.Sprintf("%d-%s", destBackendChainID, util.EthAddress)] = []string{ fmt.Sprintf("%d-%s", originBackendChainID, util.EthAddress), } var err error i.relayer, err = service.NewRelayer(i.GetTestContext(), i.metrics, cfg) i.NoError(err) dbType, err := dbcommon.DBTypeFromString(cfg.Database.Type) i.NoError(err) i.store, err = connect.Connect(i.GetTestContext(), dbType, cfg.Database.DSN, i.metrics) i.NoError(err) } func (i *IntegrationSuite) setupGuard() { // add myself as a guard var wg sync.WaitGroup wg.Add(2) for _, backend := range core.ToSlice(i.originBackend, i.destBackend) { go func(backend backends.SimulatedTestBackend) { defer wg.Done() metadata, rfqContract := i.manager.GetFastBridge(i.GetTestContext(), backend) txContext := backend.GetTxContext(i.GetTestContext(), metadata.OwnerPtr()) guardRole, err := rfqContract.GUARDROLE(&bind.CallOpts{Context: i.GetTestContext()}) i.NoError(err) tx, err := rfqContract.GrantRole(txContext.TransactOpts, guardRole, i.guardWallet.Address()) i.NoError(err) backend.WaitForConfirmation(i.GetTestContext(), tx) }(backend) } wg.Wait() relayerCfg := i.getRelayerConfig() guardCfg := guardconfig.NewGuardConfigFromRelayer(relayerCfg) guardCfg.Signer = signerConfig.SignerConfig{ Type: signerConfig.FileType.String(), File: filet.TmpFile(i.T(), "", i.guardWallet.PrivateKeyHex()).Name(), } var err error i.guard, err = guardService.NewGuard(i.GetTestContext(), i.metrics, guardCfg, nil) i.NoError(err) dbType, err := dbcommon.DBTypeFromString(guardCfg.Database.Type) i.NoError(err) i.guardStore, err = guardConnect.Connect(i.GetTestContext(), dbType, guardCfg.Database.DSN, i.metrics) i.NoError(err) }