diff --git a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol index 21d0bb825b..ab5ae3ce48 100644 --- a/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol +++ b/contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol @@ -50,6 +50,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { // Ref: https://developers.circle.com/cctp/technical-guide#message-body // Ref: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol uint8 private constant BURN_MESSAGE_V2_VERSION_INDEX = 0; + uint8 private constant BURN_MESSAGE_V2_BURN_TOKEN_INDEX = 4; uint8 private constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36; uint8 private constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68; uint8 private constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100; @@ -81,6 +82,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { /// @notice USDC address on local chain address public immutable usdcToken; + /// @notice USDC address on remote chain + address public immutable peerUsdcToken; + /// @notice Domain ID of the chain from which messages are accepted uint32 public immutable peerDomainID; @@ -141,10 +145,15 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { uint32 peerDomainID; address peerStrategy; address usdcToken; + address peerUsdcToken; } constructor(CCTPIntegrationConfig memory _config) { require(_config.usdcToken != address(0), "Invalid USDC address"); + require( + _config.peerUsdcToken != address(0), + "Invalid peer USDC address" + ); require( _config.cctpTokenMessenger != address(0), "Invalid CCTP config" @@ -182,6 +191,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { keccak256(abi.encodePacked("USDC")), "Token symbol must be USDC" ); + + // USDC address on remote chain + peerUsdcToken = _config.peerUsdcToken; } /** @@ -472,6 +484,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 { "Invalid burn message" ); + // Ensure the burn token is USDC + address burnToken = messageBody.extractAddress( + BURN_MESSAGE_V2_BURN_TOKEN_INDEX + ); + require(burnToken == peerUsdcToken, "Invalid burn token"); + // Address of caller of depositForBurn (or depositForBurnWithCaller) on source domain sender = messageBody.extractAddress( BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX diff --git a/contracts/deploy/base/041_crosschain_strategy.js b/contracts/deploy/base/041_crosschain_strategy.js index 4bee8d2d35..042b84fb9f 100644 --- a/contracts/deploy/base/041_crosschain_strategy.js +++ b/contracts/deploy/base/041_crosschain_strategy.js @@ -24,6 +24,7 @@ module.exports = deployOnBase( cctpDomainIds.Ethereum, crossChainStrategyProxyAddress, addresses.base.USDC, + addresses.mainnet.USDC, "CrossChainRemoteStrategy", addresses.CCTPTokenMessengerV2, addresses.CCTPMessageTransmitterV2, diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index f24921a838..6807da0f6e 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1833,6 +1833,7 @@ const deployCrossChainMasterStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, + peerBaseToken, vaultAddress, implementationName = "CrossChainMasterStrategy", skipInitialize = false, @@ -1860,6 +1861,7 @@ const deployCrossChainMasterStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, + peerBaseToken, ], ]); const dCrossChainMasterStrategy = await ethers.getContract( @@ -1894,6 +1896,7 @@ const deployCrossChainRemoteStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, + peerBaseToken, implementationName = "CrossChainRemoteStrategy", tokenMessengerAddress = addresses.CCTPTokenMessengerV2, messageTransmitterAddress = addresses.CCTPMessageTransmitterV2, @@ -1920,6 +1923,7 @@ const deployCrossChainRemoteStrategyImpl = async ( targetDomainId, remoteStrategyAddress, baseToken, + peerBaseToken, ], ]); const dCrossChainRemoteStrategy = await ethers.getContract( @@ -1977,6 +1981,7 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => { // unit tests differ from mainnet where remote strategy has a different address dRemoteProxy.address, usdcAddress, + usdcAddress, // Assume both are same on unit tests cVaultProxy.address, "CrossChainMasterStrategy", false, @@ -1991,6 +1996,7 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => { 0, // Ethereum domain id dMasterProxy.address, usdcAddress, + usdcAddress, // Assume both are same on unit tests "CrossChainRemoteStrategy", tokenMessenger.address, messageTransmitter.address, diff --git a/contracts/deploy/mainnet/166_crosschain_strategy.js b/contracts/deploy/mainnet/166_crosschain_strategy.js index dc1ca4ead8..8b11a023fd 100644 --- a/contracts/deploy/mainnet/166_crosschain_strategy.js +++ b/contracts/deploy/mainnet/166_crosschain_strategy.js @@ -32,6 +32,7 @@ module.exports = deploymentWithGovernanceProposal( // Same address for both master and remote strategy crossChainStrategyProxyAddress, addresses.mainnet.USDC, + addresses.base.USDC, cVaultProxy.address, "CrossChainMasterStrategy", false, diff --git a/contracts/test/strategies/crosschain/_crosschain-helpers.js b/contracts/test/strategies/crosschain/_crosschain-helpers.js index 8fb9766d03..031c15e128 100644 --- a/contracts/test/strategies/crosschain/_crosschain-helpers.js +++ b/contracts/test/strategies/crosschain/_crosschain-helpers.js @@ -147,18 +147,27 @@ const encodeCCTPMessage = ( return `0x${versionStr}${sourceDomainStr}${empty18Bytes}${senderStr}${recipientStr}${empty20Bytes}${messageBodyStr}`; }; -const encodeBurnMessageBody = (sender, recipient, amount, hookData) => { +const encodeBurnMessageBody = ( + sender, + recipient, + burnToken, + amount, + hookData +) => { const senderEncoded = ethers.utils.defaultAbiCoder .encode(["address"], [sender]) .slice(2); const recipientEncoded = ethers.utils.defaultAbiCoder .encode(["address"], [recipient]) .slice(2); + const burnTokenEncoded = ethers.utils.defaultAbiCoder + .encode(["address"], [burnToken]) + .slice(2); const amountEncoded = ethers.utils.defaultAbiCoder .encode(["uint256"], [amount]) .slice(2); const encodedHookData = hookData.slice(2); - return `0x00000001${empty16Bytes}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat( + return `0x00000001${burnTokenEncoded}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat( 3 )}${encodedHookData}`; }; diff --git a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js index ec72199028..5cb3eb150a 100644 --- a/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js @@ -316,6 +316,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () { const burnPayload = encodeBurnMessageBody( crossChainMasterStrategy.address, crossChainMasterStrategy.address, + addresses.base.USDC, usdcUnits("2342"), balancePayload ); @@ -493,5 +494,57 @@ describe("ForkTest: CrossChainMasterStrategy", function () { await crossChainMasterStrategy.remoteStrategyBalance(); expect(remoteStrategyBalanceAfter).to.eq(remoteStrategyBalanceBefore); }); + + it("Should revert if the burn token is not peer USDC", async function () { + const { crossChainMasterStrategy, strategist } = fixture; + + if (await crossChainMasterStrategy.isTransferPending()) { + // Skip if there's a pending transfer + console.log( + "Skipping balance check message fork test because there's a pending transfer" + ); + return; + } + + // set an arbitrary remote strategy balance + await setRemoteStrategyBalance( + crossChainMasterStrategy, + usdcUnits("123456") + ); + + const lastNonce = ( + await crossChainMasterStrategy.lastTransferNonce() + ).toNumber(); + + // Replace transmitter to mock transmitter + await replaceMessageTransmitter(); + + // Build check balance payload + const balancePayload = encodeBalanceCheckMessageBody( + lastNonce, + usdcUnits("12345"), + true // withdrawal confirmation + ); + const burnPayload = encodeBurnMessageBody( + crossChainMasterStrategy.address, + crossChainMasterStrategy.address, + addresses.mainnet.WETH, // Not peer USDC + usdcUnits("2342"), + balancePayload + ); + const message = encodeCCTPMessage( + 6, + addresses.CCTPTokenMessengerV2, + addresses.CCTPTokenMessengerV2, + burnPayload + ); + + // Relay the message with fake attestation + const tx = crossChainMasterStrategy + .connect(strategist) + .relay(message, "0x"); + + await expect(tx).to.be.revertedWith("Invalid burn token"); + }); }); }); diff --git a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js index 4398e768bf..ea8c2dfac2 100644 --- a/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js +++ b/contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js @@ -143,6 +143,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { const burnPayload = encodeBurnMessageBody( crossChainRemoteStrategy.address, crossChainRemoteStrategy.address, + addresses.mainnet.USDC, depositAmount, depositPayload ); @@ -246,4 +247,40 @@ describe("ForkTest: CrossChainRemoteStrategy", function () { ); expect(balanceAfter).to.approxEqual(expectedBalance); }); + + it("Should revert if the burn token is not peer USDC", async function () { + const { crossChainRemoteStrategy, strategist } = fixture; + + const nonceBefore = await crossChainRemoteStrategy.lastTransferNonce(); + + const depositAmount = usdcUnits("1234.56"); + + // Replace transmitter to mock transmitter + await replaceMessageTransmitter(); + + const nextNonce = nonceBefore.toNumber() + 1; + + // Build deposit message + const depositPayload = encodeDepositMessageBody(nextNonce, depositAmount); + const burnPayload = encodeBurnMessageBody( + crossChainRemoteStrategy.address, + crossChainRemoteStrategy.address, + addresses.base.WETH, // Not peer USDC + depositAmount, + depositPayload + ); + const message = encodeCCTPMessage( + 0, + addresses.CCTPTokenMessengerV2, + addresses.CCTPTokenMessengerV2, + burnPayload + ); + + // Relay the message + const tx = crossChainRemoteStrategy + .connect(strategist) + .relay(message, "0x"); + + await expect(tx).to.be.revertedWith("Invalid burn token"); + }); });