diff --git a/Errors.md b/Errors.md index d90a2cb6..7b6fd946 100644 --- a/Errors.md +++ b/Errors.md @@ -74,6 +74,17 @@ | `OnlyOffChain()` | `0x9cbfe066` | | `SimulationFailed()` | `0x2fbab3ac` | +## protocol/switchboard/CCTPSwitchboard.sol + +| Error | Signature | +| ------------------------------- | ------------ | +| `RemoteExecutionNotFound()` | `0xbd506972` | +| `PrevBatchDigestHashMismatch()` | `0xc9864e9d` | +| `NotAttested()` | `0x99efb890` | +| `NotExecuted()` | `0xec84b1da` | +| `InvalidSender()` | `0xddb5de5e` | +| `OnlyMessageTransmitter()` | `0x935ac89c` | + ## protocol/switchboard/FastSwitchboard.sol | Error | Signature | diff --git a/EventTopics.md b/EventTopics.md index 7daf3e8c..73f06674 100644 --- a/EventTopics.md +++ b/EventTopics.md @@ -224,6 +224,22 @@ | `TriggerFeesSet` | `(triggerFees: uint256)` | `0x7df3967b7c8727af5ac0ee9825d88aafeb899d769bc428b91f8967fa0b623084` | | `TriggerSucceeded` | `(triggerId: bytes32)` | `0x92d20fbcbf31370b8218e10ed00c5aad0e689022da30a08905ba5ced053219eb` | +## ICCTPSwitchboard + +| Event | Arguments | Topic | +| ----- | --------- | ----- | + +## CCTPSwitchboard + +| Event | Arguments | Topic | +| ---------------------------- | ----------------------------------------- | -------------------------------------------------------------------- | +| `Attested` | `(payloadId_: bytes32, watcher: address)` | `0x3d83c7bc55c269e0bc853ddc0d7b9fca30216ecc43779acb4e36b7e0ad1c71e4` | +| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | +| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | +| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | +| `RoleGranted` | `(role: bytes32, grantee: address)` | `0x2ae6a113c0ed5b78a53413ffbb7679881f11145ccfba4fb92e863dfcd5a1d2f3` | +| `RoleRevoked` | `(role: bytes32, revokee: address)` | `0x155aaafb6329a2098580462df33ec4b7441b19729b9601c5fc17ae1cf99a8a52` | + ## FastSwitchboard | Event | Arguments | Topic | diff --git a/FunctionSignatures.md b/FunctionSignatures.md index 0b76cd2b..8f333d53 100644 --- a/FunctionSignatures.md +++ b/FunctionSignatures.md @@ -86,18 +86,19 @@ ## SocketBatcher -| Function | Signature | -| ---------------------------- | ------------ | -| `attestAndExecute` | `0x66c7748a` | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `socket__` | `0xc6a261d2` | -| `transferOwnership` | `0xf2fde38b` | +| Function | Signature | +| ------------------------------ | ------------ | +| `attestAndExecute` | `0x66c7748a` | +| `attestCCTPAndProveAndExecute` | `0x6c5fd05f` | +| `cancelOwnershipHandover` | `0x54d1f13d` | +| `completeOwnershipHandover` | `0xf04e283e` | +| `owner` | `0x8da5cb5b` | +| `ownershipHandoverExpiresAt` | `0xfee81cf4` | +| `renounceOwnership` | `0x715018a6` | +| `requestOwnershipHandover` | `0x25692962` | +| `rescueFunds` | `0x6ccae054` | +| `socket__` | `0xc6a261d2` | +| `transferOwnership` | `0xf2fde38b` | ## SocketFeeManager @@ -472,6 +473,41 @@ | `watcherMultiCall` | `0x8021e82b` | | `watcher__` | `0x300bb063` | +## CCTPSwitchboard + +| Function | Signature | +| -------------------------------- | ------------ | +| `addRemoteEndpoint` | `0x7d396da5` | +| `allowPacket` | `0x21e9ec80` | +| `allowPayload` | `0x31c23f66` | +| `attest` | `0x63671b60` | +| `attestVerifyAndProveExecutions` | `0x3e9e97e2` | +| `cancelOwnershipHandover` | `0x54d1f13d` | +| `chainSlug` | `0xb349ba65` | +| `chainSlugToRemoteEndpoint` | `0xa4500424` | +| `completeOwnershipHandover` | `0xf04e283e` | +| `domainToRemoteEndpoint` | `0xc24964fe` | +| `grantRole` | `0x2f2ff15d` | +| `handleReceiveMessage` | `0x96abeb70` | +| `hasRole` | `0x91d14854` | +| `isAttested` | `0xc13c2396` | +| `isRemoteExecuted` | `0x0cd97747` | +| `isSyncedOut` | `0x5ae5dfd6` | +| `messageTransmitter` | `0x7b04c181` | +| `owner` | `0x8da5cb5b` | +| `ownershipHandoverExpiresAt` | `0xfee81cf4` | +| `proveRemoteExecutions` | `0xc36f2ca2` | +| `registerSwitchboard` | `0x74f5b1fc` | +| `remoteExecutedDigests` | `0xecbf77d9` | +| `renounceOwnership` | `0x715018a6` | +| `requestOwnershipHandover` | `0x25692962` | +| `rescueFunds` | `0x6ccae054` | +| `revokeRole` | `0xd547741f` | +| `socket__` | `0xc6a261d2` | +| `syncOut` | `0x69a60ff0` | +| `transferOwnership` | `0xf2fde38b` | +| `verifyAttestations` | `0x6f30514c` | + ## FastSwitchboard | Function | Signature | diff --git a/contracts/evmx/interfaces/IAppGateway.sol b/contracts/evmx/interfaces/IAppGateway.sol index 2321a8bc..51ba61dc 100644 --- a/contracts/evmx/interfaces/IAppGateway.sol +++ b/contracts/evmx/interfaces/IAppGateway.sol @@ -45,4 +45,8 @@ interface IAppGateway { bytes32 contractId_, uint32 chainSlug_ ) external view returns (address forwarderAddress); + + /// @notice get the switchboard type + /// @return sbType The switchboard type + function sbType() external view returns (bytes32); } diff --git a/contracts/protocol/SocketBatcher.sol b/contracts/protocol/SocketBatcher.sol index 1013af2f..8268fe65 100644 --- a/contracts/protocol/SocketBatcher.sol +++ b/contracts/protocol/SocketBatcher.sol @@ -5,8 +5,10 @@ import "solady/auth/Ownable.sol"; import "./interfaces/ISocket.sol"; import "./interfaces/ISocketBatcher.sol"; import "./interfaces/ISwitchboard.sol"; +import "./interfaces/ICCTPSwitchboard.sol"; import "../utils/RescueFundsLib.sol"; -import {ExecuteParams, TransmissionParams} from "../utils/common/Structs.sol"; +import {ExecuteParams, TransmissionParams, CCTPBatchParams, CCTPExecutionParams} from "../utils/common/Structs.sol"; +import {createPayloadId} from "../utils/common/IdUtils.sol"; /** * @title SocketBatcher @@ -55,6 +57,37 @@ contract SocketBatcher is ISocketBatcher, Ownable { ); } + function attestCCTPAndProveAndExecute( + CCTPExecutionParams calldata execParams_, + CCTPBatchParams calldata cctpParams_, + address switchboard_ + ) external payable returns (bool, bytes memory) { + bytes32 payloadId = createPayloadId( + execParams_.executeParams.requestCount, + execParams_.executeParams.batchCount, + execParams_.executeParams.payloadCount, + bytes32(uint256(uint160(address(switchboard_)))), + socket__.chainSlug() + ); + ICCTPSwitchboard(switchboard_).attestVerifyAndProveExecutions( + execParams_, + cctpParams_, + payloadId + ); + (bool success, bytes memory returnData) = socket__.execute{value: msg.value}( + execParams_.executeParams, + TransmissionParams({ + transmitterSignature: execParams_.transmitterSignature, + socketFees: 0, + extraData: execParams_.executeParams.extraData, + refundAddress: execParams_.refundAddress + }) + ); + + ICCTPSwitchboard(switchboard_).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs); + return (success, returnData); + } + /** * @notice Rescues funds from the contract * @param token_ The address of the token to rescue diff --git a/contracts/protocol/interfaces/ICCTPSwitchboard.sol b/contracts/protocol/interfaces/ICCTPSwitchboard.sol new file mode 100644 index 00000000..796ee602 --- /dev/null +++ b/contracts/protocol/interfaces/ICCTPSwitchboard.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "./ISwitchboard.sol"; +import {ExecuteParams, CCTPExecutionParams, CCTPBatchParams} from "../../utils/common/Structs.sol"; + +/** + * @title ISwitchboard + * @dev The interface for a switchboard contract that is responsible for verification of payloads if the correct + * digest is executed. + */ +interface ICCTPSwitchboard is ISwitchboard { + /** + * @notice Syncs out a payload to the remote chains + * @param payloadId_ The unique identifier for the payload + * @param remoteChainSlugs_ The remote chain slugs + */ + function syncOut(bytes32 payloadId_, uint32[] calldata remoteChainSlugs_) external; + + /** + * @notice Handles the receive message + * @param sourceDomain The source domain + * @param sender The sender + * @param messageBody The message body + */ + function handleReceiveMessage( + uint32 sourceDomain, + bytes32 sender, + bytes calldata messageBody + ) external returns (bool); + + /** + * @notice Proves the remote executions + * @param previousPayloadIds_ The previous payload ids + * @param currentPayloadId_ The current payload id + * @param transmitterSignature_ The transmitter signature + * @param executeParams_ The execute parameters + */ + function proveRemoteExecutions( + bytes32[] calldata previousPayloadIds_, + bytes32 currentPayloadId_, + bytes calldata transmitterSignature_, + ExecuteParams calldata executeParams_ + ) external; + + /** + * @notice Verifies the attestations + * @param messages_ The messages + * @param attestations_ The attestations + */ + function verifyAttestations( + bytes[] calldata messages_, + bytes[] calldata attestations_ + ) external; + + function attestVerifyAndProveExecutions( + CCTPExecutionParams calldata execParams_, + CCTPBatchParams calldata cctpParams_, + bytes32 payloadId_ + ) external; +} diff --git a/contracts/protocol/interfaces/IMessageHandler.sol b/contracts/protocol/interfaces/IMessageHandler.sol new file mode 100644 index 00000000..c6c29b10 --- /dev/null +++ b/contracts/protocol/interfaces/IMessageHandler.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; +/** + * @title IMessageHandler + * @notice Handles messages on destination domain forwarded from + * an IReceiver + */ +interface IMessageHandler { + /** + * @notice handles an incoming message from a Receiver + * @param sourceDomain the source domain of the message + * @param sender the sender of the message + * @param messageBody The message raw bytes + * @return success bool, true if successful + */ + function handleReceiveMessage( + uint32 sourceDomain, + bytes32 sender, + bytes calldata messageBody + ) external returns (bool); +} diff --git a/contracts/protocol/interfaces/IMessageTransmitter.sol b/contracts/protocol/interfaces/IMessageTransmitter.sol new file mode 100644 index 00000000..427f2813 --- /dev/null +++ b/contracts/protocol/interfaces/IMessageTransmitter.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +interface IMessageTransmitter { + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64 nonce); + + function receiveMessage( + bytes calldata message, + bytes calldata attestation + ) external returns (bool success); + + function localDomain() external view returns (uint32); + + function attestationManager() external view returns (address); +} diff --git a/contracts/protocol/interfaces/ISocket.sol b/contracts/protocol/interfaces/ISocket.sol index 74dfe535..3488f636 100644 --- a/contracts/protocol/interfaces/ISocket.sol +++ b/contracts/protocol/interfaces/ISocket.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {ExecuteParams, TransmissionParams} from "../../utils/common/Structs.sol"; +import {ExecuteParams, TransmissionParams, ExecutionStatus} from "../../utils/common/Structs.sol"; /** * @title ISocket @@ -76,4 +76,10 @@ interface ISocket { function getPlugConfig( address plugAddress_ ) external view returns (bytes32 appGatewayId, address switchboard); + + function payloadExecuted(bytes32 payloadId_) external view returns (ExecutionStatus); + + function chainSlug() external view returns (uint32); + + function payloadIdToDigest(bytes32 payloadId_) external view returns (bytes32); } diff --git a/contracts/protocol/switchboard/CCTPSwitchboard.sol b/contracts/protocol/switchboard/CCTPSwitchboard.sol new file mode 100644 index 00000000..63b39411 --- /dev/null +++ b/contracts/protocol/switchboard/CCTPSwitchboard.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "./FastSwitchboard.sol"; +import {ISocket} from "../interfaces/ISocket.sol"; +import {ExecuteParams, ExecutionStatus, CCTPExecutionParams, CCTPBatchParams} from "../../utils/common/Structs.sol"; +import {IMessageTransmitter} from "../interfaces/IMessageTransmitter.sol"; +import {IMessageHandler} from "../interfaces/IMessageHandler.sol"; + +contract CCTPSwitchboard is FastSwitchboard, IMessageHandler { + struct RemoteEndpoint { + bytes32 remoteAddress; + uint32 remoteDomain; + } + IMessageTransmitter public immutable messageTransmitter; + + // remoteChainSlug => remoteEndpoint + mapping(uint32 => RemoteEndpoint) public chainSlugToRemoteEndpoint; + // remoteDomain => remoteEndpoint + mapping(uint32 => RemoteEndpoint) public domainToRemoteEndpoint; + + mapping(bytes32 => bool) public isSyncedOut; + mapping(bytes32 => bytes32) public remoteExecutedDigests; + mapping(bytes32 => bool) public isRemoteExecuted; + + error RemoteExecutionNotFound(); + error PrevBatchDigestHashMismatch(); + error NotAttested(); + error NotExecuted(); + error InvalidSender(); + error OnlyMessageTransmitter(); + + constructor( + uint32 chainSlug_, + ISocket socket_, + address owner_, + address messageTransmitter_ + ) FastSwitchboard(chainSlug_, socket_, owner_) { + messageTransmitter = IMessageTransmitter(messageTransmitter_); + } + + function allowPacket(bytes32 digest_, bytes32 payloadId_) external view returns (bool) { + // digest has enough attestations and is remote executed + return isAttested[digest_] && isRemoteExecuted[payloadId_]; + } + + function syncOut(bytes32 payloadId_, uint32[] calldata remoteChainSlugs_) external { + bytes32 digest = socket__.payloadIdToDigest(payloadId_); + // not attested + if (digest == bytes32(0) || !isAttested[digest]) revert NotAttested(); + + // already synced out + if (isSyncedOut[digest]) return; + isSyncedOut[digest] = true; + + // not executed + ExecutionStatus isExecuted = socket__.payloadExecuted(payloadId_); + if (isExecuted != ExecutionStatus.Executed) revert NotExecuted(); + + bytes memory message = abi.encode(payloadId_, digest); + for (uint256 i = 0; i < remoteChainSlugs_.length; i++) { + RemoteEndpoint memory endpoint = chainSlugToRemoteEndpoint[remoteChainSlugs_[i]]; + messageTransmitter.sendMessage(endpoint.remoteDomain, endpoint.remoteAddress, message); + } + } + + function handleReceiveMessage( + uint32 sourceDomain, + bytes32 sender, + bytes calldata messageBody + ) external returns (bool) { + if (msg.sender != address(messageTransmitter)) revert OnlyMessageTransmitter(); + if (domainToRemoteEndpoint[sourceDomain].remoteAddress != sender) revert InvalidSender(); + + (bytes32 payloadId, bytes32 digest) = abi.decode(messageBody, (bytes32, bytes32)); + remoteExecutedDigests[payloadId] = digest; + return true; + } + + function verifyAttestations(bytes[] calldata messages, bytes[] calldata attestations) public { + for (uint256 i = 0; i < messages.length; i++) { + messageTransmitter.receiveMessage(messages[i], attestations[i]); + } + } + + function proveRemoteExecutions( + bytes32[] calldata previousPayloadIds_, + bytes32 payloadId_, + bytes calldata transmitterSignature_, + ExecuteParams calldata executeParams_ + ) public { + // Calculate prevBatchDigestHash from stored remoteExecutedDigests + bytes32 prevBatchDigestHash = bytes32(0); + for (uint256 i = 0; i < previousPayloadIds_.length; i++) { + if (remoteExecutedDigests[previousPayloadIds_[i]] == bytes32(0)) + revert RemoteExecutionNotFound(); + prevBatchDigestHash = keccak256( + abi.encodePacked(prevBatchDigestHash, remoteExecutedDigests[previousPayloadIds_[i]]) + ); + } + // Check if the calculated prevBatchDigestHash matches the one in executeParams_ + if (prevBatchDigestHash != executeParams_.prevBatchDigestHash) + revert PrevBatchDigestHashMismatch(); + + address transmitter = _recoverSigner( + keccak256(abi.encode(address(socket__), payloadId_)), + transmitterSignature_ + ); + + // Construct current digest + (bytes32 appGatewayId, ) = socket__.getPlugConfig(executeParams_.target); + bytes32 constructedDigest = _createDigest( + transmitter, + payloadId_, + appGatewayId, + executeParams_ + ); + + // Verify the constructed digest matches the stored one + if (!isAttested[constructedDigest]) revert NotAttested(); + isRemoteExecuted[payloadId_] = true; + } + + /** + * @notice creates the digest for the payload + * @param transmitter_ The address of the transmitter + * @param payloadId_ The ID of the payload + * @param appGatewayId_ The id of the app gateway + * @param executeParams_ The parameters of the payload + * @return The packed payload as a bytes32 hash + */ + function _createDigest( + address transmitter_, + bytes32 payloadId_, + bytes32 appGatewayId_, + ExecuteParams calldata executeParams_ + ) internal view returns (bytes32) { + return + keccak256( + abi.encode( + address(socket__), + transmitter_, + payloadId_, + executeParams_.deadline, + executeParams_.callType, + executeParams_.gasLimit, + executeParams_.value, + executeParams_.payload, + executeParams_.target, + appGatewayId_, + executeParams_.prevBatchDigestHash, + executeParams_.extraData + ) + ); + } + + function addRemoteEndpoint( + uint32 remoteChainSlug_, + bytes32 remoteAddress_, + uint32 remoteDomain_ + ) external onlyOwner { + chainSlugToRemoteEndpoint[remoteChainSlug_] = RemoteEndpoint({ + remoteAddress: remoteAddress_, + remoteDomain: remoteDomain_ + }); + domainToRemoteEndpoint[remoteDomain_] = RemoteEndpoint({ + remoteAddress: remoteAddress_, + remoteDomain: remoteDomain_ + }); + } + + function attestVerifyAndProveExecutions( + CCTPExecutionParams calldata execParams_, + CCTPBatchParams calldata cctpParams_, + bytes32 payloadId_ + ) external { + attest(execParams_.digest, execParams_.proof); + verifyAttestations(cctpParams_.messages, cctpParams_.attestations); + proveRemoteExecutions( + cctpParams_.previousPayloadIds, + payloadId_, + execParams_.transmitterSignature, + execParams_.executeParams + ); + } +} diff --git a/contracts/protocol/switchboard/FastSwitchboard.sol b/contracts/protocol/switchboard/FastSwitchboard.sol index 27d915fe..7ec6fef5 100644 --- a/contracts/protocol/switchboard/FastSwitchboard.sol +++ b/contracts/protocol/switchboard/FastSwitchboard.sol @@ -40,7 +40,7 @@ contract FastSwitchboard is SwitchboardBase { * @param proof_ proof from watcher * @notice we are attesting a payload uniquely identified with digest. */ - function attest(bytes32 digest_, bytes calldata proof_) external { + function attest(bytes32 digest_, bytes calldata proof_) public virtual { if (isAttested[digest_]) revert AlreadyAttested(); address watcher = _recoverSigner( diff --git a/contracts/utils/common/Constants.sol b/contracts/utils/common/Constants.sol index 62b19797..2f98d595 100644 --- a/contracts/utils/common/Constants.sol +++ b/contracts/utils/common/Constants.sol @@ -13,6 +13,7 @@ bytes4 constant SCHEDULE = bytes4(keccak256("SCHEDULE")); bytes32 constant CALLBACK = keccak256("CALLBACK"); bytes32 constant FAST = keccak256("FAST"); +bytes32 constant CCTP = keccak256("CCTP"); uint256 constant PAYLOAD_SIZE_LIMIT = 24_500; uint16 constant MAX_COPY_BYTES = 2048; // 2KB diff --git a/contracts/utils/common/Structs.sol b/contracts/utils/common/Structs.sol index b8b26830..d1a8a2ca 100644 --- a/contracts/utils/common/Structs.sol +++ b/contracts/utils/common/Structs.sol @@ -210,6 +210,21 @@ struct RequestParams { bytes onCompleteData; } +struct CCTPExecutionParams { + ExecuteParams executeParams; + bytes32 digest; + bytes proof; + bytes transmitterSignature; + address refundAddress; +} + +struct CCTPBatchParams { + bytes32[] previousPayloadIds; + uint32[] nextBatchRemoteChainSlugs; + bytes[] messages; + bytes[] attestations; +} + struct SolanaInstruction { SolanaInstructionData data; SolanaInstructionDataDescription description; diff --git a/deployments/dev_addresses.json b/deployments/dev_addresses.json index 6e0444a6..e41f9235 100644 --- a/deployments/dev_addresses.json +++ b/deployments/dev_addresses.json @@ -1,44 +1,55 @@ { + "84532": { + "CCTPSwitchboard": "0xb2B779ab8FC851bCE986d25B2824933B0Cd101d9", + "ContractFactoryPlug": "0x2e0fE75Bd247d7441f279388CD0e3a77FEcddADf", + "FastSwitchboard": "0xaFFfaD81e6DDE509Bd83Ab2024225b2FF537BeA7", + "FeesPlug": "0x1dc20d27F06876cA74ee4e2E9a4724f06a4a5E54", + "Socket": "0x9B06e2Dae351ed8B1112C036e5c306E8fBe4C9c5", + "SocketBatcher": "0xd627BFe7d2fCAC1147c996a6F2CAaB2E1e1bD344", + "startBlock": 27234828 + }, "421614": { - "ContractFactoryPlug": "0x7b9928b01272b915050aDfcba7e0a11b22271BAd", - "FastSwitchboard": "0x2974E94c0d1323D3A24f7B4F924fbdB325Be1aa3", - "FeesPlug": "0xaFD76cADB518E7e5131991Fe4403e00297916957", - "Socket": "0xb7378ae43b135988C8a83dfD1AcD71Ff39381396", - "SocketBatcher": "0x60541d31Fda60163480CAb486be3762b5793B650", - "startBlock": 159641867 + "CCTPSwitchboard": "0x74A4aa989515b088A1aC33C5D59897f69cA66B91", + "ContractFactoryPlug": "0x25190648330361f35d00a0D41BD347de8E1B838C", + "FastSwitchboard": "0x39d21e679312Bf0e3681bd0254c587E3528dd2a3", + "FeesPlug": "0xe9BDa44a39F8d29eaF1956EB05442551794871f3", + "Socket": "0x86264607bAD260e9032add9e4E2DA74f71E354E0", + "SocketBatcher": "0x5B83E4104E37c7ECDf6Aeb62a4E204E4c63ac8D5", + "startBlock": 157984273 }, "7625382": { - "AddressResolver": "0x8161cDBa2d2fCE66307254AAC1d42966D4F5353E", - "AddressResolverImpl": "0x91e548d87768313C03da8405D01171b83912c430", - "AsyncDeployer": "0x025b308371dC1C5e337527f96BE46Ba6A12c774A", - "AsyncDeployerImpl": "0x80CFbD3B6134Fb2D2B7d21FC132a9F7c115e7B72", - "AuctionManager": "0xA40aFA1632328D84226084a4539B3869D2B68e28", - "AuctionManagerImpl": "0x42109F6212765ABeb589f9b2c14Bee4b8DB3e638", - "Configurations": "0x60185198097df249B504D5A164323eBF42B3764d", - "ConfigurationsImpl": "0x0d2646fC08af29A7799Af435c5ABBA1b020C4dC7", - "DeployForwarder": "0xdC51D652B8c3cCB3cAAB9C1E2704fD4D62E76433", - "DeployForwarderImpl": "0xCe95fca954a0BF43c299c79d5152f2c164C02b7A", - "ERC1967Factory": "0xb0364Fd8f158071831ac87E7EE2C792Ab509a524", - "FeesManager": "0x09F824Eae77f71279d73Ae24FEb2163FCe88B25D", - "FeesManagerImpl": "0x5b460B29750648f6D569Ed57139967BE589174F8", - "FeesPool": "0xc20Be67ef742202dc93A78aa741E7C3715eA1DFd", - "PromiseResolver": "0xcfFda1dF8668266E6A77809EcA9CCA8A632ecaF3", - "ReadPrecompile": "0x254Dc9e0623426A79F02D2001E367cd32B50aaaA", - "RequestHandler": "0x1FE7527a8620374B3Fdb101bA1D56eC46EC9a24A", - "RequestHandlerImpl": "0x3d9578B252ed1F5A66348Cc40E482dacc32Ae790", - "SchedulePrecompile": "0x7D6F2A4aDf7e5Cfcf9627CC7FCA1d39fD19C07fc", - "startBlock": 8355289, - "Watcher": "0xD5b30DC89D96ee7303Dc2726491996B46089F693", - "WatcherImpl": "0x872bb254118a2210e3C491918133F2ab4D7Bc362", - "WritePrecompile": "0x10eaDbd1a2787ebbF4Abe9b6D79e669C0c8E8B26", - "WritePrecompileImpl": "0xD3aEb53da0a72788C16eAf5a23a5aBae6708C073" + "AddressResolver": "0x67790E222c41b0E787C278e757b7c40f03Fa5709", + "AddressResolverImpl": "0x89C928379fED43B7117b852931e2968ce39C8380", + "AsyncDeployer": "0x1C70bc3043667e884222B8835E0Ae554eb512810", + "AsyncDeployerImpl": "0x09a762309c63a4e19cd3d822aA340Fe964Ba9C92", + "AuctionManager": "0xC12aDF88dfc116CAF88816d150FE498843dABEEe", + "AuctionManagerImpl": "0x55b76897b3BF6ED04188cbaa7DC21ae14b35D3eE", + "Configurations": "0x377431bD1A3321C401542C8B1EC6E0c23E125042", + "ConfigurationsImpl": "0xDe5DedAe6e17f906D1269D5e84BEfB06F3926310", + "DeployForwarder": "0xd48218b2DafF9063177b0c6Bae229ec6C5f086a9", + "DeployForwarderImpl": "0x8e178161BB3B36a28C15DFBe3142afF8757B8993", + "ERC1967Factory": "0x870fCA8803bEFd119B1317AFB6794F97af7e515e", + "FeesManager": "0x761A9024D267006061ec943d02e3949678906f3E", + "FeesManagerImpl": "0x29C583B64FD2d7b70f8F6253C2a28D60af364Cb5", + "FeesPool": "0x9De353dD1131aB4e502590D3a1832652FA316268", + "PromiseResolver": "0x73b1B3dF6C71e0aa912f9d6933920D4461ae9718", + "ReadPrecompile": "0x58f49313816c1876417EE53De8F5de047359fB2C", + "RequestHandler": "0x63a6D7096b5a2F5c9Ce7D8632A7A2034A85b7F01", + "RequestHandlerImpl": "0x593f4844ceEA828bC6d9D78A0ef7Ce64F42190dC", + "SchedulePrecompile": "0xF77d2059a66026Efac11334D30372429553CAaC3", + "startBlock": 8626651, + "Watcher": "0xe4D1B4B8c0eEE90ac1f5314e758446CBa201BBA8", + "WatcherImpl": "0x7726e559A5129A9174f89F7E2029f7212B66dD13", + "WritePrecompile": "0xd8be408E271EEe9d3D0f28305bB9b6003589E1A9", + "WritePrecompileImpl": "0xE24c4b0f67f566Fa558b3FE85f1780CD330f1F4D" }, "11155420": { - "ContractFactoryPlug": "0x0279A18d5FC235A92fB4ABd5F7e9258e78E27948", - "FastSwitchboard": "0x6b4EF1452265193798bfa3ef6D29421da9e7E222", - "FeesPlug": "0x5E175fD699E066D6536054198d57AF0De88C7c4E", - "Socket": "0xB260A4DD0952e9A5b5F6652019469F05Fb137dC5", - "SocketBatcher": "0xc320FC7b06D4491A9E7e6fa55a3305b12548519e", - "startBlock": 28568337 + "CCTPSwitchboard": "0xf9A93a92c0754084f6320f3fC1D54584C2e0439d", + "ContractFactoryPlug": "0xAA78A6c96DF690d30eF161490f6590fCAb8f4406", + "FastSwitchboard": "0x696d7d0Af367cFE3d3c56BD61ca16B3A0939618b", + "FeesPlug": "0xC6Fb338F2009B5AD1e1bbA2dd2c9f52e9dE2C91C", + "Socket": "0x4B5718c1f739A83EF5c75f64776f2c9D4D460B1D", + "SocketBatcher": "0xc9E18b73C1A575D8A5975754a01CD19BE8400037", + "startBlock": 28356082 } } diff --git a/hardhat-scripts/config/config.ts b/hardhat-scripts/config/config.ts index 7ba92e79..5d78ae70 100644 --- a/hardhat-scripts/config/config.ts +++ b/hardhat-scripts/config/config.ts @@ -26,9 +26,17 @@ export const logConfig = () => { export const getChains = () => { switch (mode) { case DeploymentMode.LOCAL: - return [ChainSlug.ARBITRUM_SEPOLIA, ChainSlug.OPTIMISM_SEPOLIA]; + return [ + ChainSlug.ARBITRUM_SEPOLIA, + ChainSlug.OPTIMISM_SEPOLIA, + ChainSlug.BASE_SEPOLIA, + ]; case DeploymentMode.DEV: - return [ChainSlug.ARBITRUM_SEPOLIA, ChainSlug.OPTIMISM_SEPOLIA]; + return [ + ChainSlug.ARBITRUM_SEPOLIA, + ChainSlug.OPTIMISM_SEPOLIA, + ChainSlug.BASE_SEPOLIA, + ]; case DeploymentMode.STAGE: return [ ChainSlug.BASE, @@ -95,7 +103,7 @@ export const MAX_MSG_VALUE_LIMIT = ethers.utils.parseEther("0.001"); // Auction parameters export const AUCTION_END_DELAY_SECONDS = 0; export const BID_TIMEOUT = 600; // 10 minutes -export const EXPIRY_TIME = 300; // 5 minutes +export const EXPIRY_TIME = 3600; // 1 hour export const MAX_RE_AUCTION_COUNT = 5; // Fees Pool Funding Amount diff --git a/hardhat-scripts/constants/constants.ts b/hardhat-scripts/constants/constants.ts index 71aa09e1..168d2b47 100644 --- a/hardhat-scripts/constants/constants.ts +++ b/hardhat-scripts/constants/constants.ts @@ -7,6 +7,7 @@ export const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; export const FAST_SWITCHBOARD_TYPE = id("FAST"); +export const CCTP_SWITCHBOARD_TYPE = id("CCTP"); export const ZERO_APP_GATEWAY_ID = ethers.utils.hexZeroPad( constants.AddressZero, diff --git a/hardhat-scripts/deploy/1.deploy.ts b/hardhat-scripts/deploy/1.deploy.ts index ffd4d4cc..641e3a64 100644 --- a/hardhat-scripts/deploy/1.deploy.ts +++ b/hardhat-scripts/deploy/1.deploy.ts @@ -2,7 +2,12 @@ import { config } from "dotenv"; import { Contract, utils, Wallet } from "ethers"; import { formatEther } from "ethers/lib/utils"; import { ethers } from "hardhat"; -import { ChainAddressesObj, ChainSlug, Contracts } from "../../src"; +import { + ChainAddressesObj, + ChainSlug, + Contracts, + MESSAGE_TRANSMITTER, +} from "../../src"; import { AUCTION_END_DELAY_SECONDS, BID_TIMEOUT, @@ -322,6 +327,21 @@ const deploySocketContracts = async () => { ); deployUtils.addresses[contractName] = sb.address; + contractName = Contracts.CCTPSwitchboard; + const cctpSwitchboard: Contract = await getOrDeploy( + contractName, + contractName, + `contracts/protocol/switchboard/${contractName}.sol`, + [ + chain as ChainSlug, + socket.address, + socketOwner, + MESSAGE_TRANSMITTER[chain as ChainSlug], + ], + deployUtils + ); + deployUtils.addresses[contractName] = cctpSwitchboard.address; + if (getFeesPlugChains().includes(chain as ChainSlug)) { contractName = Contracts.FeesPlug; const feesPlug: Contract = await getOrDeploy( diff --git a/hardhat-scripts/deploy/2.roles.ts b/hardhat-scripts/deploy/2.roles.ts index 79723cec..99db548c 100644 --- a/hardhat-scripts/deploy/2.roles.ts +++ b/hardhat-scripts/deploy/2.roles.ts @@ -16,6 +16,7 @@ export const REQUIRED_ROLES = { }, Chain: { FastSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], + CCTPSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], Socket: [ ROLES.GOVERNANCE_ROLE, ROLES.RESCUE_ROLE, @@ -80,8 +81,9 @@ async function setRolesOnChain(chain: number, addresses: DeploymentAddresses) { for (const roleName of roles) { const targetAddress = - contractName === Contracts.FastSwitchboard && - roleName === ROLES.WATCHER_ROLE + [Contracts.FastSwitchboard, Contracts.CCTPSwitchboard].includes( + contractName as Contracts + ) && roleName === ROLES.WATCHER_ROLE ? watcher : signer.address; diff --git a/hardhat-scripts/deploy/3.configureChains.ts b/hardhat-scripts/deploy/3.configureChains.ts index dc2948a3..d1484731 100644 --- a/hardhat-scripts/deploy/3.configureChains.ts +++ b/hardhat-scripts/deploy/3.configureChains.ts @@ -2,9 +2,22 @@ import { config as dotenvConfig } from "dotenv"; dotenvConfig(); import { Contract, Signer, Wallet } from "ethers"; -import { ChainAddressesObj, ChainSlug, Contracts } from "../../src"; -import { chains, EVMX_CHAIN_ID, MAX_MSG_VALUE_LIMIT, mode } from "../config"; import { + ChainAddressesObj, + ChainSlug, + Contracts, + CCTP_DOMAINS, +} from "../../src"; +import { + chains, + EVMX_CHAIN_ID, + mainnetChains, + MAX_MSG_VALUE_LIMIT, + mode, + testnetChains, +} from "../config"; +import { + CCTP_SWITCHBOARD_TYPE, DeploymentAddresses, FAST_SWITCHBOARD_TYPE, getFeeTokens, @@ -50,6 +63,13 @@ export const configureChains = async (addresses: DeploymentAddresses) => { socketContract ); + await registerSb( + chain, + chainAddresses[Contracts.CCTPSwitchboard], + signer, + socketContract + ); + if (chainAddresses[Contracts.FeesPlug]) { await whitelistToken(chain, chainAddresses[Contracts.FeesPlug], signer); } @@ -57,6 +77,13 @@ export const configureChains = async (addresses: DeploymentAddresses) => { await setMaxMsgValueLimit(chain); await setOnchainContracts(chain, addresses); + + await addRemoteEndpointsToCCTPSwitchboard( + chain, + addresses, + signer, + socketContract + ); } }; @@ -105,6 +132,16 @@ async function setOnchainContracts( signer ); + await updateContractSettings( + EVMX_CHAIN_ID, + Contracts.Configurations, + "switchboards", + [chain, CCTP_SWITCHBOARD_TYPE], + chainAddresses[Contracts.CCTPSwitchboard], + "setSwitchboard", + [chain, CCTP_SWITCHBOARD_TYPE, chainAddresses[Contracts.CCTPSwitchboard]], + signer + ); await updateContractSettings( EVMX_CHAIN_ID, Contracts.Configurations, @@ -140,6 +177,70 @@ async function setOnchainContracts( ); } +const addRemoteEndpointsToCCTPSwitchboard = async ( + chain: number, + addresses: DeploymentAddresses, + signer: Wallet, + socket: Contract +) => { + try { + console.log("Adding remote endpoints to CCTP switchboard"); + const chainAddresses = addresses[chain] as ChainAddressesObj; + const sbAddress = chainAddresses[Contracts.CCTPSwitchboard]; + const switchboard = ( + await getInstance(Contracts.CCTPSwitchboard, sbAddress) + ).connect(signer); + const remoteChainSlugs = getRemoteChainSlugs(chain); + console.log(chain, " remoteChainSlugs: ", remoteChainSlugs); + + for (const remoteChainSlug of remoteChainSlugs) { + const remoteSwitchboardAddress = + addresses[remoteChainSlug]?.[Contracts.CCTPSwitchboard]; + const currentRemoteEndpoint = await switchboard.chainSlugToRemoteEndpoint( + remoteChainSlug + ); + if (currentRemoteEndpoint.remoteAddress == remoteSwitchboardAddress) { + console.log(`Remote endpoint ${remoteChainSlug} already exists`); + continue; + } + if (!remoteSwitchboardAddress) { + console.log( + `Remote switchboard address not found for ${remoteChainSlug}` + ); + continue; + } + const registerTx = await switchboard.addRemoteEndpoint( + remoteChainSlug, + `0x${remoteSwitchboardAddress.slice(2).padStart(64, "0")}`, + CCTP_DOMAINS[remoteChainSlug], + { + ...(await overrides(chain)), + } + ); + console.log( + `Adding remote endpoint ${remoteChainSlug} to ${sbAddress}: ${registerTx.hash}` + ); + await registerTx.wait(); + } + } catch (error) { + throw error; + } +}; + +const getRemoteChainSlugs = (chain: number) => { + if (testnetChains.includes(chain)) { + return chains.filter( + (c) => c !== chain && testnetChains.includes(c as ChainSlug) + ); + } + if (mainnetChains.includes(chain)) { + return chains.filter( + (c) => c !== chain && mainnetChains.includes(c as ChainSlug) + ); + } + return chains.filter((c) => c !== chain); +}; + const registerSb = async ( chain: number, sbAddress: string, diff --git a/hardhat-scripts/misc-scripts/getAttestations.ts b/hardhat-scripts/misc-scripts/getAttestations.ts new file mode 100644 index 00000000..dcdcc7be --- /dev/null +++ b/hardhat-scripts/misc-scripts/getAttestations.ts @@ -0,0 +1,95 @@ +import { ethers } from "ethers"; +import axios from "axios"; + +async function getAttestation(messageHash: string): Promise { + try { + const response = await axios.get( + `https://iris-api-sandbox.circle.com/v1/attestations/${messageHash}` + ); + console.log("messageHash", messageHash, "response", response.data); + if (response.data.status === "complete") { + return response.data.attestation; + } + return null; + } catch (error) { + return null; + } +} + +async function main() { + const args = process.argv.slice(2); + if (args.length !== 2) { + console.log("Usage: ts-node getAttestations.ts "); + process.exit(1); + } + + const [txHash, providerUrl] = args; + const provider = new ethers.providers.JsonRpcProvider(providerUrl); + + // Get transaction receipt + const receipt = await provider.getTransactionReceipt(txHash); + + // ABI for MessageSent event + const messageTransmitterInterface = new ethers.utils.Interface([ + "event MessageSent(bytes message)", + ]); + + // Filter logs for MessageSent event + const messageSentLogs = receipt.logs.filter((log) => { + try { + const parsedLog = messageTransmitterInterface.parseLog(log); + return parsedLog.name === "MessageSent"; + } catch { + return false; + } + }); + + if (messageSentLogs.length === 0) { + console.log("No MessageSent events found in transaction"); + process.exit(1); + } + + const messages: string[] = []; + const messageHashes: string[] = []; + const attestations: string[] = []; + + // Get messages and calculate hashes + for (const log of messageSentLogs) { + const parsedLog = messageTransmitterInterface.parseLog(log); + const message = parsedLog.args.message; + const messageHash = ethers.utils.keccak256(message); + + messages.push(message); + messageHashes.push(messageHash); + } + + // Poll for attestations + let complete = false; + while (!complete) { + complete = true; + + for (let i = 0; i < messageHashes.length; i++) { + if (!attestations[i]) { + const attestation = await getAttestation(messageHashes[i]); + if (attestation) { + attestations[i] = attestation; + } else { + complete = false; + } + } + } + + if (!complete) { + console.log("Waiting for attestations..."); + await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds + } + } + + console.log("\nMessages:", messages); + console.log("\nAttestations:", attestations); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/package.json b/package.json index 0dfa34ef..7aa6ab30 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "compile": "forge build", "deploy": "bash setupInfraContracts.sh", "publish-core": "yarn build && yarn publish --patch --no-git-tag-version", - "trace": "bash trace.sh" + "trace": "source .env && bash trace.sh" }, "pre-commit": [], "author": "", diff --git a/script/helpers/TransferRemainingCredits.s.sol b/script/helpers/TransferRemainingCredits.s.sol new file mode 100644 index 00000000..e0ae8cea --- /dev/null +++ b/script/helpers/TransferRemainingCredits.s.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {FeesManager} from "../../contracts/evmx/fees/FeesManager.sol"; +import {IAppGateway} from "../../contracts/evmx/interfaces/IAppGateway.sol"; + +contract TransferRemainingCredits is Script { + function run() external { + string memory rpc = vm.envString("EVMX_RPC"); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + FeesManager feesManager = FeesManager(payable(vm.envAddress("FEES_MANAGER"))); + address appGateway = vm.envAddress("APP_GATEWAY"); + address newAppGateway = vm.envAddress("NEW_APP_GATEWAY"); + + (uint256 totalCredits, uint256 blockedCredits) = feesManager.userCredits(appGateway); + console.log("App Gateway:", appGateway); + console.log("New App Gateway:", newAppGateway); + console.log("Fees Manager:", address(feesManager)); + console.log("totalCredits fees:", totalCredits); + console.log("blockedCredits fees:", blockedCredits); + + uint256 availableFees = feesManager.getAvailableCredits(appGateway); + console.log("Available fees:", availableFees); + bytes memory data = abi.encodeWithSignature( + "transferCredits(address,uint256)", + newAppGateway, + availableFees + ); + (bool success, ) = appGateway.call(data); + require(success, "Transfer failed"); + vm.stopBroadcast(); + } +} diff --git a/script/helpers/WithdrawRemainingCredits.s.sol b/script/helpers/WithdrawRemainingCredits.s.sol new file mode 100644 index 00000000..c0b8bd31 --- /dev/null +++ b/script/helpers/WithdrawRemainingCredits.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {FeesManager} from "../../contracts/evmx/fees/FeesManager.sol"; + +contract WithdrawRemainingCredits is Script { + function run() external { + string memory rpc = vm.envString("EVMX_RPC"); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + FeesManager feesManager = FeesManager(payable(vm.envAddress("FEES_MANAGER"))); + address appGateway = vm.envAddress("APP_GATEWAY"); + + (uint256 totalCredits, uint256 blockedCredits) = feesManager.userCredits(appGateway); + console.log("App Gateway:", appGateway); + console.log("Fees Manager:", address(feesManager)); + console.log("totalCredits fees:", totalCredits); + console.log("blockedCredits fees:", blockedCredits); + + uint256 availableFees = feesManager.getAvailableCredits(appGateway); + console.log("Available fees:", availableFees); + feesManager.transferCredits(appGateway, vm.addr(deployerPrivateKey), availableFees); + + vm.stopBroadcast(); + } +} diff --git a/script/supertoken/DeployEVMxSuperTokenApp.s.sol b/script/supertoken/DeployEVMxSuperTokenApp.s.sol new file mode 100644 index 00000000..c09efdcc --- /dev/null +++ b/script/supertoken/DeployEVMxSuperTokenApp.s.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import "../../test/apps/app-gateways/super-token/SuperTokenAppGateway.sol"; +import {CCTP} from "../../contracts/utils/common/Constants.sol"; +// source .env && forge script script/supertoken/deployEVMxSuperTokenApp.s.sol --broadcast --skip-simulation --legacy --gas-price 0 +contract SuperTokenDeploy is Script { + function run() external { + address addressResolver = vm.envAddress("ADDRESS_RESOLVER"); + string memory rpc = vm.envString("EVMX_RPC"); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + // Setting fee payment on Arbitrum Sepolia + uint256 fees = 1 ether; + + SuperTokenAppGateway gateway = new SuperTokenAppGateway( + addressResolver, + vm.addr(deployerPrivateKey), + fees, + SuperTokenAppGateway.ConstructorParams({ + name_: "SuperToken", + symbol_: "SUPER", + decimals_: 18, + initialSupplyHolder_: vm.addr(deployerPrivateKey), + initialSupply_: 1000000000000000000000000000 + }) + ); + + console.log("Contracts deployed:"); + console.log("SuperTokenAppGateway:", address(gateway)); + gateway.setSbType(CCTP); + } +} diff --git a/script/supertoken/TransferSuperToken.s.sol b/script/supertoken/TransferSuperToken.s.sol new file mode 100644 index 00000000..c1d39d2a --- /dev/null +++ b/script/supertoken/TransferSuperToken.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {SuperTokenAppGateway} from "../../test/apps/app-gateways/super-token/SuperTokenAppGateway.sol"; + +// source .env && forge script script/supertoken/TransferSuperToken.s.sol --broadcast --skip-simulation --legacy --gas-price 0 +contract TransferSuperToken is Script { + function run() external { + string memory socketRPC = vm.envString("EVMX_RPC"); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + vm.createSelectFork(socketRPC); + + SuperTokenAppGateway gateway = SuperTokenAppGateway(vm.envAddress("APP_GATEWAY")); + address forwarderArb = gateway.forwarderAddresses(gateway.superToken(), 421614); + address forwarderOpt = gateway.forwarderAddresses(gateway.superToken(), 11155420); + + SuperTokenAppGateway.TransferOrder memory transferOrder = SuperTokenAppGateway + .TransferOrder({ + srcToken: forwarderArb, + dstToken: forwarderOpt, + user: vm.addr(deployerPrivateKey), + srcAmount: 100, + deadline: block.timestamp + 1000000 + }); + + bytes memory encodedOrder = abi.encode(transferOrder); + bytes memory encodedPayload = abi.encodeWithSelector( + bytes4(keccak256("transfer(bytes)")), + encodedOrder + ); + console.logBytes(encodedPayload); + } +} diff --git a/test/SetupTest.t.sol b/test/SetupTest.t.sol index 4f761381..32123840 100644 --- a/test/SetupTest.t.sol +++ b/test/SetupTest.t.sol @@ -12,6 +12,7 @@ import "../contracts/evmx/interfaces/IForwarder.sol"; import "../contracts/protocol/Socket.sol"; import "../contracts/protocol/switchboard/FastSwitchboard.sol"; +import "../contracts/protocol/switchboard/CCTPSwitchboard.sol"; import "../contracts/protocol/SocketBatcher.sol"; import "../contracts/protocol/SocketFeeManager.sol"; @@ -32,6 +33,7 @@ import "../contracts/evmx/fees/FeesPool.sol"; import "../contracts/evmx/plugs/FeesPlug.sol"; import "../contracts/evmx/AuctionManager.sol"; import "../contracts/evmx/mocks/TestUSDC.sol"; +import "./mock/CCTPMessageTransmitter.sol"; import "solady/utils/ERC1967Factory.sol"; @@ -68,11 +70,15 @@ contract SetupStore is Test { uint256 public payloadIdCounter; uint256 public triggerCounter; uint256 public asyncPromiseCounter; + uint32 public optCCTPDomain = 2; + uint32 public arbCCTPDomain = 3; struct SocketContracts { uint32 chainSlug; Socket socket; SocketFeeManager socketFeeManager; FastSwitchboard switchboard; + CCTPSwitchboard cctpSwitchboard; + CCTPMessageTransmitter cctpMessageTransmitter; SocketBatcher socketBatcher; ContractFactoryPlug contractFactoryPlug; FeesPlug feesPlug; @@ -121,6 +127,18 @@ contract DeploySetup is SetupStore { optConfig = _deploySocket(optChainSlug); _configureChain(optChainSlug); + vm.startPrank(socketOwner); + arbConfig.cctpSwitchboard.addRemoteEndpoint( + optChainSlug, + addressToBytes32(address(optConfig.cctpSwitchboard)), + optCCTPDomain + ); + optConfig.cctpSwitchboard.addRemoteEndpoint( + arbChainSlug, + addressToBytes32(address(arbConfig.cctpSwitchboard)), + arbCCTPDomain + ); + vm.stopPrank(); // transfer eth to fees pool for native fee payouts vm.deal(address(feesPool), 100000 ether); @@ -209,13 +227,23 @@ contract DeploySetup is SetupStore { function _deploySocket(uint32 chainSlug_) internal returns (SocketContracts memory) { // socket Socket socket = new Socket(chainSlug_, socketOwner, "test"); - + CCTPMessageTransmitter cctpMessageTransmitter = new CCTPMessageTransmitter( + chainSlug_, + address(0) + ); return SocketContracts({ chainSlug: chainSlug_, socket: socket, socketFeeManager: new SocketFeeManager(socketOwner, socketFees), switchboard: new FastSwitchboard(chainSlug_, socket, socketOwner), + cctpSwitchboard: new CCTPSwitchboard( + chainSlug_, + socket, + socketOwner, + address(cctpMessageTransmitter) + ), + cctpMessageTransmitter: cctpMessageTransmitter, socketBatcher: new SocketBatcher(socketOwner, socket), contractFactoryPlug: new ContractFactoryPlug(address(socket), socketOwner), feesPlug: new FeesPlug(address(socket), socketOwner), @@ -227,6 +255,7 @@ contract DeploySetup is SetupStore { SocketContracts memory socketConfig = getSocketConfig(chainSlug_); Socket socket = socketConfig.socket; FastSwitchboard switchboard = socketConfig.switchboard; + CCTPSwitchboard cctpSwitchboard = socketConfig.cctpSwitchboard; FeesPlug feesPlug = socketConfig.feesPlug; ContractFactoryPlug contractFactoryPlug = socketConfig.contractFactoryPlug; @@ -241,6 +270,9 @@ contract DeploySetup is SetupStore { switchboard.grantRole(WATCHER_ROLE, watcherEOA); switchboard.grantRole(RESCUE_ROLE, address(socketOwner)); + cctpSwitchboard.registerSwitchboard(); + cctpSwitchboard.grantRole(WATCHER_ROLE, watcherEOA); + feesPlug.grantRole(RESCUE_ROLE, address(socketOwner)); feesPlug.whitelistToken(address(socketConfig.testUSDC)); feesPlug.connectSocket( @@ -261,6 +293,7 @@ contract DeploySetup is SetupStore { vm.startPrank(watcherEOA); configurations.setSocket(chainSlug_, toBytes32Format(address(socket))); configurations.setSwitchboard(chainSlug_, FAST, toBytes32Format(address(switchboard))); + configurations.setSwitchboard(chainSlug_, CCTP, toBytes32Format(address(cctpSwitchboard))); // plugs feesManager.setFeesPlug(chainSlug_, toBytes32Format(address(feesPlug))); @@ -893,33 +926,155 @@ contract WatcherSetup is AuctionSetup { transmitterPrivateKey ); bytes memory returnData; - (success, returnData) = getSocketConfig(chainSlug).socketBatcher.attestAndExecute( - ExecuteParams({ - callType: digestParams.callType, - deadline: digestParams.deadline, - gasLimit: digestParams.gasLimit, - value: digestParams.value, - payload: digestParams.payload, - target: fromBytes32Format(digestParams.target), - requestCount: payloadParams.requestCount, - batchCount: payloadParams.batchCount, - payloadCount: payloadParams.payloadCount, - prevBatchDigestHash: digestParams.prevBatchDigestHash, - extraData: digestParams.extraData - }), - fromBytes32Format(switchboard), - digest, - watcherProof, - transmitterSig, - transmitterEOA - ); - + ExecuteParams memory executeParams = ExecuteParams({ + callType: digestParams.callType, + deadline: digestParams.deadline, + gasLimit: digestParams.gasLimit, + value: digestParams.value, + payload: digestParams.payload, + target: fromBytes32Format(digestParams.target), + requestCount: payloadParams.requestCount, + batchCount: payloadParams.batchCount, + payloadCount: payloadParams.payloadCount, + prevBatchDigestHash: digestParams.prevBatchDigestHash, + extraData: digestParams.extraData + }); + if (fromBytes32Format(switchboard) == address(getSocketConfig(chainSlug).switchboard)) { + (success, returnData) = getSocketConfig(chainSlug).socketBatcher.attestAndExecute( + executeParams, + fromBytes32Format(switchboard), + digest, + watcherProof, + transmitterSig, + transmitterEOA + ); + } else if (fromBytes32Format(switchboard) == address(getSocketConfig(chainSlug).cctpSwitchboard)) { + (success, returnData) = _executeWithCCTPBatcher( + chainSlug, + executeParams, + digest, + watcherProof, + transmitterSig, + payloadParams + ); + } promiseReturnData = PromiseReturnData({ exceededMaxCopy: false, payloadId: payloadParams.payloadId, returnData: returnData }); } + function _executeWithCCTPBatcher( + uint32 chainSlug, + ExecuteParams memory executeParams, + bytes32 digest, + bytes memory watcherProof, + bytes memory transmitterSig, + PayloadParams memory payloadParams + ) internal returns (bool success, bytes memory returnData) { + CCTPBatchParams memory cctpBatchParams = _prepareCCTPBatchData(chainSlug, payloadParams); + + return + getSocketConfig(chainSlug).socketBatcher.attestCCTPAndProveAndExecute( + CCTPExecutionParams({ + executeParams: executeParams, + digest: digest, + proof: watcherProof, + transmitterSignature: transmitterSig, + refundAddress: transmitterEOA + }), + cctpBatchParams, + address(getSocketConfig(chainSlug).cctpSwitchboard) + ); + } + + function _prepareCCTPBatchData( + uint32 chainSlug, + PayloadParams memory payloadParams + ) internal view returns (CCTPBatchParams memory cctpBatchParams) { + uint40[] memory requestBatchIds = requestHandler.getRequestBatchIds( + payloadParams.requestCount + ); + uint40 currentBatchCount = payloadParams.batchCount; + + bytes32[] memory prevBatchPayloadIds = _getPrevBatchPayloadIds( + currentBatchCount, + requestBatchIds + ); + bytes32[] memory nextBatchPayloadIds = _getNextBatchPayloadIds( + currentBatchCount, + requestBatchIds + ); + + uint32[] memory prevBatchRemoteChainSlugs = _getRemoteChainSlugs(prevBatchPayloadIds); + uint32[] memory nextBatchRemoteChainSlugs = _getRemoteChainSlugs(nextBatchPayloadIds); + + bytes[] memory messages = _createCCTPMessages( + prevBatchPayloadIds, + prevBatchRemoteChainSlugs, + chainSlug + ); + + cctpBatchParams = CCTPBatchParams({ + previousPayloadIds: prevBatchPayloadIds, + nextBatchRemoteChainSlugs: nextBatchRemoteChainSlugs, + messages: messages, + attestations: new bytes[](prevBatchPayloadIds.length) // using mock attestations for now + }); + } + + function _getPrevBatchPayloadIds( + uint40 currentBatchCount, + uint40[] memory requestBatchIds + ) internal view returns (bytes32[] memory) { + if (currentBatchCount == requestBatchIds[0]) { + return new bytes32[](0); + } + return requestHandler.getBatchPayloadIds(currentBatchCount - 1); + } + + function _getNextBatchPayloadIds( + uint40 currentBatchCount, + uint40[] memory requestBatchIds + ) internal view returns (bytes32[] memory) { + if (currentBatchCount == requestBatchIds[requestBatchIds.length - 1]) { + return new bytes32[](0); + } + return requestHandler.getBatchPayloadIds(currentBatchCount + 1); + } + + function _getRemoteChainSlugs( + bytes32[] memory payloadIds + ) internal view returns (uint32[] memory) { + uint32[] memory chainSlugs = new uint32[](payloadIds.length); + for (uint i = 0; i < payloadIds.length; i++) { + PayloadParams memory params = requestHandler.getPayload(payloadIds[i]); + (, Transaction memory transaction, , , , ) = abi.decode( + params.precompileData, + (address, Transaction, WriteFinality, uint256, uint256, address) + ); + chainSlugs[i] = transaction.chainSlug; + } + return chainSlugs; + } + + function _createCCTPMessages( + bytes32[] memory payloadIds, + uint32[] memory remoteChainSlugs, + uint32 chainSlug + ) internal view returns (bytes[] memory) { + bytes[] memory messages = new bytes[](payloadIds.length); + for (uint i = 0; i < payloadIds.length; i++) { + messages[i] = abi.encode( + remoteChainSlugs[i], + addressToBytes32(address(getSocketConfig(remoteChainSlugs[i]).cctpSwitchboard)), + chainSlug, + addressToBytes32(address(getSocketConfig(chainSlug).cctpSwitchboard)), + abi.encode(payloadIds[i], writePrecompile.digestHashes(payloadIds[i])) + ); + } + return messages; + } function _resolvePromise(PromiseReturnData[] memory promiseReturnData) internal { watcherMultiCall( @@ -947,28 +1102,45 @@ contract WatcherSetup is AuctionSetup { IAppGateway appGateway_, bytes32[] memory contractIds_ ) internal { - AppGatewayConfig[] memory configs = new AppGatewayConfig[](contractIds_.length); + // Count valid plugs first. In some cases we might have contractIds such that oly a subset is + // deployed on a chain. for ex, vault on source, and supertoken on destination. + uint256 validPlugCount = 0; + for (uint i = 0; i < contractIds_.length; i++) { + bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); + if (plug != bytes32(0)) { + validPlugCount++; + } + } + + // Create array with exact size needed + AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount); + uint256 configIndex = 0; - SocketContracts memory socketConfig = getSocketConfig(chainSlug_); for (uint i = 0; i < contractIds_.length; i++) { bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); + bytes32 switchboard = configurations.switchboards(chainSlug_, appGateway_.sbType()); + if (plug != bytes32(0)) { + configs[configIndex] = AppGatewayConfig({ + plug: plug, + chainSlug: chainSlug_, + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(appGateway_)), + switchboard: switchboard + }) + }); + configIndex++; + } + } - configs[i] = AppGatewayConfig({ - plug: plug, - chainSlug: chainSlug_, - plugConfig: PlugConfigGeneric({ - appGatewayId: toBytes32Format(address(appGateway_)), - switchboard: toBytes32Format(address(socketConfig.switchboard)) - }) - }); + // Only call watcher if we have valid configs + if (validPlugCount > 0) { + watcherMultiCall( + address(configurations), + abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) + ); } - watcherMultiCall( - address(configurations), - abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) - ); } } - contract AppGatewayBaseSetup is WatcherSetup { function getOnChainAndForwarderAddresses( uint32 chainSlug_, @@ -1129,3 +1301,10 @@ contract AppGatewayBaseSetup is WatcherSetup { ); } } + +function addressToBytes32(address addr_) pure returns (bytes32) { + return bytes32(uint256(uint160(addr_))); +} +function bytes32ToAddress(bytes32 addrBytes32_) pure returns (address) { + return address(uint160(uint256(addrBytes32_))); +} diff --git a/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol b/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol index 0e835b7e..00870143 100644 --- a/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol +++ b/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol @@ -68,4 +68,8 @@ contract SuperTokenAppGateway is AppGatewayBase, Ownable { emit Transferred(_getCurrentRequestCount()); } + + function setSbType(bytes32 sbType_) external onlyOwner { + _setSbType(sbType_); + } } diff --git a/test/mock/CCTPMessageTransmitter.sol b/test/mock/CCTPMessageTransmitter.sol new file mode 100644 index 00000000..57eee54a --- /dev/null +++ b/test/mock/CCTPMessageTransmitter.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "../../contracts/protocol/interfaces/IMessageTransmitter.sol"; +import "../../contracts/protocol/interfaces/IMessageHandler.sol"; + +contract CCTPMessageTransmitter is IMessageTransmitter { + uint32 public immutable override localDomain; + address public immutable override attestationManager; + + // Mapping to store sent messages for verification + mapping(uint64 => bytes) public sentMessages; + uint64 public nonce; + + event MessageSent( + uint32 destinationDomain, + bytes32 recipient, + bytes messageBody, + uint64 nonce, + bytes message + ); + + event MessageReceived(bytes message, bytes attestation, bool success); + + constructor(uint32 _localDomain, address _attestationManager) { + localDomain = _localDomain; + attestationManager = _attestationManager; + } + + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external override returns (uint64) { + uint64 currentNonce = nonce++; + sentMessages[currentNonce] = messageBody; + + bytes memory message = abi.encode( + localDomain, + msg.sender, + destinationDomain, + recipient, + messageBody + ); + emit MessageSent(destinationDomain, recipient, messageBody, currentNonce, message); + + return currentNonce; + } + + function receiveMessage( + bytes calldata message, + bytes calldata attestation + ) external override returns (bool) { + ( + uint32 sourceDomain, + bytes32 sender, // destinationDomain + , + bytes32 recipient, + bytes memory messageBody + ) = abi.decode(message, (uint32, bytes32, uint32, bytes32, bytes)); + IMessageHandler(bytes32ToAddress(recipient)).handleReceiveMessage( + sourceDomain, + sender, + messageBody + ); + // In mock implementation, we'll always return true + // In real implementation, this would verify the attestation + emit MessageReceived(message, attestation, true); + return true; + } + + function addressToBytes32(address addr_) public pure returns (bytes32) { + return bytes32(uint256(uint160(addr_))); + } + function bytes32ToAddress(bytes32 addrBytes32_) public pure returns (address) { + return address(uint160(uint256(addrBytes32_))); + } +} diff --git a/test/mock/MockSocket.sol b/test/mock/MockSocket.sol index adfcc841..5cf916cf 100644 --- a/test/mock/MockSocket.sol +++ b/test/mock/MockSocket.sol @@ -25,6 +25,8 @@ contract MockSocket is ISocket { // plug => (appGateway, switchboard__) mapping(address => PlugConfigEvm) internal _plugConfigs; + mapping(bytes32 => bytes32) public payloadIdToDigest; + function getPlugConfig( address plugAddress_ ) external view returns (bytes32 appGatewayId, address switchboard__) { @@ -66,12 +68,6 @@ contract MockSocket is ISocket { uint64 public triggerCounter; uint32 public chainSlug; - enum ExecutionStatus { - NotExecuted, - Executed, - Reverted - } - /** * @dev keeps track of whether a payload has been executed or not using payload id */ diff --git a/trace.sh b/trace.sh index 3e185bc6..fc1cedf9 100644 --- a/trace.sh +++ b/trace.sh @@ -54,7 +54,7 @@ echo "txHash: $2" echo "rpcUrl: $RPC_URL" npx ts-node hardhat-scripts/misc-scripts/createLabels.ts $1 -cast run --la $2 --rpc-url $RPC_URL +cast run --la $2 --rpc-url $RPC_URL --quick # usage : # yarn trace