Skip to content

Commit

Permalink
CCIP-2881 USDC Reader integration tests (#14516)
Browse files Browse the repository at this point in the history
* Test scaffold

* Working test

* Fixes

* Fixes

* Fixes

* Bump

* Minor fixes

* [Bot] Update changeset file with jira issue

* Minor fixes

* Minor fixes

* Minor fixes

* Minor fixes

* Using facade

* Using facade

* Post review fixes

* Update gethwrappers

---------

Co-authored-by: app-token-issuer-infra-releng[bot] <120227048+app-token-issuer-infra-releng[bot]@users.noreply.github.com>
  • Loading branch information
1 parent b4360c9 commit 0e32c07
Show file tree
Hide file tree
Showing 17 changed files with 723 additions and 18 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-snails-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

Adding USDCReaderTester contract for CCIP integration tests #internal
8 changes: 8 additions & 0 deletions contracts/.changeset/orange-plums-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@chainlink/contracts': patch
---

Adding USDCReaderTester contract for CCIP integration tests #internal


CCIP-2881
5 changes: 1 addition & 4 deletions contracts/scripts/native_solc_compile_all_ccip
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ compileContract () {
echo "OnRamp uses $OPTIMIZE_RUNS_ONRAMP optimizer runs."
optimize_runs=$OPTIMIZE_RUNS_ONRAMP
;;
"ccip/test/helpers/CCIPReaderTester.sol")
echo "CCIPReaderTester uses 1 optimizer runs for reduced contract size."
optimize_runs=1
;;
esac

solc --overwrite --optimize --optimize-runs $optimize_runs --metadata-hash none \
Expand Down Expand Up @@ -95,6 +91,7 @@ compileContract ccip/test/helpers/BurnMintERC677Helper.sol
compileContract ccip/test/helpers/CommitStoreHelper.sol
compileContract ccip/test/helpers/MessageHasher.sol
compileContract ccip/test/helpers/CCIPReaderTester.sol
compileContract ccip/test/helpers/USDCReaderTester.sol
compileContract ccip/test/helpers/ReportCodec.sol
compileContract ccip/test/helpers/receivers/MaybeRevertMessageReceiver.sol
compileContract ccip/test/helpers/MultiOCR3Helper.sol
Expand Down
62 changes: 62 additions & 0 deletions contracts/src/v0.8/ccip/test/helpers/USDCReaderTester.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.24;

contract USDCReaderTester {
event MessageSent(bytes);

// emitMessageSent reflects the logic from Circle's MessageTransmitter emitting MeseageSent(bytes) events
// https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/MessageTransmitter.sol#L41
// https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/MessageTransmitter.sol#L365
function emitMessageSent(
uint32 version,
uint32 sourceDomain,
uint32 destinationDomain,
bytes32 recipient,
bytes32 destinationCaller,
bytes32 sender,
uint64 nonce,
bytes calldata messageBody
) external {
bytes memory _message =
_formatMessage(version, sourceDomain, destinationDomain, nonce, sender, recipient, destinationCaller, messageBody);
emit MessageSent(_message);
}

/**
* @notice Returns formatted (packed) message with provided fields
* It's a copy paste of the Message._formatMessage() call in MessageTransmitter.sol
* https://github.com/circlefin/evm-cctp-contracts/blob/377c9bd813fb86a42d900ae4003599d82aef635a/src/messages/Message.sol#L54C1-L65C9
* Check the chainlink-ccip repo for the offchain implementation of matching this format
* @param _msgVersion the version of the message format
* @param _msgSourceDomain Domain of home chain
* @param _msgDestinationDomain Domain of destination chain
* @param _msgNonce Destination-specific nonce
* @param _msgSender Address of sender on source chain as bytes32
* @param _msgRecipient Address of recipient on destination chain as bytes32
* @param _msgDestinationCaller Address of caller on destination chain as bytes32
* @param _msgRawBody Raw bytes of message body
* @return Formatted message
*
*/
function _formatMessage(
uint32 _msgVersion,
uint32 _msgSourceDomain,
uint32 _msgDestinationDomain,
uint64 _msgNonce,
bytes32 _msgSender,
bytes32 _msgRecipient,
bytes32 _msgDestinationCaller,
bytes memory _msgRawBody
) internal pure returns (bytes memory) {
return abi.encodePacked(
_msgVersion,
_msgSourceDomain,
_msgDestinationDomain,
_msgNonce,
_msgSender,
_msgRecipient,
_msgDestinationCaller,
_msgRawBody
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
package usdcreader

import (
"context"
"math/big"
"testing"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/accounts/abi/bind/backends"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/crypto"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"

sel "github.com/smartcontractkit/chain-selectors"

"github.com/smartcontractkit/chainlink-ccip/execute/exectypes"
"github.com/smartcontractkit/chainlink-ccip/pkg/consts"
"github.com/smartcontractkit/chainlink-ccip/pkg/contractreader"
"github.com/smartcontractkit/chainlink-ccip/pkg/reader"
"github.com/smartcontractkit/chainlink-ccip/pluginconfig"
"github.com/smartcontractkit/chainlink-common/pkg/types"
cciptypes "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3"

"github.com/smartcontractkit/chainlink/v2/core/chains/evm/client"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/usdc_reader_tester"
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/relay/evm"
evmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types"
)

func Test_USDCReader_MessageHashes(t *testing.T) {
ctx := testutils.Context(t)
ethereumChain := cciptypes.ChainSelector(sel.ETHEREUM_MAINNET_OPTIMISM_1.Selector)
ethereumDomainCCTP := reader.CCTPDestDomains[uint64(ethereumChain)]
avalancheChain := cciptypes.ChainSelector(sel.AVALANCHE_MAINNET.Selector)
avalancheDomainCCTP := reader.CCTPDestDomains[uint64(avalancheChain)]
polygonChain := cciptypes.ChainSelector(sel.POLYGON_MAINNET.Selector)
polygonDomainCCTP := reader.CCTPDestDomains[uint64(polygonChain)]

cfg := evmtypes.ChainReaderConfig{
Contracts: map[string]evmtypes.ChainContractReader{
consts.ContractNameCCTPMessageTransmitter: {
ContractPollingFilter: evmtypes.ContractPollingFilter{
GenericEventNames: []string{consts.EventNameCCTPMessageSent},
},
ContractABI: usdc_reader_tester.USDCReaderTesterABI,
Configs: map[string]*evmtypes.ChainReaderDefinition{
consts.EventNameCCTPMessageSent: {
ChainSpecificName: consts.EventNameCCTPMessageSent,
ReadType: evmtypes.Event,
},
},
},
},
}

ts := testSetup(ctx, t, ethereumChain, cfg)

usdcReader, err := reader.NewUSDCMessageReader(
map[cciptypes.ChainSelector]pluginconfig.USDCCCTPTokenConfig{
ethereumChain: {
SourceMessageTransmitterAddr: ts.contractAddr.String(),
},
},
map[cciptypes.ChainSelector]contractreader.ContractReaderFacade{
ethereumChain: ts.reader,
})
require.NoError(t, err)

emitMessageSent(t, ts, ethereumDomainCCTP, avalancheDomainCCTP, 11)
emitMessageSent(t, ts, ethereumDomainCCTP, avalancheDomainCCTP, 21)
emitMessageSent(t, ts, ethereumDomainCCTP, avalancheDomainCCTP, 31)
emitMessageSent(t, ts, ethereumDomainCCTP, avalancheDomainCCTP, 41)
emitMessageSent(t, ts, ethereumDomainCCTP, polygonDomainCCTP, 31)
emitMessageSent(t, ts, ethereumDomainCCTP, polygonDomainCCTP, 41)

tt := []struct {
name string
tokens map[exectypes.MessageTokenID]cciptypes.RampTokenAmount
sourceChain cciptypes.ChainSelector
destChain cciptypes.ChainSelector
expectedMsgIDs []exectypes.MessageTokenID
}{
{
name: "empty messages should return empty response",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{},
sourceChain: ethereumChain,
destChain: avalancheChain,
expectedMsgIDs: []exectypes.MessageTokenID{},
},
{
name: "single token message",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{
exectypes.NewMessageTokenID(1, 1): {
ExtraData: reader.NewSourceTokenDataPayload(11, ethereumDomainCCTP).ToBytes(),
},
},
sourceChain: ethereumChain,
destChain: avalancheChain,
expectedMsgIDs: []exectypes.MessageTokenID{exectypes.NewMessageTokenID(1, 1)},
},
{
name: "single token message but different chain",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{
exectypes.NewMessageTokenID(1, 2): {
ExtraData: reader.NewSourceTokenDataPayload(31, ethereumDomainCCTP).ToBytes(),
},
},
sourceChain: ethereumChain,
destChain: polygonChain,
expectedMsgIDs: []exectypes.MessageTokenID{exectypes.NewMessageTokenID(1, 2)},
},
{
name: "message without matching nonce",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{
exectypes.NewMessageTokenID(1, 1): {
ExtraData: reader.NewSourceTokenDataPayload(1234, ethereumDomainCCTP).ToBytes(),
},
},
sourceChain: ethereumChain,
destChain: avalancheChain,
expectedMsgIDs: []exectypes.MessageTokenID{},
},
{
name: "message without matching source domain",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{
exectypes.NewMessageTokenID(1, 1): {
ExtraData: reader.NewSourceTokenDataPayload(11, avalancheDomainCCTP).ToBytes(),
},
},
sourceChain: ethereumChain,
destChain: avalancheChain,
expectedMsgIDs: []exectypes.MessageTokenID{},
},
{
name: "message with multiple tokens",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{
exectypes.NewMessageTokenID(1, 1): {
ExtraData: reader.NewSourceTokenDataPayload(11, ethereumDomainCCTP).ToBytes(),
},
exectypes.NewMessageTokenID(1, 2): {
ExtraData: reader.NewSourceTokenDataPayload(21, ethereumDomainCCTP).ToBytes(),
},
},
sourceChain: ethereumChain,
destChain: avalancheChain,
expectedMsgIDs: []exectypes.MessageTokenID{
exectypes.NewMessageTokenID(1, 1),
exectypes.NewMessageTokenID(1, 2),
},
},
{
name: "message with multiple tokens, one without matching nonce",
tokens: map[exectypes.MessageTokenID]cciptypes.RampTokenAmount{
exectypes.NewMessageTokenID(1, 1): {
ExtraData: reader.NewSourceTokenDataPayload(11, ethereumDomainCCTP).ToBytes(),
},
exectypes.NewMessageTokenID(1, 2): {
ExtraData: reader.NewSourceTokenDataPayload(12, ethereumDomainCCTP).ToBytes(),
},
exectypes.NewMessageTokenID(1, 3): {
ExtraData: reader.NewSourceTokenDataPayload(31, ethereumDomainCCTP).ToBytes(),
},
},
sourceChain: ethereumChain,
destChain: avalancheChain,
expectedMsgIDs: []exectypes.MessageTokenID{
exectypes.NewMessageTokenID(1, 1),
exectypes.NewMessageTokenID(1, 3),
},
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
require.Eventually(t, func() bool {
hashes, err1 := usdcReader.MessageHashes(ctx, tc.sourceChain, tc.destChain, tc.tokens)
require.NoError(t, err1)

if len(tc.expectedMsgIDs) != len(hashes) {
return false
}

for _, msgID := range tc.expectedMsgIDs {
if _, ok := hashes[msgID]; !ok {
return false
}
}
return true
}, 2*time.Second, 100*time.Millisecond)
})
}
}

func emitMessageSent(t *testing.T, testEnv *testSetupData, source, dest uint32, nonce uint64) {
payload := utils.RandomBytes32()
_, err := testEnv.contract.EmitMessageSent(
testEnv.auth,
reader.CCTPMessageVersion,
source,
dest,
utils.RandomBytes32(),
utils.RandomBytes32(),
[32]byte{},
nonce,
payload[:],
)
require.NoError(t, err)
testEnv.sb.Commit()
}

func testSetup(ctx context.Context, t *testing.T, readerChain cciptypes.ChainSelector, cfg evmtypes.ChainReaderConfig) *testSetupData {
const chainID = 1337

// Generate a new key pair for the simulated account
privateKey, err := crypto.GenerateKey()
assert.NoError(t, err)
// Set up the genesis account with balance
blnc, ok := big.NewInt(0).SetString("999999999999999999999999999999999999", 10)
assert.True(t, ok)
alloc := map[common.Address]core.GenesisAccount{crypto.PubkeyToAddress(privateKey.PublicKey): {Balance: blnc}}
simulatedBackend := backends.NewSimulatedBackend(alloc, 0)
// Create a transactor

auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(chainID))
assert.NoError(t, err)
auth.GasLimit = uint64(0)

address, _, _, err := usdc_reader_tester.DeployUSDCReaderTester(
auth,
simulatedBackend,
)
require.NoError(t, err)
simulatedBackend.Commit()

contract, err := usdc_reader_tester.NewUSDCReaderTester(address, simulatedBackend)
require.NoError(t, err)

lggr := logger.TestLogger(t)
lggr.SetLogLevel(zapcore.ErrorLevel)
db := pgtest.NewSqlxDB(t)
lpOpts := logpoller.Opts{
PollPeriod: time.Millisecond,
FinalityDepth: 0,
BackfillBatchSize: 10,
RpcBatchSize: 10,
KeepFinalizedBlocksDepth: 100000,
}
cl := client.NewSimulatedBackendClient(t, simulatedBackend, big.NewInt(0).SetUint64(uint64(readerChain)))
headTracker := headtracker.NewSimulatedHeadTracker(cl, lpOpts.UseFinalityTag, lpOpts.FinalityDepth)
lp := logpoller.NewLogPoller(logpoller.NewORM(big.NewInt(0).SetUint64(uint64(readerChain)), db, lggr),
cl,
lggr,
headTracker,
lpOpts,
)
require.NoError(t, lp.Start(ctx))

cr, err := evm.NewChainReaderService(ctx, lggr, lp, headTracker, cl, cfg)
require.NoError(t, err)

err = cr.Start(ctx)
require.NoError(t, err)

t.Cleanup(func() {
require.NoError(t, cr.Close())
require.NoError(t, lp.Close())
require.NoError(t, db.Close())
})

return &testSetupData{
contractAddr: address,
contract: contract,
sb: simulatedBackend,
auth: auth,
cl: cl,
reader: cr,
}
}

type testSetupData struct {
contractAddr common.Address
contract *usdc_reader_tester.USDCReaderTester
sb *backends.SimulatedBackend
auth *bind.TransactOpts
cl client.Client
reader types.ContractReader
}
Loading

0 comments on commit 0e32c07

Please sign in to comment.