From 6d69c824d6fbc8a88e179ea85c974e82ec1c5b99 Mon Sep 17 00:00:00 2001 From: Anindita Ghosh <88458927+AnieeG@users.noreply.github.com> Date: Wed, 8 Nov 2023 19:58:45 -0800 Subject: [PATCH] multicall ccip-send (#250) --- .github/workflows/integration-tests.yml | 6 + .../ccip-tests/actions/ccip_helpers.go | 322 +++++++++++++----- .../ccip-tests/contracts/contract_deployer.go | 30 ++ .../contracts/laneconfig/parse_contracts.go | 1 + .../ccip-tests/contracts/multicall.go | 215 ++++++++++++ .../ccip-tests/load/ccip_loadgen.go | 8 +- .../ccip-tests/smoke/ccip_test.go | 65 +++- .../ccip-tests/testconfig/ccip.go | 13 + .../ccip-tests/testconfig/tomls/default.toml | 6 + .../ccip-tests/testreporters/ccip.go | 16 +- .../ccip-tests/testsetups/ccip.go | 23 +- 11 files changed, 601 insertions(+), 104 deletions(-) create mode 100644 integration-tests/ccip-tests/contracts/multicall.go diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 75880d69ead..1fc4e3440cd 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -269,6 +269,12 @@ jobs: os: ubuntu20.04-16cores-64GB file: ccip run: -run ^TestSmokeCCIPRateLimit$ + - name: ccip-smoke-multicall + nodes: 1 + dir: ccip-tests/smoke + os: ubuntu20.04-16cores-64GB + file: ccip + run: -run ^TestSmokeCCIPMulticall$ - name: cron nodes: 1 os: ubuntu-latest diff --git a/integration-tests/ccip-tests/actions/ccip_helpers.go b/integration-tests/ccip-tests/actions/ccip_helpers.go index 00bdbe46a0e..29354742213 100644 --- a/integration-tests/ccip-tests/actions/ccip_helpers.go +++ b/integration-tests/ccip-tests/actions/ccip_helpers.go @@ -562,6 +562,8 @@ func DefaultCCIPModule(logger zerolog.Logger, chainClient blockchain.EVMClient, type SourceCCIPModule struct { Common *CCIPCommon + MulticallEnabled bool + MulticallContract common.Address Sender common.Address TransferAmount []*big.Int DestinationChainId uint64 @@ -602,6 +604,9 @@ func (sourceCCIP *SourceCCIPModule) LoadContracts(conf *laneconfig.LaneConfig) { EthAddress: common.HexToAddress(cfg.OnRamp), } } + if common.IsHexAddress(cfg.Multicall) { + sourceCCIP.MulticallContract = common.HexToAddress(cfg.Multicall) + } if cfg.DepolyedAt > 0 { sourceCCIP.SrcStartBlock = cfg.DepolyedAt } @@ -658,6 +663,12 @@ func (sourceCCIP *SourceCCIPModule) DeployContracts(lane *laneconfig.LaneConfig) return errors.WithStack(err) } + if sourceCCIP.MulticallContract == (common.Address{}) && sourceCCIP.MulticallEnabled { + sourceCCIP.MulticallContract, err = contractDeployer.DeployMultiCallContract() + if err != nil { + return errors.WithStack(err) + } + } if sourceCCIP.OnRamp == nil { var tokensAndPools []evm_2_evm_onramp.InternalPoolUpdate var tokenTransferFeeConfig []evm_2_evm_onramp.EVM2EVMOnRampTokenTransferFeeConfigArgs @@ -837,26 +848,29 @@ func (sourceCCIP *SourceCCIPModule) UpdateBalance( func (sourceCCIP *SourceCCIPModule) AssertSendRequestedLogFinalized( lggr zerolog.Logger, - seqNum uint64, - SendRequested *evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, + txHash common.Hash, prevEventAt time.Time, - reqStat *testreporters.RequestStat, + reqStats []*testreporters.RequestStat, ) (time.Time, uint64, error) { if sourceCCIP.Common.ChainClient.NetworkSimulated() { return prevEventAt, 0, nil } lggr.Info().Msg("Waiting for CCIPSendRequested event log to be finalized") - finalizedBlockNum, finalizedAt, err := sourceCCIP.Common.ChainClient.WaitForFinalizedTx(SendRequested.Raw.TxHash) + finalizedBlockNum, finalizedAt, err := sourceCCIP.Common.ChainClient.WaitForFinalizedTx(txHash) if err != nil { - reqStat.UpdateState(lggr, seqNum, testreporters.SourceLogFinalized, time.Since(prevEventAt), testreporters.Failure) + for _, stat := range reqStats { + stat.UpdateState(lggr, stat.SeqNum, testreporters.SourceLogFinalized, time.Since(prevEventAt), testreporters.Failure) + } return time.Time{}, 0, fmt.Errorf("error waiting for CCIPSendRequested event log to be finalized - %+v", err) } - reqStat.UpdateState(lggr, seqNum, testreporters.SourceLogFinalized, finalizedAt.Sub(prevEventAt), testreporters.Success, - testreporters.TransactionStats{ - TxHash: SendRequested.Raw.TxHash.Hex(), - FinalizedByBlock: finalizedBlockNum.String(), - FinalizedAt: finalizedAt.String(), - }) + for _, stat := range reqStats { + stat.UpdateState(lggr, stat.SeqNum, testreporters.SourceLogFinalized, finalizedAt.Sub(prevEventAt), testreporters.Success, + testreporters.TransactionStats{ + TxHash: txHash.Hex(), + FinalizedByBlock: finalizedBlockNum.String(), + FinalizedAt: finalizedAt.String(), + }) + } return finalizedAt, finalizedBlockNum.Uint64(), nil } @@ -865,8 +879,8 @@ func (sourceCCIP *SourceCCIPModule) AssertEventCCIPSendRequested( txHash string, timeout time.Duration, prevEventAt time.Time, - reqStat *testreporters.RequestStat, -) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, time.Time, error) { + reqStat []*testreporters.RequestStat, +) ([]*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, time.Time, error) { lggr.Info().Msg("Waiting for CCIPSendRequested event") ticker := time.NewTicker(time.Second) defer ticker.Stop() @@ -878,61 +892,76 @@ func (sourceCCIP *SourceCCIPModule) AssertEventCCIPSendRequested( case <-ticker.C: value, ok := sourceCCIP.CCIPSendRequestedWatcher.Load(txHash) if ok { - if sendRequestedEvent, exists := value.(*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested); exists { + // if sendrequested events are found, check if the number of events are same as the number of requests + if sendRequestedEvents, exists := value.([]*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested); exists && len(sendRequestedEvents) == len(reqStat) { // if the value is processed, delete it from the map sourceCCIP.CCIPSendRequestedWatcher.Delete(txHash) - sentMsg := sendRequestedEvent.Message - seqNum := sentMsg.SequenceNumber - // prevEventAt is the time when the message was successful, this should be same as the time when the event was emitted - reqStat.UpdateState(lggr, seqNum, testreporters.CCIPSendRe, 0, testreporters.Success) - return sendRequestedEvent, prevEventAt, nil + for i, sendRequestedEvent := range sendRequestedEvents { + sentMsg := sendRequestedEvent.Message + seqNum := sentMsg.SequenceNumber + // prevEventAt is the time when the message was successful, this should be same as the time when the event was emitted + reqStat[i].UpdateState(lggr, seqNum, testreporters.CCIPSendRe, 0, testreporters.Success) + } + return sendRequestedEvents, prevEventAt, nil } } case <-ctx.Done(): - reqStat.UpdateState(lggr, 0, testreporters.CCIPSendRe, time.Since(prevEventAt), testreporters.Failure) + for _, stat := range reqStat { + stat.UpdateState(lggr, 0, testreporters.CCIPSendRe, time.Since(prevEventAt), testreporters.Failure) + } return nil, time.Now(), fmt.Errorf("CCIPSendRequested event is not found for tx %s", txHash) } } } -func (sourceCCIP *SourceCCIPModule) SendRequest( +func (sourceCCIP *SourceCCIPModule) CCIPMsg( receiver common.Address, msgType, data string, - feeToken common.Address, -) (common.Hash, time.Duration, *big.Int, error) { - var tokenAndAmounts []router.ClientEVMTokenAmount +) (router.ClientEVM2AnyMessage, error) { + tokenAndAmounts := []router.ClientEVMTokenAmount{} if msgType == TokenTransfer { - for i := range sourceCCIP.TransferAmount { + for i, amount := range sourceCCIP.TransferAmount { token := sourceCCIP.Common.BridgeTokens[i] tokenAndAmounts = append(tokenAndAmounts, router.ClientEVMTokenAmount{ - Token: common.HexToAddress(token.Address()), Amount: sourceCCIP.TransferAmount[i], + Token: common.HexToAddress(token.Address()), Amount: amount, }) } } receiverAddr, err := utils.ABIEncode(`[{"type":"address"}]`, receiver) - var d time.Duration if err != nil { - return common.Hash{}, d, nil, fmt.Errorf("failed encoding the receiver address: %+v", err) + return router.ClientEVM2AnyMessage{}, fmt.Errorf("failed encoding the receiver address: %+v", err) } extraArgsV1, err := testhelpers.GetEVMExtraArgsV1(big.NewInt(600_000), false) if err != nil { - return common.Hash{}, d, nil, fmt.Errorf("failed encoding the options field: %+v", err) - } - destChainSelector, err := chainselectors.SelectorFromChainId(sourceCCIP.DestinationChainId) - if err != nil { - return common.Hash{}, d, nil, fmt.Errorf("failed getting the chain selector: %+v", err) + return router.ClientEVM2AnyMessage{}, fmt.Errorf("failed encoding the options field: %+v", err) } // form the message for transfer - msg := router.ClientEVM2AnyMessage{ + return router.ClientEVM2AnyMessage{ Receiver: receiverAddr, Data: []byte(data), TokenAmounts: tokenAndAmounts, - FeeToken: feeToken, + FeeToken: common.HexToAddress(sourceCCIP.Common.FeeToken.Address()), ExtraArgs: extraArgsV1, - } + }, nil +} +func (sourceCCIP *SourceCCIPModule) SendRequest( + receiver common.Address, + msgType, + data string, +) (common.Hash, time.Duration, *big.Int, error) { + var d time.Duration + destChainSelector, err := chainselectors.SelectorFromChainId(sourceCCIP.DestinationChainId) + if err != nil { + return common.Hash{}, d, nil, fmt.Errorf("failed getting the chain selector: %+v", err) + } + // form the message for transfer + msg, err := sourceCCIP.CCIPMsg(receiver, msgType, data) + if err != nil { + return common.Hash{}, d, nil, fmt.Errorf("failed forming the ccip msg: %+v", err) + } fee, err := sourceCCIP.Common.Router.GetFee(destChainSelector, msg) if err != nil { reason, _ := blockchain.RPCErrorFromError(err) @@ -945,10 +974,10 @@ func (sourceCCIP *SourceCCIPModule) SendRequest( var sendTx *types.Transaction timeNow := time.Now() - + feeToken := common.HexToAddress(sourceCCIP.Common.FeeToken.Address()) // initiate the transfer // if the token address is 0x0 it will use Native as fee token and the fee amount should be mentioned in bind.TransactOpts's value - if feeToken != common.HexToAddress("0x0") { + if feeToken != (common.Address{}) { sendTx, err = sourceCCIP.Common.Router.CCIPSendAndProcessTx(destChainSelector, msg, nil) if err != nil { return common.Hash{}, time.Since(timeNow), nil, fmt.Errorf("failed initiating the transfer ccip-send: %+v", err) @@ -968,7 +997,7 @@ func (sourceCCIP *SourceCCIPModule) SendRequest( return sendTx.Hash(), time.Since(timeNow), fee, nil } -func DefaultSourceCCIPModule(logger zerolog.Logger, chainClient blockchain.EVMClient, destChainId uint64, destChain string, transferAmount []*big.Int, ccipCommon *CCIPCommon) (*SourceCCIPModule, error) { +func DefaultSourceCCIPModule(logger zerolog.Logger, chainClient blockchain.EVMClient, destChainId uint64, destChain string, transferAmount []*big.Int, multicallEnabled bool, ccipCommon *CCIPCommon) (*SourceCCIPModule, error) { cmn, err := ccipCommon.Copy(logger, chainClient) if err != nil { return nil, err @@ -976,6 +1005,7 @@ func DefaultSourceCCIPModule(logger zerolog.Logger, chainClient blockchain.EVMCl return &SourceCCIPModule{ Common: cmn, TransferAmount: transferAmount, + MulticallEnabled: multicallEnabled, DestinationChainId: destChainId, DestNetworkName: destChain, Sender: common.HexToAddress(chainClient.GetDefaultWallet().Address()), @@ -1443,6 +1473,7 @@ func DefaultDestinationCCIPModule(logger zerolog.Logger, chainClient blockchain. } type CCIPRequest struct { + ReqNo int64 txHash string txConfirmationTimestamp time.Time RequestStat *testreporters.RequestStat @@ -1482,7 +1513,7 @@ type CCIPLane struct { Balance *BalanceSheet StartBlockOnSource uint64 StartBlockOnDestination uint64 - SentReqs map[int64]CCIPRequest + SentReqs map[common.Hash][]CCIPRequest TotalFee *big.Int // total fee for all the requests. Used for balance validation. ValidationTimeout time.Duration Context context.Context @@ -1513,6 +1544,7 @@ func (lane *CCIPLane) UpdateLaneConfig() { lane.SrcNetworkLaneCfg.SrcContractsMu.Lock() lane.SrcNetworkLaneCfg.SrcContracts[lane.Source.DestNetworkName] = laneconfig.SourceContracts{ OnRamp: lane.Source.OnRamp.Address(), + Multicall: lane.Source.MulticallContract.Hex(), DepolyedAt: lane.Source.SrcStartBlock, } lane.SrcNetworkLaneCfg.SrcContractsMu.Unlock() @@ -1560,20 +1592,127 @@ func (lane *CCIPLane) RecordStateBeforeTransfer() { require.NoError(lane.Test, err, "Getting current block should be successful in dest chain") lane.TotalFee = big.NewInt(0) lane.NumberOfReq = 0 - lane.SentReqs = make(map[int64]CCIPRequest) + lane.SentReqs = make(map[common.Hash][]CCIPRequest) } -func (lane *CCIPLane) AddToSentReqs(txHash common.Hash, reqStat *testreporters.RequestStat) (*types.Receipt, error) { +func (lane *CCIPLane) AddToSentReqs(txHash common.Hash, reqStats []*testreporters.RequestStat) (*types.Receipt, error) { request, rcpt, err := CCIPRequestFromTxHash(txHash, lane.Source.Common.ChainClient) if err != nil { + for _, stat := range reqStats { + stat.UpdateState(lane.Logger, 0, testreporters.TX, 0, testreporters.Failure) + } return rcpt, fmt.Errorf("could not get request from tx hash %s: %+v", txHash.Hex(), err) } - request.RequestStat = reqStat - lane.SentReqs[int64(lane.NumberOfReq+1)] = request - lane.NumberOfReq++ + var allRequests []CCIPRequest + for _, stat := range reqStats { + allRequests = append(allRequests, CCIPRequest{ + ReqNo: stat.ReqNo, + txHash: request.txHash, + txConfirmationTimestamp: request.txConfirmationTimestamp, + RequestStat: stat, + }) + lane.NumberOfReq++ + } + lane.SentReqs[txHash] = allRequests return rcpt, nil } +// Multicall sends multiple ccip-send requests in a single transaction +// It will create one transaction for all the requests and will wait for the confirmation +func (lane *CCIPLane) Multicall(noOfRequests int, msgType string, multiSendAddr common.Address) error { + var ccipMultipleMsg []contracts.CCIPMsgData + feeToken := common.HexToAddress(lane.Source.Common.FeeToken.Address()) + genericMsg, err := lane.Source.CCIPMsg(lane.Dest.ReceiverDapp.EthAddress, msgType, "testMsg") + if err != nil { + return fmt.Errorf("failed to form the ccip message: %+v", err) + } + destChainSelector, err := chainselectors.SelectorFromChainId(lane.Source.DestinationChainId) + if err != nil { + return fmt.Errorf("failed getting the chain selector: %+v", err) + } + var reqStats []*testreporters.RequestStat + var txstats []testreporters.TransactionStats + for i := 1; i <= noOfRequests; i++ { + // form the message for transfer + msg := genericMsg + msg.Data = []byte(fmt.Sprintf("msg %d", i)) + sendData := contracts.CCIPMsgData{ + Msg: msg, + RouterAddr: lane.Source.Common.Router.EthAddress, + ChainSelector: destChainSelector, + } + + fee, err := lane.Source.Common.Router.GetFee(destChainSelector, msg) + if err != nil { + reason, _ := blockchain.RPCErrorFromError(err) + if reason != "" { + return fmt.Errorf("failed getting the fee: %s", reason) + } + return fmt.Errorf("failed getting the fee: %+v", err) + } + log.Info().Str("fee", fee.String()).Msg("calculated fee") + sendData.Fee = fee + lane.TotalFee = new(big.Int).Add(lane.TotalFee, fee) + ccipMultipleMsg = append(ccipMultipleMsg, sendData) + // if token transfer is required, transfer the token amount to multisend + if msgType == TokenTransfer { + for i, amount := range lane.Source.TransferAmount { + token := lane.Source.Common.BridgeTokens[i] + err := token.Transfer(multiSendAddr.Hex(), amount) + if err != nil { + return err + } + } + } + stat := testreporters.NewCCIPRequestStats(int64(lane.NumberOfReq + i)) + txstats = append(txstats, testreporters.TransactionStats{ + Fee: fee.String(), + NoOfTokensSent: len(msg.TokenAmounts), + MessageBytesLength: len(msg.Data), + }) + reqStats = append(reqStats, stat) + } + isNative := true + // transfer the fee amount to multisend + if feeToken != (common.Address{}) { + isNative = false + err := lane.Source.Common.FeeToken.Transfer(multiSendAddr.Hex(), lane.TotalFee) + if err != nil { + return err + } + } + + tx, err := contracts.MultiCallCCIP(lane.Source.Common.ChainClient, multiSendAddr.Hex(), ccipMultipleMsg, isNative) + if err != nil { + return fmt.Errorf("failed to send the multicall: %+v", err) + } + if err != nil { + // update the stats as failure for all the requests in the multicall tx + for _, stat := range reqStats { + stat.UpdateState(lane.Logger, 0, + testreporters.TX, 0, testreporters.Failure) + } + return fmt.Errorf("failed to send the multicall: %+v", err) + } + rcpt, err := lane.AddToSentReqs(tx.Hash(), reqStats) + if err != nil { + return err + } + var gasUsed uint64 + if rcpt != nil { + gasUsed = rcpt.GasUsed + } + // update the stats for all the requests in the multicall tx + for i, stat := range reqStats { + txstats[i].GasUsed = gasUsed + txstats[i].TxHash = tx.Hash().Hex() + stat.UpdateState(lane.Logger, 0, testreporters.TX, 0, testreporters.Success, txstats[i]) + } + return nil +} + +// SendRequests sends individual ccip-send requests in different transactions +// It will create noOfRequests transactions func (lane *CCIPLane) SendRequests(noOfRequests int, msgType string) error { for i := 1; i <= noOfRequests; i++ { msg := fmt.Sprintf("msg %d", i) @@ -1581,7 +1720,6 @@ func (lane *CCIPLane) SendRequests(noOfRequests int, msgType string) error { txHash, txConfirmationDur, fee, err := lane.Source.SendRequest( lane.Dest.ReceiverDapp.EthAddress, msgType, msg, - common.HexToAddress(lane.Source.Common.FeeToken.Address()), ) if err != nil { stat.UpdateState(lane.Logger, 0, @@ -1599,10 +1737,8 @@ func (lane *CCIPLane) SendRequests(noOfRequests int, msgType string) error { if msgType == DataOnlyTransfer { noOfTokens = 0 } - rcpt, err := lane.AddToSentReqs(txHash, stat) + rcpt, err := lane.AddToSentReqs(txHash, []*testreporters.RequestStat{stat}) if err != nil { - stat.UpdateState(lane.Logger, 0, - testreporters.TX, txConfirmationDur, testreporters.Failure) return err } var gasUsed uint64 @@ -1624,8 +1760,9 @@ func (lane *CCIPLane) SendRequests(noOfRequests int, msgType string) error { } func (lane *CCIPLane) ValidateRequests() { - for i, tx := range lane.SentReqs { - require.NoError(lane.Test, lane.ValidateRequestByTxHash(tx.txHash, tx.txConfirmationTimestamp, i), + for txHash, ccipReqs := range lane.SentReqs { + require.Greater(lane.Test, len(ccipReqs), 0, "no ccip requests found for tx hash") + require.NoError(lane.Test, lane.ValidateRequestByTxHash(txHash), "validating request events by tx hash") } // Asserting balances reliably work only for simulated private chains. The testnet contract balances might get updated by other transactions @@ -1636,40 +1773,59 @@ func (lane *CCIPLane) ValidateRequests() { } } -func (lane *CCIPLane) ValidateRequestByTxHash(txHash string, txConfirmation time.Time, reqNo int64) error { - reqStat := lane.SentReqs[reqNo].RequestStat - defer lane.Reports.UpdatePhaseStatsForReq(reqStat) - msgLog, ccipSendReqGenAt, err := lane.Source.AssertEventCCIPSendRequested( - lane.Logger, txHash, lane.ValidationTimeout, txConfirmation, reqStat) - if err != nil || msgLog == nil { - return fmt.Errorf("could not validate CCIPSendRequested event: %+v", err) +func (lane *CCIPLane) ValidateRequestByTxHash(txHash common.Hash) error { + var reqStats []*testreporters.RequestStat + ccipRequests := lane.SentReqs[txHash] + require.Greater(lane.Test, len(ccipRequests), 0, "no ccip requests found for tx hash") + txConfirmation := ccipRequests[0].txConfirmationTimestamp + for _, req := range ccipRequests { + reqStats = append(reqStats, req.RequestStat) + defer lane.Reports.UpdatePhaseStatsForReq(req.RequestStat) } - seqNumber := msgLog.Message.SequenceNumber - sourceLogFinalizedAt, _, err := lane.Source.AssertSendRequestedLogFinalized(lane.Logger, seqNumber, msgLog, ccipSendReqGenAt, reqStat) - if err != nil { - return fmt.Errorf("could not finalize CCIPSendRequested event: %+v", err) + msgLogs, ccipSendReqGenAt, err := lane.Source.AssertEventCCIPSendRequested( + lane.Logger, txHash.Hex(), lane.ValidationTimeout, txConfirmation, reqStats) + if err != nil || msgLogs == nil { + return fmt.Errorf("could not validate CCIPSendRequested event: %+v", err) } - err = lane.Dest.AssertSeqNumberExecuted(lane.Logger, seqNumber, lane.ValidationTimeout, sourceLogFinalizedAt, reqStat) + sourceLogFinalizedAt, _, err := lane.Source.AssertSendRequestedLogFinalized(lane.Logger, txHash, ccipSendReqGenAt, reqStats) if err != nil { - return fmt.Errorf("could not validate seq number increase at commit store: %+v", err) + return fmt.Errorf("could not finalize CCIPSendRequested event: %+v", err) } + for _, msgLog := range msgLogs { + seqNumber := msgLog.Message.SequenceNumber + var reqStat *testreporters.RequestStat + for _, stat := range reqStats { + if stat.SeqNum == seqNumber { + reqStat = stat + break + } + } + if reqStat == nil { + return fmt.Errorf("could not find request stat for seq number %d", seqNumber) + } - // Verify whether commitStore has accepted the report - commitReport, reportAcceptedAt, err := lane.Dest.AssertEventReportAccepted( - lane.Logger, seqNumber, lane.ValidationTimeout, sourceLogFinalizedAt, reqStat) - if err != nil || commitReport == nil { - return fmt.Errorf("could not validate ReportAccepted event: %+v", err) - } + err = lane.Dest.AssertSeqNumberExecuted(lane.Logger, seqNumber, lane.ValidationTimeout, sourceLogFinalizedAt, reqStat) + if err != nil { + return fmt.Errorf("could not validate seq number increase at commit store: %+v", err) + } - reportBlessedAt, err := lane.Dest.AssertReportBlessed(lane.Logger, seqNumber, lane.ValidationTimeout, *commitReport, reportAcceptedAt, reqStat) - if err != nil { - return fmt.Errorf("could not validate ReportBlessed event: %+v", err) - } - // Verify whether the execution state is changed and the transfer is successful - err = lane.Dest.AssertEventExecutionStateChanged(lane.Logger, seqNumber, lane.ValidationTimeout, reportBlessedAt, reqStat) - if err != nil { - return fmt.Errorf("could not validate ExecutionStateChanged event: %+v", err) + // Verify whether commitStore has accepted the report + commitReport, reportAcceptedAt, err := lane.Dest.AssertEventReportAccepted( + lane.Logger, seqNumber, lane.ValidationTimeout, sourceLogFinalizedAt, reqStat) + if err != nil || commitReport == nil { + return fmt.Errorf("could not validate ReportAccepted event: %+v", err) + } + + reportBlessedAt, err := lane.Dest.AssertReportBlessed(lane.Logger, seqNumber, lane.ValidationTimeout, *commitReport, reportAcceptedAt, reqStat) + if err != nil { + return fmt.Errorf("could not validate ReportBlessed event: %+v", err) + } + // Verify whether the execution state is changed and the transfer is successful + err = lane.Dest.AssertEventExecutionStateChanged(lane.Logger, seqNumber, lane.ValidationTimeout, reportBlessedAt, reqStat) + if err != nil { + return fmt.Errorf("could not validate ExecutionStateChanged event: %+v", err) + } } return nil } @@ -1693,7 +1849,12 @@ func (lane *CCIPLane) StartEventWatchers() error { for { e := <-sendReqEvent lane.Logger.Info().Msgf("CCIPSendRequested event received for seq number %d", e.Message.SequenceNumber) - lane.Source.CCIPSendRequestedWatcher.Store(e.Raw.TxHash.Hex(), e) + eventsForTx, ok := lane.Source.CCIPSendRequestedWatcher.Load(e.Raw.TxHash.Hex()) + if ok { + lane.Source.CCIPSendRequestedWatcher.Store(e.Raw.TxHash.Hex(), append(eventsForTx.([]*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested), e)) + } else { + lane.Source.CCIPSendRequestedWatcher.Store(e.Raw.TxHash.Hex(), []*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested{e}) + } } }() reportAcceptedEvent := make(chan *commit_store.CommitStoreReportAccepted) @@ -1781,6 +1942,7 @@ func (lane *CCIPLane) DeployNewCCIPLane( sourceCommon *CCIPCommon, destCommon *CCIPCommon, transferAmounts []*big.Int, + isMulticall bool, bootstrapAdded *atomic.Bool, configureCLNodes bool, jobErrGroup *errgroup.Group, @@ -1801,7 +1963,7 @@ func (lane *CCIPLane) DeployNewCCIPLane( lane.Source, err = DefaultSourceCCIPModule( lane.Logger, sourceChainClient, destChainClient.GetChainID().Uint64(), - destChainClient.GetNetworkName(), transferAmounts, sourceCommon) + destChainClient.GetNetworkName(), transferAmounts, isMulticall, sourceCommon) if err != nil { return nil, nil, errors.WithStack(err) } diff --git a/integration-tests/ccip-tests/contracts/contract_deployer.go b/integration-tests/ccip-tests/contracts/contract_deployer.go index 6bf15f44e76..6895f433eb6 100644 --- a/integration-tests/ccip-tests/contracts/contract_deployer.go +++ b/integration-tests/ccip-tests/contracts/contract_deployer.go @@ -1,6 +1,7 @@ package contracts import ( + "context" "crypto/ed25519" "encoding/hex" "fmt" @@ -8,6 +9,7 @@ import ( "strings" "time" + "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" @@ -55,6 +57,34 @@ func (e *CCIPContractsDeployer) Client() blockchain.EVMClient { return e.evmClient } +func (e *CCIPContractsDeployer) DeployMultiCallContract() (common.Address, error) { + multiCallABI, err := abi.JSON(strings.NewReader(MultiCallABI)) + if err != nil { + return common.Address{}, err + } + address, tx, _, err := e.evmClient.DeployContract("MultiCall Contract", func( + auth *bind.TransactOpts, + backend bind.ContractBackend, + ) (common.Address, *types.Transaction, interface{}, error) { + address, tx, contract, err := bind.DeployContract(auth, multiCallABI, common.FromHex(MultiCallBIN), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, contract, err + }) + if err != nil { + return common.Address{}, err + } + r, err := bind.WaitMined(context.Background(), e.evmClient.DeployBackend(), tx) + if err != nil { + return common.Address{}, err + } + if r.Status != types.ReceiptStatusSuccessful { + return common.Address{}, fmt.Errorf("deploy multicall failed") + } + return *address, nil +} + func (e *CCIPContractsDeployer) DeployLinkTokenContract() (*LinkToken, error) { address, _, instance, err := e.evmClient.DeployContract("Link Token", func( auth *bind.TransactOpts, diff --git a/integration-tests/ccip-tests/contracts/laneconfig/parse_contracts.go b/integration-tests/ccip-tests/contracts/laneconfig/parse_contracts.go index 15ad6b1924d..11c4da80c0f 100644 --- a/integration-tests/ccip-tests/contracts/laneconfig/parse_contracts.go +++ b/integration-tests/ccip-tests/contracts/laneconfig/parse_contracts.go @@ -31,6 +31,7 @@ type CommonContracts struct { type SourceContracts struct { OnRamp string `json:"on_ramp"` + Multicall string `json:"multicall,omitempty"` DepolyedAt uint64 `json:"deployed_at"` } diff --git a/integration-tests/ccip-tests/contracts/multicall.go b/integration-tests/ccip-tests/contracts/multicall.go new file mode 100644 index 00000000000..3f55656805d --- /dev/null +++ b/integration-tests/ccip-tests/contracts/multicall.go @@ -0,0 +1,215 @@ +package contracts + +import ( + "context" + "fmt" + "math/big" + "strings" + + "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/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/smartcontractkit/chainlink-testing-framework/blockchain" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/shared/generated/erc20" +) + +const ( + MultiCallABI = "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call3[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call3Value[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3Value\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"blockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBasefee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"basefee\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBlockNumber\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getChainId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"chainid\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryAggregate\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryBlockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"}]" + MultiCallBIN = "0x608060405234801561001057600080fd5b50610ee0806100206000396000f3fe6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c0033" +) + +type CallWithValue struct { + Target common.Address + AllowFailure bool + Value *big.Int + CallData []byte +} + +type Call struct { + Target common.Address + AllowFailure bool + CallData []byte +} + +type Result struct { + Success bool + ReturnData []byte +} +type CCIPMsgData struct { + RouterAddr common.Address + ChainSelector uint64 + Msg router.ClientEVM2AnyMessage + Fee *big.Int +} + +// ApproveTokenCallData returns the call data for approving a token with approve function of erc20 contract +func ApproveTokenCallData(to common.Address, amount *big.Int) ([]byte, error) { + erc20ABI, err := abi.JSON(strings.NewReader(erc20.ERC20ABI)) + if err != nil { + return nil, err + } + approveToken := erc20ABI.Methods["approve"] + inputs, err := approveToken.Inputs.Pack(to, amount) + if err != nil { + return nil, err + } + inputs = append(approveToken.ID[:], inputs...) + return inputs, nil +} + +// CCIPSendCallData returns the call data for sending a CCIP message with ccipSend function of router contract +func CCIPSendCallData(msg CCIPMsgData) ([]byte, error) { + routerABI, err := abi.JSON(strings.NewReader(router.RouterABI)) + if err != nil { + return nil, err + } + ccipSend := routerABI.Methods["ccipSend"] + sendID := ccipSend.ID + inputs, err := ccipSend.Inputs.Pack( + msg.ChainSelector, + msg.Msg, + ) + if err != nil { + return nil, err + } + inputs = append(sendID[:], inputs...) + return inputs, nil +} + +func WaitForSuccessfulTxMined(evmClient blockchain.EVMClient, tx *types.Transaction) error { + log.Info().Str("tx", tx.Hash().Hex()).Msg("waiting for tx to be mined") + receipt, err := bind.WaitMined(context.Background(), evmClient.DeployBackend(), tx) + if err != nil { + return err + } + if receipt.Status != types.ReceiptStatusSuccessful { + return fmt.Errorf("tx failed %s", tx.Hash().Hex()) + } + log.Info().Str("tx", tx.Hash().Hex()).Msg("tx mined successfully") + return nil +} + +// MultiCallCCIP sends multiple CCIP messages in a single transaction +// if native is true, it will send msg with native as fee. In this case the msg should be sent with a +// msg.value equivalent to the total fee with the help of aggregate3Value +// +// if native is false, it will send msg with fee in specific feetoken. In this case the msg should be sent without value with the help of aggregate3. +// In both cases, if there are any bridge tokens included in ccip transfer, the amount for corresponding token should be approved to the router contract as spender. +// The approval should be done by calling approval function as part of the call data of aggregate3 or aggregate3Value +// If feetoken is used as fee, the amount for feetoken should be approved to the router contract as spender and should be done as part of the call data of aggregate3 +// In case of native as fee, there is no need for fee amount approval +func MultiCallCCIP( + evmClient blockchain.EVMClient, + address string, + msgData []CCIPMsgData, + native bool, +) (*types.Transaction, error) { + contractAddress := common.HexToAddress(address) + multiCallABI, err := abi.JSON(strings.NewReader(MultiCallABI)) + if err != nil { + return nil, err + } + boundContract := bind.NewBoundContract(contractAddress, multiCallABI, evmClient.Backend(), evmClient.Backend(), evmClient.Backend()) + + // if native, use aggregate3Value to send msg with value + if native { + var callData []CallWithValue + allValue := big.NewInt(0) + // create call data for each msg + for _, msg := range msgData { + // approve bridge token + for _, tokenAndAmount := range msg.Msg.TokenAmounts { + inputs, err := ApproveTokenCallData(msg.RouterAddr, tokenAndAmount.Amount) + if err != nil { + return nil, err + } + data := CallWithValue{Target: tokenAndAmount.Token, AllowFailure: false, CallData: inputs} + callData = append(callData, data) + } + inputs, err := CCIPSendCallData(msg) + if err != nil { + return nil, err + } + data := CallWithValue{Target: msg.RouterAddr, AllowFailure: false, Value: msg.Fee, CallData: inputs} + callData = append(callData, data) + allValue.Add(allValue, msg.Fee) + } + opts, err := evmClient.TransactionOpts(evmClient.GetDefaultWallet()) + if err != nil { + return nil, err + } + // the value of transactionOpts is the sum of the value of all msg, which is the total fee of all ccip-sends + opts.Value = allValue + // call aggregate3Value to group all msg call data and send them in a single transaction + tx, err := boundContract.Transact(opts, "aggregate3Value", callData) + if err != nil { + return nil, err + } + err = WaitForSuccessfulTxMined(evmClient, tx) + if err != nil { + return nil, errors.Wrapf(err, "multicall failed for ccip-send; router %s", contractAddress.Hex()) + } + } + // if with feetoken, use aggregate3 to send msg without value + var callData []Call + // create call data for each msg + for _, msg := range msgData { + isFeeTokenAndBridgeTokenSame := false + // approve bridge token + for _, tokenAndAmount := range msg.Msg.TokenAmounts { + var inputs []byte + // if feetoken is same as bridge token, approve total amount including transfer amount + fee amount + if tokenAndAmount.Token == msg.Msg.FeeToken { + isFeeTokenAndBridgeTokenSame = true + inputs, err = ApproveTokenCallData(msg.RouterAddr, new(big.Int).Add(msg.Fee, tokenAndAmount.Amount)) + if err != nil { + return nil, err + } + } else { + inputs, err = ApproveTokenCallData(msg.RouterAddr, tokenAndAmount.Amount) + if err != nil { + return nil, err + } + } + + data := Call{Target: tokenAndAmount.Token, AllowFailure: false, CallData: inputs} + callData = append(callData, data) + } + // approve fee token if not already approved + if msg.Fee != nil && msg.Fee.Cmp(big.NewInt(0)) > 0 && !isFeeTokenAndBridgeTokenSame { + inputs, err := ApproveTokenCallData(msg.RouterAddr, msg.Fee) + if err != nil { + return nil, err + } + data := Call{Target: msg.Msg.FeeToken, AllowFailure: false, CallData: inputs} + callData = append(callData, data) + } + + inputs, err := CCIPSendCallData(msg) + if err != nil { + return nil, err + } + data := Call{Target: msg.RouterAddr, AllowFailure: false, CallData: inputs} + callData = append(callData, data) + } + opts, err := evmClient.TransactionOpts(evmClient.GetDefaultWallet()) + if err != nil { + return nil, err + } + + // call aggregate3 to group all msg call data and send them in a single transaction + tx, err := boundContract.Transact(opts, "aggregate3", callData) + if err != nil { + return nil, err + } + err = WaitForSuccessfulTxMined(evmClient, tx) + if err != nil { + return tx, errors.Wrapf(err, "multicall failed for ccip-send; router %s", contractAddress.Hex()) + } + return tx, nil +} diff --git a/integration-tests/ccip-tests/load/ccip_loadgen.go b/integration-tests/ccip-tests/load/ccip_loadgen.go index 7022bdf0276..2d9cf2e97b8 100644 --- a/integration-tests/ccip-tests/load/ccip_loadgen.go +++ b/integration-tests/ccip-tests/load/ccip_loadgen.go @@ -240,13 +240,15 @@ func (c *CCIPE2ELoad) Call(_ *wasp.Generator) *wasp.CallResult { }) // wait for // - CCIPSendRequested Event log to be generated, - msgLog, sourceLogTime, err := c.Lane.Source.AssertEventCCIPSendRequested(lggr, sendTx.Hash().Hex(), c.CallTimeOut, txConfirmationTime, stats) - if err != nil || msgLog == nil { + msgLogs, sourceLogTime, err := c.Lane.Source.AssertEventCCIPSendRequested(lggr, sendTx.Hash().Hex(), c.CallTimeOut, txConfirmationTime, []*testreporters.RequestStat{stats}) + + if err != nil || msgLogs == nil || len(msgLogs) == 0 { res.Error = err.Error() res.Data = stats.StatusByPhase res.Failed = true return res } + msgLog := msgLogs[0] sentMsg := msgLog.Message seqNum := sentMsg.SequenceNumber lggr = lggr.With().Str("msgId ", fmt.Sprintf("0x%x", sentMsg.MessageId[:])).Logger() @@ -275,7 +277,7 @@ func (c *CCIPE2ELoad) Call(_ *wasp.Generator) *wasp.CallResult { } else { var finalizingBlock uint64 sourceLogFinalizedAt, finalizingBlock, err = c.Lane.Source.AssertSendRequestedLogFinalized( - lggr, seqNum, msgLog, sourceLogTime, stats) + lggr, sendTx.Hash(), sourceLogTime, []*testreporters.RequestStat{stats}) if err != nil { res.Error = err.Error() res.Data = stats.StatusByPhase diff --git a/integration-tests/ccip-tests/smoke/ccip_test.go b/integration-tests/ccip-tests/smoke/ccip_test.go index 671c668c29f..b49d641a558 100644 --- a/integration-tests/ccip-tests/smoke/ccip_test.go +++ b/integration-tests/ccip-tests/smoke/ccip_test.go @@ -7,11 +7,11 @@ import ( "time" "github.com/AlekSi/pointer" - "github.com/ethereum/go-ethereum/common" "github.com/smartcontractkit/chainlink-testing-framework/logging" "github.com/stretchr/testify/require" "github.com/smartcontractkit/ccip/integration-tests/ccip-tests/testconfig" + "github.com/smartcontractkit/ccip/integration-tests/utils" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/actions" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/testsetups" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" @@ -226,7 +226,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { failedTx, _, _, err := tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, actions.TokenTransfer, "msg with token more than aggregated capacity", - common.HexToAddress(tc.lane.Source.Common.FeeToken.Address())) + ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) errReason, v, err := tc.lane.Source.Common.ChainClient.RevertReasonFromTx(failedTx, evm_2_evm_onramp.EVM2EVMOnRampABI) @@ -256,7 +256,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { failedTx, _, _, err = tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, actions.TokenTransfer, "msg with token more than aggregated rate", - common.HexToAddress(tc.lane.Source.Common.FeeToken.Address())) + ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) errReason, v, err = tc.lane.Source.Common.ChainClient.RevertReasonFromTx(failedTx, evm_2_evm_onramp.EVM2EVMOnRampABI) @@ -309,7 +309,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { failedTx, _, _, err = tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, actions.TokenTransfer, "msg with token more than token pool capacity", - common.HexToAddress(tc.lane.Source.Common.FeeToken.Address())) + ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) errReason, v, err = tc.lane.Source.Common.ChainClient.RevertReasonFromTx(failedTx, lock_release_token_pool.LockReleaseTokenPoolABI) @@ -339,7 +339,7 @@ func TestSmokeCCIPRateLimit(t *testing.T) { failedTx, _, _, err = tc.lane.Source.SendRequest( tc.lane.Dest.ReceiverDapp.EthAddress, actions.TokenTransfer, "msg with token more than token pool rate", - common.HexToAddress(tc.lane.Source.Common.FeeToken.Address())) + ) require.NoError(t, err) require.Error(t, tc.lane.Source.Common.ChainClient.WaitForEvents()) errReason, v, err = tc.lane.Source.Common.ChainClient.RevertReasonFromTx(failedTx, lock_release_token_pool.LockReleaseTokenPoolABI) @@ -358,3 +358,58 @@ func TestSmokeCCIPRateLimit(t *testing.T) { }) } } + +func TestSmokeCCIPMulticall(t *testing.T) { + t.Parallel() + type subtestInput struct { + testName string + lane *actions.CCIPLane + } + l := logging.GetTestLogger(t) + TestCfg := testsetups.NewCCIPTestConfig(t, l, testconfig.Smoke) + // enable multicall in one tx for this test + TestCfg.TestGroupInput.MulticallInOneTx = utils.Ptr(true) + setUpOutput := testsetups.CCIPDefaultTestSetUp(t, l, "smoke-ccip", nil, TestCfg) + var tcs []subtestInput + if len(setUpOutput.Lanes) == 0 { + return + } + + t.Cleanup(func() { + if TestCfg.TestGroupInput.MsgType == actions.TokenTransfer { + setUpOutput.Balance.Verify(t) + } + require.NoError(t, setUpOutput.TearDown()) + }) + for i := range setUpOutput.Lanes { + tcs = append(tcs, subtestInput{ + testName: fmt.Sprintf("CCIP message transfer from network %s to network %s", + setUpOutput.Lanes[i].ForwardLane.SourceNetworkName, setUpOutput.Lanes[i].ForwardLane.DestNetworkName), + lane: setUpOutput.Lanes[i].ForwardLane, + }) + if setUpOutput.Lanes[i].ReverseLane != nil { + tcs = append(tcs, subtestInput{ + testName: fmt.Sprintf("CCIP message transfer from network %s to network %s", + setUpOutput.Lanes[i].ReverseLane.SourceNetworkName, setUpOutput.Lanes[i].ReverseLane.DestNetworkName), + lane: setUpOutput.Lanes[i].ReverseLane, + }) + } + } + l.Info().Int("Total Lanes", len(tcs)).Msg("Starting CCIP test") + for _, testcase := range tcs { + tc := testcase + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + tc.lane.Test = t + l.Info(). + Str("Source", tc.lane.SourceNetworkName). + Str("Destination", tc.lane.DestNetworkName). + Msgf("Starting lane %s -> %s", tc.lane.SourceNetworkName, tc.lane.DestNetworkName) + + tc.lane.RecordStateBeforeTransfer() + err := tc.lane.Multicall(TestCfg.TestGroupInput.NoOfSendsInMulticall, TestCfg.TestGroupInput.MsgType, tc.lane.Source.MulticallContract) + require.NoError(t, err) + tc.lane.ValidateRequests() + }) + } +} diff --git a/integration-tests/ccip-tests/testconfig/ccip.go b/integration-tests/ccip-tests/testconfig/ccip.go index 3b35ee5cf42..de6fce473be 100644 --- a/integration-tests/ccip-tests/testconfig/ccip.go +++ b/integration-tests/ccip-tests/testconfig/ccip.go @@ -12,6 +12,8 @@ type CCIPTestConfig struct { CommitAndExecuteOnSameDON *bool `toml:",omitempty"` NumberOfCommitNodes int `toml:",omitempty"` MsgType string `toml:",omitempty"` + MulticallInOneTx *bool `toml:",omitempty"` + NoOfSendsInMulticall int `toml:",omitempty"` PhaseTimeout *models.Duration `toml:",omitempty"` TestDuration *models.Duration `toml:",omitempty"` LocalCluster *bool `toml:",omitempty"` @@ -111,6 +113,12 @@ func (c *CCIPTestConfig) ApplyOverrides(fromCfg *CCIPTestConfig) error { if fromCfg.AmountPerToken != 0 { c.AmountPerToken = fromCfg.AmountPerToken } + if fromCfg.MulticallInOneTx != nil { + c.MulticallInOneTx = fromCfg.MulticallInOneTx + } + if fromCfg.NoOfSendsInMulticall != 0 { + c.NoOfSendsInMulticall = fromCfg.NoOfSendsInMulticall + } return nil } @@ -143,6 +151,11 @@ func (c *CCIPTestConfig) Validate() error { } } + if c.MulticallInOneTx != nil { + if c.NoOfSendsInMulticall == 0 { + return errors.Errorf("number of sends in multisend should be greater than 0 if multisend is true") + } + } return nil } diff --git a/integration-tests/ccip-tests/testconfig/tomls/default.toml b/integration-tests/ccip-tests/testconfig/tomls/default.toml index dd336ff2ed5..8a2136bbf72 100644 --- a/integration-tests/ccip-tests/testconfig/tomls/default.toml +++ b/integration-tests/ccip-tests/testconfig/tomls/default.toml @@ -96,6 +96,8 @@ NoOfRoutersPerPair = 1 NoOfTokensPerChain = 2 NoOfTokensInMsg = 2 AmountPerToken = 1 +MulticallInOneTx = false +NoOfSendsInMulticall = 5 [CCIP.Groups.load] KeepEnvAlive = false @@ -116,6 +118,8 @@ NoOfRoutersPerPair = 1 NoOfTokensPerChain = 2 NoOfTokensInMsg = 2 AmountPerToken = 1 +MulticallInOneTx = false +NoOfSendsInMulticall = 5 [CCIP.Groups.chaos] KeepEnvAlive = false @@ -138,3 +142,5 @@ NoOfTokensInMsg = 2 AmountPerToken = 1 WaitBetweenChaosDuringLoad = '2m' ChaosDuration = '10m' +MulticallInOneTx = false +NoOfSendsInMulticall = 5 diff --git a/integration-tests/ccip-tests/testreporters/ccip.go b/integration-tests/ccip-tests/testreporters/ccip.go index e6c5391e19f..cff1fd04c2a 100644 --- a/integration-tests/ccip-tests/testreporters/ccip.go +++ b/integration-tests/ccip-tests/testreporters/ccip.go @@ -58,12 +58,14 @@ type PhaseStat struct { } type RequestStat struct { - reqNo int64 + ReqNo int64 + SeqNum uint64 StatusByPhase map[Phase]PhaseStat `json:"status_by_phase,omitempty"` } func (stat *RequestStat) UpdateState(lggr zerolog.Logger, seqNum uint64, step Phase, duration time.Duration, state Status, sendTransactionStats ...TransactionStats) { durationInSec := duration.Seconds() + stat.SeqNum = seqNum phaseDetails := PhaseStat{ SeqNum: seqNum, Duration: durationInSec, @@ -85,10 +87,10 @@ func (stat *RequestStat) UpdateState(lggr zerolog.Logger, seqNum uint64, step Ph } lggr.Info(). Str(fmt.Sprint(E2E), fmt.Sprintf("%s", Failure)). - Msgf("reqNo %d", stat.reqNo) - event.Str(fmt.Sprint(step), fmt.Sprintf("%s", Failure)).Msgf("reqNo %d", stat.reqNo) + Msgf("reqNo %d", stat.ReqNo) + event.Str(fmt.Sprint(step), fmt.Sprintf("%s", Failure)).Msgf("reqNo %d", stat.ReqNo) } else { - event.Str(fmt.Sprint(step), fmt.Sprintf("%s", Success)).Msgf("reqNo %d", stat.reqNo) + event.Str(fmt.Sprint(step), fmt.Sprintf("%s", Success)).Msgf("reqNo %d", stat.ReqNo) if step == Commit || step == ReportBlessed || step == ExecStateChanged { stat.StatusByPhase[E2E] = PhaseStat{ SeqNum: seqNum, @@ -98,7 +100,7 @@ func (stat *RequestStat) UpdateState(lggr zerolog.Logger, seqNum uint64, step Ph if step == ExecStateChanged { lggr.Info(). Str(fmt.Sprint(E2E), fmt.Sprintf("%s", Success)). - Msgf("reqNo %d", stat.reqNo) + Msgf("reqNo %d", stat.ReqNo) } } } @@ -106,7 +108,7 @@ func (stat *RequestStat) UpdateState(lggr zerolog.Logger, seqNum uint64, step Ph func NewCCIPRequestStats(reqNo int64) *RequestStat { return &RequestStat{ - reqNo: reqNo, + ReqNo: reqNo, StatusByPhase: make(map[Phase]PhaseStat), } } @@ -122,7 +124,7 @@ type CCIPLaneStats struct { } func (testStats *CCIPLaneStats) UpdatePhaseStatsForReq(stat *RequestStat) { - testStats.statusByPhaseByRequests.Store(stat.reqNo, stat.StatusByPhase) + testStats.statusByPhaseByRequests.Store(stat.ReqNo, stat.StatusByPhase) } func (testStats *CCIPLaneStats) Aggregate(phase Phase, durationInSec float64) { diff --git a/integration-tests/ccip-tests/testsetups/ccip.go b/integration-tests/ccip-tests/testsetups/ccip.go index eada7233028..b58105552df 100644 --- a/integration-tests/ccip-tests/testsetups/ccip.go +++ b/integration-tests/ccip-tests/testsetups/ccip.go @@ -13,6 +13,7 @@ import ( "time" "github.com/AlekSi/pointer" + "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/rs/zerolog" chainselectors "github.com/smartcontractkit/chain-selectors" @@ -368,7 +369,7 @@ func (o *CCIPTestSetUpOutputs) AddLanesForNetworkPair( SourceNetworkName: actions.NetworkName(networkA.Name), DestNetworkName: actions.NetworkName(networkB.Name), ValidationTimeout: o.Cfg.TestGroupInput.PhaseTimeout.Duration(), - SentReqs: make(map[int64]actions.CCIPRequest), + SentReqs: make(map[common.Hash][]actions.CCIPRequest), TotalFee: big.NewInt(0), Balance: o.Balance, Context: ctx, @@ -422,7 +423,7 @@ func (o *CCIPTestSetUpOutputs) AddLanesForNetworkPair( DestChain: destChainClientB2A, ValidationTimeout: o.Cfg.TestGroupInput.PhaseTimeout.Duration(), Balance: o.Balance, - SentReqs: make(map[int64]actions.CCIPRequest), + SentReqs: make(map[common.Hash][]actions.CCIPRequest), TotalFee: big.NewInt(0), Context: ctx, SrcNetworkLaneCfg: ccipLaneA2B.DstNetworkLaneCfg, @@ -456,7 +457,7 @@ func (o *CCIPTestSetUpOutputs) AddLanesForNetworkPair( setUpFuncs.Go(func() error { lggr.Info().Msgf("Setting up lane %s to %s", networkA.Name, networkB.Name) srcConfig, destConfig, err := ccipLaneA2B.DeployNewCCIPLane(numOfCommitNodes, commitAndExecOnSameDON, networkACmn, networkBCmn, - transferAmounts, o.BootstrapAdded, configureCLNode, o.JobAddGrp) + transferAmounts, pointer.GetBool(o.Cfg.TestGroupInput.MulticallInOneTx), o.BootstrapAdded, configureCLNode, o.JobAddGrp) if err != nil { allErrors.Store(multierr.Append(allErrors.Load(), fmt.Errorf("deploying lane %s to %s; err - %+v", networkA.Name, networkB.Name, err))) return err @@ -480,7 +481,7 @@ func (o *CCIPTestSetUpOutputs) AddLanesForNetworkPair( if bidirectional { lggr.Info().Msgf("Setting up lane %s to %s", networkB.Name, networkA.Name) srcConfig, destConfig, err := ccipLaneB2A.DeployNewCCIPLane(numOfCommitNodes, commitAndExecOnSameDON, networkBCmn, networkACmn, - transferAmounts, o.BootstrapAdded, configureCLNode, o.JobAddGrp) + transferAmounts, pointer.GetBool(o.Cfg.TestGroupInput.MulticallInOneTx), o.BootstrapAdded, configureCLNode, o.JobAddGrp) if err != nil { lggr.Error().Err(err).Msgf("error deploying lane %s to %s", networkB.Name, networkA.Name) allErrors.Store(multierr.Append(allErrors.Load(), fmt.Errorf("deploying lane %s to %s; err - %+v", networkB.Name, networkA.Name, err))) @@ -526,8 +527,10 @@ func (o *CCIPTestSetUpOutputs) StartEventWatchers() { for _, lane := range o.ReadLanes() { err := lane.ForwardLane.StartEventWatchers() require.NoError(o.Cfg.Test, err) - err = lane.ReverseLane.StartEventWatchers() - require.NoError(o.Cfg.Test, err) + if lane.ReverseLane != nil { + err = lane.ReverseLane.StartEventWatchers() + require.NoError(o.Cfg.Test, err) + } } } @@ -574,9 +577,11 @@ func (o *CCIPTestSetUpOutputs) WaitForPriceUpdates(ctx context.Context) { priceUpdateGrp.Go(func() error { return waitForUpdate(*lanes.ForwardLane) }) - priceUpdateGrp.Go(func() error { - return waitForUpdate(*lanes.ReverseLane) - }) + if lanes.ReverseLane != nil { + priceUpdateGrp.Go(func() error { + return waitForUpdate(*lanes.ReverseLane) + }) + } } require.NoError(t, priceUpdateGrp.Wait())