Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ contracts/dist/
contracts/.localKeyValueStorage
contracts/.localKeyValueStorage.mainnet
contracts/.localKeyValueStorage.holesky
contracts/.localKeyValueStorage.base
contracts/scripts/defender-actions/dist/

contracts/lib/defender-actions/dist/
Expand Down
5 changes: 5 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ pnpm hardhat updateAction --id f92ea662-fc34-433b-8beb-b34e9ab74685 --file sonic
pnpm hardhat updateAction --id b1d831f1-29d4-4943-bb2e-8e625b76e82c --file claimBribes
pnpm hardhat updateAction --id 6567d7c6-7ec7-44bd-b95b-470dd1ff780b --file manageBribeOnSonic
pnpm hardhat updateAction --id 6a633bb0-aff8-4b37-aaae-b4c6f244ed87 --file managePassThrough
# These are Base -> Mainnet & Mainnet -> Base actions
# they share the codebase. The direction of relaying attestations is defined by the
# network of the relayer that is attached to the action
pnpm hardhat updateAction --id bb43e5da-f936-4185-84da-253394583665 --file crossChainRelay
pnpm hardhat updateAction --id e571409b-5399-48e4-bfb2-50b7af9903aa --file crossChainRelay
```

`rollup` can be installed globally to avoid the `npx` prefix.
Expand Down
72 changes: 72 additions & 0 deletions contracts/scripts/defender-actions/crossChainRelay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const { ethers } = require("ethers");
const { Defender } = require("@openzeppelin/defender-sdk");
const { processCctpBridgeTransactions } = require("../../tasks/crossChain");
const { getNetworkName } = require("../../utils/hardhat-helpers");
const { configuration } = require("../../utils/cctp");

// Entrypoint for the Defender Action
const handler = async (event) => {
console.log(
`DEBUG env var in handler before being set: "${process.env.DEBUG}"`
);

// Initialize defender relayer provider and signer
const client = new Defender(event);
// Chain ID of the target contract relayer signer
const provider = client.relaySigner.getProvider({ ethersVersion: "v5" });
const { chainId } = await provider.getNetwork();
let sourceProvider;
const signer = await client.relaySigner.getSigner(provider, {
speed: "fastest",
ethersVersion: "v5",
});

// destinatino chain is mainnet, source chain is base
if (chainId === 1) {
if (!event.secrets.BASE_PROVIDER_URL) {
throw new Error("BASE_PROVIDER_URL env var required");
}
sourceProvider = new ethers.providers.JsonRpcProvider(
event.secrets.BASE_PROVIDER_URL
);
}
// destination chain is base, source chain is mainnet
else if (chainId === 8453) {
if (!event.secrets.PROVIDER_URL) {
throw new Error("PROVIDER_URL env var required");
}
sourceProvider = new ethers.providers.JsonRpcProvider(
event.secrets.PROVIDER_URL
);
} else {
throw new Error(`Unsupported chain id: ${chainId}`);
}

const networkName = await getNetworkName(sourceProvider);
const isMainnet = networkName === "mainnet";
const isBase = networkName === "base";

let config;
if (isMainnet) {
config = configuration.mainnetBaseMorpho.mainnet;
} else if (isBase) {
config = configuration.mainnetBaseMorpho.base;
} else {
throw new Error(`Unsupported network name: ${networkName}`);
}

await processCctpBridgeTransactions({
destinationChainSigner: signer,
sourceChainProvider: sourceProvider,
store: client.keyValueStore,
networkName,
blockLookback: config.blockLookback,
cctpDestinationDomainId: config.cctpDestinationDomainId,
cctpSourceDomainId: config.cctpSourceDomainId,
cctpIntegrationContractAddress: config.cctpIntegrationContractAddress,
cctpIntegrationContractAddressDestination:
config.cctpIntegrationContractAddressDestination,
});
};

module.exports = { handler };
1 change: 1 addition & 0 deletions contracts/scripts/defender-actions/rollup.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const actions = [
"sonicRequestWithdrawal",
"sonicClaimWithdrawals",
"claimBribes",
"crossChainRelay",
];

module.exports = actions.map((action) => ({
Expand Down
213 changes: 213 additions & 0 deletions contracts/tasks/crossChain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//const { KeyValueStoreClient } = require("@openzeppelin/defender-sdk");
const ethers = require("ethers");
const { logTxDetails } = require("../utils/txLogger");
const { api: cctpApi } = require("../utils/cctp");

const cctpOperationsConfig = async ({
destinationChainSigner,
sourceChainProvider,
networkName,
cctpIntegrationContractAddress,
cctpIntegrationContractAddressDestination,
}) => {
const cctpIntegratorAbi = [
"event TokensBridged(uint32 peerDomainID,address peerStrategy,address usdcToken,uint256 tokenAmount,uint256 maxFee,uint32 minFinalityThreshold,bytes hookData)",
"event MessageTransmitted(uint32 peerDomainID,address peerStrategy,uint32 minFinalityThreshold,bytes message)",
"function relay(bytes message, bytes attestation) external",
];

const cctpIntegrationContractSource = new ethers.Contract(
cctpIntegrationContractAddress,
cctpIntegratorAbi,
sourceChainProvider
);
const cctpIntegrationContractDestination = new ethers.Contract(
cctpIntegrationContractAddressDestination,
cctpIntegratorAbi,
destinationChainSigner
);

return {
networkName,
cctpIntegrationContractSource,
cctpIntegrationContractDestination,
};
};

const fetchAttestation = async ({ transactionHash, cctpChainId }) => {
console.log(
`Fetching attestation for transaction hash: ${transactionHash} on cctp chain id: ${cctpChainId}`
);
const response = await fetch(
`${cctpApi}/v2/messages/${cctpChainId}?transactionHash=${transactionHash}`
);
if (!response.ok) {
throw new Error(
`Error fetching attestation code: ${
response.status
} error: ${await response.text()}`
);
}
const resultJson = await response.json();

if (resultJson.messages.length !== 1) {
throw new Error(
`Expected 1 attestation, got ${resultJson.messages.length}`
);
}

const message = resultJson.messages[0];
const status = message.status;
if (status !== "complete") {
throw new Error(`Attestation is not complete, status: ${status}`);
}

return {
attestation: message.attestation,
message: message.message,
status: "ok",
};
};

// TokensBridged & MessageTransmitted are the 2 events that are emitted when a transaction is published to the CCTP contract
// One transaction containing such message can at most only contain one of these events
const fetchTxHashesFromCctpTransactions = async ({
config,
blockLookback,
overrideBlock,
sourceChainProvider,
} = {}) => {
let resolvedFromBlock, resolvedToBlock;
if (overrideBlock) {
resolvedFromBlock = overrideBlock;
resolvedToBlock = overrideBlock;
} else {
const latestBlock = await sourceChainProvider.getBlockNumber();
resolvedFromBlock = Math.max(latestBlock - blockLookback, 0);
resolvedToBlock = latestBlock;
}

const cctpIntegrationContractSource = config.cctpIntegrationContractSource;

const tokensBridgedTopic =
cctpIntegrationContractSource.interface.getEventTopic("TokensBridged");
const messageTransmittedTopic =
cctpIntegrationContractSource.interface.getEventTopic("MessageTransmitted");

console.log(
`Fetching event logs from block ${resolvedFromBlock} to block ${resolvedToBlock}`
);
const [eventLogsTokenBridged, eventLogsMessageTransmitted] =
await Promise.all([
sourceChainProvider.getLogs({
address: cctpIntegrationContractSource.address,
fromBlock: resolvedFromBlock,
toBlock: resolvedToBlock,
topics: [tokensBridgedTopic],
}),
sourceChainProvider.getLogs({
address: cctpIntegrationContractSource.address,
fromBlock: resolvedFromBlock,
toBlock: resolvedToBlock,
topics: [messageTransmittedTopic],
}),
]);

// There should be no duplicates in the event logs, but still deduplicate to be safe
const possiblyDuplicatedTxHashes = [
...eventLogsTokenBridged,
...eventLogsMessageTransmitted,
].map((log) => log.transactionHash);
const allTxHashes = Array.from(new Set([...possiblyDuplicatedTxHashes]));

console.log(`Found ${allTxHashes.length} transactions that emitted messages`);
return { allTxHashes };
};

const processCctpBridgeTransactions = async ({
block = undefined,
dryrun = false,
destinationChainSigner,
sourceChainProvider,
store,
networkName,
blockLookback,
cctpDestinationDomainId,
cctpSourceDomainId,
cctpIntegrationContractAddress,
cctpIntegrationContractAddressDestination,
}) => {
const config = await cctpOperationsConfig({
destinationChainSigner,
sourceChainProvider,
networkName,
cctpIntegrationContractAddress,
cctpIntegrationContractAddressDestination,
});
console.log(
`Fetching cctp messages posted on ${config.networkName} network.${
block ? ` Only for block: ${block}` : " Looking at most recent blocks"
}`
);

const { allTxHashes } = await fetchTxHashesFromCctpTransactions({
config,
overrideBlock: block,
sourceChainProvider,
blockLookback,
});
for (const txHash of allTxHashes) {
const storeKey = `cctp_message_${txHash}`;
const storedValue = await store.get(storeKey);

if (storedValue === "processed") {
console.log(
`Transaction with hash: ${txHash} has already been processed. Skipping...`
);
continue;
}

const { attestation, message, status } = await fetchAttestation({
transactionHash: txHash,
cctpChainId: cctpSourceDomainId,
});
if (status !== "ok") {
console.log(
`Attestation from tx hash: ${txHash} on cctp chain id: ${config.cctpSourceDomainId} is not attested yet, status: ${status}. Skipping...`
);
}

console.log(
`Attempting to relay attestation with tx hash: ${txHash} and message: ${message} to cctp chain id: ${cctpDestinationDomainId}`
);

if (dryrun) {
console.log(
`Dryrun: Would have relayed attestation with tx hash: ${txHash} to cctp chain id: ${cctpDestinationDomainId}`
);
continue;
}

const relayTx = await config.cctpIntegrationContractDestination.relay(
message,
attestation
);
console.log(
`Relay transaction with hash ${relayTx.hash} sent to cctp chain id: ${cctpDestinationDomainId}`
);
const receipt = await logTxDetails(relayTx, "CCTP relay");

// Final verification
if (receipt.status === 1) {
console.log("SUCCESS: Transaction executed successfully!");
await store.put(storeKey, "processed");
} else {
console.log("FAILURE: Transaction reverted!");
throw new Error(`Transaction reverted - status: ${receipt.status}`);
}
}
};

module.exports = {
processCctpBridgeTransactions,
};
1 change: 1 addition & 0 deletions contracts/tasks/defender.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ const updateAction = async ({ id, file }) => {
module.exports = {
setActionVars,
updateAction,
getClient,
};
Loading
Loading