Skip to content

Commit 693edb6

Browse files
authored
[OUSD-09] Check burnToken in relay method (#2782)
* [OUSD-09] Check burnToken in relay method * Fix: Check burn token is usdc on remote chain * Add tests
1 parent 92c0bea commit 693edb6

File tree

7 files changed

+127
-2
lines changed

7 files changed

+127
-2
lines changed

contracts/contracts/strategies/crosschain/AbstractCCTPIntegrator.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 {
5050
// Ref: https://developers.circle.com/cctp/technical-guide#message-body
5151
// Ref: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/v2/BurnMessageV2.sol
5252
uint8 private constant BURN_MESSAGE_V2_VERSION_INDEX = 0;
53+
uint8 private constant BURN_MESSAGE_V2_BURN_TOKEN_INDEX = 4;
5354
uint8 private constant BURN_MESSAGE_V2_RECIPIENT_INDEX = 36;
5455
uint8 private constant BURN_MESSAGE_V2_AMOUNT_INDEX = 68;
5556
uint8 private constant BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX = 100;
@@ -81,6 +82,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 {
8182
/// @notice USDC address on local chain
8283
address public immutable usdcToken;
8384

85+
/// @notice USDC address on remote chain
86+
address public immutable peerUsdcToken;
87+
8488
/// @notice Domain ID of the chain from which messages are accepted
8589
uint32 public immutable peerDomainID;
8690

@@ -141,10 +145,15 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 {
141145
uint32 peerDomainID;
142146
address peerStrategy;
143147
address usdcToken;
148+
address peerUsdcToken;
144149
}
145150

146151
constructor(CCTPIntegrationConfig memory _config) {
147152
require(_config.usdcToken != address(0), "Invalid USDC address");
153+
require(
154+
_config.peerUsdcToken != address(0),
155+
"Invalid peer USDC address"
156+
);
148157
require(
149158
_config.cctpTokenMessenger != address(0),
150159
"Invalid CCTP config"
@@ -182,6 +191,9 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 {
182191
keccak256(abi.encodePacked("USDC")),
183192
"Token symbol must be USDC"
184193
);
194+
195+
// USDC address on remote chain
196+
peerUsdcToken = _config.peerUsdcToken;
185197
}
186198

187199
/**
@@ -472,6 +484,12 @@ abstract contract AbstractCCTPIntegrator is Governable, IMessageHandlerV2 {
472484
"Invalid burn message"
473485
);
474486

487+
// Ensure the burn token is USDC
488+
address burnToken = messageBody.extractAddress(
489+
BURN_MESSAGE_V2_BURN_TOKEN_INDEX
490+
);
491+
require(burnToken == peerUsdcToken, "Invalid burn token");
492+
475493
// Address of caller of depositForBurn (or depositForBurnWithCaller) on source domain
476494
sender = messageBody.extractAddress(
477495
BURN_MESSAGE_V2_MESSAGE_SENDER_INDEX

contracts/deploy/base/041_crosschain_strategy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = deployOnBase(
2424
cctpDomainIds.Ethereum,
2525
crossChainStrategyProxyAddress,
2626
addresses.base.USDC,
27+
addresses.mainnet.USDC,
2728
"CrossChainRemoteStrategy",
2829
addresses.CCTPTokenMessengerV2,
2930
addresses.CCTPMessageTransmitterV2,

contracts/deploy/deployActions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,6 +1833,7 @@ const deployCrossChainMasterStrategyImpl = async (
18331833
targetDomainId,
18341834
remoteStrategyAddress,
18351835
baseToken,
1836+
peerBaseToken,
18361837
vaultAddress,
18371838
implementationName = "CrossChainMasterStrategy",
18381839
skipInitialize = false,
@@ -1860,6 +1861,7 @@ const deployCrossChainMasterStrategyImpl = async (
18601861
targetDomainId,
18611862
remoteStrategyAddress,
18621863
baseToken,
1864+
peerBaseToken,
18631865
],
18641866
]);
18651867
const dCrossChainMasterStrategy = await ethers.getContract(
@@ -1894,6 +1896,7 @@ const deployCrossChainRemoteStrategyImpl = async (
18941896
targetDomainId,
18951897
remoteStrategyAddress,
18961898
baseToken,
1899+
peerBaseToken,
18971900
implementationName = "CrossChainRemoteStrategy",
18981901
tokenMessengerAddress = addresses.CCTPTokenMessengerV2,
18991902
messageTransmitterAddress = addresses.CCTPMessageTransmitterV2,
@@ -1920,6 +1923,7 @@ const deployCrossChainRemoteStrategyImpl = async (
19201923
targetDomainId,
19211924
remoteStrategyAddress,
19221925
baseToken,
1926+
peerBaseToken,
19231927
],
19241928
]);
19251929
const dCrossChainRemoteStrategy = await ethers.getContract(
@@ -1977,6 +1981,7 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => {
19771981
// unit tests differ from mainnet where remote strategy has a different address
19781982
dRemoteProxy.address,
19791983
usdcAddress,
1984+
usdcAddress, // Assume both are same on unit tests
19801985
cVaultProxy.address,
19811986
"CrossChainMasterStrategy",
19821987
false,
@@ -1991,6 +1996,7 @@ const deployCrossChainUnitTestStrategy = async (usdcAddress) => {
19911996
0, // Ethereum domain id
19921997
dMasterProxy.address,
19931998
usdcAddress,
1999+
usdcAddress, // Assume both are same on unit tests
19942000
"CrossChainRemoteStrategy",
19952001
tokenMessenger.address,
19962002
messageTransmitter.address,

contracts/deploy/mainnet/166_crosschain_strategy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = deploymentWithGovernanceProposal(
3232
// Same address for both master and remote strategy
3333
crossChainStrategyProxyAddress,
3434
addresses.mainnet.USDC,
35+
addresses.base.USDC,
3536
cVaultProxy.address,
3637
"CrossChainMasterStrategy",
3738
false,

contracts/test/strategies/crosschain/_crosschain-helpers.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,27 @@ const encodeCCTPMessage = (
147147
return `0x${versionStr}${sourceDomainStr}${empty18Bytes}${senderStr}${recipientStr}${empty20Bytes}${messageBodyStr}`;
148148
};
149149

150-
const encodeBurnMessageBody = (sender, recipient, amount, hookData) => {
150+
const encodeBurnMessageBody = (
151+
sender,
152+
recipient,
153+
burnToken,
154+
amount,
155+
hookData
156+
) => {
151157
const senderEncoded = ethers.utils.defaultAbiCoder
152158
.encode(["address"], [sender])
153159
.slice(2);
154160
const recipientEncoded = ethers.utils.defaultAbiCoder
155161
.encode(["address"], [recipient])
156162
.slice(2);
163+
const burnTokenEncoded = ethers.utils.defaultAbiCoder
164+
.encode(["address"], [burnToken])
165+
.slice(2);
157166
const amountEncoded = ethers.utils.defaultAbiCoder
158167
.encode(["uint256"], [amount])
159168
.slice(2);
160169
const encodedHookData = hookData.slice(2);
161-
return `0x00000001${empty16Bytes}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat(
170+
return `0x00000001${burnTokenEncoded}${recipientEncoded}${amountEncoded}${senderEncoded}${empty16Bytes.repeat(
162171
3
163172
)}${encodedHookData}`;
164173
};

contracts/test/strategies/crosschain/crosschain-master-strategy.mainnet.fork-test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ describe("ForkTest: CrossChainMasterStrategy", function () {
316316
const burnPayload = encodeBurnMessageBody(
317317
crossChainMasterStrategy.address,
318318
crossChainMasterStrategy.address,
319+
addresses.base.USDC,
319320
usdcUnits("2342"),
320321
balancePayload
321322
);
@@ -493,5 +494,57 @@ describe("ForkTest: CrossChainMasterStrategy", function () {
493494
await crossChainMasterStrategy.remoteStrategyBalance();
494495
expect(remoteStrategyBalanceAfter).to.eq(remoteStrategyBalanceBefore);
495496
});
497+
498+
it("Should revert if the burn token is not peer USDC", async function () {
499+
const { crossChainMasterStrategy, strategist } = fixture;
500+
501+
if (await crossChainMasterStrategy.isTransferPending()) {
502+
// Skip if there's a pending transfer
503+
console.log(
504+
"Skipping balance check message fork test because there's a pending transfer"
505+
);
506+
return;
507+
}
508+
509+
// set an arbitrary remote strategy balance
510+
await setRemoteStrategyBalance(
511+
crossChainMasterStrategy,
512+
usdcUnits("123456")
513+
);
514+
515+
const lastNonce = (
516+
await crossChainMasterStrategy.lastTransferNonce()
517+
).toNumber();
518+
519+
// Replace transmitter to mock transmitter
520+
await replaceMessageTransmitter();
521+
522+
// Build check balance payload
523+
const balancePayload = encodeBalanceCheckMessageBody(
524+
lastNonce,
525+
usdcUnits("12345"),
526+
true // withdrawal confirmation
527+
);
528+
const burnPayload = encodeBurnMessageBody(
529+
crossChainMasterStrategy.address,
530+
crossChainMasterStrategy.address,
531+
addresses.mainnet.WETH, // Not peer USDC
532+
usdcUnits("2342"),
533+
balancePayload
534+
);
535+
const message = encodeCCTPMessage(
536+
6,
537+
addresses.CCTPTokenMessengerV2,
538+
addresses.CCTPTokenMessengerV2,
539+
burnPayload
540+
);
541+
542+
// Relay the message with fake attestation
543+
const tx = crossChainMasterStrategy
544+
.connect(strategist)
545+
.relay(message, "0x");
546+
547+
await expect(tx).to.be.revertedWith("Invalid burn token");
548+
});
496549
});
497550
});

contracts/test/strategies/crosschain/crosschain-remote-strategy.base.fork-test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ describe("ForkTest: CrossChainRemoteStrategy", function () {
143143
const burnPayload = encodeBurnMessageBody(
144144
crossChainRemoteStrategy.address,
145145
crossChainRemoteStrategy.address,
146+
addresses.mainnet.USDC,
146147
depositAmount,
147148
depositPayload
148149
);
@@ -246,4 +247,40 @@ describe("ForkTest: CrossChainRemoteStrategy", function () {
246247
);
247248
expect(balanceAfter).to.approxEqual(expectedBalance);
248249
});
250+
251+
it("Should revert if the burn token is not peer USDC", async function () {
252+
const { crossChainRemoteStrategy, strategist } = fixture;
253+
254+
const nonceBefore = await crossChainRemoteStrategy.lastTransferNonce();
255+
256+
const depositAmount = usdcUnits("1234.56");
257+
258+
// Replace transmitter to mock transmitter
259+
await replaceMessageTransmitter();
260+
261+
const nextNonce = nonceBefore.toNumber() + 1;
262+
263+
// Build deposit message
264+
const depositPayload = encodeDepositMessageBody(nextNonce, depositAmount);
265+
const burnPayload = encodeBurnMessageBody(
266+
crossChainRemoteStrategy.address,
267+
crossChainRemoteStrategy.address,
268+
addresses.base.WETH, // Not peer USDC
269+
depositAmount,
270+
depositPayload
271+
);
272+
const message = encodeCCTPMessage(
273+
0,
274+
addresses.CCTPTokenMessengerV2,
275+
addresses.CCTPTokenMessengerV2,
276+
burnPayload
277+
);
278+
279+
// Relay the message
280+
const tx = crossChainRemoteStrategy
281+
.connect(strategist)
282+
.relay(message, "0x");
283+
284+
await expect(tx).to.be.revertedWith("Invalid burn token");
285+
});
249286
});

0 commit comments

Comments
 (0)