diff --git a/.env.sample b/.env.sample index 3d6427f5..288139d5 100644 --- a/.env.sample +++ b/.env.sample @@ -21,4 +21,4 @@ APP_GATEWAY="0x" # FOR INFRASTRUCTURE DEPLOYMENT ONLY # Removes hardhat issues related to linting and syntax checking -SOCKET_SIGNER_KEY="0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead" +SOCKET_PRIVATE_KEY="0000dead0000dead0000dead0000dead0000dead0000dead0000dead0000dead" diff --git a/.gitignore b/.gitignore index 86c4727c..ec9ea52a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ src/types typechain-types/ .env +.env.* .DS_Store .gas-snapshot/ @@ -26,12 +27,15 @@ broadcast/ .cursorrules -deployments/local_addresses.json -deployments/local_verification.json +deployments/local_addresses*.json +deployments/local_verification*.json +deployments/stage_addresses_*.json testScript.sh CLAUDE.md .idea/ -hardhat-scripts/loadTest/* \ No newline at end of file +hardhat-scripts/loadTest/* + +coverage-report/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index da15a39d..d31c61eb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -35,3 +35,6 @@ images/ setupInfraContracts.sh testScript.sh trace.sh +coverage-report/ +internal-audit/ +foundry.lock \ No newline at end of file diff --git a/Errors.md b/Errors.md index 2acd69a3..2ac6c27d 100644 --- a/Errors.md +++ b/Errors.md @@ -16,14 +16,6 @@ | `PromiseRevertFailed()` | `0x0175b9de` | | `NotLatestPromise()` | `0x39ca95d3` | -## evmx/plugs/ContractFactoryPlug.sol - -| Error | Signature | -| -------------------------------- | ------------ | -| `DeploymentFailed()` | `0x30116425` | -| `ExecutionFailed(bytes32,bytes)` | `0xd255d8a3` | -| `information(bool,,bytes)` | `0x3a82a1f3` | - ## evmx/plugs/FeesPlug.sol | Error | Signature | @@ -32,11 +24,13 @@ | `InvalidDepositAmount()` | `0xfe9ba5cd` | | `TokenNotWhitelisted(address)` | `0xea3bff2e` | -## evmx/watcher/RequestHandler.sol +## evmx/watcher/Watcher.sol -| Error | Signature | -| ----------------------- | ------------ | -| `InsufficientMaxFees()` | `0x0e5bc492` | +| Error | Signature | +| --------------------------- | ------------ | +| `PayloadAlreadyCancelled()` | `0x999843d8` | +| `PayloadAlreadySettled()` | `0x8fce2d78` | +| `AppGatewayMismatch()` | `0x2b7236f9` | ## protocol/Socket.sol @@ -74,17 +68,6 @@ | ---------------- | ------------ | | `NotSupported()` | `0xa0387940` | -## protocol/switchboard/CCTPSwitchboard.sol - -| Error | Signature | -| ------------------------------- | ------------ | -| `RemoteExecutionNotFound()` | `0xbd506972` | -| `PrevBatchDigestHashMismatch()` | `0xc9864e9d` | -| `NotAttested()` | `0x99efb890` | -| `NotExecuted()` | `0xec84b1da` | -| `InvalidSender()` | `0xddb5de5e` | -| `OnlyMessageTransmitter()` | `0x935ac89c` | - ## protocol/switchboard/FastSwitchboard.sol | Error | Signature | @@ -145,7 +128,7 @@ | `AuctionNotOpen()` | `0xf0460077` | | `BidExceedsMaxFees()` | `0x4c923f3c` | | `LowerBidAlreadyExists()` | `0xaaa1f709` | -| `RequestCountMismatch()` | `0x98bbcbff` | +| `PayloadCountMismatch()` | `0xea50ceff` | | `InvalidAmount()` | `0x2c5211c6` | | `InsufficientCreditsAvailable()` | `0xe61dc0aa` | | `InsufficientBalance()` | `0xf4d678b8` | diff --git a/EventTopics.md b/EventTopics.md index b6bdc3df..80345067 100644 --- a/EventTopics.md +++ b/EventTopics.md @@ -1,22 +1,5 @@ # Event Topics -## AuctionManager - -| Event | Arguments | Topic | -| ---------------------------- | ------------------------------------------- | -------------------------------------------------------------------- | -| `AuctionEndDelaySecondsSet` | `(auctionEndDelaySeconds: uint256)` | `0xf38f0d9dc8459cf5426728c250d115196a4c065ebc1a6c29da24764a8c0da722` | -| `AuctionEnded` | `(requestCount: uint40, winningBid: tuple)` | `0xede4ec1efc469fac10dcb4930f70be4cd21f3700ed61c91967c19a7cd7c0d86e` | -| `AuctionRestarted` | `(requestCount: uint40)` | `0x071867b21946ec4655665f0d4515d3757a5a52f144c762ecfdfb11e1da542b82` | -| `AuctionStarted` | `(requestCount: uint40)` | `0xcd040613cf8ef0cfcaa3af0d711783e827a275fc647c116b74595bf17cb9364f` | -| `BidPlaced` | `(requestCount: uint40, bid: tuple)` | `0x7f79485e4c9aeea5d4899bc6f7c63b22ac1f4c01d2d28c801e94732fee657b5d` | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `MaxReAuctionCountSet` | `(maxReAuctionCount: uint256)` | `0x2f6fadde7ab8ab83d21ab10c3bc09dde179f8696d47c4176581facf0c6f96bbf` | -| `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` | - ## Socket | Event | Arguments | Topic | @@ -67,9 +50,9 @@ | Event | Arguments | Topic | | ----------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `Approval` | `(owner: address, spender: address, amount: uint256)` | `0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925` | -| `CreditsBlocked` | `(requestCount: uint40, consumeFrom: address, amount: uint256)` | `0xf037c15aef41440aa823cf1fdeaea332105d8b23d52557f6670189b5d76f1eed` | -| `CreditsUnblocked` | `(requestCount: uint40, consumeFrom: address)` | `0x45db29ef2701319155cac058aa2f56ce1f73e0e238161d3db9f8c9a47655210d` | -| `CreditsUnblockedAndAssigned` | `(requestCount: uint40, consumeFrom: address, transmitter: address, amount: uint256)` | `0x38fd327622576a468e1b2818b00f50c8854703633ef8e583e1f31662888ffac2` | +| `CreditsBlocked` | `(payloadId: bytes32, consumeFrom: address, amount: uint256)` | `0xe0ce1c6e339ba1b699d262b081adbc74ddc8699c19405e3a8459940944ccd9ea` | +| `CreditsUnblocked` | `(payloadId: bytes32, consumeFrom: address)` | `0xe19214f41bd8f45a4fa569e176cdb3700de18b99f163f385cdfd210118dc7aa3` | +| `CreditsUnblockedAndAssigned` | `(payloadId: bytes32, consumeFrom: address, transmitter: address, amount: uint256)` | `0xf2fa1621e1a549c353279ffa16145321c7297cd56fd8fe4fa0d6d4b9ea09518c` | | `CreditsUnwrapped` | `(consumeFrom: address, amount: uint256)` | `0xdcc9473b722b4c953617ab373840b365298a520bc7f20ce94fa7314f4a857774` | | `CreditsWrapped` | `(consumeFrom: address, amount: uint256)` | `0x40246503613721eb4acf4020c6c56b6a16e5d08713316db0bea5210e8819c592` | | `Deposited` | `(chainSlug: uint32, token: address, depositTo: address, creditAmount: uint256, nativeAmount: uint256)` | `0x72aedd284699bbd7a987e6942b824cfd6c627e354cb5a0760ac5768acd473f4a` | @@ -97,18 +80,16 @@ ## AddressResolver -| Event | Arguments | Topic | -| ------------------------------ | --------------------------------------------------- | -------------------------------------------------------------------- | -| `AsyncDeployerUpdated` | `(asyncDeployer_: address)` | `0x4df9cdd01544e8f6b0326650bc0b55611f47ce5ba2faa522d21fb675e9fc1f73` | -| `ContractAddressUpdated` | `(contractId_: bytes32, contractAddress_: address)` | `0xdf5ec2c15e11ce657bb21bc09c0b5ba95e315b4dba9934c6e311f47559babf28` | -| `DefaultAuctionManagerUpdated` | `(defaultAuctionManager_: address)` | `0x60f296739208a505ead7fb622df0f76b7791b824481b120a2300bdaf85e3e3d6` | -| `DeployForwarderUpdated` | `(deployForwarder_: address)` | `0x237b9bc9fef7508a02ca9ccca81f6965e500064a58024cae1218035da865fd2b` | -| `FeesManagerUpdated` | `(feesManager_: address)` | `0x94e67aa1341a65767dfde81e62fd265bfbade1f5744bfd3cd73f99a6eca0572a` | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | -| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | -| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | -| `WatcherUpdated` | `(watcher_: address)` | `0xc13081d38d92b454cdb6ca20bbc65c12fa43a7a14a1529204ced5b6350052bb0` | +| Event | Arguments | Topic | +| ---------------------------- | --------------------------------------------------- | -------------------------------------------------------------------- | +| `AsyncDeployerUpdated` | `(asyncDeployer_: address)` | `0x4df9cdd01544e8f6b0326650bc0b55611f47ce5ba2faa522d21fb675e9fc1f73` | +| `ContractAddressUpdated` | `(contractId_: bytes32, contractAddress_: address)` | `0xdf5ec2c15e11ce657bb21bc09c0b5ba95e315b4dba9934c6e311f47559babf28` | +| `FeesManagerUpdated` | `(feesManager_: address)` | `0x94e67aa1341a65767dfde81e62fd265bfbade1f5744bfd3cd73f99a6eca0572a` | +| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | +| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | +| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | +| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | +| `WatcherUpdated` | `(watcher_: address)` | `0xc13081d38d92b454cdb6ca20bbc65c12fa43a7a14a1529204ced5b6350052bb0` | ## AsyncDeployer @@ -128,21 +109,29 @@ | ------------- | ------------------- | -------------------------------------------------------------------- | | `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -## DeployForwarder - -| Event | Arguments | Topic | -| ---------------------------- | ---------------------------------------- | -------------------------------------------------------------------- | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | -| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | -| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | - ## Forwarder | Event | Arguments | Topic | | ------------- | ------------------- | -------------------------------------------------------------------- | | `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | +## IWatcher + +| Event | Arguments | Topic | +| -------------------- | ------------------------------------------------ | -------------------------------------------------------------------- | +| `FeesIncreased` | `(payloadId: bytes32, newMaxFees: uint256)` | `0xc065f24ea45c38ef0d9ccac911e00b29f28bc38daa87e3cc4dcf0e7ea73adc6f` | +| `MarkedRevert` | `(payloadId: bytes32, isRevertingOnchain: bool)` | `0xcf1fd844cb4d32cbebb5ca6ce4ac834fe98da3ddac44deb77fffd22ad933824c` | +| `PayloadCancelled` | `(payloadId: bytes32)` | `0xb1593a793a33ca2a894aa149ab2cfec836402714c940a8e71d58a026a74a02e4` | +| `PayloadResolved` | `(payloadId: bytes32)` | `0x8e7fa2d76fff653c56f06aad4c0cd8170dcbc5fd39bcb1844b3171c3221da43e` | +| `PayloadSettled` | `(payloadId: bytes32)` | `0x7184f20dd5708f270b73fe67e606998fd3e9173b8a2fba6b62634a6c12142d15` | +| `PayloadSubmitted` | `(payload: tuple)` | `0xdbbb90a7b644d115e3581d65b96ea409bf4f78320a7f4efbcf2aa93f8b33ffe6` | +| `PrecompileSet` | `(callType: bytes4, precompile: address)` | `0x5254189aca1b416c09dad7fb656bf0ed2c07e03ccd240bd95dfbfbaeb5e10e7b` | +| `PromiseNotResolved` | `(payloadId: bytes32, asyncPromise: address)` | `0xbcf0d0c678940566e9e64f0c871439395bd5fb5c39bca3547b126fe6ee467937` | +| `PromiseResolved` | `(payloadId: bytes32, asyncPromise: address)` | `0x1b1b5810494fb3e17f7c46547e6e67cd6ad3e6001ea6fb7d12ea0241ba13c4ba` | +| `TriggerFailed` | `(triggerId: bytes32)` | `0x4386783bb0f7cad4ba12f033dbec03dc3441e7757a122f3097a7a4d945c98040` | +| `TriggerFeesSet` | `(triggerFees: uint256)` | `0x7df3967b7c8727af5ac0ee9825d88aafeb899d769bc428b91f8967fa0b623084` | +| `TriggerSucceeded` | `(triggerId: bytes32)` | `0x92d20fbcbf31370b8218e10ed00c5aad0e689022da30a08905ba5ced053219eb` | + ## ProxyFactory | Event | Arguments | Topic | @@ -158,18 +147,6 @@ | `Approval` | `(owner: address, spender: address, amount: uint256)` | `0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925` | | `Transfer` | `(from: address, to: address, amount: uint256)` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | -## ContractFactoryPlug - -| Event | Arguments | Topic | -| ---------------------------- | --------------------------------------------------- | -------------------------------------------------------------------- | -| `ConnectorPlugDisconnected` | `()` | `0xc2af098c82dba3c4b00be8bda596d62d13b98a87b42626fefa67e0bb0e198fdd` | -| `Deployed` | `(addr: address, salt: bytes32, returnData: bytes)` | `0x1246c6f8fd9f4abc542c7c8c8f793cfcde6b67aed1976a38aa134fc24af2dfe3` | -| `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` | - ## FeesPlug | Event | Arguments | Topic | @@ -185,80 +162,36 @@ | `TokenRemovedFromWhitelist` | `(token: address)` | `0xdd2e6d9f52cbe8f695939d018b7d4a216dc613a669876163ac548b916489d917` | | `TokenWhitelisted` | `(token: address)` | `0x6a65f90b1a644d2faac467a21e07e50e3f8fa5846e26231d30ae79a417d3d262` | -## Configurations +## Watcher | Event | Arguments | Topic | | ---------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- | +| `FeesIncreased` | `(payloadId: bytes32, newMaxFees: uint256)` | `0xc065f24ea45c38ef0d9ccac911e00b29f28bc38daa87e3cc4dcf0e7ea73adc6f` | | `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | | `IsValidPlugSet` | `(isValid: bool, chainSlug: uint32, plug: bytes32, appGateway: address)` | `0xdd99f9f3d0179d3845b6c9b5e020d80c32ca46007e43c43c6ab6a86cb259ed28` | +| `MarkedRevert` | `(payloadId: bytes32, isRevertingOnchain: bool)` | `0xcf1fd844cb4d32cbebb5ca6ce4ac834fe98da3ddac44deb77fffd22ad933824c` | | `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | | `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | | `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | +| `PayloadCancelled` | `(payloadId: bytes32)` | `0xb1593a793a33ca2a894aa149ab2cfec836402714c940a8e71d58a026a74a02e4` | +| `PayloadResolved` | `(payloadId: bytes32)` | `0x8e7fa2d76fff653c56f06aad4c0cd8170dcbc5fd39bcb1844b3171c3221da43e` | +| `PayloadSettled` | `(payloadId: bytes32)` | `0x7184f20dd5708f270b73fe67e606998fd3e9173b8a2fba6b62634a6c12142d15` | +| `PayloadSubmitted` | `(payload: tuple)` | `0xdbbb90a7b644d115e3581d65b96ea409bf4f78320a7f4efbcf2aa93f8b33ffe6` | | `PlugAdded` | `(appGatewayId: bytes32, chainSlug: uint32, plug: bytes32)` | `0x3734a2406c5c2f2556c82a0819c51e42a135dd102465cc9856594481ea2f1637` | +| `PrecompileSet` | `(callType: bytes4, precompile: address)` | `0x5254189aca1b416c09dad7fb656bf0ed2c07e03ccd240bd95dfbfbaeb5e10e7b` | +| `PromiseNotResolved` | `(payloadId: bytes32, asyncPromise: address)` | `0xbcf0d0c678940566e9e64f0c871439395bd5fb5c39bca3547b126fe6ee467937` | +| `PromiseResolved` | `(payloadId: bytes32, asyncPromise: address)` | `0x1b1b5810494fb3e17f7c46547e6e67cd6ad3e6001ea6fb7d12ea0241ba13c4ba` | | `SocketSet` | `(chainSlug: uint32, socket: bytes32)` | `0x3200bf6ad2ab31b9220ed9d2f83089d7a1332f55aaa3825c57510743a315165b` | | `SwitchboardSet` | `(chainSlug: uint32, sbType: bytes32, switchboardId: uint64)` | `0x5aeb296e3ed47512d11032a96d11f93d8538b9eb87aa1db45d412e7165d6850a` | - -## PromiseResolver - -| Event | Arguments | Topic | -| -------------------- | ------------------------------------------------ | -------------------------------------------------------------------- | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `MarkedRevert` | `(payloadId: bytes32, isRevertingOnchain: bool)` | `0xcf1fd844cb4d32cbebb5ca6ce4ac834fe98da3ddac44deb77fffd22ad933824c` | -| `PromiseNotResolved` | `(payloadId: bytes32, asyncPromise: address)` | `0xbcf0d0c678940566e9e64f0c871439395bd5fb5c39bca3547b126fe6ee467937` | -| `PromiseResolved` | `(payloadId: bytes32, asyncPromise: address)` | `0x1b1b5810494fb3e17f7c46547e6e67cd6ad3e6001ea6fb7d12ea0241ba13c4ba` | - -## RequestHandler - -| Event | Arguments | Topic | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| `FeesIncreased` | `(requestCount: uint40, newMaxFees: uint256)` | `0xf258fca4e49b803ee2a4c2e33b6fcf18bc3982df21f111c00677025bf1ccbb6a` | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | -| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | -| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | -| `PrecompileSet` | `(callType: bytes4, precompile: address)` | `0x5254189aca1b416c09dad7fb656bf0ed2c07e03ccd240bd95dfbfbaeb5e10e7b` | -| `RequestCancelled` | `(requestCount: uint40)` | `0xff191657769be72fc08def44c645014c60d18cb24b9ca05c9a33406a28253245` | -| `RequestCompletedWithErrors` | `(requestCount: uint40)` | `0xd8d9915dc14b5a29b66cb263e1ea1e99e60418fc21d97f0fbf09cae1281291e2` | -| `RequestPayloadCountLimitSet` | `(requestPayloadCountLimit: uint128)` | `0x67f58095e99ad7f9519f3b80372f6bab373a6217d08c9479fe58b80dcd5b4b7d` | -| `RequestSettled` | `(requestCount: uint40, winner: address)` | `0x1234f98acbe1548b214f4528461a5377f1e2349569c04caa59325e488e7d2aa4` | -| `RequestSubmitted` | `(hasWrite: bool, requestCount: uint40, totalEstimatedWatcherFees: uint256, requestParams: tuple, payloadParamsArray: tuple[])` | `0xb730ca5523e3f80e88b4bb71e1e78d447553069cd9a7143bb0032b957135b530` | - -## Watcher - -| Event | Arguments | Topic | -| ---------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| `AppGatewayCallFailed` | `(triggerId: bytes32)` | `0xcaf8475fdade8465ea31672463949e6cf1797fdcdd11eeddbbaf857e1e5907b7` | -| `CalledAppGateway` | `(triggerId: bytes32)` | `0xf659ffb3875368f54fb4ab8f5412ac4518af79701a48076f7a58d4448e4bdd0b` | -| `CoreContractsSet` | `(requestHandler: address, configManager: address, promiseResolver: address)` | `0x32f3480588270473dc6418270d922a820dd9e914739e09a98241457dca2fd560` | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | -| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | -| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | -| `TriggerFailed` | `(triggerId: bytes32)` | `0x4386783bb0f7cad4ba12f033dbec03dc3441e7757a122f3097a7a4d945c98040` | -| `TriggerFeesSet` | `(triggerFees: uint256)` | `0x7df3967b7c8727af5ac0ee9825d88aafeb899d769bc428b91f8967fa0b623084` | -| `TriggerSucceeded` | `(triggerId: bytes32)` | `0x92d20fbcbf31370b8218e10ed00c5aad0e689022da30a08905ba5ced053219eb` | +| `TriggerFailed` | `(triggerId: bytes32)` | `0x4386783bb0f7cad4ba12f033dbec03dc3441e7757a122f3097a7a4d945c98040` | +| `TriggerFeesSet` | `(triggerFees: uint256)` | `0x7df3967b7c8727af5ac0ee9825d88aafeb899d769bc428b91f8967fa0b623084` | +| `TriggerSucceeded` | `(triggerId: bytes32)` | `0x92d20fbcbf31370b8218e10ed00c5aad0e689022da30a08905ba5ced053219eb` | ## IMessageSwitchboard | Event | Arguments | Topic | | ----- | --------- | ----- | -## 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 | @@ -306,15 +239,14 @@ ## WritePrecompile -| Event | Arguments | Topic | -| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| `ChainMaxMsgValueLimitsUpdated` | `(chainSlug: uint32, maxMsgValueLimit: uint256)` | `0x439087d094fe7dacbba3f0c67032041952d8bd58a891e15af10ced28fed0eb91` | -| `ContractFactoryPlugSet` | `(chainSlug: uint32, contractFactoryPlug: bytes32)` | `0xfad552a6feb82bef23201b8dce04b2460bff41b00f26fef3d791572cfdab49c2` | -| `ExpiryTimeSet` | `(expiryTime: uint256)` | `0x07e837e13ad9a34715a6bd45f49bbf12de19f06df79cb0be12b3a7d7f2397fa9` | -| `FeesSet` | `(writeFees: uint256)` | `0x3346af6da1932164d501f2ec28f8c5d686db5828a36b77f2da4332d89184fe7b` | -| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | -| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | -| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | -| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | -| `WriteProofRequested` | `(transmitter: address, digest: bytes32, prevBatchDigestHash: bytes32, deadline: uint256, payloadParams: tuple)` | `0xe3e3e322b3c2964670f4b62d06647c2f711440be782105fc1c0a60cc934bb40a` | -| `WriteProofUploaded` | `(payloadId: bytes32, proof: bytes)` | `0xd8fe3a99a88c9630360418877afdf14e3e79f0f25fee162aeb230633ea740156` | +| Event | Arguments | Topic | +| ------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- | +| `ChainMaxMsgValueLimitsUpdated` | `(chainSlug: uint32, maxMsgValueLimit: uint256)` | `0x439087d094fe7dacbba3f0c67032041952d8bd58a891e15af10ced28fed0eb91` | +| `ExpiryTimeSet` | `(expiryTime: uint256)` | `0x07e837e13ad9a34715a6bd45f49bbf12de19f06df79cb0be12b3a7d7f2397fa9` | +| `FeesSet` | `(writeFees: uint256)` | `0x3346af6da1932164d501f2ec28f8c5d686db5828a36b77f2da4332d89184fe7b` | +| `Initialized` | `(version: uint64)` | `0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2` | +| `OwnershipHandoverCanceled` | `(pendingOwner: address)` | `0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92` | +| `OwnershipHandoverRequested` | `(pendingOwner: address)` | `0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d` | +| `OwnershipTransferred` | `(oldOwner: address, newOwner: address)` | `0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0` | +| `WriteProofRequested` | `(digest: bytes32, deadline: uint256, rawPayload: tuple)` | `0x2f8f00af5d1898838cff7c39a4fad4673d18a1be179d2548cdc0fe51321b8aac` | +| `WriteProofUploaded` | `(payloadId: bytes32, proof: bytes)` | `0xd8fe3a99a88c9630360418877afdf14e3e79f0f25fee162aeb230633ea740156` | diff --git a/FunctionSignatures.md b/FunctionSignatures.md index 858e5b70..721919c8 100644 --- a/FunctionSignatures.md +++ b/FunctionSignatures.md @@ -1,55 +1,5 @@ # Function Signatures -## AuctionManager - -| Function | Signature | -| ---------------------------- | ------------ | -| `addressResolver__` | `0x6a750469` | -| `asyncDeployer__` | `0x2a39e801` | -| `auctionEndDelaySeconds` | `0x9087dfdb` | -| `auctionManager` | `0xb0192f9a` | -| `auctionStatus` | `0xd7d5fbf6` | -| `bid` | `0xfcdf49c2` | -| `bidTimeout` | `0x94090d0b` | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `consumeFrom` | `0x40dd78be` | -| `creationCodeWithArgs` | `0xc126dcc4` | -| `deployForwarder__` | `0xd4e3b034` | -| `endAuction` | `0x7426f0f6` | -| `evmxSlug` | `0x8bae77c2` | -| `expireBid` | `0x33b5b234` | -| `feesManager__` | `0x70568b58` | -| `forwarderAddresses` | `0x5390fdcb` | -| `getOnChainAddress` | `0xb6abffd7` | -| `getOverrideParams` | `0x54f0a866` | -| `grantRole` | `0x2f2ff15d` | -| `handleRevert` | `0x44792f25` | -| `hasRole` | `0x91d14854` | -| `initialize` | `0x86891c9b` | -| `initializeOnChain` | `0x86f01739` | -| `isAsyncModifierSet` | `0xb69e0c4a` | -| `isValidPromise` | `0xb690b962` | -| `maxFees` | `0xe83e34b1` | -| `maxReAuctionCount` | `0xc367b376` | -| `onCompleteData` | `0xb52fa926` | -| `onDeployComplete` | `0xfa3dbd1e` | -| `overrideParams` | `0xec5490fe` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `reAuctionCount` | `0x9b4b22d3` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `revokeRole` | `0xd547741f` | -| `sbType` | `0x745de344` | -| `setAddress` | `0x85bf312c` | -| `setAuctionEndDelaySeconds` | `0x88606b1a` | -| `setMaxReAuctionCount` | `0x64c71403` | -| `transferOwnership` | `0xf2fde38b` | -| `watcher__` | `0x300bb063` | -| `winningBids` | `0x9133f232` | - ## Socket | Function | Signature | @@ -119,23 +69,14 @@ | `addressResolver__` | `0x6a750469` | | `allowance` | `0xdd62ed3e` | | `approve` | `0x095ea7b3` | -| `approveWithSignature` | `0xf65de26c` | | `asyncDeployer__` | `0x2a39e801` | -| `auctionManager` | `0xb0192f9a` | | `balanceOf` | `0x70a08231` | -| `batchApprove` | `0x525b3861` | -| `blockCredits` | `0x9e434307` | +| `blockCredits` | `0xa7bf4a36` | +| `blockedCredits` | `0x31f81b5c` | | `cancelOwnershipHandover` | `0x54d1f13d` | | `completeOwnershipHandover` | `0xf04e283e` | -| `consumeFrom` | `0x40dd78be` | -| `creationCodeWithArgs` | `0xc126dcc4` | | `decimals` | `0x313ce567` | -| `deployForwarder__` | `0xd4e3b034` | | `deposit` | `0x5671d329` | -| `deprecated2` | `0x9897ed76` | -| `deprecated3` | `0x690e4d24` | -| `deprecated4` | `0xefe1358a` | -| `deprecatedSbType` | `0x5a783900` | | `evmxSlug` | `0x8bae77c2` | | `feesManager__` | `0x70568b58` | | `feesPlugs` | `0x23f5ee8a` | @@ -147,30 +88,24 @@ | `getOnChainAddress` | `0xb6abffd7` | | `getOverrideParams` | `0x54f0a866` | | `handleRevert` | `0x44792f25` | -| `increaseFees` | `0xe9b304da` | +| `increaseFees` | `0xf0f0beba` | | `initialize` | `0xc13547c5` | -| `initializeOnChain` | `0x86f01739` | | `isApproved` | `0xa389783e` | | `isAsyncModifierSet` | `0xb69e0c4a` | | `isCreditSpendable` | `0x4f8990fd` | | `isNonceUsed` | `0xcab7e8eb` | | `isValidPromise` | `0xb690b962` | -| `maxFees` | `0xe83e34b1` | | `maxFeesPerChainSlug` | `0xe06340d4` | | `name` | `0x06fdde03` | | `nonces` | `0x7ecebe00` | | `onCompleteData` | `0xb52fa926` | -| `onDeployComplete` | `0xfa3dbd1e` | | `overrideParams` | `0xec5490fe` | | `owner` | `0x8da5cb5b` | | `ownershipHandoverExpiresAt` | `0xfee81cf4` | | `permit` | `0xd505accf` | | `renounceOwnership` | `0x715018a6` | -| `requestBlockedCredits` | `0xb62d25ac` | | `requestOwnershipHandover` | `0x25692962` | | `rescueFunds` | `0x6ccae054` | -| `sbType` | `0x745de344` | -| `setAddress` | `0x85bf312c` | | `setChainMaxFees` | `0x7a3c3970` | | `setFeesPlug` | `0xd6a9a8b7` | | `setFeesPool` | `0xd6684588` | @@ -182,8 +117,8 @@ | `transfer` | `0xa9059cbb` | | `transferFrom` | `0x23b872dd` | | `transferOwnership` | `0xf2fde38b` | -| `unblockAndAssignCredits` | `0x01958181` | -| `unblockCredits` | `0xa0b32314` | +| `unblockAndAssignCredits` | `0xc330b8de` | +| `unblockCredits` | `0x2f1dfdda` | | `unwrap` | `0x7647691d` | | `userBlockedCredits` | `0x17fa5fb9` | | `watcher__` | `0x300bb063` | @@ -215,8 +150,6 @@ | `cancelOwnershipHandover` | `0x54d1f13d` | | `completeOwnershipHandover` | `0xf04e283e` | | `contractAddresses` | `0xf689e892` | -| `defaultAuctionManager` | `0x8f27cdc6` | -| `deployForwarder__` | `0xd4e3b034` | | `feesManager__` | `0x70568b58` | | `initialize` | `0xc4d66de8` | | `owner` | `0x8da5cb5b` | @@ -226,8 +159,6 @@ | `rescueFunds` | `0x6ccae054` | | `setAsyncDeployer` | `0xcb0ffff8` | | `setContractAddress` | `0xe001f841` | -| `setDefaultAuctionManager` | `0xede8b4b5` | -| `setDeployForwarder` | `0xaeaee8a6` | | `setFeesManager` | `0x1c89382a` | | `setWatcher` | `0x24f48bc5` | | `transferOwnership` | `0xf2fde38b` | @@ -244,12 +175,11 @@ | `asyncPromiseImplementation` | `0x59531b8d` | | `cancelOwnershipHandover` | `0x54d1f13d` | | `completeOwnershipHandover` | `0xf04e283e` | -| `deployAsyncPromiseContract` | `0x9851be0b` | -| `deployForwarder__` | `0xd4e3b034` | +| `deployAsyncPromiseContract` | `0x07613b26` | | `feesManager__` | `0x70568b58` | | `forwarderBeacon` | `0x945709ae` | | `forwarderImplementation` | `0xe38d60a1` | -| `getAsyncPromiseAddress` | `0x104f39b4` | +| `getAsyncPromiseAddress` | `0xd2397050` | | `getForwarderAddress` | `0x9c038b01` | | `getOrDeployForwarderContract` | `0xe9bf1edf` | | `initialize` | `0x485cc955` | @@ -265,47 +195,27 @@ ## AsyncPromise -| Function | Signature | -| ------------------- | ------------ | -| `addressResolver__` | `0x6a750469` | -| `asyncDeployer__` | `0x2a39e801` | -| `callbackData` | `0xef44c272` | -| `callbackSelector` | `0x2764f92f` | -| `deployForwarder__` | `0xd4e3b034` | -| `exceededMaxCopy` | `0xaf598c7c` | -| `feesManager__` | `0x70568b58` | -| `initialize` | `0x0ece6089` | -| `localInvoker` | `0x45eb87f4` | -| `markOnchainRevert` | `0xd0e7af1b` | -| `markResolved` | `0x822d5d1f` | -| `requestCount` | `0x5badbe4c` | -| `rescueFunds` | `0x6ccae054` | -| `returnData` | `0xebddbaf6` | -| `state` | `0xc19d93fb` | -| `then` | `0x0bf2ba15` | -| `watcher__` | `0x300bb063` | - -## DeployForwarder - -| Function | Signature | -| ---------------------------- | ------------ | -| `addressResolver__` | `0x6a750469` | -| `asyncDeployer__` | `0x2a39e801` | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `deploy` | `0x940f11af` | -| `deployForwarder__` | `0xd4e3b034` | -| `deployerSwitchboardType` | `0xaa381f9a` | -| `feesManager__` | `0x70568b58` | -| `initialize` | `0x6133f985` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `saltCounter` | `0xa04c6809` | -| `transferOwnership` | `0xf2fde38b` | -| `watcher__` | `0x300bb063` | +| Function | Signature | +| ----------------------- | ------------ | +| `addressResolver__` | `0x6a750469` | +| `asyncDeployer__` | `0x2a39e801` | +| `callbackData` | `0xef44c272` | +| `callbackSelector` | `0x2764f92f` | +| `error` | `0x08fb3c19` | +| `exceededMaxCopy` | `0xaf598c7c` | +| `feesManager__` | `0x70568b58` | +| `initialize` | `0x88b117b3` | +| `localInvoker` | `0x45eb87f4` | +| `markOnchainRevert` | `0xd0e7af1b` | +| `markResolved` | `0x822d5d1f` | +| `payloadId` | `0x03806d9d` | +| `rescueFunds` | `0x6ccae054` | +| `returnData` | `0xebddbaf6` | +| `revertHandlerData` | `0xd0b8f467` | +| `revertHandlerSelector` | `0xc2d97923` | +| `state` | `0xc19d93fb` | +| `then` | `0x0bf2ba15` | +| `watcher__` | `0x300bb063` | ## Forwarder @@ -314,7 +224,6 @@ | `addressResolver__` | `0x6a750469` | | `asyncDeployer__` | `0x2a39e801` | | `chainSlug` | `0xb349ba65` | -| `deployForwarder__` | `0xd4e3b034` | | `feesManager__` | `0x70568b58` | | `getChainSlug` | `0x0b8c6568` | | `getOnChainAddress` | `0x9da48789` | @@ -357,30 +266,6 @@ | `transfer` | `0xa9059cbb` | | `transferFrom` | `0x23b872dd` | -## ContractFactoryPlug - -| Function | Signature | -| ---------------------------- | ------------ | -| `appGatewayId` | `0x1c335f49` | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `connectSocket` | `0x943103c3` | -| `deployContract` | `0xff8caf37` | -| `getAddress` | `0x94ca2cb5` | -| `grantRole` | `0x2f2ff15d` | -| `hasRole` | `0x91d14854` | -| `initSocket` | `0x18b7ff72` | -| `isSocketInitialized` | `0x9a7d9a9b` | -| `overrides` | `0x4a85f041` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `revokeRole` | `0xd547741f` | -| `socket__` | `0xc6a261d2` | -| `transferOwnership` | `0xf2fde38b` | - ## FeesPlug | Function | Signature | @@ -411,163 +296,57 @@ | `whitelistedTokens` | `0xdaf9c210` | | `withdrawFees` | `0xe55dc4e6` | -## Configurations - -| Function | Signature | -| ---------------------------- | ------------ | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `getPlugConfigs` | `0x25945c1a` | -| `initialize` | `0x485cc955` | -| `isValidPlug` | `0x00f9b9f4` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `setAppGatewayConfigs` | `0x831c8195` | -| `setIsValidPlug` | `0x4842c37a` | -| `setSocket` | `0x38d4de67` | -| `setSwitchboard` | `0x4fc059a0` | -| `sockets` | `0xb44a23ab` | -| `switchboards` | `0xaa539546` | -| `transferOwnership` | `0xf2fde38b` | -| `verifyConnections` | `0x36cb19fb` | -| `watcher__` | `0x300bb063` | - -## PromiseResolver - -| Function | Signature | -| ----------------- | ------------ | -| `initialize` | `0xc4d66de8` | -| `markRevert` | `0x56501015` | -| `rescueFunds` | `0x6ccae054` | -| `resolvePromises` | `0xbf8484b8` | -| `watcher__` | `0x300bb063` | - -## RequestHandler - -| Function | Signature | -| ------------------------------ | ------------ | -| `addressResolver__` | `0x6a750469` | -| `assignTransmitter` | `0xae5e9c48` | -| `asyncDeployer__` | `0x2a39e801` | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `cancelRequest` | `0x3b5fd6fb` | -| `cancelRequestForReverts` | `0x82970278` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `deployForwarder__` | `0xd4e3b034` | -| `feesManager__` | `0x70568b58` | -| `getBatchPayloadIds` | `0xfd83cd1f` | -| `getPayload` | `0xb48fd0fe` | -| `getPrecompileFees` | `0xabac263c` | -| `getRequest` | `0xcf39abf6` | -| `getRequestBatchIds` | `0xe138fadb` | -| `handleRevert` | `0xcc88d3f9` | -| `increaseFees` | `0x10205541` | -| `initialize` | `0x485cc955` | -| `nextBatchCount` | `0x333a3963` | -| `nextRequestCount` | `0xfef72893` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `payloadCounter` | `0x550ce1d5` | -| `precompiles` | `0x9932450b` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `setPrecompile` | `0x122e0042` | -| `setRequestPayloadCountLimit` | `0x8526582b` | -| `submitRequest` | `0xf91ba7cc` | -| `transferOwnership` | `0xf2fde38b` | -| `updateRequestAndProcessBatch` | `0x46464471` | -| `watcher__` | `0x300bb063` | - ## Watcher | Function | Signature | | ---------------------------- | ------------ | +| `addPayloadData` | `0x6c608e9e` | | `addressResolver__` | `0x6a750469` | -| `appGatewayTemp` | `0x1394c029` | | `asyncDeployer__` | `0x2a39e801` | | `callAppGateways` | `0x0050bef1` | +| `cancelExecution` | `0x5bfc52ba` | | `cancelOwnershipHandover` | `0x54d1f13d` | -| `cancelRequest` | `0x50ad0779` | -| `clearQueue` | `0xf22cb874` | | `completeOwnershipHandover` | `0xf04e283e` | -| `configurations__` | `0x52a3bbeb` | -| `deployForwarder__` | `0xd4e3b034` | +| `currentPayloadId` | `0x86b4bd7e` | | `evmxSlug` | `0x8bae77c2` | +| `executePayload` | `0x63946d7b` | | `feesManager__` | `0x70568b58` | -| `getCurrentRequestCount` | `0x5715abbb` | -| `getPayloadParams` | `0xae5eeb77` | +| `getCurrentPayloadId` | `0x6c927966` | +| `getPayload` | `0xb48fd0fe` | +| `getPlugConfigs` | `0x25945c1a` | | `getPrecompileFees` | `0xabac263c` | -| `getRequestParams` | `0x71263d0d` | -| `increaseFees` | `0xe9b304da` | -| `initialize` | `0xaaf7fc1a` | +| `increaseFees` | `0xf0f0beba` | +| `initialize` | `0xd7954788` | | `isAppGatewayCalled` | `0xa79da6c7` | | `isNonceUsed` | `0x5d00bb12` | -| `isWatcher` | `0x84785ecd` | +| `isValidPlug` | `0x00f9b9f4` | +| `latestAppGateway` | `0x9148c40c` | | `latestAsyncPromise` | `0xb8a8ba52` | +| `markRevert` | `0x56501015` | +| `nextPayloadCount` | `0x1a82285a` | | `owner` | `0x8da5cb5b` | | `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `payloadQueue` | `0x74f00ffb` | -| `promiseResolver__` | `0xdee152be` | -| `queue` | `0x65967f1a` | -| `queueAndSubmit` | `0x9d4c9df7` | +| `payloadData` | `0xdc984dd4` | +| `precompiles` | `0x9932450b` | | `renounceOwnership` | `0x715018a6` | -| `requestHandler__` | `0x55184561` | | `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0xa58c6fc5` | -| `resetIsAppGatewayCalled` | `0xd19cd269` | -| `setCoreContracts` | `0xefa891c4` | +| `resolvePayload` | `0x2b76024c` | +| `setAppGatewayConfigs` | `0xf06913dc` | | `setIsValidPlug` | `0x06c0a40a` | -| `setTriggerFees` | `0xaeb30511` | -| `submitRequest` | `0x4890b5ef` | +| `setPrecompile` | `0x122e0042` | +| `setSocket` | `0x38d4de67` | +| `setSwitchboard` | `0x4fc059a0` | +| `setTriggerFees` | `0x752ad486` | +| `sockets` | `0xb44a23ab` | +| `switchboards` | `0xaa539546` | | `transferOwnership` | `0xf2fde38b` | +| `transmitter` | `0xcec46f6c` | | `triggerFees` | `0x73f76aec` | | `triggerFromChainSlug` | `0xd12b4f12` | | `triggerFromPlug` | `0x3b847d12` | -| `watcherMultiCall` | `0x8021e82b` | +| `verifyConnections` | `0x36cb19fb` | | `watcher__` | `0x300bb063` | -## CCTPSwitchboard - -| Function | Signature | -| -------------------------------- | ------------ | -| `addRemoteEndpoint` | `0x7d396da5` | -| `allowPacket` | `0x21e9ec80` | -| `allowPayload` | `0x31c23f66` | -| `attest` | `0x63671b60` | -| `attestVerifyAndProveExecutions` | `0x6c913e2f` | -| `cancelOwnershipHandover` | `0x54d1f13d` | -| `chainSlug` | `0xb349ba65` | -| `chainSlugToRemoteEndpoint` | `0xa4500424` | -| `completeOwnershipHandover` | `0xf04e283e` | -| `domainToRemoteEndpoint` | `0xc24964fe` | -| `getTransmitter` | `0x73e7d880` | -| `grantRole` | `0x2f2ff15d` | -| `handleReceiveMessage` | `0x96abeb70` | -| `hasRole` | `0x91d14854` | -| `isAttested` | `0xc13c2396` | -| `isRemoteExecuted` | `0x0cd97747` | -| `isSyncedOut` | `0x5ae5dfd6` | -| `messageTransmitter` | `0x7b04c181` | -| `owner` | `0x8da5cb5b` | -| `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `processTrigger` | `0x7f3352bc` | -| `proveRemoteExecutions` | `0x893289f8` | -| `registerSwitchboard` | `0x74f5b1fc` | -| `remoteExecutedDigests` | `0xecbf77d9` | -| `renounceOwnership` | `0x715018a6` | -| `requestOwnershipHandover` | `0x25692962` | -| `rescueFunds` | `0x6ccae054` | -| `revokeRole` | `0xd547741f` | -| `socket__` | `0xc6a261d2` | -| `switchboardId` | `0xd3be4120` | -| `syncOut` | `0x69a60ff0` | -| `transferOwnership` | `0xf2fde38b` | -| `verifyAttestations` | `0x6f30514c` | - ## FastSwitchboard | Function | Signature | @@ -583,7 +362,7 @@ | `isAttested` | `0xc13c2396` | | `owner` | `0x8da5cb5b` | | `ownershipHandoverExpiresAt` | `0xfee81cf4` | -| `processTrigger` | `0x7f3352bc` | +| `processPayload` | `0x7f3352bc` | | `registerSwitchboard` | `0x74f5b1fc` | | `renounceOwnership` | `0x715018a6` | | `requestOwnershipHandover` | `0x25692962` | @@ -610,7 +389,7 @@ | `owner` | `0x8da5cb5b` | | `ownershipHandoverExpiresAt` | `0xfee81cf4` | | `payloadCounter` | `0x550ce1d5` | -| `processTrigger` | `0x7f3352bc` | +| `processPayload` | `0x7f3352bc` | | `registerSibling` | `0x4f58b88c` | | `registerSwitchboard` | `0x74f5b1fc` | | `renounceOwnership` | `0x715018a6` | @@ -629,18 +408,17 @@ ## ReadPrecompile -| Function | Signature | -| ------------------------------ | ------------ | -| `expiryTime` | `0x99bc0aea` | -| `getPrecompileFees` | `0xb7a3d04c` | -| `handlePayload` | `0x62974d96` | -| `readFees` | `0xe06357a2` | -| `rescueFunds` | `0x6ccae054` | -| `resolvePayload` | `0x7f0b2207` | -| `setExpiryTime` | `0x30fc4cff` | -| `setFees` | `0x3d18678e` | -| `validateAndGetPrecompileData` | `0x997f5bef` | -| `watcher__` | `0x300bb063` | +| Function | Signature | +| ------------------- | ------------ | +| `expiryTime` | `0x99bc0aea` | +| `getPrecompileFees` | `0xb7a3d04c` | +| `handlePayload` | `0xe801184c` | +| `readFees` | `0xe06357a2` | +| `rescueFunds` | `0x6ccae054` | +| `resolvePayload` | `0xa6e2bb76` | +| `setExpiryTime` | `0x30fc4cff` | +| `setFees` | `0x3d18678e` | +| `watcher__` | `0x300bb063` | ## SchedulePrecompile @@ -648,17 +426,16 @@ | ------------------------------ | ------------ | | `expiryTime` | `0x99bc0aea` | | `getPrecompileFees` | `0xb7a3d04c` | -| `handlePayload` | `0x62974d96` | +| `handlePayload` | `0xe801184c` | | `maxScheduleDelayInSeconds` | `0x3ef01cdb` | | `rescueFunds` | `0x6ccae054` | -| `resolvePayload` | `0x7f0b2207` | +| `resolvePayload` | `0xa6e2bb76` | | `scheduleCallbackFees` | `0x4c5b6007` | | `scheduleFeesPerSecond` | `0x852a74c1` | | `setExpiryTime` | `0x30fc4cff` | | `setMaxScheduleDelayInSeconds` | `0x12953318` | | `setScheduleCallbackFees` | `0xec8fd71e` | | `setScheduleFeesPerSecond` | `0x28e59e57` | -| `validateAndGetPrecompileData` | `0x997f5bef` | | `watcher__` | `0x300bb063` | ## WritePrecompile @@ -673,22 +450,19 @@ | `expiryTime` | `0x99bc0aea` | | `getDigest` | `0x3554edc7` | | `getPrecompileFees` | `0xb7a3d04c` | -| `getPrevBatchDigestHash` | `0x372863a1` | -| `handlePayload` | `0x62974d96` | +| `handlePayload` | `0xe801184c` | | `initialize` | `0xeb990c59` | | `owner` | `0x8da5cb5b` | | `ownershipHandoverExpiresAt` | `0xfee81cf4` | | `renounceOwnership` | `0x715018a6` | | `requestOwnershipHandover` | `0x25692962` | | `rescueFunds` | `0x6ccae054` | -| `resolvePayload` | `0x7f0b2207` | -| `setContractFactoryPlugs` | `0x8b198f5c` | +| `resolvePayload` | `0xa6e2bb76` | | `setExpiryTime` | `0x30fc4cff` | | `setFees` | `0x3d18678e` | | `transferOwnership` | `0xf2fde38b` | | `updateChainMaxMsgValueLimits` | `0x6a7aa6ac` | | `uploadProof` | `0x81b48fcf` | -| `validateAndGetPrecompileData` | `0x997f5bef` | | `watcherProofs` | `0x3fa3166b` | | `watcher__` | `0x300bb063` | | `writeFees` | `0x5c664aeb` | diff --git a/PAYLOAD_ID_ARCHITECTURE.md b/PAYLOAD_ID_ARCHITECTURE.md new file mode 100644 index 00000000..c2e2c665 --- /dev/null +++ b/PAYLOAD_ID_ARCHITECTURE.md @@ -0,0 +1,161 @@ +# Payload ID Architecture - Unified Design + +## Overview + +Unified payload ID structure for all three payload types: Write, Trigger, and Message. + +## Payload ID Structure + +### Bit Layout (256 bits total) + +``` +[Source: 64 bits][Verification: 64 bits][Pointer: 64 bits][Reserved: 64 bits] +``` + +Each component breakdown: + +- **Source (64 bits)**: `chainSlug (32 bits) | switchboardId/watcherId (32 bits)` +- **Verification (64 bits)**: `chainSlug (32 bits) | switchboardId/watcherId (32 bits)` +- **Pointer (64 bits)**: Counter value +- **Reserved (64 bits)**: For future extensibility + +## Payload Type Specifications + +### 1. Write Payloads (EVMX → On-chain) + +- **Source**: `evmxChainSlug (32) | watcherId (32)` + - Generated by: Watcher (on EVMX) + - Verified by: Watcher offchain (links source) +- **Verification**: `dstChainSlug (32) | dstSwitchboardId (32)` + - Generated by: Watcher (from config) + - Used by: Socket for routing +- **Pointer**: `payloadCounter (64)` + - Generated by: Watcher (switchboard-specific counter) + +**Where Created**: `Watcher.sol` → `getCurrentPayloadId()` + +### 2. Trigger Payloads (On-chain → EVMX) + +- **Source**: `srcChainSlug (32) | srcSwitchboardId (32)` + - Generated by: FastSwitchboard + - Verified by: Watcher offchain (verifies source) +- **Verification**: `evmxChainSlug (32) | watcherId (32)` + - Generated by: FastSwitchboard (from stored config) + - Used by: Socket for routing +- **Pointer**: `switchboardCounter (64)` + - Generated by: FastSwitchboard (switchboard-specific counter) + +**Where Created**: `FastSwitchboard.sol` → `processPayload()` + +### 3. Message Payloads (Plug → Plug) + +- **Source**: `srcChainSlug (32) | srcSwitchboardId (32)` + - Generated by: MessageSwitchboard + - Verified by: Destination switchboard (checks source) +- **Verification**: `dstChainSlug (32) | dstSwitchboardId (32)` + - Generated by: MessageSwitchboard + - Used by: Socket for routing +- **Pointer**: `switchboardCounter (64)` + - Generated by: MessageSwitchboard (switchboard-specific counter) + +**Where Created**: `MessageSwitchboard.sol` → `_createDigestAndPayloadId()` + +## Decoding and Verification + +### Socket Verification (Destination) + +1. Decode `payloadId` using `getVerificationInfo(payloadId)` +2. Extract `verificationChainSlug` and `verificationSwitchboardId` +3. Verify against local config: + - `verificationChainSlug == local chainSlug` + - `verificationSwitchboardId == local switchboardId` + +### Source Verification (Off-chain Watcher) + +1. Decode `payloadId` using `getSourceInfo(payloadId)` +2. Extract `sourceChainSlug` and `sourceId` +3. Verify source configuration matches expected values + +### Payload Type Detection + +- Check if `sourceChainSlug` or `verificationChainSlug` matches `evmxChainSlug` + - If `sourceChainSlug == evmxChainSlug`: **Write Payload** + - If `verificationChainSlug == evmxChainSlug`: **Trigger Payload** + - If neither: **Message Payload** + +## Implementation Details + +### IdUtils.sol Functions + +#### Encoding + +- `createPayloadId(sourceChainSlug, sourceId, verificationChainSlug, verificationId, pointer)` + - Creates new payload ID with all components + +#### Decoding + +- `decodePayloadId(payloadId)` - Full decode +- `getVerificationInfo(payloadId)` - Gets verification components (for Socket routing) +- `getSourceInfo(payloadId)` - Gets source components (for source verification) + +### Required Updates + +1. **Watcher.sol** + + - Update `getCurrentPayloadId()` to use new format + - Use `evmxSlug` as source chain slug + - Use hardcoded `watcherId = 1` for now + - Get `dstSwitchboardId` from `switchboards` mapping + +2. **FastSwitchboard.sol** + + - Add state variables: `evmxChainSlug`, `watcherId` (with onlyOwner setters) + - Implement `processPayload()` to create payload ID + - Add counter: `uint64 public triggerPayloadCounter` + - Use: `source = (chainSlug, switchboardId)`, `verification = (evmxChainSlug, watcherId)` + +3. **MessageSwitchboard.sol** + + - Update `_createDigestAndPayloadId()` to use new format + - Use: `source = (chainSlug, switchboardId)`, `verification = (dstChainSlug, dstSwitchboardId)` + +4. **Socket.sol** + + - Update `execute()` to decode payload ID and verify verification components + - Remove old `createPayloadId` usage + - Use `getVerificationInfo()` to extract routing info + +5. **SocketConfig.sol** + - Update `plugSwitchboardIds` type from `uint64` to `uint32` if needed (or keep uint64 and cast) + +## Security Considerations + +### Verification Flow + +1. **Destination (Socket)**: Verifies verification component matches local config +2. **Source (Watcher offchain)**: Verifies source component matches expected source +3. **Pointer verification**: Skipped for now (to be added later) + +### Counter Management + +- Each switchboard maintains its own counter +- Prevents cross-switchboard collisions +- Counters are monotonic (never decrease) + +### ID Uniqueness + +- Guaranteed by switchboard-specific counters +- Source + Verification provide additional context +- Reserved bits allow future expansion without breaking changes + +## Migration Notes + +- No production deployments yet, so no migration needed +- All existing test code will need updates +- Backward compatibility not required + +## Future Enhancements + +- Add pointer verification mechanism +- Use reserved bits for additional metadata (payload version, flags, etc.) +- Support multiple watchers (remove hardcoded watcherId = 1) diff --git a/auditor-docs/AUDIT_FOCUS_AREAS.md b/auditor-docs/AUDIT_FOCUS_AREAS.md new file mode 100644 index 00000000..acf1d669 --- /dev/null +++ b/auditor-docs/AUDIT_FOCUS_AREAS.md @@ -0,0 +1,728 @@ +# Audit Focus Areas + +## Priority 1: Critical Functions + +### Socket.execute() - Main Entry Point + +**File**: `contracts/protocol/Socket.sol` (lines 46-74) + +**Why Critical**: + +- Handles all inbound payload execution +- Processes value transfers +- Makes external calls to untrusted contracts +- Single point of failure for cross-chain execution + +**Key Validations to Verify**: + +- Deadline enforcement +- Replay protection via executionStatus +- msg.value sufficiency check +- Payload ID routing validation +- Call type restriction (WRITE only) + +**Security Pattern**: CEI (Checks-Effects-Interactions) + +- executionStatus set BEFORE external call to plug +- payloadIdToDigest stored BEFORE external call +- Different payloadIds during reentrancy are legitimate + +**Note**: Reentrancy is allowed but safe due to CEI pattern and unique payload IDs per call. + +--- + +### Socket.\_execute() - Payload Execution + +**File**: `contracts/protocol/Socket.sol` (lines 122-161) + +**Why Critical**: + +- Performs untrusted external call to plug +- Handles value transfer to plug +- Manages execution success/failure +- Collects network fees + +**Key Checks**: + +- Gas limit validation: `gasleft() >= (gasLimit * gasLimitBuffer) / 100` +- gasLimit type: uint64 (prevents overflow) +- External call isolation (tryCall usage) +- Return data length limiting (maxCopyBytes) +- State changes before external calls + +**Post-Execution Flow**: + +- Success: NetworkFeeCollector.collectNetworkFee() (trusted contract) +- Failure: Full refund to refundAddress or msg.sender + +**Note**: NetworkFeeCollector is trusted per system assumptions. + +--- + +### Switchboard.processPayload() - Payload Creation + +**Files**: + +- `contracts/protocol/switchboard/MessageSwitchboard.sol` (lines 165-238) +- `contracts/protocol/switchboard/FastSwitchboard.sol` (lines 146-178) + +**Why Critical**: + +- Creates unique payload IDs +- Stores fee information +- Validates sibling configuration +- Emits events for off-chain watchers + +**Key Checks**: + +- Counter overflow protection (uint64) +- Sibling validation completeness +- Fee tracking accuracy +- Payload ID uniqueness +- Proper encoding of digest parameters + +**Counter Note**: uint64 = 18 quintillion payloads. Not realistically exploitable. + +--- + +### Switchboard.allowPayload() - Verification Gate + +**Files**: + +- `contracts/protocol/switchboard/MessageSwitchboard.sol` (lines 667-677) +- `contracts/protocol/switchboard/FastSwitchboard.sol` (lines 111-122) + +**Why Critical**: + +- Final authorization check before execution +- Validates source-target pairing +- Checks attestation status +- Cannot be bypassed + +**Key Checks**: + +- Source validation logic correctness +- Attestation requirement enforcement +- No bypass conditions exist + +--- + +### SocketUtils.\_createDigest() - Parameter Binding + +**File**: `contracts/protocol/SocketUtils.sol` (lines 70-100) + +**Why Critical**: + +- Binds all execution parameters to single hash +- Used for attestation verification +- Prevents parameter manipulation + +**Key Checks**: + +- Length prefix usage for variable fields (payload, source, extraData) +- Inclusion of all critical parameters +- Proper encoding preventing collisions +- Deterministic hashing + +**Important**: Length prefixes prevent collision attacks where: + +- `payload="AAAA", source="BB"` +- `payload="AAA", source="ABB"` +- Would hash to same value without length prefixes + +--- + +## Priority 2: Value Flow Points + +### ETH Transfer Locations + +#### 1. Socket.\_execute() → Plug + +```solidity +executionParams.target.tryCall( + executionParams.value, // ← Value transferred here + executionParams.gasLimit, + maxCopyBytes, + executionParams.payload +) +``` + +**Verify**: + +- Value comes from msg.value +- Validated in execute(): `msg.value >= executionParams.value + socketFees` +- Isolated execution environment + +--- + +#### 2. Socket.\_handleSuccessfulExecution() → NetworkFeeCollector + +```solidity +networkFeeCollector.collectNetworkFee{value: transmissionParams.socketFees}(...) +``` + +**Verify**: + +- Called after execution completes +- socketFees portion of msg.value +- State updated before external call +- NetworkFeeCollector is trusted (per assumptions) + +--- + +#### 3. Socket.\_handleFailedExecution() → Refund Address + +```solidity +SafeTransferLib.safeTransferETH(receiver, msg.value) +``` + +**Verify**: + +- Full msg.value refunded on failure +- Correct recipient (refundAddress or msg.sender) +- executionStatus set to Reverted first + +**Design Note**: Transmitters should simulate before sending. External reimbursement for failed txs. + +--- + +#### 4. MessageSwitchboard.refund() → Refund Address + +```solidity +SafeTransferLib.safeTransferETH(fees.refundAddress, feesToRefund) +``` + +**Verify**: + +- ReentrancyGuard applied ✓ +- isRefunded flag set before transfer ✓ +- nativeFees zeroed before transfer ✓ +- Only eligible payloads can claim ✓ + +**Status**: Properly secured + +--- + +#### 5. MessageSwitchboard.processPayload() - Fee Storage + +```solidity +payloadFees[payloadId] = PayloadFees({ + nativeFees: msg.value, + ... +}) +``` + +**Verify**: + +- msg.value properly tracked +- Sufficient fees checked against minimums +- Cannot be decreased except via refund + +--- + +### Fee Accounting Checks + +**Verify These Invariants**: + +1. Total ETH in = Total ETH out (no leakage) +2. Fee increases are monotonic (only up, never down) +3. Refunds only happen once per payload +4. Fees cannot be stolen or redirected + +--- + +## Priority 3: Cross-Contract Interactions + +### Socket → Switchboard Calls + +#### 1. getTransmitter() + +**File**: `contracts/protocol/Socket.sol` (line 92) + +```solidity +address transmitter = ISwitchboard(switchboardAddress).getTransmitter(...) +``` + +**Note**: Returns address(0) if no signature. Switchboard is trusted per system assumptions. + +--- + +#### 2. allowPayload() + +**File**: `contracts/protocol/Socket.sol` (line 105) + +```solidity +bool allowed = ISwitchboard(switchboardAddress).allowPayload(...) +``` + +**Critical**: Switchboards are trusted by plugs who choose to connect to them. + +--- + +#### 3. processPayload() + +**File**: `contracts/protocol/Socket.sol` (line 259) + +```solidity +payloadId = ISwitchboard(switchboardAddress).processPayload{value: value_}(...) +``` + +**Verify**: Switchboard receives value, creates unique payloadId + +--- + +### Socket → Plug Calls + +#### 1. overrides() + +**File**: `contracts/protocol/Socket.sol` (line 256) + +```solidity +bytes memory plugOverrides = IPlug(plug_).overrides() +``` + +**Note**: View function, safe + +--- + +#### 2. Execution Call + +**File**: `contracts/protocol/Socket.sol` (line 137) + +```solidity +executionParams.target.tryCall(value, gasLimit, maxCopyBytes, payload) +``` + +**Security**: + +- Reentrancy allowed but safe (CEI pattern followed) +- Gas griefing mitigated (gas limit enforced) +- Always reverts scenario acceptable (plug's responsibility) +- Excessive return data limited (maxCopyBytes) + +**Note**: Plugs are untrusted, but isolated execution prevents impact on other plugs. + +--- + +## Priority 4: Signature Verification + +### Watcher Attestation Signatures + +**MessageSwitchboard.attest()** + +```solidity +digest_to_sign = keccak256(abi.encodePacked( + toBytes32Format(address(this)), // ← Switchboard address + chainSlug, // ← Chain identifier + digest // ← Payload commitment +)) +watcher = _recoverSigner(digest_to_sign, proof) +``` + +**Protection Against Replay**: + +- ✓ Includes contract address (prevents cross-contract replay) +- ✓ Includes chainSlug (prevents cross-chain replay) +- ✓ chainSlug typically = block.chainid (additional protection) +- ✓ Includes digest (the actual payload commitment) + +**Design**: chainSlug is uint32. For chains with chainid > uint32.max, custom chainSlug is used with unique mapping. + +--- + +### Transmitter Signatures + +**SwitchboardBase.getTransmitter()** + +```solidity +digest_to_sign = keccak256(abi.encodePacked( + address(socket__), + payloadId_ +)) +transmitter = _recoverSigner(digest_to_sign, transmitterSignature_) +``` + +**Note**: + +- Transmitter signature is optional (returns address(0) if not provided) +- Used for accountability and reputation tracking +- Does NOT affect authorization (only attestation matters) + +--- + +### Nonce-Based Signatures + +**Functions Using Nonces**: + +1. `markRefundEligible(payloadId, nonce, signature)` +2. `setMinMsgValueFees(chainSlug, minFees, nonce, signature)` +3. `setMinMsgValueFeesBatch(chainSlugs, minFees, nonce, signature)` + +**Nonce Management**: + +- ✓ Namespace isolation per function type (using function selectors) +- ✓ Nonces cannot be replayed within same namespace +- ✓ Off-chain uses UUIDv4 (128-bit) for nonce generation +- ✓ Collision extremely unlikely + +**Implementation**: + +```solidity +function _validateAndUseNonce(bytes4 selector_, address signer_, uint256 nonce_) internal { + uint256 namespacedNonce = uint256(keccak256(abi.encodePacked(selector_, nonce_))); + if (usedNonces[signer_][namespacedNonce]) revert NonceAlreadyUsed(); + usedNonces[signer_][namespacedNonce] = true; +} +``` + +**Function Selectors Used**: + +- `markRefundEligible`: `this.markRefundEligible.selector` +- `setMinMsgValueFees` & `setMinMsgValueFeesBatch`: `this.setMinMsgValueFees.selector` (shared namespace) + +--- + +### Signature Format + +All signatures use Ethereum Signed Message format: + +```solidity +"\x19Ethereum Signed Message:\n32" + digest +``` + +**Verify**: + +- Consistent usage across all contracts +- No raw signature verification (all prefixed) +- Using Solady's ECDSA.recover (assumed secure) + +--- + +## Priority 5: Replay Protection Mechanisms + +### 1. Execution Status + +**Location**: `Socket.sol` - `executionStatus[bytes32 payloadId]` + +**Mechanism**: + +```solidity +if (executionStatus[payloadId] == ExecutionStatus.Executed) + revert PayloadAlreadyExecuted(); +executionStatus[payloadId] = ExecutionStatus.Executed; +``` + +**Verify**: + +- Check happens before any external calls ✓ +- Status set before execution ✓ +- No way to reset status ✓ + +--- + +### 2. Attestation One-Way + +**Location**: Both switchboards - `isAttested[bytes32 digest]` + +**Mechanism**: + +```solidity +if (isAttested[digest]) revert AlreadyAttested(); +isAttested[digest] = true; +``` + +**Verify**: + +- Cannot un-attest a digest ✓ +- Check happens early in attestation flow ✓ + +**Note**: Transaction ordering is serial on blockchains. No concurrent execution race conditions. + +--- + +### 3. Nonce System + +**Location**: `MessageSwitchboard.sol` - `usedNonces[address][uint256]` + +**Mechanism**: + +```solidity +uint256 namespacedNonce = uint256(keccak256(abi.encodePacked(selector_, nonce_))); +if (usedNonces[signer][namespacedNonce]) revert NonceAlreadyUsed(); +usedNonces[signer][namespacedNonce] = true; +``` + +**Verify**: + +- Nonce checked before performing action ✓ +- No nonce reuse possible ✓ +- Namespace isolation prevents cross-function replay ✓ + +--- + +### 4. Payload ID Uniqueness + +**Mechanism**: Counter-based with chain/switchboard encoding + +**Verify**: + +- Counters only increment (never decrement) ✓ +- Counter overflow handling (uint64) - not a realistic concern ✓ +- Payload ID includes source and destination info ✓ + +--- + +## Priority 6: Gas Handling + +### Gas Limit Validation + +**Location**: `Socket.sol:130` + +```solidity +if (gasleft() < (executionParams_.gasLimit * gasLimitBuffer) / 100) + revert LowGasLimit(); +``` + +**Type**: gasLimit is uint64 + +**Overflow Analysis**: + +- `uint64.max * 105 / 100` = fits within uint256 ✓ +- No overflow risk ✓ +- Allows flexibility for different chains (Ethereum: 30M, Mantle: 4B) + +**Note**: No hardcoded max limit to support future high-throughput chains + +--- + +### Gas Limit Forwarding + +**Location**: `Socket.sol:137-142` + +```solidity +(success, exceededMaxCopy, returnData) = executionParams.target.tryCall( + executionParams.value, + executionParams.gasLimit, // ← Forwarded to external call + maxCopyBytes, + executionParams.payload +) +``` + +**Verify**: + +- tryCall properly limits gas ✓ +- Doesn't forward more gas than available ✓ +- 63/64 rule respected by EVM ✓ + +--- + +### Return Data Limitation + +**Location**: `Socket.sol:118` and config + +```solidity +maxCopyBytes = 2048 (default) +``` + +**Purpose**: Prevent DOS from excessive return data copying + +**Verify**: + +- Properly limits memory allocation ✓ +- exceededMaxCopy flag set correctly ✓ +- Events still emitted even when exceeded ✓ + +--- + +## Priority 7: Configuration Management + +### Switchboard Registration + +**Function**: `SocketConfig.registerSwitchboard()` + +**Design Decision**: No contract existence check + +**Rationale**: + +- Switchboards are trusted by plugs who choose to connect +- Plugs verify switchboard implementation before connecting +- Invalid switchboards simply won't work (plug's responsibility) + +**Note**: This is intentional per trust model + +--- + +### Plug Connection + +**Function**: `SocketConfig.connect()` + +**Transaction Ordering**: + +- Switchboard status checked at entry +- Status could change in same block (different tx) +- Low probability: only when exploit found +- Low impact: plug can disconnect and reconnect + +**Note**: Blockchains process transactions serially, but ordering within block can vary + +--- + +## Priority 8: Edge Cases + +### Payload Execution + +**Edge Case 1**: Plug always reverts + +- executionStatus set to Reverted ✓ +- msg.value refunded ✓ +- Cannot retry execution ✓ +- **Impact**: Funds returned, no loss + +**Edge Case 2**: Plug consumes all gas + +- tryCall limits gas, execution fails ✓ +- Status set to Reverted ✓ +- **Verify**: Gas checks prevent complete exhaustion ✓ + +**Edge Case 3**: Deadline expires during execution + +- Deadline checked before execution starts ✓ +- Not checked during execution ✓ +- **Impact**: Payload could execute slightly after deadline (acceptable) + +**Edge Case 4**: Multiple transmitters race to execute + +- First transaction sets execution status ✓ +- Later transactions revert (already executed) ✓ +- **Impact**: Wasted gas for losing transmitters (acceptable) + +--- + +### Fee Management + +**Edge Case 1**: Fees increased after attestation + +- Allowed by design ✓ +- Doesn't invalidate attestation ✓ +- **Impact**: Can incentivize execution of slow payloads ✓ + +**Edge Case 2**: Refund claimed before execution attempted + +- Only possible if watcher marks eligible ✓ +- Watcher shouldn't mark if execution possible ✓ +- **Impact**: Payload never executes (intentional) + +**Edge Case 3**: Fee increase causes overflow + +- Solidity 0.8+ prevents overflow with revert ✓ +- **Impact**: Cannot increase fees beyond max ✓ + +--- + +### Griefing Vectors + +**Transmitter Griefing**: Malicious plug could make payload look valid (passes simulation) then revert in production + +- **Mitigation**: Transmitters blacklist bad plugs +- **Market Solution**: Reputation systems +- **Impact**: LOW - Market-based solution adequate + +--- + +## Suggested Testing Scenarios + +### Reentrancy Tests + +1. Malicious plug calls Socket.sendPayload() during execution ✓ (safe - new payload) +2. Malicious plug calls Socket.execute() during execution ✓ (safe - different payloadId) +3. Refund recipient attempts reentrancy during refund ✓ (protected by ReentrancyGuard) + +### Replay Tests + +1. Attempt to execute same payloadId twice ✓ (blocked by executionStatus) +2. Attempt to attest same digest twice ✓ (blocked by isAttested) +3. Attempt to reuse nonce within namespace ✓ (blocked by usedNonces) +4. Attempt to reuse nonce across functions ✓ (namespace isolation prevents) + +### Gas Tests + +1. Execute with gasLimit = 0 (should handle gracefully) +2. Execute with gasLimit = type(uint64).max (should not overflow) +3. Execute with minimal gas (just above threshold) +4. Payload that consumes exactly gasLimit + +### Value Tests + +1. Execute with msg.value = executionParams.value + socketFees (exact) +2. Execute with msg.value < required (should revert) +3. Execute with msg.value > required (excess stays in Socket) +4. Increase fees with msg.value causing nativeFees overflow (should revert) + +### Signature Tests + +1. Invalid signature format +2. Signature from non-watcher address +3. Nonce reuse within namespace (should revert) +4. Nonce reuse across namespaces (should succeed with namespace isolation) + +--- + +## Security Properties to Verify + +### Correctness Properties + +- ✓ Every executed payload was properly attested +- ✓ Every executed payload came from authorized source +- ✓ Every payload executes at most once +- ✓ Execution respects all specified parameters (gas, value, deadline) + +### Safety Properties + +- ✓ User funds never lost or stolen +- ✓ Fees properly accounted for +- ✓ Refunds only issued for unexecuted payloads +- ✓ No unauthorized state modifications + +### Liveness Properties + +- ✓ Valid payloads can eventually execute (if attested) +- ✓ Plugs can always disconnect +- ✓ Governance can always pause in emergency + +### Economic Properties + +- ✓ Transmitters incentivized to deliver payloads +- ✓ Griefing attacks mitigated by market mechanisms +- ✓ Fee increases benefit protocol/transmitters + +--- + +## Tools Recommended + +- **Static Analysis**: Slither, Mythril +- **Symbolic Execution**: Manticore, HEVM +- **Fuzzing**: Echidna, Foundry invariant tests +- **Manual Review**: Focus on areas above +- **Gas Profiling**: Identify optimization opportunities + +--- + +## Summary + +The Socket Protocol follows security best practices with: + +- ✅ CEI (Checks-Effects-Interactions) pattern throughout +- ✅ Replay protection at multiple levels +- ✅ Namespace-isolated nonces +- ✅ Length-prefixed digest creation +- ✅ Trusted entity assumptions clearly documented +- ✅ One-time execution with clear finality + +Main audit focus should be on: + +1. Value flow tracking +2. Signature verification completeness +3. Edge case handling +4. Invariant properties + +The system is well-designed with clear trust boundaries and appropriate security measures. diff --git a/auditor-docs/AUDIT_PREP_SUMMARY.md b/auditor-docs/AUDIT_PREP_SUMMARY.md new file mode 100644 index 00000000..25d79e6a --- /dev/null +++ b/auditor-docs/AUDIT_PREP_SUMMARY.md @@ -0,0 +1,367 @@ +# Audit Preparation Summary + +## Overview + +This document summarizes the pre-audit review conducted on Socket Protocol's core contracts. The review identified design decisions, validated security patterns, and implemented improvements based on senior developer feedback. + +--- + +## Pre-Audit Review Results + +### Contracts Reviewed + +- ✅ Socket.sol (286 lines) +- ✅ SocketUtils.sol (210 lines) +- ✅ SocketConfig.sol (203 lines) +- ✅ MessageSwitchboard.sol (763 lines) +- ✅ FastSwitchboard.sol (244 lines) +- ✅ SwitchboardBase.sol (115 lines) +- ✅ IdUtils.sol (75 lines) +- ✅ OverrideParamsLib.sol (148 lines) + +**Total**: ~2,044 lines of Solidity code + +--- + +## Key Findings & Resolutions + +### ✅ Design Patterns Validated + +**1. Checks-Effects-Interactions (CEI) Pattern** + +- **Status**: ✅ Properly implemented throughout +- **Key Functions**: execute(), \_execute(), processPayload() +- **Result**: Reentrancy protection without ReentrancyGuard overhead + +**2. Replay Protection** + +- **Status**: ✅ Multi-layer protection in place +- **Mechanisms**: executionStatus, isAttested, nonce system +- **Result**: No double-execution or replay possible + +**3. Gas Limit Handling** + +- **Status**: ✅ Appropriate for multi-chain deployment +- **Type**: uint64 (prevents overflow, supports high-throughput chains) +- **Result**: Flexible without hardcoded limits + +**4. Signature Verification** + +- **Status**: ✅ Includes necessary anti-replay components +- **Protection**: address(this), chainSlug (= block.chainid typically) +- **Result**: Cross-chain replay prevented + +--- + +### 🔧 Improvements Implemented + +**1. Nonce Namespace Isolation** ✅ IMPLEMENTED + +- **Issue**: Single nonce mapping shared across different function types +- **Solution**: Function selector-based namespace isolation +- **Implementation**: `_validateAndUseNonce(bytes4 selector, address signer, uint256 nonce)` +- **Benefit**: Prevents cross-function nonce exhaustion, cleaner off-chain management + +**Code Added**: + +```solidity +function _validateAndUseNonce(bytes4 selector_, address signer_, uint256 nonce_) internal { + uint256 namespacedNonce = uint256(keccak256(abi.encodePacked(selector_, nonce_))); + if (usedNonces[signer_][namespacedNonce]) revert NonceAlreadyUsed(); + usedNonces[signer_][namespacedNonce] = true; +} +``` + +**Rationale for Function Selectors**: + +- Deterministic encoding (same on-chain and off-chain) +- Gas efficient (bytes4 vs string) +- Type-safe (compiler verification) + +--- + +### ❌ Issues Dismissed (Not Actual Vulnerabilities) + +The following items were initially flagged but determined to be non-issues after analysis: + +**1. Reentrancy in Execution Flow** + +- **Reason**: CEI pattern properly followed, different payloadIds are independent +- **Verdict**: Safe by design + +**2. Gas Limit Overflow** + +- **Reason**: uint64 \* 105 / 100 fits within uint256, no overflow +- **Verdict**: Not an issue + +**3. Deadline Validation (Max Limit)** + +- **Reason**: Application-layer responsibility, different apps need different deadlines +- **Verdict**: Intentional design decision + +**4. msg.value Full Refund on Failure** + +- **Reason**: Transmitters should simulate; external reimbursement exists +- **Verdict**: Acceptable trade-off + +**5. increaseFeesForPayload Validation** + +- **Reason**: Multi-layer validation (Socket + Switchboard + off-chain) +- **Verdict**: Properly secured + +**6. Counter Overflow Risk** + +- **Reason**: uint64 = 18 quintillion, not realistically exploitable +- **Verdict**: Acceptable + +**7. Double Attestation Race** + +- **Reason**: Transactions execute serially, not concurrently +- **Verdict**: Not possible + +**8. Transaction Ordering "Race"** + +- **Reason**: Block-level ordering, not race condition; low probability, low impact +- **Verdict**: Acceptable + +**9. Cross-Contract Reentrancy** + +- **Reason**: CEI pattern + unique payloadIds per call +- **Verdict**: Safe by design + +**10. Signature Replay Across Chains** + +- **Reason**: chainSlug = block.chainid (typically), unique per chain +- **Verdict**: Properly protected + +--- + +## System Assumptions (Critical for Auditors) + +### Trust Model + +1. **Switchboards are Trusted by Plugs** + + - Anyone can register, but plugs choose whom to trust + - Plug's responsibility to verify switchboard implementation + +2. **NetworkFeeCollector is Trusted by Socket** + + - Set by governance + - Called after successful execution for fee collection + +3. **Target Plugs are Trusted by Source Plugs** + + - Source specifies destination plug + - Cross-chain trust established at application level + +4. **simulate() is Off-Chain Only** + + - Gated by OFF_CHAIN_CALLER (0xDEAD) + - Used for gas estimation by transmitters + +5. **Watchers Act Honestly** + + - At least one honest watcher per payload + - Verify source chain correctly + - Respect finality before attesting + +6. **Transmitters are Rational** + - Should simulate before executing + - External reimbursement for failures + - Market-based reputation systems + +--- + +## Security Properties Verified + +### Core Invariants + +- ✓ Each payload executes at most once +- ✓ Execution status transitions are one-way +- ✓ Digests are immutable once stored +- ✓ Attestations cannot be revoked +- ✓ Payload IDs are globally unique +- ✓ Nonces cannot be replayed within namespace +- ✓ Source validation prevents unauthorized execution + +### Protection Mechanisms + +- ✓ CEI pattern throughout execution flow +- ✓ Replay protection via executionStatus mapping +- ✓ Nonce management with namespace isolation +- ✓ Length-prefixed digest creation (collision-resistant) +- ✓ Gas limit buffer for contract overhead +- ✓ Return data limiting (maxCopyBytes) + +--- + +## Testing Recommendations + +### High-Priority Test Scenarios + +**1. Reentrancy Tests** + +- Malicious plug calls sendPayload() during execution (should create new payload) +- Malicious plug calls execute() with different payloadId (should succeed) +- Refund recipient attempts reentrancy (should be blocked by ReentrancyGuard) + +**2. Replay Protection** + +- Attempt double execution of same payloadId (should revert) +- Attempt double attestation of same digest (should revert) +- Reuse nonce within namespace (should revert) +- Reuse nonce across namespaces (should succeed with isolation) + +**3. Gas Limit Edge Cases** + +- gasLimit = 0 (should handle) +- gasLimit = type(uint64).max (should not overflow) +- gasLimit exceeds block limit (should naturally fail) + +**4. Value Flow** + +- Exact msg.value (should succeed) +- Insufficient msg.value (should revert) +- Excess msg.value (stays in contract) + +**5. Fee Management** + +- Increase fees causing overflow (should revert) +- Refund double-claim (should revert) +- Unauthorized fee increase (should revert) + +--- + +## Documentation Status + +### Files Created/Updated + +- ✅ SYSTEM_OVERVIEW.md - Updated with assumptions +- ✅ CONTRACTS_REFERENCE.md - Comprehensive reference +- ✅ MESSAGE_FLOW.md - Detailed flow documentation +- ✅ SECURITY_MODEL.md - Trust model and invariants +- ✅ AUDIT_FOCUS_AREAS.md - Updated with validated patterns +- ✅ SETUP_GUIDE.md - Environment and testing +- ✅ TESTING_COVERAGE.md - Test scenarios +- ✅ FAQ.md - Extended with design rationale +- ✅ README.md - Navigation and overview +- ✅ AUDIT_PREP_SUMMARY.md - This document + +--- + +## Code Changes Made + +### File: MessageSwitchboard.sol + +**Change 1: Added Nonce Validation Utility** + +- Location: ~Line 354 +- Added: `_validateAndUseNonce()` internal function +- Purpose: DRY principle, namespace isolation + +**Change 2: Updated markRefundEligible()** + +- Location: ~Line 459 +- Changed: From inline nonce check to utility function call +- Namespace: `this.markRefundEligible.selector` + +**Change 3: Updated setMinMsgValueFees()** + +- Location: ~Line 500 +- Changed: From inline nonce check to utility function call +- Namespace: `this.setMinMsgValueFees.selector` + +**Change 4: Updated setMinMsgValueFeesBatch()** + +- Location: ~Line 533 +- Changed: From inline nonce check to utility function call +- Namespace: `this.setMinMsgValueFees.selector` (shares namespace) + +**Change 5: Added Missing Event** + +- Added: `event DefaultDeadlineSet(uint256 defaultDeadline);` +- Purpose: Complete event coverage + +**Net Result**: + +- Reduced code duplication +- Improved maintainability +- Added namespace isolation +- Fixed compilation error + +--- + +## Remaining Considerations + +### For Auditors to Evaluate + +1. **Gas Limit Flexibility** + + - No hardcoded max supports diverse chains + - Could extreme values cause unforeseen issues? + +2. **Switchboard Trust Model** + + - Is plug-level trust verification sufficient? + - Should protocol add reputation mechanisms? + +3. **Fee Economic Sustainability** + + - External transmitter reimbursement model + - Market-based griefing protection + - Are these adequate long-term? + +4. **Upgrade Strategy** + + - Currently no upgrade mechanism + - Security issues require redeployment + - Is this acceptable for critical infrastructure? + +5. **Edge Case Trade-offs** + - Always-reverting plugs: acceptable (funds refunded) + - Deadline precision: block.timestamp (±15 seconds) + - Return data limits: 2KB default + - Are these appropriate? + +--- + +## Audit Readiness Checklist + +- ✅ All contracts compile successfully +- ✅ Core security patterns validated +- ✅ System assumptions documented +- ✅ Nonce namespace isolation implemented +- ✅ Comprehensive documentation created +- ✅ Focus areas identified for auditors +- ✅ Test scenarios recommended +- ✅ Trust model clearly defined +- ✅ Design rationale explained +- ✅ Edge cases acknowledged + +--- + +## Summary + +Socket Protocol demonstrates: + +- ✅ Strong security patterns (CEI, replay protection) +- ✅ Clear trust boundaries +- ✅ Appropriate trade-offs for cross-chain infrastructure +- ✅ Well-documented assumptions and design decisions + +The protocol is **audit-ready** with: + +- Solid architectural foundation +- Security-first design +- Clear documentation for auditors +- Minor improvement implemented (nonce namespacing) + +**Recommended**: Focus audit efforts on value flows, signature verification, and edge case handling as outlined in AUDIT_FOCUS_AREAS.md. + +--- + +**Prepared**: [Date] +**Protocol Version**: [Version] +**Pre-Audit Review**: Complete ✅ +**Status**: Ready for formal audit diff --git a/auditor-docs/CONTRACTS_REFERENCE.md b/auditor-docs/CONTRACTS_REFERENCE.md new file mode 100644 index 00000000..205d96eb --- /dev/null +++ b/auditor-docs/CONTRACTS_REFERENCE.md @@ -0,0 +1,396 @@ +# Contracts Reference + +## Contract Inventory + +| Contract | LOC | Purpose | Inheritance | Key External Calls | +| ---------------------- | --- | -------------------------- | -------------------------------- | ----------------------------------------- | +| Socket.sol | 286 | Main execution & routing | SocketUtils | ISwitchboard, IPlug, INetworkFeeCollector | +| SocketUtils.sol | 210 | Utilities & verification | SocketConfig | ISwitchboard | +| SocketConfig.sol | 203 | Configuration management | AccessControl, Pausable | ISwitchboard | +| MessageSwitchboard.sol | 740 | Message-based verification | SwitchboardBase, ReentrancyGuard | ISocket | +| FastSwitchboard.sol | 244 | Fast EVMX verification | SwitchboardBase | ISocket | +| SwitchboardBase.sol | 115 | Base switchboard logic | ISwitchboard, AccessControl | ISocket | +| IdUtils.sol | 75 | Payload ID utilities | None | None (pure functions) | +| OverrideParamsLib.sol | 148 | Parameter builder | None | None (pure functions) | + +--- + +## Detailed Contract Descriptions + +### 1. Socket.sol + +**Purpose**: Core contract for cross-chain payload execution and transmission. Main entry point for both inbound (execute) and outbound (sendPayload) operations. + +**Key State Variables**: + +- `executionStatus[bytes32]`: Tracks whether payload has been executed/reverted +- `payloadIdToDigest[bytes32]`: Stores digest for each payload ID + +**Critical Functions**: + +- `execute()`: Executes incoming payload from remote chain + - Validates deadline, call type, plug connection, msg.value + - Verifies digest through switchboard + - Prevents replay attacks via execution status + - Calls target plug with payload +- `sendPayload()`: Sends payload to remote chain + - Verifies plug is connected + - Gets plug overrides configuration + - Delegates to switchboard for processing +- `fallback()`: Alternative entry point for sendPayload + - Double-encodes return value for raw calldata compatibility + +**Access Control**: Inherits from SocketConfig (RESCUE_ROLE, PAUSER_ROLE, UNPAUSER_ROLE) + +**External Dependencies**: + +- Calls switchboard for verification (`allowPayload`, `getTransmitter`) +- Calls plug for overrides (`IPlug.overrides()`) +- Calls network fee collector for fee collection + +--- + +### 2. SocketUtils.sol + +**Purpose**: Provides utility functions for digest creation, simulation, and verification helpers. + +**Key State Variables**: + +- `OFF_CHAIN_CALLER`: Special address (0xDEAD) for off-chain simulations +- `chainSlug`: Immutable chain identifier + +**Critical Functions**: + +- `_createDigest()`: Creates deterministic hash of execution parameters + - Uses length prefixes for variable-length fields (payload, source, extraData) + - Includes transmitter, payloadId, deadline, gasLimit, value, target +- `simulate()`: Off-chain only - tests payload execution for gas estimation + - Only callable by OFF_CHAIN_CALLER + - Returns success/failure and return data +- `_verifyPlugSwitchboard()`: Validates plug connection and switchboard status +- `_verifyPayloadId()`: Validates payload routing information +- `increaseFeesForPayload()`: Allows plugs to top up fees for pending payloads + +**Access Control**: RESCUE_ROLE for fund recovery, onlyOffChain modifier + +--- + +### 3. SocketConfig.sol + +**Purpose**: Manages socket configuration including switchboard registration, plug connections, and system parameters. + +**Key State Variables**: + +- `switchboardIdCounter`: Incrementing counter for switchboard IDs +- `switchboardStatus[uint32]`: Tracks REGISTERED/DISABLED status +- `plugSwitchboardIds[address]`: Maps plugs to their connected switchboards +- `switchboardAddresses[uint32]`: Maps IDs to addresses +- `gasLimitBuffer`: Percentage buffer for gas calculations +- `maxCopyBytes`: Maximum bytes to copy from return data + +**Critical Functions**: + +- `registerSwitchboard()`: Assigns unique ID to switchboard + - Called by switchboard contract + - Sets status to REGISTERED + - Increments counter +- `connect()`: Connects plug to switchboard + - Validates switchboard is registered + - Stores plug-switchboard mapping + - Forwards config to switchboard if provided +- `disconnect()`: Removes plug connection +- `disableSwitchboard()`: Governance can disable switchboards +- `enableSwitchboard()`: Governance can re-enable switchboards + +**Access Control**: + +- GOVERNANCE_ROLE: Enable switchboards, set parameters +- SWITCHBOARD_DISABLER_ROLE: Disable switchboards + +--- + +### 4. MessageSwitchboard.sol + +**Purpose**: Full-featured switchboard with watcher attestations, fee management (native + sponsored), refunds, and cross-chain routing. + +**Key State Variables**: + +- `payloadCounter`: Incrementing counter for payload IDs +- `isAttested[bytes32]`: Tracks attested digests +- `siblingSockets[uint32]`: Destination chain socket addresses +- `siblingSwitchboards[uint32]`: Destination chain switchboard addresses +- `siblingPlugs[uint32][address]`: Source plug to destination plug mappings +- `payloadFees[bytes32]`: Native token fee tracking (with refund eligibility) +- `sponsoredPayloadFees[bytes32]`: Sponsored fee tracking +- `sponsorApprovals[address][address]`: Sponsor to plug approvals +- `usedNonces[address][uint256]`: Prevents nonce replay attacks +- `minMsgValueFees[uint32]`: Minimum fees per destination chain + +**Critical Functions**: + +- `processPayload()`: Handles outbound payload requests + - Decodes overrides (version 1: native, version 2: sponsored) + - Validates sibling configuration exists + - Creates digest and payload ID + - Tracks fees for refund eligibility + - Emits MessageOutbound event +- `attest()`: Watchers attest to payloads + - Verifies watcher signature + - Checks watcher has WATCHER_ROLE + - Marks digest as attested +- `allowPayload()`: Verifies payload can execute + - Checks source plug matches expected sibling + - Checks digest is attested +- `markRefundEligible()`: Watchers mark payloads for refund + - Validates watcher signature with nonce + - Prevents nonce replay +- `refund()`: Claims refund for eligible payloads + - Protected by ReentrancyGuard + - Transfers native fees back to refund address +- `increaseFeesForPayload()`: Top up fees + - Supports both native and sponsored flows +- `setMinMsgValueFees()`: Updates minimum fees + - Requires FEE_UPDATER_ROLE signature with nonce + +**Access Control**: + +- WATCHER_ROLE: Attest payloads, mark refunds +- FEE_UPDATER_ROLE: Update fee parameters +- onlySocket: Called by Socket for payload processing + +**Fee Flows**: + +- Native: User pays ETH when sending payload +- Sponsored: Sponsor pre-approves plugs, maxFees tracked off-chain + +--- + +### 5. FastSwitchboard.sol + +**Purpose**: Simplified switchboard for fast finality using EVMX chain verification. + +**Key State Variables**: + +- `evmxChainSlug`: EVMX chain identifier for verification +- `watcherId`: Watcher ID for EVMX verification +- `payloadCounter`: Incrementing counter +- `isAttested[bytes32]`: Tracks attested digests +- `plugAppGatewayIds[address]`: Maps plugs to app gateway IDs +- `payloadIdToPlug[bytes32]`: Maps payload IDs to source plugs +- `defaultDeadline`: Default execution deadline (1 day) + +**Critical Functions**: + +- `processPayload()`: Creates payload with EVMX verification + - Validates EVMX config is set + - Decodes deadline from overrides (or uses default) + - Creates payload ID with: source=(chainSlug, switchboardId), verification=(evmxChainSlug, watcherId) + - Emits PayloadRequested +- `attest()`: Watchers attest digest + - Similar to MessageSwitchboard but simpler + - Verifies watcher signature +- `allowPayload()`: Checks attestation and source + - Validates app gateway ID matches + - Returns attestation status +- `updatePlugConfig()`: Sets plug's app gateway ID +- `setEvmxConfig()`: Owner configures EVMX chain and watcher + +**Access Control**: + +- WATCHER_ROLE: Attest payloads +- onlyOwner: Configure EVMX, set defaults + +**Differences from MessageSwitchboard**: + +- No fee management (fees handled on EVMX) +- Simpler attestation model +- App gateway ID based routing vs. sibling plug mapping + +--- + +### 6. SwitchboardBase.sol + +**Purpose**: Abstract base providing common functionality for all switchboards. + +**Key State Variables**: + +- `socket__`: Immutable reference to Socket contract +- `chainSlug`: Chain identifier +- `switchboardId`: Assigned by Socket during registration +- `revertingPayloadIds[bytes32]`: Marks payloads as known reverting + +**Critical Functions**: + +- `registerSwitchboard()`: Calls Socket to get unique ID + - Only callable by owner + - Must be called after deployment +- `getTransmitter()`: Recovers transmitter from signature + - Returns address(0) if no signature provided + - Uses Ethereum signed message format +- `_recoverSigner()`: Internal ECDSA recovery + - Adds "\x19Ethereum Signed Message:\n32" prefix + - Uses Solady's ECDSA library + +**Access Control**: RESCUE_ROLE for fund recovery + +**Modifiers**: `onlySocket` - restricts calls to Socket contract + +--- + +### 7. IdUtils.sol + +**Purpose**: Pure utility functions for encoding/decoding payload IDs. + +**No State Variables** (all pure functions) + +**Functions**: + +- `createPayloadId()`: Encodes components into bytes32 + - Takes: sourceChainSlug, sourceId, verificationChainSlug, verificationId, pointer + - Bit layout: [Source: 64][Verification: 64][Pointer: 64][Reserved: 64] +- `decodePayloadId()`: Extracts all components from bytes32 +- `getVerificationInfo()`: Extracts verification chain and ID +- `getSourceInfo()`: Extracts source chain and ID + +**Usage**: Imported and used by Socket and Switchboards for payload ID management. + +--- + +### 8. OverrideParamsLib.sol + +**Purpose**: Builder pattern library for constructing OverrideParams structs. + +**No State Variables** (all pure functions) + +**Functions**: + +- `clear()`: Creates new OverrideParams with defaults +- `setRead()`, `setParallel()`, `setWriteFinality()`: Set flags +- `setGasLimit()`, `setValue()`, `setMaxFees()`: Set numeric values +- `setReadAtBlock()`, `setDelay()`: Set timing parameters +- `setConsumeFrom()`, `setSwitchboardType()`: Set addresses/identifiers + +**Usage**: Used by plugs to construct override parameters for payload requests. + +--- + +## Contract Interactions + +### Execution Flow (Inbound) + +``` +Transmitter → Socket.execute() + ├─> SocketUtils._verifyPlugSwitchboard() + ├─> SocketUtils._verifyPayloadId() + ├─> Socket._verify() + │ └─> Switchboard.getTransmitter() + │ └─> Switchboard.allowPayload() + └─> Socket._execute() + └─> Plug.call{value, gas}(payload) + └─> NetworkFeeCollector.collectNetworkFee() +``` + +### Sending Flow (Outbound) + +``` +Plug → Socket.sendPayload() + ├─> SocketUtils._verifyPlugSwitchboard() + ├─> Plug.overrides() + └─> Switchboard.processPayload() + └─> emit PayloadRequested +``` + +### Registration Flow + +``` +Switchboard → Socket.registerSwitchboard() + └─> Assign ID, set status REGISTERED + +Plug → Socket.connect(switchboardId, config) + └─> Switchboard.updatePlugConfig(plug, config) +``` + +--- + +## Key Data Structures + +### ExecutionParams + +```solidity +struct ExecutionParams { + bytes4 callType; // WRITE, READ, or SCHEDULE + uint256 deadline; // Execution deadline timestamp + uint256 gasLimit; // Gas limit for execution + address target; // Target plug address + uint256 value; // Native value to send + bytes32 payloadId; // Unique payload identifier + bytes32 prevBatchDigestHash; // For batch processing + bytes source; // Encoded source info + bytes payload; // Call data + bytes extraData; // Additional data +} +``` + +### TransmissionParams + +```solidity +struct TransmissionParams { + uint256 socketFees; // Fees for Socket/transmitter + address refundAddress; // Where to refund on failure + bytes extraData; // Additional parameters + bytes transmitterProof; // Transmitter signature +} +``` + +### DigestParams + +```solidity +struct DigestParams { + bytes32 socket; // Destination socket address + bytes32 transmitter; // Transmitter address + bytes32 payloadId; // Unique identifier + uint256 deadline; // Execution deadline + bytes4 callType; // Call type + uint256 gasLimit; // Gas limit + uint256 value; // Native value + bytes32 target; // Target address + bytes32 prevBatchDigestHash; + bytes payload; // Payload data + bytes source; // Source information + bytes extraData; // Extra data +} +``` + +--- + +## Access Control Roles + +| Role | Purpose | Holders | +| ------------------------- | ----------------------------------- | ----------------------- | +| Owner | Full admin control | Deployer initially | +| GOVERNANCE_ROLE | Enable switchboards, set parameters | Multi-sig/DAO | +| SWITCHBOARD_DISABLER_ROLE | Emergency disable switchboards | Security team | +| RESCUE_ROLE | Recover stuck funds | Governance | +| PAUSER_ROLE | Pause socket operations | Emergency responders | +| UNPAUSER_ROLE | Unpause socket operations | Governance | +| WATCHER_ROLE | Attest payloads | Off-chain watcher nodes | +| FEE_UPDATER_ROLE | Update fee parameters | Fee oracle | + +--- + +## Constants + +```solidity +// Call Types +bytes4 constant READ = bytes4(keccak256('READ')); +bytes4 constant WRITE = bytes4(keccak256('WRITE')); +bytes4 constant SCHEDULE = bytes4(keccak256('SCHEDULE')); + +// Switchboard Types +bytes32 constant FAST = keccak256('FAST'); +bytes32 constant CCTP = keccak256('CCTP'); + +// Limits +uint256 constant PAYLOAD_SIZE_LIMIT = 24_500; +uint16 constant MAX_COPY_BYTES = 2048; +``` diff --git a/auditor-docs/FAQ.md b/auditor-docs/FAQ.md new file mode 100644 index 00000000..9ac79356 --- /dev/null +++ b/auditor-docs/FAQ.md @@ -0,0 +1,1236 @@ +# Frequently Asked Questions + +## System Assumptions + +### Core Assumptions + +**A1: Switchboards are trusted by Plugs/Apps** + +- Anyone can register as a switchboard on Socket +- Plugs only connect to switchboards they have verified and trust +- Invalid or malicious switchboards only affect plugs that choose to connect to them +- Users must perform due diligence before connecting + +**A2: NetworkFeeCollector is trusted by Socket** + +- Socket calls networkFeeCollector.collectNetworkFee() after successful execution +- No reentrancy concerns as the collector is a trusted contract +- Governance sets the networkFeeCollector address + +**A3: Target Plugs are trusted by Source Plugs** + +- Source plugs specify and trust their sibling plugs on destination chains +- Invalid target plug configurations only affect the plug that set them +- Cross-chain trust is established at plug level, not protocol level + +**A4: simulate() function is for off-chain use only** + +- Gated by OFF_CHAIN_CALLER address (0xDEAD) +- Only used by off-chain services for gas estimation and revert checking +- Not accessible on mainnet (msg.sender can never be 0xDEAD in normal operation) +- Results used by transmitters to avoid failed transactions + +**A5: Watchers act honestly** + +- At least one honest watcher per payload is assumed +- Watchers verify source chain state correctly before attesting +- Watchers respect finality periods before attesting +- Compromised watcher can DOS (refuse to attest) but not forge invalid payloads + +**A6: Transmitters are rational economic actors** + +- Should call simulate() before sending transactions +- External reimbursement mechanisms exist for failed deliveries +- May blacklist/whitelist plugs based on historical behavior +- Compete for fees through efficient delivery + +--- + +## Architecture & Design + +### Q1: Why use a switchboard architecture instead of built-in verification? + +**Answer**: The switchboard architecture provides flexibility and upgradability: + +- **Different Security Models**: Some applications need fast finality (FastSwitchboard), others need stronger guarantees (MessageSwitchboard with multiple watchers) +- **Upgradability**: Can deploy new switchboard types without changing core Socket +- **Competition**: Multiple switchboards can compete on speed, cost, and security +- **Specialization**: Switchboards can be optimized for specific chains or use cases + +The Socket contract remains simple and focused on execution, while switchboards handle the complex verification logic. + +--- + +### Q2: Why are payload IDs structured as bytes32 with encoded information? + +**Answer**: The payload ID structure `[Source: 64 bits][Verification: 64 bits][Counter: 64 bits][Reserved: 64 bits]` provides several benefits: + +- **Self-Describing**: Contains routing information without additional lookups +- **Validation**: Easy to verify payload is for correct chain and switchboard +- **Uniqueness**: Counter ensures global uniqueness across all chains +- **Compact**: Single bytes32 is gas-efficient for storage and events +- **Future-Proof**: Reserved 64 bits for future extensions + +See `PAYLOAD_ID_ARCHITECTURE.md` for detailed explanation. + +--- + +### Q3: Why can payloads only be executed once, even if they fail? + +**Answer**: This is an intentional design choice: + +- **Simplicity**: Prevents complex retry logic and state management +- **Determinism**: Clear finality - each payload has one outcome +- **Security**: Prevents replay attacks and complex re-execution scenarios +- **Gas Efficiency**: No need to track retry counts or conditions + +If a payload fails due to temporary conditions, the application layer can: + +- Send a new payload with updated parameters +- Use the refund mechanism (MessageSwitchboard) +- Build retry logic in the plug contract itself + +--- + +### Q4: What's the difference between FastSwitchboard and MessageSwitchboard? + +**Answer**: + +**FastSwitchboard**: + +- Optimized for speed via EVMX verification +- Simpler fee model (fees managed on EVMX) +- App gateway ID-based routing +- Single watcher per payload (EVMX consensus) +- Best for: High-throughput, fast finality needs + +**MessageSwitchboard**: + +- Full-featured with native and sponsored fees +- Complex fee management with refunds +- Sibling plug mapping for routing +- Multiple watchers possible (more decentralized) +- Best for: Applications needing refunds, complex fee logic + +--- + +### Q5: Why does the fallback function double-encode the return value? + +**Answer**: Due to Solidity's behavior with fallback functions: + +```solidity +fallback(bytes calldata) external payable returns (bytes memory) { + bytes32 payloadId = _sendPayload(...); + return abi.encode(abi.encode(payloadId)); // Double encoding +} +``` + +- Raw calldata → raw returndata (no ABI encoding by Solidity) +- `abi.encode(payloadId)` converts bytes32 → bytes +- Outer `abi.encode()` adds offset + length for proper ABI decoding +- Alternative: Use `sendPayload()` directly for standard encoding + +This maintains compatibility with raw calls while providing proper ABI-decodable returns. + +--- + +## Security & Trust + +### Q5: Is reentrancy a concern in this protocol? + +**Answer**: Reentrancy is allowed but safe due to the Checks-Effects-Interactions (CEI) pattern. + +**During Execution**: + +```solidity +// State updated FIRST +executionStatus[payloadId] = Executed; +payloadIdToDigest[payloadId] = digest; + +// THEN external call to plug +(success, ...) = target.tryCall(...); + +// THEN fee collection (to trusted networkFeeCollector) +if (success && networkFeeCollector != address(0)) { + networkFeeCollector.collectNetworkFee{value: socketFees}(...); +} +``` + +**If Plug Reenters**: + +- Calls `execute()` with different payload → New unique payloadId, safe ✓ +- Calls `sendPayload()` → Creates new unique payloadId, safe ✓ +- Calls `execute()` with same payload → Reverts (PayloadAlreadyExecuted) ✓ + +**During Refund**: + +- Protected by Solady's ReentrancyGuard ✓ +- State updated before transfer ✓ + +**Verdict**: No reentrancy guard needed on Socket itself. CEI pattern is sufficient. + +--- + +### Q6: What happens if a watcher is compromised? + +**Answer**: Impact depends on the switchboard type: + +**FastSwitchboard**: + +- Single compromised watcher can attest malicious payloads +- Relies on EVMX chain consensus (multiple validators) +- System security = EVMX security + +**MessageSwitchboard**: + +- Can configure multiple watchers (M-of-N threshold) +- Single compromised watcher cannot authorize alone +- System security depends on watcher set size and threshold + +**Mitigation Strategies**: + +- Use multiple independent watcher nodes +- Implement watcher rotation +- Monitor watcher behavior off-chain +- Enable governance to disable compromised switchboards + +--- + +### Q7: Can the Socket owner or governance steal user funds? + +**Answer**: No, for several reasons: + +**What Governance CAN do**: + +- Pause the contract (prevents new operations) +- Disable switchboards (prevents new connections) +- Change network fee collector +- Update gas/copy byte limits + +**What Governance CANNOT do**: + +- Modify past execution status +- Change payloadIdToDigest mappings +- Execute payloads without valid attestation +- Access user funds directly +- Cancel attested payloads + +User funds are protected by: + +- Immutable execution logic +- Cryptographic attestation requirements +- Replay protection +- Source validation in switchboards + +**Worst Case Scenario**: Governance could DOS the system by pausing, but cannot steal funds. + +--- + +### Q8: What prevents a malicious plug from attacking the system? + +**Answer**: Multiple layers of protection: + +**Isolation**: + +- External call via tryCall with gas limit +- Return data limited to maxCopyBytes +- Value transfer limited to executionParams.value + +**State Protection**: + +- Execution status set BEFORE plug call +- Digest stored BEFORE plug call +- Reentrancy guard (recommended) + +**Economic Disincentives**: + +- Malicious behavior only affects the malicious plug +- Cannot impact other plugs' payloads +- Reverting payloads lose fees (fail to execute) + +**What Malicious Plug Can Do**: + +- Revert its own executions +- Consume all provided gas +- Attempt reentrancy (should fail) + +**What Malicious Plug Cannot Do**: + +- Execute payloads multiple times +- Access other plugs' funds +- Forge attestations +- Bypass verification + +--- + +### Q9: How are cross-chain signature replays prevented? + +**Answer**: Multiple mechanisms: + +**In Signature Digest**: + +```solidity +digest = keccak256(abi.encodePacked( + toBytes32Format(address(this)), // Contract address + chainSlug, // Chain identifier + // ... other parameters +)) +``` + +**Protection Layers**: + +1. **Contract Address**: Different addresses on different chains +2. **Chain Slug**: Explicit chain identifier in signature +3. **Payload ID**: Includes source and destination chain info +4. **Nonces**: Prevent replay within same chain + +**Note**: If same switchboard deployed at same address on multiple chains with same chainSlug (admin error), signatures could theoretically replay. Recommended to also include `block.chainid` for additional protection. + +--- + +## Operations & Behavior + +### Q10: What happens if a payload deadline passes before execution? + +**Answer**: + +**Before Execution Starts**: + +```solidity +if (executionParams_.deadline < block.timestamp) revert DeadlinePassed(); +``` + +- Payload cannot be executed +- Reverts immediately +- Funds not lost (not yet transferred) + +**During Execution**: + +- Deadline not checked during plug execution +- Payload could finish slightly after deadline + +**After Deadline**: + +- Payload remains unexecutable +- In MessageSwitchboard: Eligible for refund (watcher must mark) +- In FastSwitchboard: Fees not refunded (managed on EVMX) + +**Best Practice**: Set deadlines with sufficient buffer (default 1 day) + +--- + +### Q11: Can payload execution order be controlled? + +**Answer**: No, by design: + +**Current Behavior**: + +- Payloads can be executed in any order +- First transmitter to call execute() wins +- `prevBatchDigestHash` exists in params but not enforced + +**Why Not Enforced**: + +- Cross-chain messaging is inherently async +- Different chain finality times +- Transmitter competition for fees +- Simpler implementation + +**Application-Level Solutions**: + +- Plugs should handle out-of-order messages +- Use nonces/sequence numbers in payload data +- Build state machines that accept messages in any order +- Use `prevBatchDigestHash` for optional ordering + +--- + +### Q12: How do refunds work in MessageSwitchboard? + +**Answer**: Two-step process: + +**Step 1: Mark Eligible** + +```solidity +messageSwitchboard.markRefundEligible(payloadId, nonce, signature) +``` + +- Requires watcher signature +- Watcher verifies payload won't execute (e.g., deadline passed) +- Sets `isRefundEligible = true` + +**Step 2: Claim Refund** + +```solidity +messageSwitchboard.refund(payloadId) +``` + +- Anyone can call (if eligible) +- Protected by ReentrancyGuard +- Transfers nativeFees to refundAddress +- Sets `isRefunded = true` + +**Conditions for Eligibility**: + +- Payload has not executed +- Deadline passed or other non-executable condition +- Watcher has attested to eligibility + +**Security**: Two-step process prevents unauthorized refunds + +--- + +### Q13: What is the purpose of transmitterProof? + +**Answer**: Optional accountability mechanism: + +**If Provided**: + +- Signature over (socket address + payloadId) +- Proves which transmitter delivered payload +- Enables reputation systems +- Allows dispute resolution + +**If Not Provided** (empty bytes): + +- Returns address(0) +- Execution still works +- Anonymous delivery + +**Use Cases**: + +- Track transmitter performance +- Reward reliable transmitters +- Slash misbehaving transmitters (off-chain) +- Audit trail for executed payloads + +**Note**: Transmitter signature does NOT affect authorization - only attestation matters. + +--- + +### Q13A: Why is gasLimit uint64 instead of uint256? + +**Answer**: To prevent overflow issues while maintaining flexibility: + +**With uint64**: + +- Max value: 18,446,744,073,709,551,616 (18 quintillion) +- Calculation: `uint64.max * 105 / 100` fits within uint256 ✓ +- Supports high-throughput chains (Ethereum: 30M, Mantle: 4B for ERC20) +- Prevents type(uint256).max attacks + +**Why No Hardcoded Max**: + +- Different chains have vastly different gas models +- Future chains may have even higher limits +- Natural failure if insufficient gas provided +- Allows protocol flexibility across diverse ecosystems + +**Overflow Safety**: Solidity 0.8+ prevents overflow with revert ✓ + +--- + +### Q13B: Are race conditions possible in blockchain execution? + +**Answer**: Not concurrent races, but transaction ordering matters. + +**Concurrent Execution**: ❌ Impossible + +- Transactions execute serially within a block +- No parallel thread execution +- State changes are atomic per transaction + +**Transaction Ordering**: ✓ Possible + +``` +Block N contains: + Tx1: plug.connect(switchboardId) + Tx2: governance.disableSwitchboard(switchboardId) +``` + +**Execution Order**: + +- If Tx1 first: plug connects, then switchboard disabled (plug can disconnect) +- If Tx2 first: switchboard disabled, plug connection fails + +**Impact**: Minimal - clear state after block, no undefined behavior + +**Note**: This is NOT a race condition in the traditional concurrent programming sense. + +--- + +### Q14: Why is there a gasLimitBuffer? + +**Answer**: Accounts for contract execution overhead: + +```solidity +// User specifies gasLimit for plug execution +executionParams.gasLimit = 200_000; + +// Socket needs extra gas for its own operations: +// - Verification logic +// - State updates +// - Event emissions +// - Fee collection + +requiredGas = (200_000 * 105) / 100 = 210_000 +``` + +**Default Buffer**: 105 (5% overhead) + +**Why Needed**: + +- Socket operations consume gas before/after plug call +- Prevents "out of gas" errors in Socket logic +- Ensures clean error handling + +**Configurable**: Governance can adjust via `setGasLimitBuffer()` + +--- + +## Fees & Economics + +### Q15: How are fees distributed? + +**Answer**: Depends on switchboard type: + +**MessageSwitchboard (Native Fees)**: + +``` +User pays: msg.value +├─ executionParams.value → Plug +├─ transmissionParams.socketFees → NetworkFeeCollector +└─ Remainder stays in MessageSwitchboard (excess/refund) +``` + +**MessageSwitchboard (Sponsored)**: + +``` +User pays: 0 ETH (msg.value = 0) +Sponsor: Pre-approved plug, maxFees tracked off-chain +Fees: Managed by off-chain system, charged to sponsor +``` + +**FastSwitchboard**: + +``` +Fees: Managed entirely on EVMX chain +Socket/FastSwitchboard: No fee handling +``` + +--- + +### Q16: Can fees be increased after payload is created? + +**Answer**: Yes, via `increaseFeesForPayload()`: + +**Purpose**: + +- Incentivize slow payloads +- Increase priority +- Adjust for changing gas prices + +**Restrictions**: + +- Only the source plug can increase fees +- Can only increase, not decrease +- Native fees: Add more ETH +- Sponsored fees: Update maxFees value + +**Effect**: + +- Does not invalidate attestation +- Off-chain watchers/transmitters see updated fees +- Makes execution more attractive + +--- + +### Q16A: How does nonce namespace isolation work? + +**Answer**: Function selectors create isolated nonce spaces to prevent cross-function replay. + +**Implementation**: + +```solidity +function _validateAndUseNonce( + bytes4 selector_, // Function selector for namespace + address signer_, + uint256 nonce_ +) internal { + // Namespace nonce with function selector + uint256 namespacedNonce = uint256(keccak256(abi.encodePacked(selector_, nonce_))); + if (usedNonces[signer_][namespacedNonce]) revert NonceAlreadyUsed(); + usedNonces[signer_][namespacedNonce] = true; +} +``` + +**Usage**: + +```solidity +// Different functions, different namespaces +_validateAndUseNonce(this.markRefundEligible.selector, watcher, nonce); +_validateAndUseNonce(this.setMinMsgValueFees.selector, feeUpdater, nonce); +``` + +**Benefits**: + +- ✓ Same nonce value can be used across different functions +- ✓ Prevents accidental cross-function replay +- ✓ Cleaner off-chain nonce management +- ✓ Function selectors are deterministic (on-chain and off-chain) + +**Off-Chain Nonce Generation**: Uses UUIDv4 (128-bit) for collision resistance + +--- + +### Q16B: Why use function selectors instead of strings for namespaces? + +**Answer**: Deterministic encoding and gas efficiency. + +**Problem with Strings**: + +- Encoding can differ between Solidity and off-chain code +- Variable length increases gas cost +- Potential for encoding mismatches + +**Benefits of Function Selectors**: + +- ✓ Fixed size (bytes4 = 4 bytes) +- ✓ Deterministically computed: `keccak256("functionName(params)")[:4]` +- ✓ Same computation on-chain and off-chain +- ✓ Type-safe (compiler ensures function exists) +- ✓ Lower gas cost + +**Example**: + +```javascript +// Off-chain (JavaScript/TypeScript) +const selector = ethers.utils.id('markRefundEligible(bytes32,uint256,bytes)').slice(0, 10); +const namespacedNonce = ethers.utils.keccak256( + ethers.utils.solidityPack(['bytes4', 'uint256'], [selector, nonce]), +); +``` + +--- + +### Q17: What prevents fee manipulation or theft? + +**Answer**: Multiple safeguards: + +**Fee Storage**: + +```solidity +struct PayloadFees { + uint256 nativeFees; // Immutable except increase/refund + address refundAddress; // Set at creation + bool isRefundEligible; // Only watcher can set + bool isRefunded; // One-time flag + address plug; // Ownership tracking +} +``` + +**Protections**: + +1. Only source plug can increase fees +2. Refunds only to specified refundAddress +3. Refunds only when watcher-approved +4. Refunds only possible once +5. Fees in successful execution go to NetworkFeeCollector (governance-set) + +**Cannot**: + +- Decrease fees +- Redirect refund address +- Claim refund without watcher signature +- Double refund + +--- + +### Q18: What happens to excess msg.value? + +**Answer**: + +**On Successful Execution**: + +```solidity +msg.value = executionParams.value + socketFees + excess +├─ executionParams.value → Plug +├─ socketFees → NetworkFeeCollector +└─ excess → Stays in Socket contract ⚠️ +``` + +**On Failed Execution**: + +```solidity +msg.value (all) → Refunded to refundAddress +``` + +**Recommendation**: + +- Send exact amount (value + socketFees) +- Or accept that excess stays in Socket +- Use `rescueFunds()` if significant amounts stuck + +**Design Note**: Consider adding excess refund in future version + +--- + +## Technical Details + +### Q19: Why use length prefixes in digest creation? + +**Answer**: Prevents collision attacks: + +**Without Length Prefixes**: + +```solidity +// Collision possible: +payload1 = "AAAA", source1 = "BB" +payload2 = "AAA", source2 = "ABB" +// Both hash to same: keccak256("AAAABB") +``` + +**With Length Prefixes**: + +```solidity +// Unique hashes: +digest1 = keccak256(uint32(4) + "AAAA" + uint32(2) + "BB") +digest2 = keccak256(uint32(3) + "AAA" + uint32(3) + "ABB") +``` + +**Applied To**: + +- payload (variable length) +- source (variable length) +- extraData (variable length) + +**Fixed-Size Fields**: Don't need prefixes (deadline, gasLimit, etc.) + +--- + +### Q20: What is maxCopyBytes and why limit return data? + +**Answer**: Security mechanism against DOS: + +**Problem**: + +```solidity +// Malicious plug returns huge data +return new bytes(10_000_000); // Would consume excessive gas to copy +``` + +**Solution**: + +```solidity +maxCopyBytes = 2048; // Default 2KB + +(success, exceededMaxCopy, returnData) = target.tryCall(..., maxCopyBytes, ...) +// If return data > 2KB: +// - exceededMaxCopy = true +// - returnData = first 2KB only +``` + +**Benefits**: + +- Prevents DOS via excessive memory allocation +- Predictable gas costs +- Still allows reasonable return data + +**Configurable**: Governance can update via `setMaxCopyBytes()` + +--- + +### Q21: How does tryCall work and why use it? + +**Answer**: Solady's tryCall provides safe external call handling: + +**Features**: + +```solidity +(bool success, bool exceededMaxCopy, bytes memory returnData) = + target.tryCall(value, gasLimit, maxCopyBytes, payload); +``` + +**Advantages over raw call**: + +- Explicit gas limit forwarding +- Return data size limiting (DOS protection) +- Doesn't revert on failure (returns success flag) +- Safely handles all failure modes + +**Why Not Raw Call**: + +```solidity +(bool success, bytes memory data) = target.call{value: value, gas: gasLimit}(payload); +// Issues: +// - No return data limiting +// - Could copy unbounded data +// - Less explicit gas handling +``` + +--- + +### Q22: What is the significance of WRITE/READ/SCHEDULE call types? + +**Answer**: Call types define execution context: + +**WRITE** (Currently Only Supported): + +- State-changing operations +- Executed on destination chain +- Default for cross-chain messages + +**READ** (Not Yet Implemented): + +- View/pure functions +- Would read state without changes +- Planned for future versions + +**SCHEDULE** (Not Yet Implemented): + +- Delayed execution +- Would schedule for future block +- Planned for EVMX integration + +**Current Check**: + +```solidity +if (executionParams_.callType != WRITE) revert InvalidCallType(); +``` + +**Future**: Additional call types may be supported + +--- + +### Q23: Why is Socket immutable for chainSlug? + +**Answer**: Fundamental identity: + +```solidity +uint32 public immutable chainSlug; +``` + +**Reasons**: + +- Each Socket instance tied to specific chain +- Cannot migrate Socket to different chain +- Prevents misconfiguration +- Ensures payload routing integrity +- Gas optimization (immutable vs storage) + +**If Chain Slug Needs to Change**: + +- Deploy new Socket contract +- Cannot modify existing deployment +- By design - prevents critical errors + +--- + +## Edge Cases & Scenarios + +### Q24: What happens if a plug connects then immediately disconnects? + +**Answer**: + +**State After Disconnect**: + +```solidity +plugSwitchboardIds[plug] = 0; // Cleared in Socket +// But: switchboard still has plug config stored +``` + +**Implications**: + +- Cannot send new payloads (not connected) +- Existing attested payloads still executable +- Switchboard retains config (stale data) + +**Cleanup**: + +- Switchboard config not automatically cleared +- May want to call `updatePlugConfig(plug, "")` to clear +- Low impact - just storage inefficiency + +--- + +### Q25: Can a switchboard be disabled while plugs are connected? + +**Answer**: Yes, and it's an intended emergency mechanism: + +**Process**: + +```solidity +socketConfig.disableSwitchboard(switchboardId); +// Status: REGISTERED → DISABLED +``` + +**Effect on Connected Plugs**: + +- Plugs remain connected (mapping not cleared) +- New payloads fail (processPayload checks status) +- Existing attested payloads still executable +- Plugs must manually disconnect and reconnect elsewhere + +**Purpose**: Emergency stop for compromised switchboards + +**Recovery**: Plugs should monitor switchboard status and disconnect if disabled + +--- + +### Q26: What if EVMX chain itself has issues? + +**Answer**: Impacts FastSwitchboard only: + +**Scenario**: EVMX chain offline or compromised + +**Impact**: + +- FastSwitchboard payloads cannot be attested +- MessageSwitchboard unaffected (independent) +- Plugs can disconnect from FastSwitchboard +- Can connect to MessageSwitchboard instead + +**Mitigation**: + +- Deploy multiple switchboard types +- Don't rely solely on FastSwitchboard +- Have fallback verification method + +**Design Benefit**: Switchboard modularity allows failover + +--- + +### Q27: What happens at payload counter overflow? + +**Answer**: + +**Scenario**: `payloadCounter = type(uint64).max`, then processPayload() called + +**Behavior**: + +```solidity +payloadCounter++ // Overflows in Solidity 0.8+ +// Reverts with panic(0x11) - arithmetic overflow +``` + +**Impact**: + +- Cannot create new payloads on this switchboard +- DOS condition +- Existing payloads unaffected + +**Likelihood**: + +- 2^64 = 18,446,744,073,709,551,616 payloads needed +- At 1000 payloads/second = 584 million years + +**Practical**: Not a realistic concern for any deployment + +--- + +## Integration Questions + +### Q28: How should plugs handle out-of-order message delivery? + +**Answer**: Design patterns: + +**Pattern 1: Idempotent Operations** + +```solidity +// Make operations safe to replay +function inbound(bytes memory data) external { + (uint256 id, ...) = abi.decode(data, (uint256, ...)); + if (processed[id]) return; // Already processed + processed[id] = true; + // ... process +} +``` + +**Pattern 2: Sequence Numbers** + +```solidity +uint256 public expectedNonce; +function inbound(bytes memory data) external { + (uint256 nonce, ...) = abi.decode(data, (uint256, ...)); + if (nonce < expectedNonce) revert AlreadyProcessed(); + if (nonce > expectedNonce) revert OutOfOrder(); + expectedNonce++; + // ... process +} +``` + +**Pattern 3: State Machine** + +```solidity +enum State { A, B, C } +State public state; + +function inbound(bytes memory data) external { + State requiredState = abi.decode(data, (State)); + require(state == requiredState, "Invalid state"); + // ... process and transition +} +``` + +--- + +### Q29: How to estimate gas for cross-chain calls? + +**Answer**: Multi-step process: + +**Step 1: Simulate on Destination** + +```solidity +// Off-chain: Call Socket.simulate() on destination chain +SimulateParams[] memory params = [...]; +SimulationResult[] memory results = socket.simulate(params); +// Get actual gas used + success/failure +``` + +**Step 2: Add Safety Buffer** + +```solidity +uint256 estimatedGas = results[0].gasUsed; +uint256 gasLimit = (estimatedGas * 150) / 100; // 50% buffer +``` + +**Step 3: Include in Overrides** + +```solidity +bytes memory overrides = abi.encode( + version, + dstChainSlug, + gasLimit, // From estimation + value, + ... +); +socket.sendPayload{value: fees + value}(overrides, payload); +``` + +**Best Practices**: + +- Always add buffer (at least 20%) +- Test on destination chain +- Monitor actual vs estimated +- Adjust based on historical data + +--- + +### Q30: Can Socket work with non-EVM chains? + +**Answer**: Partially: + +**Source Chain** (Non-EVM → EVM): + +- Possible with appropriate switchboard +- Switchboard must verify non-EVM chain proofs +- Source encoding in bytes format + +**Destination Chain** (EVM → Non-EVM): + +- Socket must be on EVM chain +- Target must be EVM contract +- Current: EVM-only execution + +**Solana Support**: + +- Structs defined for Solana integration +- `SolanaInstruction`, `SolanaReadRequest` in Structs.sol +- Not fully implemented in current contracts + +**Future**: May expand to non-EVM destinations with adapted Socket + +--- + +## Design Rationale + +### Q30: Why don't you enforce maximum deadline limits? + +**Answer**: Application-level responsibility, not protocol concern. + +**Rationale**: + +- Different applications have different time requirements +- Some need hours, others need weeks or months +- Protocol shouldn't impose business logic constraints +- If app sets far-future deadline, it's their design choice + +**Application Responsibility**: + +- Apps should handle stale state appropriately +- Can implement their own deadline logic +- Can check conditions before execution + +**Example**: DeFi app might want 1-hour deadline, governance proposal might want 30-day deadline. + +--- + +### Q31: Why refund full msg.value on failed execution? + +**Answer**: Balance between simplicity and transmitter incentives. + +**Current Design**: + +- Failed execution → Full refund to refundAddress +- Transmitter loses gas cost for failed transaction + +**Rationale**: + +1. **Transmitters Should Simulate**: Off-chain simulate() function available +2. **External Reimbursement**: Transmitters compensated externally for failures +3. **Market Solution**: Bad plugs get blacklisted by transmitters +4. **Simplicity**: No complex partial refund logic needed + +**Griefing Vector**: Malicious plug could pass simulation but revert in production + +- **Mitigation**: Market-based reputation system +- **Impact**: Low - transmitters adapt behavior + +**Alternative Considered**: Keep socketFees even on failure + +- **Downside**: Legitimate failures (network issues, gas spikes) penalize users +- **Current**: More user-friendly, relies on transmitter rationality + +--- + +### Q32: Why allow reentrancy instead of using ReentrancyGuard? + +**Answer**: Gas optimization - unnecessary when CEI pattern is followed. + +**Gas Cost**: ReentrancyGuard adds ~2,500 gas per protected function + +**Why It's Safe**: + +```solidity +// Checks-Effects-Interactions pattern +function execute(...) { + // CHECKS + if (deadline < block.timestamp) revert; + if (executionStatus[id] == Executed) revert; + + // EFFECTS + executionStatus[id] = Executed; + payloadIdToDigest[id] = digest; + + // INTERACTIONS + target.tryCall(...); // Reentrancy here is safe +} +``` + +**Reentrancy Scenarios**: + +1. Same payloadId → Reverts (status already Executed) +2. Different payloadId → New execution, independent state +3. sendPayload() → Creates new payload, no state conflict + +**Verdict**: CEI pattern provides protection without gas overhead. + +**Note**: MessageSwitchboard.refund() DOES use ReentrancyGuard as extra safety for value transfers. + +--- + +### Q33: Why is increaseFeesForPayload() safe without additional checks? + +**Answer**: Multi-layer validation prevents abuse. + +**Validation Layers**: + +1. **Socket Layer**: `_verifyPlugSwitchboard(msg.sender)` - ensures plug is connected +2. **onlySocket Modifier**: Only Socket can call switchboard +3. **Plug Ownership**: Switchboard checks `payloadFees[id].plug == plug_` +4. **Off-Chain**: Watchers verify before applying fee updates + +**Attack Attempt**: + +```solidity +// Attacker tries to increase fees for someone else's payload +attacker.increaseFeesForPayload(victimPayloadId, feeData) + → Socket checks: attacker is connected ✓ + → Socket forwards to switchboard + → Switchboard checks: payloadFees[victimPayloadId].plug != attacker ✗ + → Reverts: UnauthorizedFeeIncrease +``` + +**Verdict**: Cannot increase fees for payloads you didn't create. + +--- + +## Open Questions for Auditors + +### Q34: Areas We'd Like Feedback On + +**1. Gas Limit Flexibility**: + +- No hardcoded maximum gas limit to support diverse chains +- Is this appropriate, or should we have a configurable max per chain? +- Could extremely high gasLimit values cause issues we haven't considered? + +**2. Switchboard Trust Model**: + +- Is the trust assumption on switchboards acceptable for production? +- Should we add on-chain reputation/bonding mechanisms? +- How should plugs evaluate switchboard trustworthiness? + +**3. Fee Economic Model**: + +- Native fee model: Is external transmitter reimbursement sufficient? +- Griefing attacks: Should protocol provide on-chain mitigation? +- Fee market: Will competition drive efficient delivery? + +**4. Counter Exhaustion**: + +- uint64 payloadCounter: ~18 quintillion payloads +- Should we add explicit handling for counter approaching max? +- Is revert-on-overflow the right approach, or should we allow rollover? + +**5. Upgrade Path**: + +- Contracts currently not upgradeable +- Is this appropriate for critical infrastructure? +- If security issue found, migration path is deploy-new-contracts +- Should we consider proxy pattern for critical contracts? + +**6. Cross-Chain State Synchronization**: + +- Protocol assumes eventual consistency +- No built-in ordering enforcement +- Is this appropriate for all use cases? +- Should we provide optional ordering mechanisms? + +**7. Edge Case Handling**: + +- Plug that always reverts: Acceptable? (Currently: yes, funds refunded) +- Excessive return data: Limited to maxCopyBytes (Currently: 2KB) +- Deadline precision: Uses block.timestamp (±15 seconds) +- Are these trade-offs appropriate? + +--- + +## Contact & Support + +**For Audit Questions**: + +- Open issue in repository with [AUDIT] tag +- Email: [audit-support@example.com] +- Discord: [#auditor-support channel] + +**For Technical Clarifications**: + +- Reference this FAQ first +- Check other documentation files +- Ask in audit communication channel + +**For Security Issues**: + +- DO NOT post publicly +- Email: [security@example.com] +- Use PGP key if available + +--- + +## Document Updates + +This FAQ is maintained during the audit process. If you have questions not covered here, please ask - we'll add them to help future auditors. + +**Last Updated**: [Date] +**Version**: 1.0 diff --git a/auditor-docs/MESSAGE_FLOW.md b/auditor-docs/MESSAGE_FLOW.md new file mode 100644 index 00000000..cae256b7 --- /dev/null +++ b/auditor-docs/MESSAGE_FLOW.md @@ -0,0 +1,665 @@ +# Message Flow Documentation + +## Overview + +This document details the step-by-step flows for cross-chain message passing through the Socket Protocol. There are three main flows: Outbound (sending), Inbound (executing), and Fee Management. + +--- + +## 1. Outbound Flow (Sending Payloads) + +### High-Level Sequence + +``` +[Plug] → [Socket] → [Switchboard] → [Event Emission] → [Off-chain Watchers] +``` + +### Detailed Steps + +#### Step 1: Plug Initiates Send + +``` +Plug calls: socket.sendPayload(callData) OR fallback() +``` + +**Checks Performed**: + +- Socket is not paused +- Plug has sufficient balance for msg.value (if any) + +**State Changes**: None yet + +--- + +#### Step 2: Socket Validates Plug Connection + +``` +Function: Socket._sendPayload() → SocketUtils._verifyPlugSwitchboard() +``` + +**Checks Performed**: + +- `plugSwitchboardIds[plug] != 0` (plug is connected) +- `switchboardStatus[switchboardId] == REGISTERED` (switchboard is active) + +**Returns**: switchboard address + +--- + +#### Step 3: Socket Retrieves Plug Overrides + +``` +Call: IPlug(plug).overrides() +``` + +**Purpose**: Plug specifies destination chain, gas limit, deadline, fees, etc. + +**Format**: Depends on switchboard type + +- FastSwitchboard: `abi.encode(deadline)` or empty for default +- MessageSwitchboard: Version-based encoding (see below) + +--- + +#### Step 4: Switchboard Processes Payload + +##### FastSwitchboard.processPayload() + +``` +Sequence: +1. Validate evmxChainSlug and watcherId are configured +2. Decode deadline from overrides (or use defaultDeadline) +3. Create payload ID: + - source: (chainSlug, switchboardId) + - verification: (evmxChainSlug, watcherId) + - pointer: payloadCounter++ +4. Store payloadIdToPlug[payloadId] = plug +5. Emit PayloadRequested(payloadId, plug, switchboardId, overrides, payload) +``` + +**State Changes**: + +- `payloadCounter` increments +- `payloadIdToPlug[payloadId]` set + +--- + +##### MessageSwitchboard.processPayload() + +``` +Sequence: +1. Decode overrides based on version: + + Version 1 (Native Fees): + (uint8, uint32 dstChainSlug, uint256 gasLimit, uint256 value, + address refundAddress, uint256 deadline) + + Version 2 (Sponsored): + (uint8, uint32 dstChainSlug, uint256 gasLimit, uint256 value, + uint256 maxFees, address sponsor, uint256 deadline) + +2. Validate sibling configuration exists: + - siblingSockets[dstChainSlug] != 0 + - siblingSwitchboards[dstChainSlug] != 0 + - siblingPlugs[dstChainSlug][plug] != 0 + +3. Create digest and payload ID: + - Get dstSwitchboardId from siblingSwitchboardIds[dstChainSlug] + - Create payload ID: source=(chainSlug, switchboardId), + verification=(dstChainSlug, dstSwitchboardId), pointer=payloadCounter++ + - Build DigestParams with destination socket/plug addresses + - Hash digest + +4. Handle fees: + + If Sponsored: + - Check sponsorApprovals[sponsor][plug] == true + - Store sponsoredPayloadFees[payloadId] = (maxFees, plug) + - Emit MessageOutbound with isSponsored=true + + If Native: + - Check msg.value >= minMsgValueFees[dstChainSlug] + value + - Store payloadFees[payloadId] = (nativeFees=msg.value, refundAddress, + isRefundEligible=false, isRefunded=false, plug) + - Emit MessageOutbound with isSponsored=false + +5. Emit PayloadRequested(payloadId, plug, switchboardId, overrides, payload) +``` + +**State Changes**: + +- `payloadCounter` increments +- `payloadFees[payloadId]` or `sponsoredPayloadFees[payloadId]` set +- Native fees stored in contract balance + +--- + +#### Step 5: Off-Chain Processing (Not in Scope) + +Watchers monitoring source chain: + +1. See PayloadRequested event +2. Validate payload and source +3. Submit attestation to destination chain switchboard + +--- + +## 2. Inbound Flow (Executing Payloads) + +### High-Level Sequence + +``` +[Transmitter] → [Socket] → [Switchboard Verification] → [Plug Execution] → [Fee Collection] +``` + +### Detailed Steps + +#### Step 1: Transmitter Submits Execution + +``` +Transmitter calls: socket.execute(executionParams, transmissionParams) +``` + +**executionParams** contains: + +- payloadId, target, payload, gasLimit, value, deadline +- callType (must be WRITE) +- source (encoded source chain + plug) +- prevBatchDigestHash, extraData + +**transmissionParams** contains: + +- socketFees (amount for transmitter/protocol) +- refundAddress (where to refund on failure) +- transmitterProof (optional signature) +- extraData + +**Requirements**: + +- `msg.value >= executionParams.value + transmissionParams.socketFees` + +--- + +#### Step 2: Socket Validates Execution Request + +``` +Function: Socket.execute() +``` + +**Validations (in order)**: + +1. **Deadline Check**: + + ``` + if (executionParams.deadline < block.timestamp) revert DeadlinePassed() + ``` + +2. **Call Type Check**: + + ``` + if (executionParams.callType != WRITE) revert InvalidCallType() + ``` + +3. **Plug Connection**: + + ``` + _verifyPlugSwitchboard(executionParams.target) + → Checks plug is connected and switchboard is REGISTERED + → Returns switchboard address + ``` + +4. **Value Check**: + + ``` + if (msg.value < executionParams.value + transmissionParams.socketFees) + revert InsufficientMsgValue() + ``` + +5. **Payload ID Routing**: + + ``` + _verifyPayloadId(executionParams.payloadId, switchboardAddress) + → Extract verification chain slug and switchboard ID from payloadId + → Check verificationChainSlug == chainSlug (this chain) + → Check switchboard address matches + ``` + +6. **Replay Protection**: + ``` + _validateExecutionStatus(executionParams.payloadId) + → Check executionStatus[payloadId] != Executed + → Set executionStatus[payloadId] = Executed + ``` + +--- + +#### Step 3: Verify Digest Through Switchboard + +``` +Function: Socket._verify() +``` + +**Sequence**: + +1. **Recover Transmitter**: + + ``` + address transmitter = switchboard.getTransmitter( + msg.sender, + executionParams.payloadId, + transmissionParams.transmitterProof + ) + ``` + + - If no proof provided, returns address(0) + - If proof provided, recovers signer from signature + +2. **Create Digest**: + + ``` + bytes32 digest = _createDigest(transmitter, executionParams) + ``` + + Digest includes (with length prefixes for variable fields): + + - socket address, transmitter, payloadId, deadline + - callType, gasLimit, value, target + - prevBatchDigestHash + - uint32(payload.length) + payload + - uint32(source.length) + source + - uint32(extraData.length) + extraData + +3. **Store Digest**: + + ``` + payloadIdToDigest[payloadId] = digest + ``` + +4. **Verify with Switchboard**: + ``` + bool allowed = switchboard.allowPayload( + digest, + executionParams.payloadId, + executionParams.target, + executionParams.source + ) + if (!allowed) revert VerificationFailed() + ``` + +--- + +#### Step 4: Execute on Target Plug + +``` +Function: Socket._execute() +``` + +**Sequence**: + +1. **Gas Check**: + + ``` + if (gasleft() < (executionParams.gasLimit * gasLimitBuffer) / 100) + revert LowGasLimit() + ``` + + - gasLimitBuffer typically 105 (5% overhead) + +2. **External Call**: + + ``` + (bool success, bool exceededMaxCopy, bytes memory returnData) = + executionParams.target.tryCall( + executionParams.value, + executionParams.gasLimit, + maxCopyBytes, + executionParams.payload + ) + ``` + + - Uses Solady's LibCall.tryCall() + - Limits return data to maxCopyBytes (default 2048) + +3. **Handle Result**: + + **If Success**: + + ``` + _handleSuccessfulExecution() + → Emit ExecutionSuccess(payloadId, exceededMaxCopy, returnData) + → If networkFeeCollector != address(0): + networkFeeCollector.collectNetworkFee{value: socketFees}( + executionParams, + transmissionParams + ) + ``` + + **If Failure**: + + ``` + _handleFailedExecution() + → Set executionStatus[payloadId] = Reverted + → Refund msg.value to refundAddress (or msg.sender) + → Emit ExecutionFailed(payloadId, exceededMaxCopy, returnData) + ``` + +**State Changes**: + +- `executionStatus[payloadId]` = Executed or Reverted +- `payloadIdToDigest[payloadId]` = digest +- Fees transferred (success) or refunded (failure) + +--- + +## 3. Attestation Flow (Switchboard-Specific) + +### FastSwitchboard Attestation + +``` +Watcher calls: fastSwitchboard.attest(digest, proof) +``` + +**Sequence**: + +1. Check `!isAttested[digest]` (prevent double attestation) +2. Recover watcher from signature: + ``` + digest_hash = keccak256(abi.encodePacked( + toBytes32Format(address(this)), + chainSlug, + digest + )) + watcher = recoverSigner(digest_hash, proof) + ``` +3. Verify `_hasRole(WATCHER_ROLE, watcher)` +4. Set `isAttested[digest] = true` +5. Emit `Attested(digest, watcher)` + +**allowPayload Check**: + +``` +1. Decode source: bytes32 appGatewayId = abi.decode(source) +2. Check plugAppGatewayIds[target] == appGatewayId +3. Return isAttested[digest] +``` + +--- + +### MessageSwitchboard Attestation + +``` +Watcher calls: messageSwitchboard.attest(digestParams, proof) +``` + +**Sequence**: + +1. Create digest from DigestParams: `digest = _createDigest(digestParams)` +2. Recover watcher from signature: + ``` + digest_hash = keccak256(abi.encodePacked( + toBytes32Format(address(this)), + chainSlug, + digest + )) + watcher = recoverSigner(digest_hash, proof) + ``` +3. Verify `_hasRole(WATCHER_ROLE, watcher)` +4. Check `!isAttested[digest]` +5. Set `isAttested[digest] = true` +6. Emit `Attested(payloadId, digest, watcher)` + +**allowPayload Check**: + +``` +1. Decode source: (uint32 srcChainSlug, bytes32 srcPlug) = _decodePackedSource(source) +2. Check siblingPlugs[srcChainSlug][target] == srcPlug +3. Return isAttested[digest] +``` + +--- + +## 4. Fee Management Flow + +### Increasing Fees (Native) + +``` +Plug calls: socket.increaseFeesForPayload(payloadId, feesData) {value: amount} +``` + +**Sequence**: + +1. Socket validates plug is connected: `_verifyPlugSwitchboard(msg.sender)` +2. Socket forwards to switchboard: + ``` + switchboard.increaseFeesForPayload{value: msg.value}( + payloadId, + msg.sender, + feesData + ) + ``` + +**MessageSwitchboard Processing**: + +``` +1. Decode feesType from feesData (first byte) +2. If feesType == 1 (Native): + - Check payloadFees[payloadId].plug == plug + - Add msg.value to payloadFees[payloadId].nativeFees + - Emit NativeFeesIncreased +3. If feesType == 2 (Sponsored): + - Check sponsoredPayloadFees[payloadId].plug == plug + - Decode newMaxFees from feesData + - Set sponsoredPayloadFees[payloadId].maxFees = newMaxFees + - Emit SponsoredFeesIncreased +``` + +**FastSwitchboard Processing**: + +``` +1. Check payloadIdToPlug[payloadId] == plug +2. Emit FeesIncreased (event only, no state change) + Note: FastSwitchboard fees managed on EVMX +``` + +--- + +### Refund Flow (MessageSwitchboard Only) + +#### Mark Eligible for Refund + +``` +Anyone calls: messageSwitchboard.markRefundEligible(payloadId, nonce, signature) +``` + +**Sequence**: + +1. Check `!payloadFees[payloadId].isRefundEligible` +2. Check `payloadFees[payloadId].nativeFees > 0` +3. Create digest: + ``` + digest = keccak256(abi.encodePacked( + toBytes32Format(address(this)), + chainSlug, + payloadId, + nonce + )) + ``` +4. Recover watcher: `watcher = _recoverSigner(digest, signature)` +5. Verify `_hasRole(WATCHER_ROLE, watcher)` +6. Check `!usedNonces[watcher][nonce]` +7. Set `usedNonces[watcher][nonce] = true` +8. Set `payloadFees[payloadId].isRefundEligible = true` +9. Emit `RefundEligibilityMarked(payloadId, watcher)` + +--- + +#### Claim Refund + +``` +Anyone calls: messageSwitchboard.refund(payloadId) +``` + +**Sequence** (protected by ReentrancyGuard): + +1. Check `payloadFees[payloadId].isRefundEligible == true` +2. Check `payloadFees[payloadId].isRefunded == false` +3. Cache `feesToRefund = payloadFees[payloadId].nativeFees` +4. Set `payloadFees[payloadId].isRefunded = true` +5. Set `payloadFees[payloadId].nativeFees = 0` +6. Transfer: `SafeTransferLib.safeTransferETH(refundAddress, feesToRefund)` +7. Emit `Refunded(payloadId, refundAddress, feesToRefund)` + +--- + +## 5. Configuration Flows + +### Switchboard Registration + +``` +Switchboard calls: socket.registerSwitchboard() +``` + +**Sequence**: + +1. Check `switchboardAddressToId[msg.sender] == 0` (not already registered) +2. Assign ID: `switchboardId = switchboardIdCounter++` +3. Store mappings: + - `switchboardAddressToId[msg.sender] = switchboardId` + - `switchboardAddresses[switchboardId] = msg.sender` +4. Set status: `switchboardStatus[switchboardId] = REGISTERED` +5. Emit `SwitchboardAdded(msg.sender, switchboardId)` +6. Return switchboardId + +--- + +### Plug Connection + +``` +Plug calls: socket.connect(switchboardId, plugConfig) +``` + +**Sequence**: + +1. Validate `switchboardId != 0` +2. Check `switchboardStatus[switchboardId] == REGISTERED` +3. Store: `plugSwitchboardIds[msg.sender] = switchboardId` +4. If `plugConfig.length > 0`: + ``` + switchboard.updatePlugConfig(msg.sender, plugConfig) + ``` + - **FastSwitchboard**: Stores `plugAppGatewayIds[plug] = appGatewayId` + - **MessageSwitchboard**: Stores `siblingPlugs[srcChainSlug][plug] = siblingPlug` +5. Emit `PlugConnected(msg.sender, switchboardId, plugConfig)` + +--- + +### Plug Disconnection + +``` +Plug calls: socket.disconnect() +``` + +**Sequence**: + +1. Check `plugSwitchboardIds[msg.sender] != 0` (is connected) +2. Set `plugSwitchboardIds[msg.sender] = 0` +3. Emit `PlugDisconnected(msg.sender)` + +**Note**: Switchboard configuration is NOT automatically cleared + +--- + +## 6. State Transition Summary + +### Payload Lifecycle + +``` +[Not Created] + ↓ processPayload() +[Created/Pending] ────────────────┐ + ↓ attest() │ +[Attested] ──────────────────┐ │ markRefundEligible() + ↓ execute() │ ↓ +[Executed/Reverted] ←────────┴─[Refund Eligible] + ↓ refund() + [Refunded] +``` + +### Execution Status Transitions + +``` +NotExecuted → Executed (success path) +NotExecuted → Reverted (failure path) + +Note: One-way transitions, no re-execution +``` + +### Attestation Transitions + +``` +unattested → attested (one-way, cannot un-attest) +``` + +### Connection Status + +``` +disconnected ↔ connected (bidirectional via connect/disconnect) +``` + +--- + +## 7. Critical Checkpoints + +### Execution Must Pass + +1. ✓ Contract not paused +2. ✓ Deadline not passed +3. ✓ Call type is WRITE +4. ✓ Plug is connected +5. ✓ Switchboard is REGISTERED +6. ✓ Sufficient msg.value provided +7. ✓ Payload ID routes to this chain and switchboard +8. ✓ Payload not already executed +9. ✓ Digest verified by switchboard +10. ✓ Source matches expected sibling (in switchboard) +11. ✓ Digest is attested + +### Sending Must Pass + +1. ✓ Contract not paused +2. ✓ Plug is connected +3. ✓ Switchboard is REGISTERED +4. ✓ Sibling configuration exists (MessageSwitchboard) +5. ✓ EVMX config set (FastSwitchboard) +6. ✓ Sufficient fees provided (native flow) +7. ✓ Sponsor approval exists (sponsored flow) + +--- + +## 8. Event Emission Order + +### Successful Execution + +``` +1. ExecutionSuccess(payloadId, exceededMaxCopy, returnData) +2. [NetworkFeeCollector events - if configured] +``` + +### Failed Execution + +``` +1. ExecutionFailed(payloadId, exceededMaxCopy, returnData) +``` + +### Payload Sending + +``` +1. MessageOutbound (MessageSwitchboard only) +2. PayloadRequested +``` + +### Attestation + +``` +1. Attested(digest/payloadId, watcher) +``` diff --git a/auditor-docs/README.md b/auditor-docs/README.md new file mode 100644 index 00000000..8b14882c --- /dev/null +++ b/auditor-docs/README.md @@ -0,0 +1,597 @@ +# Socket Protocol - Auditor Documentation + +Welcome to the Socket Protocol auditor documentation package. This collection of documents provides comprehensive information about the protocol architecture, security model, and testing coverage to facilitate thorough security audits. + +**Status**: ✅ Audit-Ready | Pre-audit review complete with improvements implemented + +--- + +## ⚡ Quick Links + +- **NEW**: [Audit Prep Summary](./AUDIT_PREP_SUMMARY.md) - Review findings & improvements made +- **START HERE**: [System Overview](./SYSTEM_OVERVIEW.md) - Protocol architecture & assumptions +- **FOCUS**: [Audit Focus Areas](./AUDIT_FOCUS_AREAS.md) - Priority areas for review + +--- + +## 📚 Documentation Index + +### 0. [AUDIT_PREP_SUMMARY.md](./AUDIT_PREP_SUMMARY.md) - **NEW** + +**Pre-audit review results** and improvements made. + +**Contents**: + +- Validated security patterns (CEI, replay protection) +- Nonce namespace isolation improvement implemented +- Issues analyzed and dismissed with rationale +- System assumptions critical for audit context +- Code changes summary +- Audit readiness checklist + +**Read this if**: You want to understand what was already reviewed and improved. + +### 1. [SYSTEM_OVERVIEW.md](./SYSTEM_OVERVIEW.md) + +**Start here** for a high-level understanding of the protocol. + +**Contents**: + +- Protocol purpose and value proposition +- High-level architecture diagram +- Core components (Socket, Switchboards, Plugs, Watchers) +- Key design decisions and rationale +- Trust model and security assumptions +- Scope boundaries (what's in/out of audit) + +**Read this if**: You're new to the protocol and need a conceptual overview. + +--- + +### 2. [CONTRACTS_REFERENCE.md](./CONTRACTS_REFERENCE.md) + +**Complete reference** for all contracts in scope. + +**Contents**: + +- Contract inventory table with LOC and purpose +- Detailed descriptions of each contract +- Key state variables and functions +- Access control roles and permissions +- Contract interaction flows +- Important data structures + +**Read this if**: You need technical details about specific contracts. + +--- + +### 3. [MESSAGE_FLOW.md](./MESSAGE_FLOW.md) + +**Step-by-step flows** through the system. + +**Contents**: + +- Outbound flow (sending payloads) +- Inbound flow (executing payloads) +- Attestation flows per switchboard type +- Fee management flows +- Configuration flows +- State transition diagrams +- Critical checkpoints + +**Read this if**: You want to trace execution paths and understand state changes. + +--- + +### 4. [SECURITY_MODEL.md](./SECURITY_MODEL.md) + +**Security properties and assumptions**. + +**Contents**: + +- Trusted vs untrusted entities +- Access control matrix +- Critical invariants that must hold +- Attack surface analysis +- External call points and value transfers +- Signature verification mechanisms +- Known limitations and tradeoffs + +**Read this if**: You're focusing on security analysis and threat modeling. + +--- + +### 5. [AUDIT_FOCUS_AREAS.md](./AUDIT_FOCUS_AREAS.md) + +**Priority areas for audit attention**. + +**Contents**: + +- Critical functions ranked by priority +- Value flow points (all ETH transfers) +- Cross-contract interaction risks +- Signature verification checks +- Replay protection mechanisms +- Gas handling edge cases +- Suggested testing scenarios +- Security properties to verify + +**Read this if**: You want to know where to focus your audit efforts. + +--- + +### 6. [SETUP_GUIDE.md](./SETUP_GUIDE.md) + +**Get the codebase running**. + +**Contents**: + +- Environment setup (Node.js, Foundry) +- Build and compile instructions +- Running tests (Foundry and Hardhat) +- Static analysis tools setup +- Deployment instructions (testnet) +- Verification procedures +- Debugging commands and techniques + +**Read this if**: You need to set up the development environment. + +--- + +### 7. [TESTING_COVERAGE.md](./TESTING_COVERAGE.md) + +**Existing tests and coverage gaps**. + +**Contents**: + +- Current test organization +- Existing test coverage summary +- Coverage metrics by contract +- Suggested additional test scenarios +- Invariant properties to test +- Fuzzing strategies +- Testing gaps and auditor action items + +**Read this if**: You want to understand what's already tested and what needs more coverage. + +--- + +### 8. [FAQ.md](./FAQ.md) + +**Answers to common questions**. + +**Contents**: + +- Architecture and design rationale +- Security and trust questions +- Operations and behavior clarifications +- Fee and economic model explanations +- Technical implementation details +- Edge cases and scenarios +- Open questions for auditors + +**Read this if**: You have specific questions about design choices or behavior. + +--- + +## 🎯 Quick Start Guide + +### For First-Time Auditors + +**Step 1**: Read [SYSTEM_OVERVIEW.md](./SYSTEM_OVERVIEW.md) + +- Understand the big picture +- Learn the key components +- Grasp the trust model + +**Step 2**: Skim [CONTRACTS_REFERENCE.md](./CONTRACTS_REFERENCE.md) + +- Get familiar with contract names and purposes +- Note the contract interaction patterns + +**Step 3**: Follow [SETUP_GUIDE.md](./SETUP_GUIDE.md) + +- Set up your environment +- Compile the contracts +- Run the test suite + +**Step 4**: Dive into [AUDIT_FOCUS_AREAS.md](./AUDIT_FOCUS_AREAS.md) + +- Start with Priority 1 functions +- Check value flow points +- Verify signature mechanisms + +**Step 5**: Trace flows using [MESSAGE_FLOW.md](./MESSAGE_FLOW.md) + +- Follow an execution from start to finish +- Understand state changes at each step + +**Step 6**: Review [SECURITY_MODEL.md](./SECURITY_MODEL.md) + +- Verify invariants hold +- Check attack surface areas +- Validate access controls + +**Step 7**: Reference [TESTING_COVERAGE.md](./TESTING_COVERAGE.md) & [FAQ.md](./FAQ.md) as needed + +- Check what's already tested +- Find answers to specific questions + +--- + +## 📊 Audit Scope Summary + +### Contracts In Scope (8 files) + +| Contract | LOC | Complexity | Priority | +| ---------------------- | --- | ---------- | ------------- | +| Socket.sol | 286 | High | P0 - Critical | +| SocketUtils.sol | 210 | Medium | P0 - Critical | +| MessageSwitchboard.sol | 740 | High | P0 - Critical | +| FastSwitchboard.sol | 244 | Medium | P1 - High | +| SocketConfig.sol | 203 | Medium | P1 - High | +| SwitchboardBase.sol | 115 | Low | P2 - Medium | +| IdUtils.sol | 75 | Low | P2 - Medium | +| OverrideParamsLib.sol | 148 | Low | P3 - Low | + +**Total Lines of Code**: ~2,000 LOC + +--- + +### Key Areas of Focus + +🔴 **Critical** (Must Review): + +- Socket.execute() - Main execution entry point +- Socket.\_execute() - External call to plugs +- Digest creation and verification +- Replay protection mechanisms +- Value transfers and fee handling + +🟠 **High** (Should Review): + +- Switchboard attestation flows +- Fee increase and refund logic +- Nonce management +- Gas limit validation +- Configuration management + +🟡 **Medium** (Nice to Review): + +- Payload ID encoding/decoding +- Parameter builder utilities +- Event emissions +- Helper functions + +--- + +## 🔍 System Assumptions (Critical Context) + +These assumptions are fundamental to the protocol's security model: + +### Trust Model + +1. **Switchboards are Trusted by Plugs** + + - Anyone can register, plugs choose whom to trust + - Plug's responsibility to verify switchboard before connecting + +2. **NetworkFeeCollector is Trusted by Socket** + + - Set by governance, called after successful execution + - No reentrancy concerns (trusted entity) + +3. **Target Plugs are Trusted by Source Plugs** + + - Source specifies destination plug + - Invalid target only affects the configuring plug + +4. **simulate() is Off-Chain Only** + + - Gated by OFF_CHAIN_CALLER (0xDEAD) + - Used for gas estimation, not accessible on mainnet + +5. **Watchers Act Honestly** + + - At least one honest watcher assumed per payload + - Verify source chain correctly, respect finality + +6. **Transmitters are Rational Economic Actors** + - Should simulate before executing + - External reimbursement for failed deliveries + - Market-based reputation systems + +### Design Tradeoffs + +1. **Payload Execution is One-Time Only** + + - No retry mechanism for failed payloads + - Simplicity & security over retry complexity + - Application layer can send new payloads if needed + +2. **No Built-in Ordering Enforcement** + + - Payloads can execute in any order + - Asynchronous cross-chain messaging by nature + - Applications must handle out-of-order delivery + +3. **No Maximum Gas Limit** + + - Supports diverse chains (Ethereum: 30M, Mantle: 4B) + - Flexibility over restrictive limits + - Natural failure if insufficient gas provided + +4. **Full Refund on Failed Execution** + - Transmitters should simulate first + - External reimbursement model + - User-friendly over transmitter protection + +### Security Patterns + +1. **CEI (Checks-Effects-Interactions)** + + - State updated before external calls + - Reentrancy allowed but safe + +2. **Multi-Layer Replay Protection** + + - executionStatus prevents double execution + - isAttested prevents double attestation + - Namespace-isolated nonces prevent cross-function replay + +3. **Length-Prefixed Digest Creation** + - Prevents collision attacks + - Deterministic parameter binding + +### Out of Scope + +- ❌ Off-chain watcher infrastructure +- ❌ Off-chain transmitter services +- ❌ EVMX chain implementation +- ❌ Frontend/API layers +- ❌ Specific plug implementations + +--- + +## 🛠 Tools & Resources + +### Recommended Tools + +**Static Analysis**: + +- Slither +- Mythril +- Aderyn + +**Dynamic Analysis**: + +- Foundry (fuzzing & invariant testing) +- Echidna +- Manticore + +**Gas Analysis**: + +- Foundry gas reports +- Hardhat gas reporter + +### External Dependencies + +**Solady Library** (`lib/solady/`): + +- Gas-optimized implementations +- Widely used and audited +- Key modules: LibCall, ECDSA, SafeTransferLib, ReentrancyGuard + +**Forge Standard Library** (`lib/forge-std/`): + +- Testing utilities only +- Not deployed on-chain + +--- + +## 📞 Communication + +### Questions During Audit + +**Technical Questions**: + +1. Check [FAQ.md](./FAQ.md) first +2. Review relevant documentation sections +3. Open issue with [AUDIT-QUESTION] tag + +**Clarifications Needed**: + +1. Consult [CONTRACTS_REFERENCE.md](./CONTRACTS_REFERENCE.md) +2. Review [MESSAGE_FLOW.md](./MESSAGE_FLOW.md) +3. Request clarification via designated channel + +**Security Concerns**: + +1. Note in your audit report +2. Verify with [SECURITY_MODEL.md](./SECURITY_MODEL.md) +3. Discuss in secure audit channel + +### Feedback Welcome + +We appreciate feedback on: + +- Documentation clarity and completeness +- Missing information or unclear explanations +- Suggested improvements to the protocol +- Additional test scenarios to cover + +--- + +## 📝 Document Conventions + +### Terminology + +- **Socket**: Core contract on each chain +- **Switchboard**: Verification contract (pluggable) +- **Plug**: User application contract +- **Watcher**: Off-chain node that attests payloads +- **Transmitter**: Off-chain service that delivers payloads +- **Payload**: Cross-chain message to be executed +- **Digest**: Hash of all execution parameters +- **Attestation**: Watcher signature approving a payload + +### Notation + +- ✅ Implemented and working +- ⚠️ Note/Warning +- ❌ Not implemented or out of scope +- 🔴 Critical priority +- 🟠 High priority +- 🟡 Medium priority +- 🟢 Low priority + +--- + +## 📅 Audit Timeline + +**Suggested Schedule**: + +- **Week 1**: Setup, overview, and architecture review + + - Days 1-2: Environment setup and documentation review + - Days 3-5: High-level architecture and flow tracing + +- **Week 2**: Deep dive into critical functions + + - Days 1-2: Socket.sol and SocketUtils.sol + - Days 3-4: MessageSwitchboard.sol + - Day 5: FastSwitchboard.sol + +- **Week 3**: Security analysis and testing + + - Days 1-2: Attack surface analysis + - Days 3-4: Writing additional tests + - Day 5: Fuzzing and invariant testing + +- **Week 4**: Report writing and review + - Days 1-3: Compile findings + - Days 4-5: Review and deliver report + +--- + +## 🎓 Learning Path + +### For Auditors New to Cross-Chain Protocols + +**Day 1**: Conceptual Understanding + +- Read SYSTEM_OVERVIEW.md thoroughly +- Understand the problem Socket solves +- Learn about switchboard architecture + +**Day 2**: Contract Familiarity + +- Skim all contracts in CONTRACTS_REFERENCE.md +- Draw your own architecture diagram +- Identify entry and exit points + +**Day 3**: Flow Tracing + +- Pick one successful execution path +- Trace it step-by-step using MESSAGE_FLOW.md +- Note all state changes + +**Day 4**: Security Focus + +- Read SECURITY_MODEL.md +- List all trust assumptions +- Identify attack vectors + +**Day 5**: Hands-On + +- Set up environment per SETUP_GUIDE.md +- Run tests +- Try breaking things + +**Ongoing**: Reference FAQ.md and AUDIT_FOCUS_AREAS.md as needed + +--- + +## 📌 Quick Reference + +### Key Addresses (Example - Update for Actual Deployment) + +**Ethereum Sepolia**: + +- Socket: `0x...` +- MessageSwitchboard: `0x...` +- FastSwitchboard: `0x...` + +**Arbitrum Sepolia**: + +- Socket: `0x...` +- MessageSwitchboard: `0x...` + +### Key Parameters + +- **Solidity Version**: 0.8.28 +- **Default Gas Limit Buffer**: 105 (5% overhead) +- **Default Max Copy Bytes**: 2048 (2KB) +- **Default Deadline**: 1 day +- **Payload Size Limit**: 24,500 bytes + +### Roles + +- **Owner**: Full control over contract +- **GOVERNANCE_ROLE**: Enable switchboards, set parameters +- **WATCHER_ROLE**: Attest payloads +- **PAUSER_ROLE**: Emergency pause +- **UNPAUSER_ROLE**: Remove pause +- **RESCUE_ROLE**: Recover stuck funds +- **FEE_UPDATER_ROLE**: Update fee parameters + +--- + +## 📖 Additional Resources + +### In Repository + +- `PAYLOAD_ID_ARCHITECTURE.md`: Detailed payload ID structure explanation +- `contracts/utils/common/Structs.sol`: All data structure definitions +- `contracts/utils/common/Errors.sol`: All error definitions +- `contracts/utils/common/Constants.sol`: Protocol constants + +### External + +- **Solidity Documentation**: https://docs.soliditylang.org/ +- **Foundry Book**: https://book.getfoundry.sh/ +- **Solady Repository**: https://github.com/Vectorized/solady +- **Smart Contract Security Best Practices**: https://consensys.github.io/smart-contract-best-practices/ + +--- + +## ✅ Pre-Audit Checklist + +Before starting the audit, ensure: + +- [ ] All 8 documentation files reviewed +- [ ] Development environment set up +- [ ] Contracts compiled successfully +- [ ] Test suite runs without errors +- [ ] Static analysis tools installed +- [ ] Communication channel established +- [ ] Audit timeline agreed upon +- [ ] Scope confirmed and documented + +--- + +## 🙏 Thank You + +Thank you for taking the time to audit Socket Protocol. Your expertise helps ensure the security and reliability of our cross-chain infrastructure. We value your thoroughness and look forward to your insights. + +If you need any clarification or additional information, please don't hesitate to reach out. + +**Happy Auditing! 🔍** + +--- + +**Documentation Version**: 1.0 +**Last Updated**: [Date] +**Protocol Version**: [Version] +**Audit Firm**: [Firm Name] +**Point of Contact**: [Name/Email] diff --git a/auditor-docs/SECURITY_MODEL.md b/auditor-docs/SECURITY_MODEL.md new file mode 100644 index 00000000..df582d36 --- /dev/null +++ b/auditor-docs/SECURITY_MODEL.md @@ -0,0 +1,538 @@ +# Security Model + +## Trust Assumptions + +### Trusted Entities + +#### 1. **Governance** + +**Trust Level**: High + +**Capabilities**: + +- Enable/re-enable switchboards via `enableSwitchboard()` +- Set network fee collector address +- Set gas limit buffer (minimum 100%) +- Set max copy bytes limit +- Grant/revoke roles to other addresses + +**Cannot Do**: + +- Directly execute payloads +- Access user funds +- Modify past execution status +- Change immutable configuration (chainSlug) + +**Assumption**: Acts in protocol's best interest, does not collude with attackers + +--- + +#### 2. **Watchers** + +**Trust Level**: High (Critical for security) + +**Capabilities**: + +- Attest to payload digests via `attest()` +- Mark payloads as refund eligible +- Sign off-chain for fee updates (FEE_UPDATER_ROLE) + +**Cannot Do**: + +- Execute payloads directly +- Withdraw fees +- Modify switchboard configuration +- Change payload content after attestation + +**Assumption**: + +- At least one honest watcher per payload +- Watchers verify source chain state correctly +- Watchers respect finality before attesting +- Will not attest to invalid payloads + +**Attack Vector if Compromised**: + +- Could attest to malicious payloads +- Could refuse to attest legitimate payloads (liveness failure) + +--- + +#### 3. **Switchboard Owners** + +**Trust Level**: Medium-High + +**Capabilities**: + +- Configure EVMX settings (FastSwitchboard) +- Set default deadlines +- Mark payloads as reverting +- Grant WATCHER_ROLE to addresses + +**Cannot Do**: + +- Modify payload content +- Access fees directly +- Override Socket validation + +**Assumption**: Configure switchboards correctly and maintain watcher set integrity + +--- + +#### 4. **Socket Owner (Initial)** + +**Trust Level**: High (Initial deployment only) + +**Capabilities**: + +- Deploy contracts with correct parameters +- Set initial role holders +- Transfer ownership to governance + +**Assumption**: Deploys with correct chainSlug and initial configuration + +--- + +### Untrusted Entities + +#### 1. **Plugs (Application Contracts)** + +**Trust Level**: None (Fully adversarial) + +**Behavior**: + +- May be malicious or buggy +- Can attempt reentrancy +- Can revert on execution +- Can consume all provided gas +- Can emit misleading events + +**Protections**: + +- Isolated execution environment +- Gas limits enforced +- Execution status prevents replay +- Return data limited to maxCopyBytes +- Reentrancy guard on Socket (recommended) + +--- + +#### 2. **Transmitters** + +**Trust Level**: Low (Economic actors) + +**Behavior**: + +- Rational economic actors seeking fees +- May try to extract MEV +- May deliver payloads in any order +- May delay delivery + +**Protections**: + +- Cannot forge attestations (requires watcher signature) +- Cannot modify payload content (digest verification) +- Deadlines prevent indefinite delays +- Optional transmitter signature for accountability + +**Note**: Transmitters cannot steal funds or bypass verification + +--- + +#### 3. **Fee Payers** + +**Trust Level**: None + +**Behavior**: + +- May underpay fees +- May try to DOS system with spam +- May attempt double-spending + +**Protections**: + +- Minimum fee requirements enforced +- Insufficient fees cause revert +- No refund on successful execution + +--- + +#### 4. **Sponsor Accounts** + +**Trust Level**: None (User-controlled) + +**Behavior**: + +- May approve malicious plugs +- May revoke approvals mid-flight + +**Protections**: + +- Explicit approval required via `approvePlug()` +- Only affects sponsored payloads +- Cannot affect native fee payloads + +--- + +## Access Control Matrix + +| Function | Contract | Roles Required | Restriction | +| -------------------------- | ------------------------ | ------------------------------- | -------------------------- | +| `execute()` | Socket | None | Not paused, valid params | +| `sendPayload()` | Socket | None | Not paused, connected plug | +| `connect()` | Socket | None (msg.sender = plug) | Valid switchboard | +| `disconnect()` | Socket | None (msg.sender = plug) | Currently connected | +| `registerSwitchboard()` | Socket | None (msg.sender = switchboard) | Not already registered | +| `disableSwitchboard()` | SocketConfig | SWITCHBOARD_DISABLER_ROLE | - | +| `enableSwitchboard()` | SocketConfig | GOVERNANCE_ROLE | - | +| `setNetworkFeeCollector()` | SocketConfig | GOVERNANCE_ROLE | - | +| `setGasLimitBuffer()` | SocketConfig | GOVERNANCE_ROLE | >= 100 | +| `setMaxCopyBytes()` | SocketConfig | GOVERNANCE_ROLE | - | +| `pause()` | SocketUtils | PAUSER_ROLE | - | +| `unpause()` | SocketUtils | UNPAUSER_ROLE | - | +| `rescueFunds()` | SocketUtils/Switchboards | RESCUE_ROLE | - | +| `attest()` | Switchboards | WATCHER_ROLE | Valid signature | +| `markRefundEligible()` | MessageSwitchboard | WATCHER_ROLE | Valid signature + nonce | +| `refund()` | MessageSwitchboard | None | Must be eligible | +| `setMinMsgValueFees()` | MessageSwitchboard | FEE_UPDATER_ROLE | Valid signature + nonce | +| `setEvmxConfig()` | FastSwitchboard | onlyOwner | - | +| `setRevertingPayload()` | Switchboards | onlyOwner | - | + +--- + +## Critical Invariants + +These properties must ALWAYS hold true: + +### 1. Execution Uniqueness + +``` +∀ payloadId: executionStatus[payloadId] ∈ {NotExecuted, Executed, Reverted} +``` + +Once set to Executed or Reverted, status cannot change. + +**Consequence**: No payload can be executed twice. + +--- + +### 2. Digest Immutability + +``` +∀ payloadId: payloadIdToDigest[payloadId] is write-once +``` + +Once digest is stored, it cannot be modified. + +**Consequence**: Execution parameters cannot be changed after verification. + +--- + +### 3. Attestation Permanence + +``` +∀ digest: isAttested[digest] = true ⟹ always true +``` + +Attestations cannot be revoked. + +**Consequence**: Attested payloads remain attested forever. + +--- + +### 4. Switchboard ID Uniqueness + +``` +∀ address A: switchboardAddressToId[A] assigned once and never changes +∀ id: switchboardAddresses[id] assigned once and never changes +``` + +**Consequence**: Switchboard identity is permanent. + +--- + +### 5. Monotonic Counters + +``` +payloadCounter only increases (never decreases or resets) +switchboardIdCounter only increases +``` + +**Consequence**: Payload IDs and switchboard IDs are globally unique. + +--- + +### 6. Fee Conservation (Native) + +``` +payloadFees[id].nativeFees can only: +- Increase via increaseFeesForPayload() +- Decrease to 0 via refund() +``` + +**Consequence**: Fees cannot disappear or be stolen. + +--- + +### 7. Refund Single-Use + +``` +payloadFees[id].isRefunded = true ⟹ payloadFees[id].nativeFees = 0 +``` + +**Consequence**: Refunds can only happen once. + +--- + +### 8. Execution Value Constraint + +``` +At execute(): msg.value >= executionParams.value + transmissionParams.socketFees +``` + +**Consequence**: Sufficient funds always provided for execution and fees. + +--- + +### 9. Payload ID Routing + +``` +∀ payload executed on chainSlug C via switchboard S: + getVerificationInfo(payloadId) = (C, S.switchboardId) +``` + +**Consequence**: Payloads only execute on intended chain with intended switchboard. + +--- + +### 10. Source Validation + +``` +∀ payload with source S executing on target T: + switchboard.allowPayload() validates S matches expected source for T +``` + +**Consequence**: Only authorized sources can call specific targets. + +--- + +## Attack Surface Analysis + +### 1. External Call Points (High Risk) + +| Location | Called Contract | Protection | +| ------------------------------------ | ------------------------------ | ---------------------------------------------- | +| Socket.\_execute() | Plug (target) | Gas limit, tryCall, execution status set first | +| Socket.\_handleSuccessfulExecution() | NetworkFeeCollector | After execution status set | +| Socket.\_sendPayload() | Plug.overrides() | View function, no state change | +| Socket.\_verify() | Switchboard (allowPayload) | Before execution, read-only | +| SocketConfig.connect() | Switchboard.updatePlugConfig() | Plug is msg.sender | +| MessageSwitchboard.refund() | refundAddress | ReentrancyGuard, state updated first | + +**Key Risk**: Reentrancy through plug execution + +--- + +### 2. Value Transfer Points (High Risk) + +| Location | Recipient | Amount | Condition | +| ------------------------------------ | ------------------------ | --------------------- | -------------------------- | +| Socket.\_execute() | Plug | executionParams.value | During execution | +| Socket.\_handleSuccessfulExecution() | NetworkFeeCollector | socketFees | After successful execution | +| Socket.\_handleFailedExecution() | refundAddress/msg.sender | msg.value | On execution failure | +| MessageSwitchboard.refund() | fees.refundAddress | nativeFees | When refund eligible | + +**Key Risk**: Incorrect refund logic or missing reentrancy protection + +--- + +### 3. Signature Verification Points (Critical) + +| Location | Signer Role | Digest Components | +| --------------------------------------- | ---------------------- | ------------------------------------------- | +| SwitchboardBase.getTransmitter() | Transmitter (optional) | socket address + payloadId | +| FastSwitchboard.attest() | WATCHER_ROLE | switchboard address + chainSlug + digest | +| MessageSwitchboard.attest() | WATCHER_ROLE | switchboard address + chainSlug + digest | +| MessageSwitchboard.markRefundEligible() | WATCHER_ROLE | switchboard + chainSlug + payloadId + nonce | +| MessageSwitchboard.setMinMsgValueFees() | FEE_UPDATER_ROLE | switchboard + chainSlug + params + nonce | + +**Key Risk**: Signature replay, malleability, or missing components in digest + +--- + +### 4. State Modification Points + +#### High Impact State Changes + +- `executionStatus[payloadId]`: Replay protection +- `payloadIdToDigest[payloadId]`: Parameter binding +- `isAttested[digest]`: Authorization +- `payloadFees[payloadId]`: Fee tracking +- `usedNonces[signer][nonce]`: Replay protection + +#### Configuration Changes + +- `switchboardStatus[id]`: Can disable verification path +- `plugSwitchboardIds[plug]`: Routes plug to switchboard +- `siblingPlugs[chain][plug]`: Controls source validation + +--- + +### 5. Arithmetic Operations + +| Location | Operation | Overflow Risk | +| ----------------------------------------- | -------------------------------- | ------------------------- | +| Socket.\_execute() | gasLimit \* gasLimitBuffer / 100 | Medium (large gasLimit) | +| MessageSwitchboard.\_increaseNativeFees() | nativeFees += msg.value | Low (Solidity 0.8+) | +| MessageSwitchboard.processPayload() | minFees + value | Low (Solidity 0.8+) | +| IdUtils.createPayloadId() | Bit shifting | None (explicit positions) | +| Payload counters | counter++ | Low (uint64 sufficient) | + +**Key Risk**: Gas limit arithmetic with extreme values + +--- + +### 6. Nonce Management + +| Function | Nonce Space | Collision Risk | +| ------------------------- | ----------------------------- | ------------------------ | +| markRefundEligible() | usedNonces[watcher][nonce] | Cross-function collision | +| setMinMsgValueFees() | usedNonces[feeUpdater][nonce] | Cross-function collision | +| setMinMsgValueFeesBatch() | usedNonces[feeUpdater][nonce] | Cross-function collision | + +**Key Risk**: Same nonce mapping shared across different function types + +--- + +## Known Limitations + +### 1. Execution Ordering + +- Payloads can be executed in any order +- `prevBatchDigestHash` exists but not enforced on-chain +- Applications must handle out-of-order execution + +### 2. Deadline Granularity + +- Deadlines use block.timestamp (manipulable by ±15 seconds) +- Not suitable for time-critical applications requiring second-level precision + +### 3. Gas Estimation + +- Actual gas usage may vary from estimated gasLimit +- gasLimitBuffer provides cushion but not guaranteed + +### 4. Return Data Limitation + +- Return data limited to maxCopyBytes (default 2048) +- Large return data truncated with exceededMaxCopy flag + +### 5. Finality Assumptions + +- Protocol assumes source chain finality before attestation +- Reorg on source chain could invalidate attested payloads +- Watchers responsible for respecting finality + +### 6. Switchboard Trust + +- Socket trusts switchboard's allowPayload() decision +- Malicious switchboard could authorize invalid payloads +- Users must verify switchboard implementation before connecting + +### 7. No Built-in Rate Limiting + +- No on-chain rate limits for payload submission +- Spam protection relies on fees and gas costs + +### 8. Single Switchboard Per Plug + +- Each plug connects to exactly one switchboard +- Cannot use multiple switchboards simultaneously +- Must disconnect and reconnect to switch + +--- + +## Security Assumptions Summary + +### Must Hold for Security + +1. ✓ At least one honest watcher per payload +2. ✓ Watchers respect source chain finality +3. ✓ Switchboard verification logic is correct +4. ✓ Governance does not act maliciously +5. ✓ External contracts (Solady) are secure + +### Design Tradeoffs + +- **Flexibility vs. Complexity**: Multiple switchboard types increase attack surface +- **Speed vs. Security**: FastSwitchboard trades off for speed +- **Decentralization vs. Performance**: Watcher set must be managed + +### Responsibility Boundaries + +- **Protocol**: Routing, replay protection, digest verification +- **Switchboards**: Attestation verification, source validation +- **Plugs**: Application logic, parameter construction +- **Watchers**: Source chain monitoring, honest attestation +- **Governance**: Emergency response, parameter tuning + +--- + +## Emergency Response Capabilities + +### Immediate (PAUSER_ROLE) + +- Pause Socket: Stops all `execute()` and `sendPayload()` operations +- Existing in-flight payloads not affected + +### Fast (SWITCHBOARD_DISABLER_ROLE) + +- Disable specific switchboard: Prevents new connections +- Existing connections remain but can be individually disconnected by plugs + +### Governance (GOVERNANCE_ROLE) + +- Re-enable disabled switchboards +- Update fee collector (including setting to address(0) to disable) +- Adjust gas parameters + +### Fund Recovery (RESCUE_ROLE) + +- Recover accidentally sent tokens/ETH +- Cannot access user funds in proper flow + +### No Emergency Stop For + +- Cannot cancel already executed payloads +- Cannot revoke attestations +- Cannot modify past execution status +- Cannot force refunds + +--- + +## Threat Model Summary + +### In Scope Threats + +- ✓ Malicious plugs attempting reentrancy +- ✓ Replay attacks on payloads +- ✓ Signature replay attacks +- ✓ Parameter manipulation after attestation +- ✓ Fee manipulation or theft +- ✓ DOS through gas exhaustion +- ✓ Cross-chain routing attacks +- ✓ Nonce exhaustion attacks + +### Out of Scope (Trusted Components) + +- Watcher infrastructure security +- Off-chain monitoring systems +- EVMX chain implementation +- Source chain consensus attacks +- Network-level DOS attacks + +### Partially In Scope + +- Economic attacks (fee griefing) - mitigated by design +- Front-running - limited impact due to commit-reveal via attestation +- MEV extraction - not prevented but contained diff --git a/auditor-docs/SETUP_GUIDE.md b/auditor-docs/SETUP_GUIDE.md new file mode 100644 index 00000000..5034906a --- /dev/null +++ b/auditor-docs/SETUP_GUIDE.md @@ -0,0 +1,629 @@ +# Setup Guide for Auditors + +## Environment Setup + +### Prerequisites + +**Required Software**: + +- Node.js >= 18.x +- Yarn or npm +- Foundry (for Solidity testing) +- Git + +**Installation Commands**: + +```bash +# Install Node.js (if not installed) +# Visit: https://nodejs.org/ + +# Install Foundry +curl -L https://foundry.paradigm.xyz | bash +foundryup + +# Verify installations +node --version +forge --version +cast --version +``` + +--- + +### Repository Setup + +**Clone and Install**: + +```bash +# Clone repository +git clone +cd socket-protocol + +# Install dependencies +yarn install +# or +npm install + +# Install Foundry dependencies +forge install +``` + +**Project Structure**: + +``` +socket-protocol/ +├── contracts/ +│ ├── protocol/ # Core Socket contracts +│ │ ├── Socket.sol +│ │ ├── SocketUtils.sol +│ │ ├── SocketConfig.sol +│ │ └── switchboard/ # Switchboard implementations +│ ├── utils/ # Utility contracts and libraries +│ └── evmx/ # EVMX-related contracts (optional) +├── test/ # Foundry tests +├── hardhat-scripts/ # Deployment and utility scripts +├── lib/ # Dependencies (forge-std, solady) +├── foundry.toml # Foundry configuration +├── hardhat.config.ts # Hardhat configuration +└── package.json +``` + +--- + +## Build & Compile + +### Using Foundry + +**Compile Contracts**: + +```bash +# Clean previous build +forge clean + +# Compile all contracts +forge build + +# Compile with specific compiler version +forge build --use 0.8.28 + +# Show warnings +forge build --force +``` + +**Compilation Output**: + +- Artifacts in: `out/` +- Build info in: `artifacts/build-info/` + +--- + +### Using Hardhat + +**Compile Contracts**: + +```bash +# Clean and compile +npx hardhat clean +npx hardhat compile + +# Compile specific file +npx hardhat compile contracts/protocol/Socket.sol +``` + +**Compilation Output**: + +- Artifacts in: `artifacts/` +- Typechain types in: `typechain-types/` + +--- + +## Running Tests + +### Foundry Tests + +**Run All Tests**: + +```bash +# Run all tests +forge test + +# Run with verbosity (show logs) +forge test -vv + +# Run with gas reporting +forge test --gas-report + +# Run specific test file +forge test --match-path test/Socket.t.sol + +# Run specific test function +forge test --match-test testExecuteSuccess +``` + +**Test Coverage**: + +```bash +# Generate coverage report +forge coverage + +# Generate detailed HTML report +forge coverage --report lcov +genhtml lcov.info -o coverage/ + +# Open in browser +open coverage/index.html +``` + +--- + +### Hardhat Tests + +**Run Tests**: + +```bash +# Run all tests +npx hardhat test + +# Run specific test file +npx hardhat test test/socket.test.ts + +# Run with gas reporting +REPORT_GAS=true npx hardhat test +``` + +--- + +## Static Analysis + +### Slither + +**Installation**: + +```bash +pip3 install slither-analyzer +# or +pip install slither-analyzer +``` + +**Run Analysis**: + +```bash +# Analyze all contracts +slither . + +# Analyze specific contract +slither contracts/protocol/Socket.sol + +# Generate report +slither . --json slither-report.json + +# Focus on high/medium severity +slither . --exclude low,informational +``` + +--- + +### Mythril + +**Installation**: + +```bash +pip3 install mythril +# or via Docker +docker pull mythril/myth +``` + +**Run Analysis**: + +```bash +# Analyze contract +myth analyze contracts/protocol/Socket.sol + +# With specific timeout +myth analyze contracts/protocol/Socket.sol --execution-timeout 300 +``` + +--- + +## Key Configuration Files + +### foundry.toml + +```toml +[profile.default] +src = "contracts" +out = "out" +libs = ["lib"] +solc_version = "0.8.28" +optimizer = true +optimizer_runs = 200 +via_ir = false + +[profile.default.fuzz] +runs = 256 + +[profile.default.invariant] +runs = 256 +depth = 15 +``` + +**Key Settings**: + +- Solidity version: 0.8.28 +- Optimizer: Enabled with 200 runs +- Fuzz runs: 256 (can be increased for thorough testing) + +--- + +### remappings.txt + +``` +solady/=lib/solady/src/ +forge-std/=lib/forge-std/src/ +``` + +**Purpose**: Maps imports to library locations + +--- + +## Deployment (Testnet) + +### Environment Variables + +Create `.env` file: + +```bash +# RPC URLs +ETHEREUM_SEPOLIA_RPC=https://sepolia.infura.io/v3/YOUR_KEY +ARBITRUM_SEPOLIA_RPC=https://sepolia-rollup.arbitrum.io/rpc + +# Private keys (for testnet only!) +PRIVATE_KEY=your_testnet_private_key + +# Etherscan API keys (for verification) +ETHERSCAN_API_KEY=your_etherscan_key +ARBISCAN_API_KEY=your_arbiscan_key +``` + +**⚠️ Security**: Never commit `.env` file with real keys + +--- + +### Deploy Socket + +**Using Foundry Script**: + +```bash +# Deploy to testnet +forge script script/deploy/DeploySocket.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC \ + --broadcast \ + --verify + +# Deploy locally (for testing) +forge script script/deploy/DeploySocket.s.sol \ + --fork-url $ETHEREUM_SEPOLIA_RPC +``` + +--- + +### Deploy Switchboard + +**Using Foundry Script**: + +```bash +# Deploy MessageSwitchboard +forge script script/deploy/DeployMessageSwitchboard.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC \ + --broadcast + +# Deploy FastSwitchboard +forge script script/deploy/DeployFastSwitchboard.s.sol \ + --rpc-url $ETHEREUM_SEPOLIA_RPC \ + --broadcast +``` + +--- + +## Key Addresses & Configuration + +### Deployment Parameters + +**Socket Deployment**: + +- `chainSlug`: Unique chain identifier (e.g., 1 for Ethereum, 42161 for Arbitrum) +- `owner`: Initial owner address (should be multi-sig in production) + +**MessageSwitchboard Deployment**: + +- `chainSlug`: Same as Socket +- `socket`: Address of deployed Socket contract +- `owner`: Switchboard owner (can be same as Socket owner) + +**FastSwitchboard Deployment**: + +- `chainSlug`: Same as Socket +- `socket`: Address of deployed Socket contract +- `owner`: Switchboard owner + +--- + +### Post-Deployment Configuration + +**1. Register Switchboard**: + +```solidity +// Switchboard calls Socket +socket.registerSwitchboard() +// Returns switchboard ID +``` + +**2. Set EVMX Config (FastSwitchboard)**: + +```solidity +fastSwitchboard.setEvmxConfig(evmxChainSlug, watcherId) +``` + +**3. Grant Roles**: + +```solidity +// Grant WATCHER_ROLE to watcher addresses +switchboard.grantRole(WATCHER_ROLE, watcherAddress) + +// Grant GOVERNANCE_ROLE +socket.grantRole(GOVERNANCE_ROLE, governanceAddress) +``` + +**4. Set Sibling Config (MessageSwitchboard)**: + +```solidity +// Configure destination chains +messageSwitchboard.setSiblingConfig( + dstChainSlug, + dstSocketAddress, + dstSwitchboardAddress, + dstSwitchboardId +) +``` + +--- + +## Verification + +### Verify on Etherscan + +**Using Foundry**: + +```bash +forge verify-contract \ + --chain-id 11155111 \ + --constructor-args $(cast abi-encode "constructor(uint32,address)" 11155111 0xYourOwner) \ + 0xYourContractAddress \ + contracts/protocol/Socket.sol:Socket \ + YOUR_ETHERSCAN_API_KEY +``` + +**Using Hardhat**: + +```bash +npx hardhat verify \ + --network sepolia \ + --constructor-args arguments.js \ + 0xYourContractAddress +``` + +--- + +## Useful Commands + +### Foundry + +**Inspect Contract**: + +```bash +# Get contract size +forge build --sizes + +# Get function selectors +cast sig "execute((bytes4,uint256,uint256,address,uint256,bytes32,bytes32,bytes,bytes,bytes),(uint256,address,bytes,bytes))" + +# Decode transaction +cast 4byte 0x6a761202 +``` + +**Interact with Contracts**: + +```bash +# Read contract +cast call 0xSocketAddress "chainSlug()(uint32)" --rpc-url $RPC_URL + +# Write contract (send transaction) +cast send 0xSocketAddress "pause()" --private-key $PRIVATE_KEY --rpc-url $RPC_URL + +# Get logs +cast logs --address 0xSocketAddress --rpc-url $RPC_URL +``` + +--- + +### Debugging + +**Run Tests with Traces**: + +```bash +# Show execution traces +forge test -vvvv + +# Show only failing tests +forge test --fail-fast + +# Debug specific test +forge test --debug testExecuteSuccess +``` + +**Forge Debugger**: + +```bash +# Enter interactive debugger +forge test --match-test testName --debug +``` + +**Commands in debugger**: + +- `s` - step over +- `n` - step into +- `c` - continue +- `q` - quit + +--- + +## Code Navigation + +### Key Files for Audit + +**Priority 1 (Critical)**: + +1. `contracts/protocol/Socket.sol` - Main execution contract +2. `contracts/protocol/SocketUtils.sol` - Digest creation & verification +3. `contracts/protocol/switchboard/MessageSwitchboard.sol` - Full-featured switchboard +4. `contracts/protocol/switchboard/FastSwitchboard.sol` - Fast switchboard + +**Priority 2 (Important)**: 5. `contracts/protocol/SocketConfig.sol` - Configuration management 6. `contracts/protocol/switchboard/SwitchboardBase.sol` - Base functionality 7. `contracts/utils/common/IdUtils.sol` - Payload ID utilities + +**Priority 3 (Supporting)**: 8. `contracts/utils/OverrideParamsLib.sol` - Parameter builder 9. `contracts/utils/common/Structs.sol` - Data structures 10. `contracts/utils/common/Errors.sol` - Error definitions + +--- + +## External Dependencies + +### Solady Library + +**Location**: `lib/solady/` + +**Key Used Modules**: + +- `LibCall.sol` - Safe external call handling +- `ECDSA.sol` - Signature verification +- `SafeTransferLib.sol` - Safe ETH/token transfers +- `ReentrancyGuard.sol` - Reentrancy protection +- `Ownable.sol` - Ownership management + +**Audit Note**: Solady is a gas-optimized library. Review usage but assume library code is secure (widely used). + +--- + +### Forge Standard Library + +**Location**: `lib/forge-std/` + +**Usage**: Testing utilities only (not deployed) + +--- + +## Common Issues & Troubleshooting + +### Compilation Issues + +**Issue**: "Compiler version mismatch" + +```bash +# Solution: Install correct version +foundryup --version 0.8.28 +``` + +**Issue**: "Stack too deep" + +```bash +# Solution: Enable via-ir +forge build --via-ir +``` + +--- + +### Test Issues + +**Issue**: "Fuzz test failing intermittently" + +```bash +# Solution: Increase runs or set specific seed +forge test --fuzz-runs 1000 --fuzz-seed 42 +``` + +**Issue**: "Invariant test failing" + +```bash +# Solution: Check invariant properties and increase depth +forge test --invariant-runs 256 --invariant-depth 20 +``` + +--- + +### RPC Issues + +**Issue**: "Rate limited" + +```bash +# Solution: Use dedicated RPC endpoint or local node +forge test --fork-url http://localhost:8545 +``` + +**Issue**: "Chain fork failing" + +```bash +# Solution: Specify block number +forge test --fork-url $RPC_URL --fork-block-number 12345678 +``` + +--- + +## Quick Reference + +### Contract Addresses (Example Testnet) + +**Sepolia**: + +``` +Socket: 0x... (to be deployed) +MessageSwitchboard: 0x... (to be deployed) +FastSwitchboard: 0x... (to be deployed) +``` + +**Arbitrum Sepolia**: + +``` +Socket: 0x... (to be deployed) +MessageSwitchboard: 0x... (to be deployed) +``` + +--- + +### Role Addresses + +**Production Setup Recommendation**: + +- Owner: Multi-sig wallet (e.g., Gnosis Safe) +- GOVERNANCE_ROLE: DAO/Multi-sig +- WATCHER_ROLE: Off-chain watcher nodes (multiple) +- PAUSER_ROLE: Emergency responder (fast multi-sig) +- UNPAUSER_ROLE: Governance (slower, more secure) +- RESCUE_ROLE: Governance +- FEE_UPDATER_ROLE: Fee oracle service + +--- + +## Additional Resources + +**Documentation**: + +- Solidity Docs: https://docs.soliditylang.org/ +- Foundry Book: https://book.getfoundry.sh/ +- Solady Docs: https://github.com/Vectorized/solady + +**Security Resources**: + +- Smart Contract Security Best Practices: https://consensys.github.io/smart-contract-best-practices/ +- DeFi Security Tools: https://github.com/crytic/building-secure-contracts + +**Questions?** + +- Open issue in repository +- Contact: [team contact info] diff --git a/auditor-docs/SYSTEM_OVERVIEW.md b/auditor-docs/SYSTEM_OVERVIEW.md new file mode 100644 index 00000000..fc60be98 --- /dev/null +++ b/auditor-docs/SYSTEM_OVERVIEW.md @@ -0,0 +1,282 @@ +# System Overview + +## Protocol Purpose + +Socket Protocol is a cross-chain messaging infrastructure that enables secure communication and payload execution between different blockchain networks. The protocol acts as a universal message bus, allowing smart contracts (Plugs) to send arbitrary data and trigger executions on remote chains. + +## Core Value Proposition + +- **Chain Abstraction**: Developers write once, deploy anywhere +- **Flexible Verification**: Multiple switchboard implementations for different security/speed tradeoffs +- **Modular Design**: Pluggable architecture for verification mechanisms +- **Native & Sponsored Fees**: Support for both direct payment and sponsored execution models + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Chain A (Source) │ +│ │ +│ ┌──────┐ ┌────────┐ ┌─────────────┐ │ +│ │ Plug │────────>│ Socket │────────>│ Switchboard │ │ +│ └──────┘ └────────┘ └─────────────┘ │ +│ │ │ │ │ +│ │ │ │ │ +│ └───sendPayload()───┘ │ │ +│ │ │ +│ emit PayloadRequested│ +└────────────────────────────────────────────────│────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ Off-Chain Watchers │ + │ (Attestation Layer) │ + └────────────────────────┘ + │ + ▼ +┌────────────────────────────────────────────────│────────────┐ +│ Chain B (Destination) │ │ +│ │ │ +│ ┌─────────────┐ ┌────────┐ ┌──────▼───┐ │ +│ │ Switchboard │<───│ Socket │<────────│Transmitter│ │ +│ └─────────────┘ └────────┘ └──────────┘ │ +│ │ │ │ +│ │ │ │ +│ │ ┌────▼────┐ │ +│ └──verify──>│ Plug │ │ +│ └─────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Core Components + +### 1. Socket (Entry Point) + +- Central contract on each chain +- Handles payload execution (inbound) and submission (outbound) +- Manages plug connections to switchboards +- Enforces execution rules (deadlines, gas limits, replay protection) + +### 2. Switchboards (Verification Layer) + +- Verify payload authenticity through attestations +- Multiple implementations: + - **FastSwitchboard**: EVMX-based fast finality + - **MessageSwitchboard**: Watcher-based with fee management +- Register with Socket and get unique IDs +- Maintain plug configurations and routing information + +### 3. Plugs (Application Layer) + +- User-deployed smart contracts +- Connect to Socket via specific switchboard +- Implement application logic for cross-chain interactions +- Call `socket.sendPayload()` to initiate cross-chain messages + +### 4. Watchers (Off-Chain) + +- Monitor source chain for payload requests +- Attest to payloads on destination chain +- Sign attestations for switchboard verification +- NOT in audit scope (off-chain infrastructure) + +### 5. Transmitters (Off-Chain) + +- Deliver payloads to destination chain +- Call `socket.execute()` with execution parameters +- Optionally sign for additional verification +- NOT in audit scope (off-chain infrastructure) + +## Trust Model & Assumptions + +### System Assumptions + +1. **Switchboards are trusted by Plugs/Apps** + + - Anyone can register as a switchboard + - Plugs only connect to switchboards they trust + - Users verify switchboard implementation before connecting + +2. **NetworkFeeCollector is trusted by Socket** + + - Socket calls networkFeeCollector after successful execution + - No reentrancy concerns as collector is trusted + +3. **Target Plugs are trusted by Source Plugs** + + - Source plugs specify and trust their sibling plugs on destination chains + - Invalid target plugs only affect the plug that configured them + +4. **simulate() function is for off-chain use only** + + - Gated by OFF_CHAIN_CALLER address (0xDEAD) + - Only used by off-chain services for gas estimation + - Not accessible on mainnet + +5. **Watchers act honestly** + + - At least one honest watcher per payload + - Watchers verify source chain state correctly + - Watchers respect finality before attesting + +6. **Transmitters are rational economic actors** + - Should simulate before sending transactions + - External reimbursement for failed deliveries + - May blacklist/whitelist plugs based on behavior + +## Key Design Decisions + +### Modular Switchboard Architecture + +**Decision**: Socket delegates verification to pluggable switchboard contracts rather than implementing verification directly. + +**Rationale**: + +- Different applications need different security/speed tradeoffs +- Allows upgrading verification mechanisms without changing core Socket +- Enables competition between verification methods + +### Payload ID Structure + +**Decision**: Payload IDs encode source, verification, and counter information in a single bytes32. + +**Format**: `[Source: 64 bits][Verification: 64 bits][Counter: 64 bits][Reserved: 64 bits]` + +**Rationale**: + +- Uniquely identifies payloads across all chains +- Enables routing validation (correct source → correct destination) +- Self-describing without additional lookups + +### Two-Phase Execution + +**Decision**: Separate payload creation (source chain) from execution (destination chain). + +**Rationale**: + +- Asynchronous cross-chain messaging +- Allows off-chain attestation layer +- Enables retry mechanisms and fee adjustments + +### Digest-Based Verification + +**Decision**: All execution parameters are hashed into a digest that switchboards attest to. + +**Rationale**: + +- Single attestation covers all parameters +- Prevents parameter manipulation after attestation +- Length-prefixed encoding prevents collision attacks + +### One-Time Execution + +**Decision**: Payloads can only be executed once, even if they fail. + +**Rationale**: + +- Simplicity: No complex retry logic needed +- Determinism: Clear finality for each payload +- Security: Prevents replay attacks and complex re-execution scenarios +- Application Layer: Apps can send new payloads if needed + +### No Ordering Enforcement + +**Decision**: Payloads can execute in any order on destination chain. + +**Rationale**: + +- Cross-chain messaging is inherently asynchronous +- Different chain finality times make ordering impractical +- Transmitter competition for fees +- Application layer can handle ordering if needed + +## Scope Boundaries + +### In Scope (Smart Contracts) + +- ✅ Socket.sol - Main execution contract +- ✅ SocketUtils.sol - Utility functions +- ✅ SocketConfig.sol - Configuration management +- ✅ FastSwitchboard.sol - Fast verification implementation +- ✅ MessageSwitchboard.sol - Message-based verification +- ✅ SwitchboardBase.sol - Base switchboard functionality +- ✅ IdUtils.sol - Payload ID encoding/decoding +- ✅ OverrideParamsLib.sol - Parameter builder library + +### Out of Scope + +- ❌ Off-chain watcher infrastructure +- ❌ Off-chain transmitter infrastructure +- ❌ Frontend/API layers +- ❌ Deployment scripts +- ❌ EVMX chain implementation +- ❌ Specific plug implementations + +## Security Properties + +### Critical Invariants (Must Always Hold) + +1. ✓ Each payload executes at most once +2. ✓ Execution status transitions are one-way (cannot revert from Executed) +3. ✓ Digests are immutable once stored +4. ✓ Attestations cannot be revoked +5. ✓ Payload IDs are globally unique +6. ✓ Nonces cannot be replayed within same namespace +7. ✓ Source validation prevents unauthorized executions + +### Design Patterns Used + +- ✅ **Checks-Effects-Interactions (CEI)**: State updated before external calls +- ✅ **Replay Protection**: executionStatus prevents double execution +- ✅ **Nonce Management**: Namespace-isolated nonces prevent cross-function replay +- ✅ **Length Prefixes**: Prevent collision attacks in digest creation +- ✅ **Gas Limit Buffer**: Accounts for contract execution overhead + +## Key Metrics + +- **Total Contracts**: 8 core contracts +- **Lines of Code**: ~2,000 LOC (excluding tests) +- **Solidity Version**: 0.8.28 +- **External Dependencies**: Solady library +- **Chains Supported**: Any EVM-compatible chain + Solana (partial) + +## Integration Points + +### For Plug Developers + +1. Deploy plug contract +2. Call `socket.connect(switchboardId, config)` +3. Send payloads via `socket.sendPayload(data)` or fallback +4. Implement inbound handler for receiving executions + +### For Switchboard Developers + +1. Inherit from `SwitchboardBase` +2. Implement `allowPayload()` verification logic +3. Implement `processPayload()` for outbound handling +4. Call `socket.registerSwitchboard()` after deployment + +## Critical State Transitions + +1. **Switchboard Registration**: NOT_REGISTERED → REGISTERED +2. **Switchboard Status**: REGISTERED ↔ DISABLED +3. **Plug Connection**: disconnected → connected (with switchboardId) +4. **Payload Execution**: NotExecuted → Executed/Reverted (one-way) +5. **Attestation**: unattested → attested (one-way) + +## Economic Model + +### Fee Flows + +- **Socket Fees**: Paid to transmitters/protocol for execution +- **Native Fees**: Paid in ETH on source chain +- **Sponsored Fees**: Pre-approved spending by sponsor accounts +- **Refunds**: Eligible if payload never executed (watcher-approved) + +### Fee Management + +- Fees tracked per payload +- Can be increased before execution +- Refund mechanism for failed deliveries +- Network fee collector receives execution fees on success diff --git a/auditor-docs/TESTING_COVERAGE.md b/auditor-docs/TESTING_COVERAGE.md new file mode 100644 index 00000000..261fbe6d --- /dev/null +++ b/auditor-docs/TESTING_COVERAGE.md @@ -0,0 +1,905 @@ +# Testing Coverage + +## Current Test Status + +This document outlines the existing test coverage and suggests additional test scenarios that auditors should validate. + +--- + +## Test Organization + +### Test Structure + +``` +test/ +├── Socket.t.sol # Core Socket functionality tests +├── SocketConfig.t.sol # Configuration tests +├── MessageSwitchboard.t.sol # MessageSwitchboard tests +├── FastSwitchboard.t.sol # FastSwitchboard tests +├── Integration.t.sol # Cross-contract integration tests +└── utils/ + └── TestHelpers.sol # Shared test utilities +``` + +--- + +## Existing Test Coverage + +### Socket.sol Tests + +**execute() Function**: + +- ✅ Successful execution flow +- ✅ Deadline validation (reverts if passed) +- ✅ Call type validation (only WRITE allowed) +- ✅ Plug connection validation +- ✅ Insufficient msg.value handling +- ✅ Payload ID routing validation +- ✅ Replay protection (double execution attempt) +- ✅ Failed execution with refund +- ✅ Execution with network fee collection + +**sendPayload() Function**: + +- ✅ Basic payload submission +- ✅ Plug disconnected scenario +- ✅ Paused contract scenario +- ✅ Switchboard processPayload integration +- ✅ Fallback function alternative + +**State Management**: + +- ✅ executionStatus transitions +- ✅ payloadIdToDigest storage +- ✅ Pause/unpause functionality + +--- + +### SocketConfig.sol Tests + +**Switchboard Registration**: + +- ✅ Register new switchboard +- ✅ Duplicate registration prevention +- ✅ Counter increment verification +- ✅ Status set to REGISTERED + +**Plug Connection/Disconnection**: + +- ✅ Connect to valid switchboard +- ✅ Connect with configuration +- ✅ Connect to invalid/disabled switchboard (reverts) +- ✅ Disconnect when connected +- ✅ Disconnect when not connected (reverts) + +**Switchboard Management**: + +- ✅ Disable switchboard (authorized) +- ✅ Enable switchboard (authorized) +- ✅ Access control enforcement + +**Parameter Updates**: + +- ✅ Set gas limit buffer (valid values) +- ✅ Set max copy bytes +- ✅ Set network fee collector + +--- + +### MessageSwitchboard.sol Tests + +**processPayload()**: + +- ✅ Native fee flow (version 1) +- ✅ Sponsored fee flow (version 2) +- ✅ Sibling validation +- ✅ Insufficient fees handling +- ✅ Deadline encoding +- ✅ Digest creation +- ✅ Payload counter increment + +**Attestation**: + +- ✅ Valid watcher attestation +- ✅ Invalid watcher (no role) rejection +- ✅ Double attestation prevention +- ✅ allowPayload check with attestation +- ✅ Source validation in allowPayload + +**Fee Management**: + +- ✅ Increase native fees +- ✅ Increase sponsored fees +- ✅ Unauthorized fee increase (reverts) +- ✅ Mark refund eligible with valid signature +- ✅ Claim refund when eligible +- ✅ Refund double-claim prevention + +**Configuration**: + +- ✅ Set sibling config +- ✅ Update plug config +- ✅ Sponsor approve/revoke plug +- ✅ Set minimum fees (owner) +- ✅ Set minimum fees (signature-based) + +--- + +### FastSwitchboard.sol Tests + +**processPayload()**: + +- ✅ Basic payload creation +- ✅ EVMX config validation +- ✅ Deadline handling +- ✅ Payload ID generation +- ✅ payloadIdToPlug mapping + +**Attestation**: + +- ✅ Valid watcher attestation +- ✅ Invalid watcher rejection +- ✅ allowPayload with app gateway validation + +**Configuration**: + +- ✅ Set EVMX config +- ✅ Update plug config (app gateway) +- ✅ Set default deadline + +--- + +### Integration Tests + +**End-to-End Flows**: + +- ✅ Full outbound flow: plug → socket → switchboard → event +- ✅ Full inbound flow: execute → verify → call plug → fees +- ✅ Cross-switchboard scenarios +- ✅ Plug reconnection to different switchboard + +--- + +## Coverage Metrics + +**Overall Coverage** (estimated): + +- Line Coverage: ~85% +- Branch Coverage: ~80% +- Function Coverage: ~90% + +**High Coverage Areas**: + +- Core execution logic: >95% +- Access control: >90% +- State transitions: >90% + +**Lower Coverage Areas**: + +- Edge cases with extreme values: ~60% +- Complex error conditions: ~70% +- Rare configuration scenarios: ~65% + +--- + +## Suggested Additional Test Scenarios + +### Priority 1: Critical Path Testing + +#### Reentrancy Attack Tests + +**Test 1: Reentrant Plug During Execution** + +```solidity +// Scenario: Malicious plug calls back into Socket +contract MaliciousPlug { + function inbound(bytes memory) external payable { + // Attempt reentrancy + socket.execute(...); // Should fail + socket.sendPayload(...); // Should fail + } +} +``` + +**Expected**: All reentrant calls should fail (via reentrancy guard or state checks) + +--- + +**Test 2: Reentrant Fee Collection** + +```solidity +// Scenario: NetworkFeeCollector attempts reentrancy +contract MaliciousFeeCollector { + function collectNetworkFee(...) external payable { + // Attempt reentrancy + socket.execute(...); // Should fail + } +} +``` + +**Expected**: Reentrancy should be prevented + +--- + +**Test 3: Reentrant Refund Recipient** + +```solidity +// Scenario: Refund recipient attempts reentrancy +contract MaliciousRefundRecipient { + receive() external payable { + messageSwitchboard.refund(payloadId); // Should fail + } +} +``` + +**Expected**: ReentrancyGuard should prevent double refund + +--- + +#### Gas Limit Edge Cases + +**Test 4: Maximum Gas Limit** + +```solidity +// Execute with gasLimit = type(uint256).max +executionParams.gasLimit = type(uint256).max; +``` + +**Expected**: Should handle gracefully (revert or cap appropriately) + +--- + +**Test 5: Zero Gas Limit** + +```solidity +// Execute with gasLimit = 0 +executionParams.gasLimit = 0; +``` + +**Expected**: Should revert or handle appropriately + +--- + +**Test 6: Gas Limit Overflow in Calculation** + +```solidity +// gasLimit * gasLimitBuffer might overflow +executionParams.gasLimit = type(uint256).max / 104; // Just under overflow +``` + +**Expected**: Should not overflow, handle safely + +--- + +**Test 7: Exact Gas Boundary** + +```solidity +// Provide exactly the required gas (no buffer) +uint256 exactGas = (executionParams.gasLimit * gasLimitBuffer) / 100; +``` + +**Expected**: Should execute successfully + +--- + +#### Value Handling Tests + +**Test 8: Exact msg.value Requirement** + +```solidity +// msg.value = executionParams.value + socketFees (exact) +``` + +**Expected**: Should succeed + +--- + +**Test 9: Excess msg.value** + +```solidity +// msg.value > executionParams.value + socketFees +``` + +**Expected**: Should succeed, excess handled appropriately + +--- + +**Test 10: Failed Execution Refund Recipient** + +```solidity +// Test with refundAddress = address(0) +// Test with refundAddress = valid address +// Test with msg.sender as fallback +``` + +**Expected**: Correct recipient receives refund + +--- + +### Priority 2: Signature & Replay Protection + +#### Signature Replay Tests + +**Test 11: Cross-Chain Signature Replay** + +```solidity +// Use same signature on different chain (if multi-chain deployment) +``` + +**Expected**: Should fail due to chainSlug inclusion + +--- + +**Test 12: Cross-Function Nonce Replay** + +```solidity +// Use nonce from markRefundEligible in setMinMsgValueFees +watcher signs: markRefundEligible(payloadId, nonce=1, sig) +// Later, same watcher signs: setMinMsgValueFees(..., nonce=1, sig2) +``` + +**Expected**: Currently might fail due to shared nonce mapping (potential issue) + +--- + +**Test 13: Attestation Signature Malleability** + +```solidity +// Try (r, s) and (r, -s) signature variants +``` + +**Expected**: ECDSA library should prevent, but verify + +--- + +**Test 14: Invalid Signature Format** + +```solidity +// Provide signature with wrong length +// Provide all-zero signature +``` + +**Expected**: Should revert with appropriate error + +--- + +### Priority 3: State Consistency + +#### Execution Status Tests + +**Test 15: Concurrent Execution Attempts** + +```solidity +// Two transmitters try to execute same payloadId in same block +// (Requires forking or simulation) +``` + +**Expected**: First succeeds, second reverts with PayloadAlreadyExecuted + +--- + +**Test 16: Execute After Reverted** + +```solidity +// First execution fails (sets status to Reverted) +// Attempt second execution +``` + +**Expected**: Should revert (no retry allowed) + +--- + +**Test 17: Status Transition Validation** + +```solidity +// Verify status can only transition: +// NotExecuted → Executed +// NotExecuted → Reverted +// Never: Executed → NotExecuted +// Never: Reverted → Executed +``` + +--- + +#### Fee Accounting Tests + +**Test 18: Fee Increase Overflow** + +```solidity +// Set nativeFees to near max +payloadFees[id].nativeFees = type(uint256).max - 100; +// Try to increase by more than 100 +increaseFeesForPayload{value: 200}(...); +``` + +**Expected**: Should revert on overflow (Solidity 0.8+) + +--- + +**Test 19: Fee Accounting Conservation** + +```solidity +// Track: total ETH in contract = sum of all payloadFees +// After refund: verify conservation +// After execution: verify fees distributed correctly +``` + +**Expected**: No ETH leakage + +--- + +**Test 20: Refund Edge Cases** + +```solidity +// Refund with nativeFees = 0 (should revert) +// Refund when not eligible (should revert) +// Refund twice (should revert) +// Refund after execution (should revert) +``` + +--- + +### Priority 4: Configuration & Access Control + +#### Switchboard Management Tests + +**Test 21: Connect to Disabled Switchboard** + +```solidity +// Register switchboard +// Disable switchboard +// Plug attempts to connect +``` + +**Expected**: Should revert + +--- + +**Test 22: Execute with Disabled Switchboard** + +```solidity +// Plug connected to switchboard +// Switchboard gets disabled +// Attempt execution +``` + +**Expected**: Should revert (switchboard status checked) + +--- + +**Test 23: EOA as Switchboard** + +```solidity +// Register EOA as switchboard +// Plug connects to it +// Attempt to send payload +``` + +**Expected**: Should fail when calling switchboard functions + +--- + +#### Role Management Tests + +**Test 24: Role Escalation Attempt** + +```solidity +// Non-admin tries to grant themselves admin role +// Non-watcher tries to attest +``` + +**Expected**: Should revert with access control error + +--- + +**Test 25: Role Transfer** + +```solidity +// Transfer ownership +// Old owner can no longer perform owner actions +// New owner can perform actions +``` + +--- + +### Priority 5: Payload ID & Routing + +#### Payload ID Tests + +**Test 26: Payload ID Collision** + +```solidity +// Create many payloads, check for duplicate IDs +// With same source/dest but different counters +``` + +**Expected**: All IDs should be unique + +--- + +**Test 27: Payload ID Routing Validation** + +```solidity +// Create payload for chainA +// Attempt to execute on chainB +``` + +**Expected**: Should revert (chain slug mismatch) + +--- + +**Test 28: Counter Boundary** + +```solidity +// Set counter to near max (type(uint64).max - 1) +// Create multiple payloads +``` + +**Expected**: Should revert on overflow or handle gracefully + +--- + +#### Source Validation Tests + +**Test 29: Invalid Source Format** + +```solidity +// Provide source with wrong encoding +// Provide source with wrong length +``` + +**Expected**: Should revert during decode + +--- + +**Test 30: Source Mismatch** + +```solidity +// Payload from plugA on chainX +// Source claims plugB on chainY +``` + +**Expected**: allowPayload should return false + +--- + +### Priority 6: Integration & Cross-Contract + +#### Socket ↔ Switchboard Tests + +**Test 31: Malicious Switchboard** + +```solidity +// Switchboard always returns true for allowPayload +// Switchboard returns address(0) for getTransmitter +// Switchboard reverts on processPayload +``` + +**Expected**: System should handle gracefully + +--- + +**Test 32: Switchboard State Inconsistency** + +```solidity +// Switchboard says payload is attested +// But never actually called attest() +``` + +**Expected**: Depends on switchboard implementation trust + +--- + +#### Socket ↔ Plug Tests + +**Test 33: Plug Always Reverts** + +```solidity +// Plug.inbound() always reverts +// Multiple payloads to same plug +``` + +**Expected**: All marked as Reverted, funds refunded + +--- + +**Test 34: Plug Consumes All Gas** + +```solidity +// Plug has infinite loop or expensive operation +``` + +**Expected**: tryCall should limit gas, execution fails safely + +--- + +**Test 35: Plug Returns Large Data** + +```solidity +// Plug returns data > maxCopyBytes +``` + +**Expected**: exceededMaxCopy flag set, execution succeeds + +--- + +### Priority 7: Economic & Incentive Tests + +**Test 36: Fee Griefing** + +```solidity +// Attacker creates many payloads with minimum fee +// Clogs system or causes transmitter losses +``` + +**Expected**: Minimum fees should prevent economic attack + +--- + +**Test 37: Transmitter Competition** + +```solidity +// Multiple transmitters race to execute +// First gets reward +``` + +**Expected**: Fair competition, no funds lost + +--- + +**Test 38: Sponsor Approval Manipulation** + +```solidity +// Sponsor approves plug +// Plug creates sponsored payload +// Sponsor revokes approval mid-flight +``` + +**Expected**: Payload still executable (approval checked at creation) + +--- + +## Invariant Properties to Test + +### Critical Invariants + +**Invariant 1: Execution Uniqueness** + +```solidity +// Property: ∀ payloadId, executed at most once +function invariant_executionUniqueness() public { + // Track all executed payloadIds + // Verify no duplicates +} +``` + +--- + +**Invariant 2: Fee Conservation** + +```solidity +// Property: Total ETH in = Total ETH out + Contract Balance +function invariant_feeConservation() public { + // Sum all payloadFees.nativeFees + // Should equal contract balance +} +``` + +--- + +**Invariant 3: Refund Single-Use** + +```solidity +// Property: If isRefunded = true, then nativeFees = 0 +function invariant_refundSingleUse() public { + for each payload: + assert(!(isRefunded && nativeFees > 0)) +} +``` + +--- + +**Invariant 4: Status Monotonicity** + +```solidity +// Property: Status never regresses +function invariant_statusMonotonic() public { + // NotExecuted can → Executed or Reverted + // Executed/Reverted never change +} +``` + +--- + +**Invariant 5: Counter Monotonicity** + +```solidity +// Property: Counters only increase +function invariant_counterMonotonic() public { + // payloadCounter only increases + // switchboardIdCounter only increases +} +``` + +--- + +## Fuzzing Strategies + +### Fuzz Testing Configuration + +**Foundry Fuzzing**: + +```toml +[profile.default.fuzz] +runs = 10000 +max_test_rejects = 100000 +``` + +--- + +### Key Fuzz Targets + +**Fuzz 1: execute() Parameters** + +```solidity +function testFuzz_execute( + uint256 gasLimit, + uint256 value, + uint256 deadline, + uint256 socketFees +) public { + // Bound inputs to reasonable ranges + gasLimit = bound(gasLimit, 0, 10_000_000); + value = bound(value, 0, 100 ether); + deadline = bound(deadline, block.timestamp, block.timestamp + 365 days); + socketFees = bound(socketFees, 0, 10 ether); + + // Test execution with fuzzed params +} +``` + +--- + +**Fuzz 2: Digest Creation** + +```solidity +function testFuzz_digestCreation( + bytes calldata payload, + bytes calldata source, + bytes calldata extraData +) public { + // Test digest with various lengths and content + // Should always produce deterministic hash +} +``` + +--- + +**Fuzz 3: Payload ID Encoding/Decoding** + +```solidity +function testFuzz_payloadId( + uint32 srcChain, + uint32 srcId, + uint32 dstChain, + uint32 dstId, + uint64 counter +) public { + bytes32 id = createPayloadId(...); + // Decode and verify matches input +} +``` + +--- + +## Testing Gaps & Auditor Recommendations + +### Known Gaps + +1. **Limited Gas Exhaustion Testing** + + - Need more tests with boundary gas values + - Test gas griefing scenarios + +2. **Cross-Chain Replay Scenarios** + + - If deployed on multiple chains, test signature replay + - Test chainSlug protections + +3. **Race Condition Coverage** + + - Limited concurrent transaction testing + - Need forking tests for realistic conditions + +4. **Economic Attack Vectors** + + - Fee manipulation strategies + - Transmitter incentive edge cases + +5. **Integration with Real Plugs** + - Most tests use mock plugs + - Need tests with realistic plug implementations + +--- + +### Auditor Action Items + +**Recommended Tests to Add**: + +1. ✅ Implement all Priority 1 tests (reentrancy, gas, value) +2. ✅ Add comprehensive signature replay tests +3. ✅ Test all invariants with Echidna/Foundry +4. ✅ Fuzz test with extreme values +5. ✅ Add multi-block/forking tests for race conditions + +**Manual Review Focus**: + +1. Review gas calculations for overflow/underflow +2. Verify all signature formats include necessary components +3. Check state update ordering (CEI pattern) +4. Validate all access control modifiers +5. Verify external call safety + +**Tools to Use**: + +- Foundry invariant testing +- Echidna for property-based testing +- Slither for static analysis +- Manual code review with checklist + +--- + +## Test Execution Guide + +### Run All Tests + +```bash +forge test +``` + +### Run with Coverage + +```bash +forge coverage +``` + +### Run Specific Test Suite + +```bash +forge test --match-path test/Socket.t.sol +``` + +### Run Fuzz Tests with High Runs + +```bash +forge test --fuzz-runs 10000 +``` + +### Run Invariant Tests + +```bash +forge test --match-test invariant +``` + +--- + +## Expected Test Outcomes + +### All Tests Should Pass + +- ✅ Unit tests: 100% pass rate +- ✅ Integration tests: 100% pass rate +- ✅ Invariant tests: No violations +- ✅ Fuzz tests: No unexpected failures + +### Coverage Targets + +- 📊 Line coverage: >90% +- 📊 Branch coverage: >85% +- 📊 Function coverage: >95% + +### Performance Benchmarks + +- ⚡ execute() gas: <300k gas +- ⚡ sendPayload() gas: <200k gas +- ⚡ attest() gas: <100k gas diff --git a/contracts/evmx/base/AppGatewayBase.sol b/contracts/evmx/base/AppGatewayBase.sol index 5726d8af..576538ad 100644 --- a/contracts/evmx/base/AppGatewayBase.sol +++ b/contracts/evmx/base/AppGatewayBase.sol @@ -1,39 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "../helpers/AddressResolverUtil.sol"; import "../interfaces/IAppGateway.sol"; import "../interfaces/IForwarder.sol"; import "../interfaces/IPromise.sol"; -import "../interfaces/IERC20.sol"; +import "../interfaces/IGasAccountToken.sol"; import {InvalidPromise, AsyncModifierNotSet} from "../../utils/common/Errors.sol"; import {FAST, READ, WRITE, SCHEDULE} from "../../utils/common/Constants.sol"; -import {IsPlug, QueueParams, Read, WriteFinality, Parallel} from "../../utils/common/Structs.sol"; +import {RawPayload, WriteFinality, OverrideParams} from "../../utils/common/Structs.sol"; import {toBytes32Format} from "../../utils/common/Converters.sol"; +import "../../utils/OverrideParamsLib.sol"; /// @title AppGatewayBase /// @notice Abstract contract for the app gateway -/// @dev This contract contains helpers for contract deployment, overrides, hooks and request processing +/// @dev This contract contains helpers for contract deployment, overrides, hooks and payload processing abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { + using OverrideParamsLib for OverrideParams; // 50 slots reserved for address resolver util - // slot 51 - bool public isAsyncModifierSet; - address public consumeFrom; - - // slot 52 - address public auctionManager; // slot 53 - uint256 public maxFees; + bool public isAsyncModifierSet; // slot 54 - bytes32 public sbType; - - // slot 55 - bytes public onCompleteData; - - // slot 56 OverrideParams public overrideParams; // slot 57 @@ -42,9 +32,6 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { // slot 58 mapping(bytes32 => mapping(uint32 => address)) public override forwarderAddresses; - // slot 59 - mapping(bytes32 => bytes) public creationCodeWithArgs; - /// @notice Modifier to treat functions async modifier async() { _preAsync(); @@ -64,7 +51,7 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { /// @notice Initializer for AppGatewayBase /// @param addressResolver_ The address resolver address function _initializeAppGateway(address addressResolver_) internal { - sbType = FAST; + overrideParams.switchboardType = FAST; _setAddressResolver(addressResolver_); } @@ -73,21 +60,14 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { //////////////////////////////////////////////////////////////////////////////////////////////// function _preAsync() internal { - isAsyncModifierSet = true; _clearOverrides(); - watcher__().clearQueue(); + isAsyncModifierSet = true; } function _postAsync() internal { - isAsyncModifierSet = false; - - (, address[] memory promises) = watcher__().submitRequest( - maxFees, - auctionManager, - consumeFrom, - onCompleteData - ); - _markValidPromises(promises); + _clearOverrides(); + address promise_ = watcher__().executePayload(); + isValidPromise[promise_] = true; } function then(bytes4 selector_, bytes memory data_) internal { @@ -102,63 +82,21 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { overrideParams.callType = SCHEDULE; overrideParams.delayInSeconds = delayInSeconds_; - QueueParams memory queueParams; - queueParams.overrideParams = overrideParams; - watcher__().queue(queueParams, address(this)); - } - - ///////////////////////////////// DEPLOY HELPERS /////////////////////////////////////////////////// - - function _deploy(bytes32 contractId_, uint32 chainSlug_, IsPlug isPlug_) internal { - _deploy(contractId_, chainSlug_, isPlug_, bytes("")); - } - - /// @notice Deploys a contract - /// @param contractId_ The contract ID - /// @param chainSlug_ The chain slug - function _deploy( - bytes32 contractId_, - uint32 chainSlug_, - IsPlug isPlug_, - bytes memory initCallData_ - ) internal { - onCompleteData = abi.encodeWithSelector( - this.onDeployComplete.selector, - watcher__().getCurrentRequestCount(), - abi.encode(chainSlug_) - ); - - deployForwarder__().deploy( - isPlug_, - chainSlug_, - initCallData_, - creationCodeWithArgs[contractId_] - ); - - then(this.setAddress.selector, abi.encode(chainSlug_, contractId_)); - } - - /// @notice Sets the address for a deployed contract - /// @param data_ The data - /// @param returnData_ The return data - function setAddress(bytes memory data_, bytes memory returnData_) external onlyPromises { - (uint32 chainSlug, bytes32 contractId) = abi.decode(data_, (uint32, bytes32)); - forwarderAddresses[contractId][chainSlug] = asyncDeployer__().getOrDeployForwarderContract( - toBytes32Format(abi.decode(returnData_, (address))), - chainSlug - ); + RawPayload memory rawPayload; + rawPayload.overrideParams = overrideParams; + watcher__().addPayloadData(rawPayload, address(this)); } /// @notice Reverts the transaction - /// @param requestCount_ The request count - function _revertTx(uint40 requestCount_) internal { - watcher__().cancelRequest(requestCount_); + /// @param payloadId_ The payload id + function _revertTx(bytes32 payloadId_) internal { + watcher__().cancelExecution(payloadId_); } /// @notice increases the transaction maxFees - /// @param requestCount_ The request count - function _increaseFees(uint40 requestCount_, uint256 newMaxFees_) internal { - watcher__().increaseFees(requestCount_, newMaxFees_); + /// @param payloadId_ The payload id + function _increaseFees(bytes32 payloadId_, uint256 newMaxFees_) internal { + watcher__().increaseFees(payloadId_, newMaxFees_); } /// @notice Gets the on-chain address @@ -173,6 +111,7 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { return bytes32(0); } + // todo: different for solana, need to handle here onChainAddress = IForwarder(forwarderAddresses[contractId_][chainSlug_]) .getOnChainAddress(); } @@ -188,49 +127,40 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { return keccak256(abi.encode(contractName_)); } - /// @notice Gets the current request count - /// @return uint40 The current request count - function _getCurrentRequestCount() internal view returns (uint40) { - return watcher__().getCurrentRequestCount(); - } - - /// @notice Marks the promises as valid - function _markValidPromises(address[] memory promises_) internal { - for (uint256 i = 0; i < promises_.length; i++) { - isValidPromise[promises_[i]] = true; - } + /// @notice Gets the current payload id + /// @return uint40 The current payload count + function _getCurrentPayloadId() internal view returns (bytes32) { + return watcher__().currentPayloadId(); } //////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// ADMIN HELPERS //////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// - - /// @notice Sets the auction manager - /// @param auctionManager_ The auction manager - function _setAuctionManager(address auctionManager_) internal { - auctionManager = auctionManager_; - } - /// @notice Sets the switchboard type /// @param sbType_ The switchboard type function _setSbType(bytes32 sbType_) internal { - sbType = sbType_; + overrideParams.switchboardType = sbType_; } /// @notice Sets the validity of an onchain contract (plug) to authorize it to send information to a specific AppGateway /// @param chainSlug_ The unique identifier of the chain where the contract resides - /// @param contractId_ The bytes32 identifier of the contract to be validated + /// @param plugAddress_ The address of the plug to be validated /// @param isValid Boolean flag indicating whether the contract is authorized (true) or not (false) - /// @dev This function retrieves the onchain address using the contractId_ and chainSlug, then calls the watcher precompile to update the plug's validity status - function _setValidPlug(bool isValid, uint32 chainSlug_, bytes32 contractId_) internal { - bytes32 onchainAddress = getOnChainAddress(contractId_, chainSlug_); - watcher__().setIsValidPlug(isValid, chainSlug_, onchainAddress); + /// @dev This function calls the watcher precompile to update the plug's validity status + function _setValidPlug(bool isValid, uint32 chainSlug_, bytes32 plugAddress_) internal { + watcher__().setIsValidPlug(isValid, chainSlug_, plugAddress_); } function _permit(bytes memory feesApprovalData_) internal { if (feesApprovalData_.length == 0) return; - (address spender, uint256 value, uint256 deadline, uint256 nonce, bytes memory signature) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes)); - IERC20(address(feesManager__())).permit(spender, value, deadline, nonce, signature); + ( + address spender, + uint256 value, + uint256 deadline, + uint256 nonce, + bytes memory signature + ) = abi.decode(feesApprovalData_, (address, uint256, uint256, uint256, bytes)); + gasAccountToken__().permit(spender, value, deadline, nonce, signature); } /// @notice Withdraws fee tokens @@ -238,149 +168,51 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway { /// @param token_ The token address /// @param amount_ The amount /// @param receiver_ The receiver address - function _withdrawCredits( + function _withdrawToChain( uint32 chainSlug_, address token_, uint256 amount_, address receiver_ ) internal { - IERC20(address(feesManager__())).approve(address(feesManager__()), type(uint256).max); - feesManager__().withdrawCredits(chainSlug_, token_, amount_, maxFees, receiver_); + gasAccountToken__().approve(address(gasAccountManager__()), type(uint256).max); + gasAccountManager__().withdrawToChain( + chainSlug_, + token_, + amount_, + overrideParams.maxFees, + receiver_ + ); } //////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// TX OVERRIDE HELPERS /////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// - function _clearOverrides() internal { - overrideParams.callType = WRITE; - overrideParams.isParallelCall = Parallel.OFF; - overrideParams.gasLimit = 0; - overrideParams.value = 0; - overrideParams.readAtBlockNumber = 0; - overrideParams.writeFinality = WriteFinality.LOW; - overrideParams.delayInSeconds = 0; - consumeFrom = address(this); - onCompleteData = bytes(""); - } - - /// @notice Sets multiple overrides in one call - /// @param isReadCall_ The read call flag - /// @param fees_ The maxFees configuration - /// @param gasLimit_ The gas limit - /// @param isParallelCall_ The sequential call flag - function _setOverrides( - Read isReadCall_, - Parallel isParallelCall_, - uint256 gasLimit_, - uint256 fees_ - ) internal { - _setCallType(isReadCall_); - overrideParams.isParallelCall = isParallelCall_; - overrideParams.gasLimit = gasLimit_; - maxFees = fees_; - } - - /// @notice Modifier to treat functions async with consume from address - function _setOverrides(address consumeFrom_) internal { - consumeFrom = consumeFrom_; - } - - /// @notice Sets isReadCall, maxFees and gasLimit overrides - /// @param isReadCall_ The read call flag - /// @param isParallelCall_ The sequential call flag - /// @param gasLimit_ The gas limit - function _setOverrides(Read isReadCall_, Parallel isParallelCall_, uint256 gasLimit_) internal { - _setCallType(isReadCall_); - overrideParams.isParallelCall = isParallelCall_; - overrideParams.gasLimit = gasLimit_; - } - - /// @notice Sets isReadCall and isParallelCall overrides - /// @param isReadCall_ The read call flag - /// @param isParallelCall_ The sequential call flag - function _setOverrides(Read isReadCall_, Parallel isParallelCall_) internal { - _setCallType(isReadCall_); - overrideParams.isParallelCall = isParallelCall_; - } - - /// @notice Sets isParallelCall overrides - /// @param writeFinality_ The write finality - function _setOverrides(WriteFinality writeFinality_) internal { - overrideParams.writeFinality = writeFinality_; - } - - /// @notice Sets isParallelCall overrides - /// @param isParallelCall_ The sequential call flag - function _setOverrides(Parallel isParallelCall_) internal { - overrideParams.isParallelCall = isParallelCall_; - } - - /// @notice Sets isParallelCall overrides - /// @param isParallelCall_ The sequential call flag - /// @param readAtBlockNumber_ The read anchor value. Currently block number. - function _setOverrides(Parallel isParallelCall_, uint256 readAtBlockNumber_) internal { - overrideParams.isParallelCall = isParallelCall_; - overrideParams.readAtBlockNumber = readAtBlockNumber_; + /// @notice Sets the override parameters + /// @return overrideParams The override parameters + function getOverrideParams() public view returns (OverrideParams memory) { + return overrideParams; } - /// @notice Sets isReadCall overrides - /// @param isReadCall_ The read call flag - function _setOverrides(Read isReadCall_) internal { - _setCallType(isReadCall_); - } - - /// @notice Sets isReadCall overrides - /// @param isReadCall_ The read call flag - /// @param readAtBlockNumber_ The read anchor value. Currently block number. - function _setOverrides(Read isReadCall_, uint256 readAtBlockNumber_) internal { - _setCallType(isReadCall_); - overrideParams.readAtBlockNumber = readAtBlockNumber_; - } - - /// @notice Sets gasLimit overrides - /// @param gasLimit_ The gas limit - function _setOverrides(uint256 gasLimit_) internal { - overrideParams.gasLimit = gasLimit_; - } - - function _setCallType(Read isReadCall_) internal { - overrideParams.callType = isReadCall_ == Read.OFF ? WRITE : READ; - } - - function _setMsgValue(uint256 value_) internal { - overrideParams.value = value_; - } - - /// @notice Sets maxFees overrides - /// @param fees_ The maxFees configuration - function _setMaxFees(uint256 fees_) internal { - maxFees = fees_; + /// @notice Clears the override parameters + function _clearOverrides() internal { + bytes32 sbType = overrideParams.switchboardType; + uint256 maxFees = overrideParams.maxFees; + overrideParams = OverrideParamsLib.clear(sbType).setConsumeFrom(address(this)).setMaxFees( + maxFees + ); } - function getOverrideParams() public view returns (OverrideParams memory, bytes32) { - return (overrideParams, sbType); + /// @notice Applies an OverrideParams configuration to this AppGatewayBase instance + /// @param params The OverrideParams containing the configuration to apply + function applyOverride(OverrideParams memory params) internal { + overrideParams = params; } //////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////// HOOKS ///////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////// - /// @notice Callback in pd promise to be called after all contracts are deployed - /// @param onCompleteData_ The on complete data - /// @dev only payload delivery can call this - /// @dev callback in pd promise to be called after all contracts are deployed - function onDeployComplete(uint40, bytes calldata onCompleteData_) external virtual onlyWatcher { - if (onCompleteData_.length == 0) return; - uint32 chainSlug = abi.decode(onCompleteData_, (uint32)); - initializeOnChain(chainSlug); - } - - /// @notice Initializes the contract after deployment - /// @dev can be overridden by the app gateway to add custom logic - /// @param chainSlug_ The chain slug - function initializeOnChain(uint32 chainSlug_) public virtual {} - /// @notice hook to handle the revert in callbacks or onchain executions /// @dev can be overridden by the app gateway to add custom logic /// @param payloadId_ The payload ID diff --git a/contracts/evmx/fees/Credit.sol b/contracts/evmx/fees/Credit.sol deleted file mode 100644 index e7dcc0e9..00000000 --- a/contracts/evmx/fees/Credit.sol +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "solady/utils/Initializable.sol"; -import "solady/utils/ECDSA.sol"; -import "solady/utils/SafeTransferLib.sol"; -import "solady/auth/Ownable.sol"; -import "solady/tokens/ERC20.sol"; - -import "../interfaces/IFeesManager.sol"; -import "../interfaces/IFeesPlug.sol"; -import "../interfaces/IFeesPool.sol"; -import "../interfaces/IReceiver.sol"; - -import {AddressResolverUtil} from "../helpers/AddressResolverUtil.sol"; -import {NonceUsed, InvalidAmount, InsufficientCreditsAvailable, InsufficientBalance, InvalidChainSlug, NotRequestHandler, InvalidReceiver} from "../../utils/common/Errors.sol"; -import {WRITE} from "../../utils/common/Constants.sol"; -import "../../utils/RescueFundsLib.sol"; -import "../base/AppGatewayBase.sol"; -import {toBytes32Format} from "../../utils/common/Converters.sol"; - -abstract contract FeesManagerStorage is IFeesManager { - // slots [0-49] reserved for gap - uint256[50] _gap_before; - - // slot 50 - /// @notice evmx slug - uint32 public evmxSlug; - IFeesPool public feesPool; - - // slot 51 - /// @notice Mapping to track blocked credits for each user - /// @dev address => userBlockedCredits - mapping(address => uint256) public userBlockedCredits; - - // slot 52 - /// @notice Mapping to track request credits details for each request count - /// @dev requestCount => RequestFee - mapping(uint40 => uint256) public requestBlockedCredits; - - // slot 53 - // token pool balances - // chainSlug => token address => amount - mapping(uint32 => mapping(address => uint256)) public tokenOnChainBalances; - - // slot 54 - /// @notice Mapping to track nonce to whether it has been used - /// @dev address => signatureNonce => isNonceUsed - /// @dev used by watchers or other users in signatures - mapping(address => mapping(uint256 => bool)) public isNonceUsed; - - // slot 55 - /// @notice Mapping to track fees plug for each chain slug - /// @dev chainSlug => fees plug address - mapping(uint32 => bytes32) public feesPlugs; - - // slot 56 - /// @notice Mapping to track max fees per chain slug - /// @dev chainSlug => max fees - mapping(uint32 => uint256) public maxFeesPerChainSlug; - - // slots [57-106] reserved for gap - uint256[50] _gap_after; - - // slots [107-156] 50 slots reserved for address resolver util - // 9 slots for app gateway base -} - -/// @title SocketUSDC -/// @notice ERC20 token for managing credits with blocking/unblocking functionality -abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatewayBase, ERC20 { - /// @notice Emitted when fees deposited are updated - /// @param chainSlug The chain identifier - /// @param token The token address - /// @param depositTo The address to deposit to - /// @param creditAmount The credit amount added - /// @param nativeAmount The native amount transferred - event Deposited( - uint32 indexed chainSlug, - address indexed token, - address indexed depositTo, - uint256 creditAmount, - uint256 nativeAmount - ); - - /// @notice Emitted when credits are wrapped - event CreditsWrapped(address indexed consumeFrom, uint256 amount); - - /// @notice Emitted when credits are unwrapped - event CreditsUnwrapped(address indexed consumeFrom, uint256 amount); - - /// @notice Emitted when fees plug is set - event FeesPlugSet(uint32 indexed chainSlug, bytes32 indexed feesPlug); - - /// @notice Emitted when fees pool is set - event FeesPoolSet(address indexed feesPool); - - /// @notice Emitted when withdraw fails - event WithdrawFailed(bytes32 indexed payloadId); - - function setFeesPlug(uint32 chainSlug_, bytes32 feesPlug_) external onlyOwner { - feesPlugs[chainSlug_] = feesPlug_; - emit FeesPlugSet(chainSlug_, feesPlug_); - } - - function setFeesPool(address feesPool_) external onlyOwner { - feesPool = IFeesPool(feesPool_); - emit FeesPoolSet(feesPool_); - } - - function getMaxFees(uint32 chainSlug_) public view returns (uint256) { - return - maxFeesPerChainSlug[chainSlug_] == 0 - ? maxFeesPerChainSlug[evmxSlug] - : maxFeesPerChainSlug[chainSlug_]; - } - - function isApproved(address user_, address appGateway_) public view returns (bool) { - return allowance(user_, appGateway_) > 0; - } - - /// @notice Deposits credits and native tokens to a user - /// @param depositTo_ The address to deposit the credits to - /// @param chainSlug_ The chain slug - /// @param token_ The token address - /// @param nativeAmount_ The native amount - /// @param creditAmount_ The credit amount - function deposit( - uint32 chainSlug_, - address token_, - address depositTo_, - uint256 nativeAmount_, - uint256 creditAmount_ - ) external override onlyWatcher { - tokenOnChainBalances[chainSlug_][token_] += creditAmount_ + nativeAmount_; - - // Mint tokens to the user - _mint(depositTo_, creditAmount_); - - if (nativeAmount_ > 0) { - // if native transfer fails, add to credit - bool success = feesPool.withdraw(depositTo_, nativeAmount_); - - if (!success) { - _mint(depositTo_, creditAmount_); - nativeAmount_ = 0; - creditAmount_ += nativeAmount_; - } - } - - emit Deposited(chainSlug_, token_, depositTo_, creditAmount_, nativeAmount_); - } - - function wrap(address receiver_) external payable override { - uint256 amount = msg.value; - if (amount == 0) revert InvalidAmount(); - - // Mint tokens to receiver - _mint(receiver_, amount); - - // reverts if transfer fails - SafeTransferLib.safeTransferETH(address(feesPool), amount); - emit CreditsWrapped(receiver_, amount); - } - - function unwrap(uint256 amount_, address receiver_) external { - if (balanceOf(msg.sender) < amount_) revert InsufficientCreditsAvailable(); - - // Burn tokens from sender - _burn(msg.sender, amount_); - - bool success = feesPool.withdraw(receiver_, amount_); - if (!success) revert InsufficientBalance(); - - emit CreditsUnwrapped(receiver_, amount_); - } - - /// @notice Override balanceOf to return available (unblocked) credits - function balanceOf(address account) public view override returns (uint256) { - return super.balanceOf(account) - userBlockedCredits[account]; - } - - /// @notice Get total balance including blocked credits - function totalBalanceOf(address account) public view returns (uint256) { - return super.balanceOf(account); - } - - /// @notice Get blocked credits for an account - function getBlockedCredits(address account) public view returns (uint256) { - return userBlockedCredits[account]; - } - - /// @notice Checks if the user has enough credits - /// @param consumeFrom_ address to consume from - /// @param spender_ address to spend from - /// @param amount_ amount to spend - /// @return True if the user has enough credits, false otherwise - function isCreditSpendable( - address consumeFrom_, - address spender_, - uint256 amount_ - ) public view override returns (bool) { - // If consumeFrom_ is not same as spender_ or spender_ is not watcher, check if it is approved - if (!_isWatcher(spender_) && consumeFrom_ != spender_) { - if (allowance(consumeFrom_, spender_) == 0) return false; - } - - return balanceOf(consumeFrom_) >= amount_; - } - - // ERC20 Overrides to handle blocked credits - - /// @notice Override transfer to check for blocked credits - function transfer(address to, uint256 amount) public override returns (bool) { - if (balanceOf(msg.sender) < amount) revert InsufficientCreditsAvailable(); - return super.transfer(to, amount); - } - - /// @notice Override transferFrom to check for blocked credits - function transferFrom( - address from_, - address to_, - uint256 amount_ - ) public override returns (bool) { - if (!isCreditSpendable(from_, msg.sender, amount_)) revert InsufficientCreditsAvailable(); - - if (_isWatcher(msg.sender)) _approve(from_, msg.sender, amount_); - return super.transferFrom(from_, to_, amount_); - } - - /// @notice Withdraws funds to a specified receiver - /// @dev This function is used to withdraw fees from the fees plug - /// @dev assumed that transmitter can bid for their request on AM - /// @param chainSlug_ The chain identifier - /// @param token_ The address of the token - /// @param credits_ The amount of tokens to withdraw - /// @param maxFees_ The fees needed to process the withdraw - /// @param receiver_ The address of the receiver - function withdrawCredits( - uint32 chainSlug_, - address token_, - uint256 credits_, - uint256 maxFees_, - address receiver_ - ) public override { - address consumeFrom = msg.sender; - - // Check if amount is available in fees plug - uint256 availableCredits = balanceOf(consumeFrom); - if (availableCredits < credits_ + maxFees_) revert InsufficientCreditsAvailable(); - - // Burn tokens from sender - _burn(consumeFrom, credits_); - tokenOnChainBalances[chainSlug_][token_] -= credits_; - - // Add it to the queue and submit request - _createRequest( - chainSlug_, - consumeFrom, - maxFees_, - abi.encodeCall(IFeesPlug.withdrawFees, (token_, receiver_, credits_)) - ); - } - - function _createRequest( - uint32 chainSlug_, - address consumeFrom_, - uint256 maxFees_, - bytes memory payload_ - ) internal async { - _setMaxFees(getMaxFees(chainSlug_)); - _setOverrides(consumeFrom_); - - QueueParams memory queueParams; - queueParams.overrideParams = overrideParams; - queueParams.transaction = Transaction({ - chainSlug: chainSlug_, - target: _getFeesPlugAddress(chainSlug_), - payload: payload_ - }); - queueParams.switchboardType = sbType; - watcher__().queue(queueParams, address(this)); - } - - function increaseFees(uint40 requestCount_, uint256 newMaxFees_) public { - _increaseFees(requestCount_, newMaxFees_); - } - - function _getFeesPlugAddress(uint32 chainSlug_) internal view returns (bytes32) { - if (feesPlugs[chainSlug_] == bytes32(0)) revert InvalidChainSlug(); - return feesPlugs[chainSlug_]; - } - - function _getRequestParams(uint40 requestCount_) internal view returns (RequestParams memory) { - return watcher__().getRequestParams(requestCount_); - } - - function _recoverSigner( - bytes32 digest_, - bytes memory signature_ - ) internal view returns (address signer) { - bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); - // recovered signer is checked for the valid roles later - signer = ECDSA.recover(digest, signature_); - } - - // ERC20 metadata - function name() public pure override returns (string memory) { - return "Socket Credits"; - } - - function symbol() public pure override returns (string memory) { - return "credits"; - } - - function decimals() public pure override returns (uint8) { - return 18; - } -} diff --git a/contracts/evmx/fees/FeesManager.sol b/contracts/evmx/fees/FeesManager.sol deleted file mode 100644 index a4330222..00000000 --- a/contracts/evmx/fees/FeesManager.sol +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "./Credit.sol"; - -/// @title FeesManager -/// @notice Contract for managing fees -contract FeesManager is Credit { - /// @notice Emitted when fees are blocked for a batch - /// @param requestCount The batch identifier - /// @param consumeFrom The consume from address - /// @param amount The blocked amount - event CreditsBlocked(uint40 indexed requestCount, address indexed consumeFrom, uint256 amount); - - /// @notice Emitted when fees are unblocked and assigned to a transmitter - /// @param requestCount The batch identifier - /// @param consumeFrom The consume from address - /// @param transmitter The transmitter address - /// @param amount The unblocked amount - event CreditsUnblockedAndAssigned( - uint40 indexed requestCount, - address indexed consumeFrom, - address indexed transmitter, - uint256 amount - ); - - /// @notice Emitted when max fees per chain slug is set - /// @param chainSlug The chain slug - /// @param fees The max fees - event MaxFeesPerChainSlugSet(uint32 indexed chainSlug, uint256 fees); - - /// @notice Emitted when fees are unblocked - /// @param requestCount The batch identifier - /// @param consumeFrom The consume from address - event CreditsUnblocked(uint40 indexed requestCount, address indexed consumeFrom); - - modifier onlyRequestHandler() { - if (msg.sender != address(watcher__().requestHandler__())) revert NotRequestHandler(); - _; - } - - constructor() { - _disableInitializers(); // disable for implementation - } - - /// @notice Initializer function to replace constructor - /// @param addressResolver_ The address of the address resolver - /// @param owner_ The address of the owner - /// @param evmxSlug_ The evmx chain slug - function initialize( - uint32 evmxSlug_, - address addressResolver_, - address feesPool_, - address owner_, - uint256 fees_, - bytes32 sbType_ - ) public reinitializer(2) { - evmxSlug = evmxSlug_; - sbType = sbType_; - feesPool = IFeesPool(feesPool_); - maxFeesPerChainSlug[evmxSlug_] = fees_; - - _setMaxFees(fees_); - _initializeOwner(owner_); - _initializeAppGateway(addressResolver_); - } - - function setChainMaxFees( - uint32[] calldata chainSlugs_, - uint256[] calldata maxFees_ - ) external onlyOwner { - if (chainSlugs_.length != maxFees_.length) revert("Array length mismatch"); - - for (uint256 i = 0; i < chainSlugs_.length; i++) { - maxFeesPerChainSlug[chainSlugs_[i]] = maxFees_[i]; - emit MaxFeesPerChainSlugSet(chainSlugs_[i], maxFees_[i]); - } - } - - function getChainMaxFees( - uint32[] calldata chainSlugs_ - ) external view returns (uint256[] memory) { - uint256[] memory maxFeesArray = new uint256[](chainSlugs_.length); - for (uint256 i = 0; i < chainSlugs_.length; i++) { - maxFeesArray[i] = maxFeesPerChainSlug[chainSlugs_[i]]; - } - return maxFeesArray; - } - - function setMaxFees(uint256 fees_) external onlyOwner { - _setMaxFees(fees_); - } - - /////////////////////// FEES MANAGEMENT /////////////////////// - - /// @notice Blocks fees for a request count - /// @param requestCount_ The batch identifier - /// @param consumeFrom_ The fees payer address - /// @param credits_ The total fees to block - /// @dev Only callable by delivery helper - function blockCredits( - uint40 requestCount_, - address consumeFrom_, - uint256 credits_ - ) external override onlyRequestHandler { - if (balanceOf(consumeFrom_) < credits_) revert InsufficientCreditsAvailable(); - - userBlockedCredits[consumeFrom_] += credits_; - requestBlockedCredits[requestCount_] = credits_; - emit CreditsBlocked(requestCount_, consumeFrom_, credits_); - } - - /// @notice Unblocks fees after successful execution and assigns them to the transmitter - /// @param requestCount_ The request count of the executed batch - /// @param assignTo_ The address of the transmitter - function unblockAndAssignCredits( - uint40 requestCount_, - address assignTo_ - ) external override onlyRequestHandler { - uint256 blockedCredits = requestBlockedCredits[requestCount_]; - if (blockedCredits == 0) return; - - address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom; - - // Unblock credits from the original user - userBlockedCredits[consumeFrom] -= blockedCredits; - - // Burn tokens from the original user - _burn(consumeFrom, blockedCredits); - - // Mint tokens to the transmitter - _mint(assignTo_, blockedCredits); - - // Clean up storage - delete requestBlockedCredits[requestCount_]; - emit CreditsUnblockedAndAssigned(requestCount_, consumeFrom, assignTo_, blockedCredits); - } - - function unblockCredits(uint40 requestCount_) external override onlyRequestHandler { - uint256 blockedCredits = requestBlockedCredits[requestCount_]; - if (blockedCredits == 0) return; - - // Unblock credits from the original user - address consumeFrom = _getRequestParams(requestCount_).requestFeesDetails.consumeFrom; - userBlockedCredits[consumeFrom] -= blockedCredits; - - delete requestBlockedCredits[requestCount_]; - emit CreditsUnblocked(requestCount_, consumeFrom); - } - - /** - * @notice Rescues funds from the contract if they are locked by mistake. This contract does not - * theoretically need this function but it is added for safety. - * @param token_ The address of the token contract. - * @param rescueTo_ The address where rescued tokens need to be sent. - * @param amount_ The amount of tokens to be rescued. - */ - function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { - RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); - } -} diff --git a/contracts/evmx/fees/GasAccountManager.sol b/contracts/evmx/fees/GasAccountManager.sol new file mode 100644 index 00000000..035c4578 --- /dev/null +++ b/contracts/evmx/fees/GasAccountManager.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; +import "solady/auth/Ownable.sol"; +import "solady/utils/Initializable.sol"; +import "solady/utils/SafeTransferLib.sol"; +import "../interfaces/IGasAccountManager.sol"; +import "../interfaces/IGasStation.sol"; +import "../../utils/AccessControl.sol"; +import "../../utils/common/AccessRoles.sol"; +import "../../utils/OverrideParamsLib.sol"; +import {OverrideParams, SolanaInstruction, SolanaInstructionData, SolanaInstructionDataDescription} from "../../utils/common/Structs.sol"; +import {toBytes32Format} from "../../utils/common/Converters.sol"; +import {NonceUsed, InvalidAmount, InsufficientGasAvailable, InsufficientBalance, InvalidChainSlug, InvalidReceiver} from "../../utils/common/Errors.sol"; + +import "../../utils/RescueFundsLib.sol"; +import {ForwarderSolana} from "../helpers/ForwarderSolana.sol"; +import {GasStationProgramPda} from "../helpers/solana-utils/program-pda/GasStationPdas.sol"; +import {SolanaPDA} from "../helpers/solana-utils/SolanaPda.sol"; +import {TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID} from "../helpers/solana-utils/SolanaPda.sol"; +import "../base/AppGatewayBase.sol"; + +abstract contract GasAccountManagerStorage is IGasAccountManager { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + // token pool balances + // chainSlug => token address => amount + mapping(uint32 => mapping(bytes32 => uint256)) public tokenOnChainBalances; + + /// @notice Mapping to track max fees per chain slug + /// @dev chainSlug => max fees + mapping(uint32 => uint256) public maxGasPerChainSlug; + + /////////////////////// SOLANA /////////////////////// + ForwarderSolana public forwarderSolana; + + /// @notice Mapping to track fees plug for each chain slug + /// @dev chainSlug => fees plug address + mapping(uint32 => bytes32) public gasStations; + + // slots [50-99] reserved for gap + uint256[50] _gap_after; +} + +contract GasAccountManager is + GasAccountManagerStorage, + Ownable, + AccessControl, + AppGatewayBase, + Initializable +{ + using OverrideParamsLib for OverrideParams; + error OnlyPayloadConsumer(); + + constructor() { + _disableInitializers(); + } + + function initialize( + address addressResolver_, + address owner_, + uint256 fees_, + bytes32 sbType_, + address forwarderSolana_ + ) external reinitializer(1) { + forwarderSolana = ForwarderSolana(forwarderSolana_); + overrideParams = overrideParams.setSwitchboardType(sbType_).setMaxFees(fees_); + _initializeOwner(owner_); + _initializeAppGateway(addressResolver_); + } + + // ============ GAS ACCOUNT OPERATIONS ============ + + /// @notice Wraps native token into SGAS tokens for cross-chain gas management + /// @dev Receives native token via payable modifier, mints equivalent SGAS tokens to receiver, + /// then transfers native to GasVault for secure storage. + /// @param receiver The address that will receive the minted SGAS token + function wrapToGas(address receiver) external payable override { + uint256 amount = msg.value; + if (amount == 0) revert InvalidAmount(); + + // Mint tokens to receiver + gasAccountToken__().mint(receiver, amount); + + // reverts if transfer fails + SafeTransferLib.safeTransferETH(address(gasVault__()), amount); + emit GasWrapped(receiver, amount); + } + + /// @notice Unwraps SGAS tokens back to native token + /// @dev Burns SGAS tokens from msg.sender, then withdraws equivalent of native from GasVault + /// to the specified receiver. Reverts if sender lacks sufficient tokens or vault + /// withdrawal fails. + /// @param amount The amount of SGAS tokens to burn and native to withdraw + /// @param receiver The address that will receive the native token + function unwrapFromGas(uint256 amount, address receiver) external { + // todo: use isGasAvailable, check all gasAccountToken__().balanceOf instances + if (gasAccountToken__().balanceOf(msg.sender) < amount) revert InsufficientGasAvailable(); + + // Burn tokens from sender + gasAccountToken__().burn(msg.sender, amount); + + bool success = gasVault__().withdraw(receiver, amount); + if (!success) revert InsufficientBalance(); + + emit GasUnwrapped(msg.sender, amount); + } + + // ============ CROSS-CHAIN OPERATIONS ============ + + /// @notice Deposit tokens from a chain into gas account + /// @dev Called by watcher after detecting GasStation deposit + function depositFromChain( + address token_, + address depositTo_, + uint256 gasAmount_, + uint256 nativeAmount_ + ) external onlyWatcher { + uint32 chainSlug = watcher__().triggerFromChainSlug(); + tokenOnChainBalances[chainSlug][toBytes32Format(token_)] += gasAmount_ + nativeAmount_; + + // Mint tokens to the user + gasAccountToken__().mint(depositTo_, gasAmount_); + if (nativeAmount_ > 0) { + // if native transfer fails, add to gas + bool success = gasVault__().withdraw(depositTo_, nativeAmount_); + + if (!success) { + // Convert failed native amount to gas + gasAccountToken__().mint(depositTo_, nativeAmount_); + gasAmount_ += nativeAmount_; + nativeAmount_ = 0; + } + } + + emit Deposited(chainSlug, token_, depositTo_, gasAmount_, nativeAmount_); + } + + /// @notice Withdraw SGAS to tokens on another chain + function withdrawToChain( + uint32 chainSlug, + address token, + uint256 amount, + uint256 bridgeFee, + address receiver + ) external { + address consumeFrom = msg.sender; + + // Check if amount is available in fees plug + uint256 gasBalance = gasAccountToken__().balanceOf(consumeFrom); + if (gasBalance < amount + bridgeFee) revert InsufficientGasAvailable(); + + // Burn tokens from sender + gasAccountToken__().burn(consumeFrom, amount); + tokenOnChainBalances[chainSlug][toBytes32Format(token)] -= amount; + + // Add it to the queue and submit payload + _submitPayload( + chainSlug, + consumeFrom, + bridgeFee, + abi.encodeCall(IGasStation.withdrawTokens, (token, receiver, amount)) + ); + } + + function _submitPayload( + uint32 chainSlug_, + address consumeFrom_, + uint256 bridgeFee_, + bytes memory payload_ + ) internal async { + overrideParams = overrideParams.setMaxFees(bridgeFee_).setConsumeFrom(consumeFrom_); + + RawPayload memory rawPayload; + rawPayload.overrideParams = overrideParams; + rawPayload.transaction = Transaction({ + chainSlug: chainSlug_, + target: _getGasStationAddress(chainSlug_), + payload: payload_ + }); + watcher__().addPayloadData(rawPayload, address(this)); + } + + function _getGasStationAddress(uint32 chainSlug_) internal view returns (bytes32) { + if (gasStations[chainSlug_] == bytes32(0)) revert InvalidChainSlug(); + return gasStations[chainSlug_]; + } + + // ============ PAYLOAD LIFECYCLE (Internal) ============ + + /// @notice Escrow gas for a payload + /// @dev Called by Watcher when payload is submitted + function escrowGas(bytes32 payloadId, address account, uint256 amount) external onlyWatcher { + if (gasAccountToken__().balanceOf(account) < amount) revert InsufficientGasAvailable(); + gasEscrow__().escrowGas(payloadId, account, amount); + } + + /// @notice Release escrowed gas back to account + /// @dev Called when transmitter changes or payload cancelled + function releaseEscrow(bytes32 payloadId) external onlyWatcher { + gasEscrow__().releaseEscrow(payloadId); + } + + /// @notice Settle escrowed gas to transmitter + /// @dev Called when payload completes successfully + function settleGasPayment( + bytes32 payloadId, + address consumeFrom, + address transmitter, + uint256 amount + ) external onlyWatcher { + gasEscrow__().settleGasPayment(payloadId, transmitter, amount); + gasAccountToken__().burn(consumeFrom, amount); + gasAccountToken__().mint(transmitter, amount); + } + + function setGasStation(uint32 chainSlug_, bytes32 gasStation_) external onlyOwner { + gasStations[chainSlug_] = gasStation_; + _setValidPlug(true, chainSlug_, gasStation_); + emit GasStationSet(chainSlug_, gasStation_); + } + + function setForwarderSolana(address forwarderSolana_) external onlyOwner { + forwarderSolana = ForwarderSolana(forwarderSolana_); + emit ForwarderSolanaSet(forwarderSolana_); + } + + function setChainMaxFees( + uint32[] calldata chainSlugs_, + uint256[] calldata maxFees_ + ) external onlyOwner { + if (chainSlugs_.length != maxFees_.length) revert("Array length mismatch"); + + for (uint256 i = 0; i < chainSlugs_.length; i++) { + maxGasPerChainSlug[chainSlugs_[i]] = maxFees_[i]; + emit MaxGasPerChainSlugSet(chainSlugs_[i], maxFees_[i]); + } + } + + function getChainMaxFees( + uint32[] calldata chainSlugs_ + ) external view returns (uint256[] memory) { + uint256[] memory maxFeesArray = new uint256[](chainSlugs_.length); + for (uint256 i = 0; i < chainSlugs_.length; i++) { + maxFeesArray[i] = maxGasPerChainSlug[chainSlugs_[i]]; + } + return maxFeesArray; + } + + function setMaxFees(uint256 fees_) external onlyOwner { + overrideParams = overrideParams.setMaxFees(fees_); + } + + function increaseFees(bytes32 payloadId_, uint256 newMaxFees_) public { + if (msg.sender != watcher__().getPayload(payloadId_).consumeFrom) + revert OnlyPayloadConsumer(); + + // fees deducted from consumeFrom account + _increaseFees(payloadId_, newMaxFees_); + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } +} diff --git a/contracts/evmx/fees/GasAccountToken.sol b/contracts/evmx/fees/GasAccountToken.sol new file mode 100644 index 00000000..a337c5fe --- /dev/null +++ b/contracts/evmx/fees/GasAccountToken.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "solady/auth/Ownable.sol"; +import "solady/tokens/ERC20.sol"; +import "../../utils/RescueFundsLib.sol"; +import "solady/utils/Initializable.sol"; +import "../interfaces/IAddressResolver.sol"; + +/// @title Socket Gas Token (SGAS) +/// @notice ERC20 token representing prepaid gas for Socket operations +/// @dev Balances are split between available and escrowed +contract GasAccountToken is ERC20, Ownable, Initializable { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + /// @notice Escrow tracker for gas in active payloads + IAddressResolver public addressResolver__; + + // slots [50-99] reserved for gap + uint256[50] _gap_after; + + error NotGasAccountManager(); + error InsufficientGasAvailable(); + + modifier onlyGasAccountManager() { + if (msg.sender != address(addressResolver__.gasAccountManager__())) + revert NotGasAccountManager(); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize(address owner_, address addressResolver_) external reinitializer(1) { + addressResolver__ = IAddressResolver(addressResolver_); + _setOwner(owner_); + } + + function decimals() public pure override returns (uint8) { + return 18; + } + + function symbol() public pure override returns (string memory) { + return "SGAS"; + } + + function name() public pure override returns (string memory) { + return "Socket Gas"; + } + + function mint(address account, uint256 amount) external onlyGasAccountManager { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external onlyGasAccountManager { + _burn(account, amount); + } + + /// @notice Returns available (spendable) gas balance + /// @dev Subtracts escrowed amount from total balance + function balanceOf(address account) public view override returns (uint256) { + return + super.balanceOf(account) - addressResolver__.gasEscrow__().getEscrowedAmount(account); + } + + /// @notice Returns total gas balance including escrowed + function totalBalanceOf(address account) public view returns (uint256) { + return super.balanceOf(account); + } + + // ERC20 Overrides to handle escrowed gas + /// @notice Override transfer to check for escrowed gas + function transfer(address to, uint256 amount) public override returns (bool) { + if (balanceOf(msg.sender) < amount) revert InsufficientGasAvailable(); + return super.transfer(to, amount); + } + + /// @notice Override transferFrom to check for escrowed gas + function transferFrom( + address from_, + address to_, + uint256 amount_ + ) public override returns (bool) { + if (!isGasAvailable(from_, msg.sender, amount_)) revert InsufficientGasAvailable(); + if (msg.sender == address(addressResolver__.watcher__())) + _approve(from_, msg.sender, amount_); + return super.transferFrom(from_, to_, amount_); + } + + /// @notice Checks if the user has enough gas + /// @param consumeFrom_ address to consume from + /// @param spender_ address to spend from + /// @param amount_ amount to spend + /// @return True if the user has enough gas, false otherwise + function isGasAvailable( + address consumeFrom_, + address spender_, + uint256 amount_ + ) public view returns (bool) { + // If consumeFrom_ is not same as spender_ or spender_ is not watcher, check if it is approved + if (spender_ != address(addressResolver__.watcher__()) && consumeFrom_ != spender_) { + if (allowance(consumeFrom_, spender_) < amount_) return false; + } + + return balanceOf(consumeFrom_) >= amount_; + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyOwner { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } +} diff --git a/contracts/evmx/fees/GasEscrow.sol b/contracts/evmx/fees/GasEscrow.sol new file mode 100644 index 00000000..4b02c789 --- /dev/null +++ b/contracts/evmx/fees/GasEscrow.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "../interfaces/IGasEscrow.sol"; +import "../../utils/RescueFundsLib.sol"; +import "solady/utils/Initializable.sol"; +import "solady/auth/Ownable.sol"; + +abstract contract GasEscrowStorage is IGasEscrow { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + address public gasAccountManager; + + /// @notice Tracks escrowed gas per account + mapping(address => uint256) public accountEscrow; + + /// @notice Tracks escrowed gas per payload + mapping(bytes32 => EscrowEntry) public payloadEscrow; + + // slots [50-99] reserved for gap + uint256[50] _gap_after; +} + +/// @title Gas Escrow Manager +/// @notice Tracks escrowed gas during payload lifecycle +/// @dev Separates escrow logic from token logic for clarity +contract GasEscrow is IGasEscrow, GasEscrowStorage, Initializable, Ownable { + error NotGasAccountManager(); + error NotActive(); + error NoEscrow(); + + modifier onlyGasAccountManager() { + if (msg.sender != gasAccountManager) revert NotGasAccountManager(); + _; + } + + constructor() { + _disableInitializers(); + } + + function initialize(address owner_, address gasAccountManager_) external reinitializer(1) { + _setOwner(owner_); + gasAccountManager = gasAccountManager_; + } + + /// @notice Escrow gas for a payload + function escrowGas( + bytes32 payloadId_, + address consumeFrom_, + uint256 amount_ + ) external onlyGasAccountManager { + accountEscrow[consumeFrom_] += amount_; + + uint256 amount = amount_; + // todo: remove release status and this line + if (payloadEscrow[payloadId_].amount > 0) amount += payloadEscrow[payloadId_].amount; + payloadEscrow[payloadId_] = EscrowEntry({ + account: consumeFrom_, + amount: amount, + state: EscrowState.Active + }); + + emit GasEscrowed(payloadId_, consumeFrom_, amount_); + } + + /// @notice Release escrow back to account, cases where payload is not executed + function releaseEscrow(bytes32 payloadId) external onlyGasAccountManager { + EscrowEntry storage entry = payloadEscrow[payloadId]; + if (entry.amount == 0) return; + if (entry.state != EscrowState.Active) revert NotActive(); + + accountEscrow[entry.account] -= entry.amount; + entry.state = EscrowState.Released; + emit EscrowReleased(payloadId, entry.account); + } + + /// @notice Mark escrow as settled (paid to transmitter) + // todo: what are final states? when part amount is used, how to settle+settle+release work + function settleGasPayment( + bytes32 payloadId, + address transmitter, + uint256 amount + ) external onlyGasAccountManager { + EscrowEntry storage entry = payloadEscrow[payloadId]; + if (entry.state != EscrowState.Active) revert NotActive(); + if (entry.amount == 0) revert NoEscrow(); + + accountEscrow[entry.account] -= amount; + entry.amount -= amount; + + if (entry.amount == 0) entry.state = EscrowState.Settled; + emit EscrowSettled(payloadId, entry.account, transmitter, amount); + } + + /// @notice Get total escrowed amount for an account + function getEscrowedAmount(address account) external view returns (uint256) { + return accountEscrow[account]; + } + + /// @notice Get payload escrow details + function getPayloadEscrow(bytes32 payloadId) external view returns (EscrowEntry memory) { + return payloadEscrow[payloadId]; + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds( + address token_, + address rescueTo_, + uint256 amount_ + ) external onlyGasAccountManager { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } +} diff --git a/contracts/evmx/fees/FeesPool.sol b/contracts/evmx/fees/GasVault.sol similarity index 72% rename from contracts/evmx/fees/FeesPool.sol rename to contracts/evmx/fees/GasVault.sol index f7aeb73d..8ef9b5e5 100644 --- a/contracts/evmx/fees/FeesPool.sol +++ b/contracts/evmx/fees/GasVault.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "../../utils/AccessControl.sol"; import "../../utils/common/AccessRoles.sol"; -import "../interfaces/IFeesPool.sol"; +import "../interfaces/IGasVault.sol"; import "solady/utils/SafeTransferLib.sol"; /** - * @title FeesPool + * @title GasVault * @notice Contract to store native fees that can be pulled by fees manager */ -contract FeesPool is IFeesPool, AccessControl { +contract GasVault is IGasVault, AccessControl { error TransferFailed(); + event VaultDeposit(address indexed from, uint256 amount); /** * @param owner_ The address of the owner */ constructor(address owner_) { _setOwner(owner_); - // to rescue funds - _grantRole(FEE_MANAGER_ROLE, owner_); + _grantRole(GAS_MANAGER_ROLE, owner_); } /** @@ -32,7 +32,7 @@ contract FeesPool is IFeesPool, AccessControl { function withdraw( address to_, uint256 amount_ - ) external onlyRole(FEE_MANAGER_ROLE) returns (bool success) { + ) external onlyRole(GAS_MANAGER_ROLE) returns (bool success) { if (amount_ == 0) return true; success = SafeTransferLib.trySafeTransferETH(to_, amount_, gasleft()); emit NativeWithdrawn(success, to_, amount_); @@ -41,11 +41,11 @@ contract FeesPool is IFeesPool, AccessControl { /** * @notice Returns the current balance of native tokens in the pool */ - function getBalance() external view returns (uint256) { + function vaultBalance() external view returns (uint256) { return address(this).balance; } receive() external payable { - emit NativeDeposited(msg.sender, msg.value); + emit VaultDeposit(msg.sender, msg.value); } } diff --git a/contracts/evmx/fees/MessageResolver.sol b/contracts/evmx/fees/MessageResolver.sol new file mode 100644 index 00000000..bc98f063 --- /dev/null +++ b/contracts/evmx/fees/MessageResolver.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "solady/utils/Initializable.sol"; +import "solady/auth/Ownable.sol"; +import {ECDSA} from "solady/utils/ECDSA.sol"; +import {WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; +import {toBytes32Format} from "../../utils/common/Converters.sol"; +import "../../utils/AccessControl.sol"; +import "../helpers/AddressResolverUtil.sol"; + +/** + * @title MessageResolver Storage + * @notice Storage contract for MessageResolver with proper slot management + */ +abstract contract MessageResolverStorage { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + // slot 50 + /// @notice Chain slug for EVMx + uint32 public evmxChainSlug; + + // Input struct for adding message details + struct MessageDetailsInput { + bytes32 payloadId; + uint32 srcChainSlug; + uint32 dstChainSlug; + bytes32 srcPlug; + bytes32 dstPlug; + uint256 deadline; + address sponsor; + address transmitter; + uint256 feeAmount; + uint256 nonce; + } + + // Struct to store message details + struct MessageDetails { + uint32 srcChainSlug; + uint32 dstChainSlug; + bytes32 srcPlug; + bytes32 dstPlug; + uint256 deadline; + address sponsor; + address transmitter; + uint256 feeAmount; + MessageStatus status; + } + + // Execution status enum + enum MessageStatus { + NotAdded, // Message not yet added + Pending, // Message added, awaiting execution + Executed // Payment completed + } + + // slot 51 + /// @notice Mapping from payloadId to message details + mapping(bytes32 => MessageDetails) public messageDetails; + + // slot 52 + /// @notice Mapping to track used nonces for watcher signatures + mapping(address => mapping(uint256 => bool)) public usedNonces; + + // slots [53-102] reserved for gap + uint256[50] _gap_after; + + // slots [103-152] 50 slots reserved for address resolver util +} + +/** + * @title MessageResolver + * @notice Contract for resolving payments to transmitters for relaying messages on EVMx + * @dev This contract tracks message details and handles payment settlement after execution + * @dev Uses Gas Fees (ERC20) from GasAccountManager for payment settlement + * @dev Upgradeable proxy pattern with AddressResolverUtil + */ +contract MessageResolver is + MessageResolverStorage, + Initializable, + AccessControl, + AddressResolverUtil +{ + //////////////////////////////////////////////////////// + ////////////////////// ERRORS ////////////////////////// + //////////////////////////////////////////////////////// + + /// @notice Thrown when watcher is not authorized + error UnauthorizedWatcher(); + + /// @notice Thrown when nonce has already been used + error NonceAlreadyUsed(); + + /// @notice Thrown when message is already added + error MessageAlreadyExists(); + + /// @notice Thrown when message is not found + error MessageNotFound(); + + /// @notice Thrown when message is not in pending status + error MessageNotPending(); + + /// @notice Thrown when payment transfer fails + error PaymentFailed(); + + /// @notice Thrown when sponsor has insufficient gas + error InsufficientSponsorGas(); + + //////////////////////////////////////////////////////// + ////////////////////// EVENTS ////////////////////////// + //////////////////////////////////////////////////////// + + /// @notice Emitted when message details are added + event MessageDetailsAdded( + bytes32 indexed payloadId, + uint32 srcChainSlug, + uint32 dstChainSlug, + bytes32 srcPlug, + bytes32 dstPlug, + address indexed sponsor, + address indexed transmitter, + uint256 feeAmount, + uint256 deadline + ); + + /// @notice Emitted when transmitter is paid + event TransmitterPaid( + bytes32 indexed payloadId, + address indexed sponsor, + address indexed transmitter, + uint256 feeAmount + ); + + /// @notice Emitted when message is marked as executed by watcher + event MessageMarkedExecuted(bytes32 indexed payloadId, address indexed watcher); + + //////////////////////////////////////////////////////// + ////////////////////// CONSTRUCTOR ///////////////////// + //////////////////////////////////////////////////////// + + constructor() { + _disableInitializers(); // disable for implementation + } + + /** + * @notice Initializer function to replace constructor for upgradeable contracts + * @param evmxChainSlug_ Chain slug for EVMx + * @param addressResolver_ AddressResolver contract address + * @param owner_ Owner of the contract + */ + function initialize( + uint32 evmxChainSlug_, + address addressResolver_, + address owner_ + ) public reinitializer(1) { + evmxChainSlug = evmxChainSlug_; + _setAddressResolver(addressResolver_); + _initializeOwner(owner_); + } + + //////////////////////////////////////////////////////// + ////////////////////// FUNCTIONS /////////////////////// + //////////////////////////////////////////////////////// + + /** + * @notice Add message details for payment resolution + * @dev Called with watcher signature to update details from MessageOutbound event + * @dev Can be routed through watcher for common nonce tracking if needed + * @param input_ Message details input struct + * @param signature_ Watcher signature + */ + function addMessageDetails( + MessageDetailsInput calldata input_, + bytes calldata signature_ + ) external { + // Verify message doesn't already exist + if (messageDetails[input_.payloadId].status != MessageStatus.NotAdded) { + revert MessageAlreadyExists(); + } + + // Create digest for signature verification + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(this)), + evmxChainSlug, + input_.payloadId, + input_.srcChainSlug, + input_.dstChainSlug, + input_.srcPlug, + input_.dstPlug, + input_.deadline, + input_.sponsor, + input_.transmitter, + input_.feeAmount, + input_.nonce + ) + ); + + // Recover signer from signature + address watcher = _recoverSigner(digest, signature_); + + // Verify signer has WATCHER_ROLE + if (!_hasRole(WATCHER_ROLE, watcher)) revert UnauthorizedWatcher(); + + // Check nonce hasn't been used + if (usedNonces[watcher][input_.nonce]) revert NonceAlreadyUsed(); + usedNonces[watcher][input_.nonce] = true; + + // Store message details + messageDetails[input_.payloadId] = MessageDetails({ + srcChainSlug: input_.srcChainSlug, + dstChainSlug: input_.dstChainSlug, + srcPlug: input_.srcPlug, + dstPlug: input_.dstPlug, + deadline: input_.deadline, + sponsor: input_.sponsor, + transmitter: input_.transmitter, + feeAmount: input_.feeAmount, + status: MessageStatus.Pending + }); + + emit MessageDetailsAdded( + input_.payloadId, + input_.srcChainSlug, + input_.dstChainSlug, + input_.srcPlug, + input_.dstPlug, + input_.sponsor, + input_.transmitter, + input_.feeAmount, + input_.deadline + ); + } + + /** + * @notice Mark message as executed and pay transmitter + * @dev Called by watcher after confirming execution on destination + * @dev Uses Gas Fees from GasAccountManager for payment + * @param payloadId_ Unique identifier for the payload + * @param signature_ Watcher signature confirming execution + * @param nonce_ Nonce to prevent replay attacks + */ + function markExecuted(bytes32 payloadId_, uint256 nonce_, bytes calldata signature_) external { + MessageDetails storage details = messageDetails[payloadId_]; + + // Verify message exists + if (details.status == MessageStatus.NotAdded) revert MessageNotFound(); + + // Verify message is in pending status + if (details.status != MessageStatus.Pending) revert MessageNotPending(); + + // Create digest for signature verification + bytes32 digest = keccak256( + abi.encodePacked(toBytes32Format(address(this)), evmxChainSlug, payloadId_, nonce_) + ); + + // Recover signer from signature + address watcher = _recoverSigner(digest, signature_); + + // Verify signer has WATCHER_ROLE + if (!_hasRole(WATCHER_ROLE, watcher)) revert UnauthorizedWatcher(); + + // Check nonce hasn't been used + if (usedNonces[watcher][nonce_]) revert NonceAlreadyUsed(); + usedNonces[watcher][nonce_] = true; + + // Check sponsor has sufficient gas (uses AddressResolver to get latest GasAccountManager) + if ( + !gasAccountToken__().isGasAvailable(details.sponsor, address(this), details.feeAmount) + ) { + revert InsufficientSponsorGas(); + } + + // Mark message as executed + details.status = MessageStatus.Executed; + + // Transfer gas from sponsor to transmitter using GasAccountManager from AddressResolver + bool success = gasAccountToken__().transferFrom( + details.sponsor, + details.transmitter, + details.feeAmount + ); + if (!success) revert PaymentFailed(); + + emit MessageMarkedExecuted(payloadId_, watcher); + emit TransmitterPaid(payloadId_, details.sponsor, details.transmitter, details.feeAmount); + } + + //////////////////////////////////////////////////////// + ////////////////// INTERNAL FUNCTIONS ////////////////// + //////////////////////////////////////////////////////// + + /** + * @notice Recover signer from signature + * @param digest_ The digest that was signed + * @param signature_ The signature + * @return signer The address of the signer + */ + function _recoverSigner( + bytes32 digest_, + bytes memory signature_ + ) internal view returns (address signer) { + bytes32 ethSignedMessageHash = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_) + ); + signer = ECDSA.recover(ethSignedMessageHash, signature_); + } + + //////////////////////////////////////////////////////// + ////////////////// VIEW FUNCTIONS ////////////////////// + //////////////////////////////////////////////////////// + + /** + * @notice Get message details for a payload + * @param payloadId_ Unique identifier for the payload + * @return Message details struct + */ + function getMessageDetails(bytes32 payloadId_) external view returns (MessageDetails memory) { + return messageDetails[payloadId_]; + } + + /** + * @notice Check if a message is pending + * @param payloadId_ Unique identifier for the payload + * @return True if message is pending execution + */ + function isMessagePending(bytes32 payloadId_) external view returns (bool) { + return messageDetails[payloadId_].status == MessageStatus.Pending; + } + + /** + * @notice Check if a message is executed + * @param payloadId_ Unique identifier for the payload + * @return True if message is executed and payment completed + */ + function isMessageExecuted(bytes32 payloadId_) external view returns (bool) { + return messageDetails[payloadId_].status == MessageStatus.Executed; + } + + /** + * @notice Get pending fee amount for a payload + * @param payloadId_ Unique identifier for the payload + * @return Fee amount if pending, 0 otherwise + */ + function getPendingFeeAmount(bytes32 payloadId_) external view returns (uint256) { + MessageDetails memory details = messageDetails[payloadId_]; + if (details.status == MessageStatus.Pending) { + return details.feeAmount; + } + return 0; + } + + /** + * @notice Get execution status for a payload + * @param payloadId_ Unique identifier for the payload + * @return MessageStatus enum value + */ + function getMessageStatus(bytes32 payloadId_) external view returns (MessageStatus) { + return messageDetails[payloadId_].status; + } +} diff --git a/contracts/evmx/helpers/AddressResolver.sol b/contracts/evmx/helpers/AddressResolver.sol index f11c1a10..62acdf70 100644 --- a/contracts/evmx/helpers/AddressResolver.sol +++ b/contracts/evmx/helpers/AddressResolver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Initializable} from "solady/utils/Initializable.sol"; import "solady/auth/Ownable.sol"; @@ -15,16 +15,19 @@ abstract contract AddressResolverStorage is IAddressResolver { IWatcher public override watcher__; // slot 51 - IFeesManager public override feesManager__; + IGasAccountManager public override gasAccountManager__; // slot 52 IAsyncDeployer public override asyncDeployer__; // slot 53 - IDeployForwarder public override deployForwarder__; + IGasVault public override gasVault__; // slot 54 - address public override defaultAuctionManager; + IGasEscrow public override gasEscrow__; + + // slot 55 + IGasAccountToken public override gasAccountToken__; // slot 55 mapping(bytes32 => address) public override contractAddresses; @@ -58,35 +61,21 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { /// @param watcher_ The address of the watcher contract function setWatcher(address watcher_) external override onlyOwner { watcher__ = IWatcher(watcher_); - emit WatcherUpdated(watcher_); + emit WatcherSet(watcher_); } /// @notice Updates the address of the fees manager - /// @param feesManager_ The address of the fees manager - function setFeesManager(address feesManager_) external override onlyOwner { - feesManager__ = IFeesManager(feesManager_); - emit FeesManagerUpdated(feesManager_); + /// @param gasAccountManager_ The address of the fees manager + function setGasAccountManager(address gasAccountManager_) external override onlyOwner { + gasAccountManager__ = IGasAccountManager(gasAccountManager_); + emit GasAccountManagerSet(gasAccountManager_); } /// @notice Updates the address of the async deployer /// @param asyncDeployer_ The address of the async deployer function setAsyncDeployer(address asyncDeployer_) external override onlyOwner { asyncDeployer__ = IAsyncDeployer(asyncDeployer_); - emit AsyncDeployerUpdated(asyncDeployer_); - } - - /// @notice Updates the address of the default auction manager - /// @param defaultAuctionManager_ The address of the default auction manager - function setDefaultAuctionManager(address defaultAuctionManager_) external override onlyOwner { - defaultAuctionManager = defaultAuctionManager_; - emit DefaultAuctionManagerUpdated(defaultAuctionManager_); - } - - /// @notice Updates the address of the deploy forwarder - /// @param deployForwarder_ The address of the deploy forwarder - function setDeployForwarder(address deployForwarder_) external override onlyOwner { - deployForwarder__ = IDeployForwarder(deployForwarder_); - emit DeployForwarderUpdated(deployForwarder_); + emit AsyncDeployerSet(asyncDeployer_); } /// @notice Updates the address of a contract @@ -97,7 +86,28 @@ contract AddressResolver is AddressResolverStorage, Initializable, Ownable { address contractAddress_ ) external override onlyOwner { contractAddresses[contractId_] = contractAddress_; - emit ContractAddressUpdated(contractId_, contractAddress_); + emit ContractAddressSet(contractId_, contractAddress_); + } + + /// @notice Updates the address of the gas vault + /// @param gasVault_ The address of the gas vault + function setGasVault(address gasVault_) external override onlyOwner { + gasVault__ = IGasVault(gasVault_); + emit GasVaultSet(gasVault_); + } + + /// @notice Updates the address of the gas escrow + /// @param gasEscrow_ The address of the gas escrow + function setGasEscrow(address gasEscrow_) external override onlyOwner { + gasEscrow__ = IGasEscrow(gasEscrow_); + emit GasEscrowSet(gasEscrow_); + } + + /// @notice Updates the address of the gas account token + /// @param gasAccountToken_ The address of the gas account token + function setGasAccountToken(address gasAccountToken_) external override onlyOwner { + gasAccountToken__ = IGasAccountToken(gasAccountToken_); + emit GasAccountTokenSet(gasAccountToken_); } /** diff --git a/contracts/evmx/helpers/AddressResolverUtil.sol b/contracts/evmx/helpers/AddressResolverUtil.sol index 66663c7d..00f44e20 100644 --- a/contracts/evmx/helpers/AddressResolverUtil.sol +++ b/contracts/evmx/helpers/AddressResolverUtil.sol @@ -1,10 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "../interfaces/IAddressResolver.sol"; import "../interfaces/IWatcher.sol"; -import "../interfaces/IFeesManager.sol"; +import "../interfaces/IGasAccountManager.sol"; import "../interfaces/IAsyncDeployer.sol"; +import "../interfaces/IGasVault.sol"; +import "../interfaces/IGasEscrow.sol"; +import "../interfaces/IGasAccountToken.sol"; + import {OnlyWatcherAllowed} from "../../utils/common/Errors.sol"; /// @title AddressResolverUtil @@ -22,12 +26,15 @@ abstract contract AddressResolverUtil { /// @notice Restricts function access to the watcher precompile contract /// @dev Validates that msg.sender matches the registered watcher precompile address modifier onlyWatcher() { - if (!_isWatcher(msg.sender)) revert OnlyWatcherAllowed(); + if (!isWatcher()) revert OnlyWatcherAllowed(); _; } - function _isWatcher(address account_) internal view returns (bool) { - return (watcher__().isWatcher(account_) || account_ == address(watcher__())); + /// @notice Restricts function access to the watcher owner + function isWatcher() internal view returns (bool) { + return + msg.sender == address(watcher__()) || + msg.sender == IWatcherOwner(address(watcher__())).owner(); } /// @notice Gets the watcher precompile contract interface @@ -40,8 +47,29 @@ abstract contract AddressResolverUtil { /// @notice Gets the watcher precompile contract interface /// @return IWatcher interface of the registered watcher precompile /// @dev Resolves and returns the watcher precompile contract for interaction - function feesManager__() public view returns (IFeesManager) { - return addressResolver__.feesManager__(); + function gasAccountManager__() public view returns (IGasAccountManager) { + return addressResolver__.gasAccountManager__(); + } + + /// @notice Gets the gas account manager contract interface + /// @return IGasAccountManager interface of the registered gas account manager + /// @dev Resolves and returns the gas account manager contract for interaction + function gasVault__() public view returns (IGasVault) { + return addressResolver__.gasVault__(); + } + + /// @notice Gets the gas vault contract interface + /// @return IGasVault interface of the registered gas vault + /// @dev Resolves and returns the gas vault contract for interaction + function gasEscrow__() public view returns (IGasEscrow) { + return addressResolver__.gasEscrow__(); + } + + /// @notice Gets the gas escrow contract interface + /// @return IGasEscrow interface of the registered gas escrow + /// @dev Resolves and returns the gas escrow contract for interaction + function gasAccountToken__() public view returns (IGasAccountToken) { + return addressResolver__.gasAccountToken__(); } /// @notice Gets the async deployer contract interface @@ -51,13 +79,6 @@ abstract contract AddressResolverUtil { return addressResolver__.asyncDeployer__(); } - /// @notice Gets the deploy forwarder contract interface - /// @return IDeployForwarder interface of the registered deploy forwarder - /// @dev Resolves and returns the deploy forwarder contract for interaction - function deployForwarder__() public view returns (IDeployForwarder) { - return addressResolver__.deployForwarder__(); - } - /// @notice Internal function to set the address resolver /// @param _addressResolver The address of the resolver contract /// @dev Should be called in the initialization of inheriting contracts diff --git a/contracts/evmx/helpers/AsyncDeployer.sol b/contracts/evmx/helpers/AsyncDeployer.sol index da19b34d..530006ec 100644 --- a/contracts/evmx/helpers/AsyncDeployer.sol +++ b/contracts/evmx/helpers/AsyncDeployer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {LibClone} from "solady/utils/LibClone.sol"; import {UpgradeableBeacon} from "solady/utils/UpgradeableBeacon.sol"; @@ -31,6 +31,9 @@ abstract contract AsyncDeployerStorage is IAsyncDeployer { // slot 54 uint256 public asyncPromiseCounter; + // slot 55 + uint256 public defaultDeadline; + // slots [55-104] reserved for gap uint256[50] _gap_after; @@ -50,9 +53,14 @@ contract AsyncDeployer is AsyncDeployerStorage, Initializable, AddressResolverUt /// @dev it deploys the forwarder and async promise implementations and beacons for them /// @dev this contract is owner of the beacons for upgrading later /// @param owner_ The address of the contract owner - function initialize(address owner_, address addressResolver_) public reinitializer(1) { + function initialize( + address owner_, + address addressResolver_, + uint256 defaultDeadline_ + ) public reinitializer(1) { _initializeOwner(owner_); _setAddressResolver(addressResolver_); + defaultDeadline = defaultDeadline_; forwarderImplementation = address(new Forwarder()); asyncPromiseImplementation = address(new AsyncPromise()); @@ -98,10 +106,10 @@ contract AsyncDeployer is AsyncDeployerStorage, Initializable, AddressResolverUt /// @return newAsyncPromise The address of the deployed AsyncPromise proxy contract function deployAsyncPromiseContract( address invoker_, - uint40 requestCount_ + bytes32 payloadId_ ) external override onlyWatcher returns (address newAsyncPromise) { // creates init data and salt - (bytes32 salt, bytes memory initData) = _createAsyncPromiseParams(invoker_, requestCount_); + (bytes32 salt, bytes memory initData) = _createAsyncPromiseParams(invoker_, payloadId_); asyncPromiseCounter++; // deploys the proxy @@ -131,20 +139,17 @@ contract AsyncDeployer is AsyncDeployerStorage, Initializable, AddressResolverUt function _createAsyncPromiseParams( address invoker_, - uint40 requestCount_ + bytes32 payloadId_ ) internal view returns (bytes32 salt, bytes memory initData) { - bytes memory constructorArgs = abi.encode( - requestCount_, - invoker_, - address(addressResolver__) - ); + bytes memory constructorArgs = abi.encode(payloadId_, invoker_, address(addressResolver__)); // creates init data initData = abi.encodeWithSelector( AsyncPromise.initialize.selector, - requestCount_, + payloadId_, invoker_, - address(addressResolver__) + address(addressResolver__), + defaultDeadline ); // creates salt with a counter @@ -183,9 +188,9 @@ contract AsyncDeployer is AsyncDeployerStorage, Initializable, AddressResolverUt /// @return The predicted address of the AsyncPromise proxy contract function getAsyncPromiseAddress( address invoker_, - uint40 requestCount_ + bytes32 payloadId_ ) public view override returns (address) { - (bytes32 salt, ) = _createAsyncPromiseParams(invoker_, requestCount_); + (bytes32 salt, ) = _createAsyncPromiseParams(invoker_, payloadId_); return _predictProxyAddress(salt, address(asyncPromiseBeacon)); } diff --git a/contracts/evmx/helpers/AsyncPromise.sol b/contracts/evmx/helpers/AsyncPromise.sol index 3f140c48..84088fb1 100644 --- a/contracts/evmx/helpers/AsyncPromise.sol +++ b/contracts/evmx/helpers/AsyncPromise.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Initializable} from "solady/utils/Initializable.sol"; import {LibCall} from "solady/utils/LibCall.sol"; @@ -7,7 +7,7 @@ import {AddressResolverUtil} from "./AddressResolverUtil.sol"; import {IAppGateway} from "../interfaces/IAppGateway.sol"; import "../interfaces/IPromise.sol"; import "../../utils/RescueFundsLib.sol"; -import {NotInvoker, RequestCountMismatch} from "../../utils/common/Errors.sol"; +import {NotInvoker, PayloadCountMismatch, DeadlinePassed} from "../../utils/common/Errors.sol"; abstract contract AsyncPromiseStorage is IPromise { // slots [0-49] reserved for gap @@ -23,13 +23,16 @@ abstract contract AsyncPromiseStorage is IPromise { /// @notice The current state of the async promise. AsyncPromiseState public override state; - /// @notice The request count of the promise - uint40 public override requestCount; + /// @notice The payload count of the promise + bytes32 public override payloadId; /// @notice The local contract which initiated the call. /// @dev The callback will be executed on this address address public override localInvoker; + /// @notice The deadline timestamp (seconds since epoch) for promise resolution + uint256 public promiseDeadline; + // slot 51 /// @notice The return data of the promise bytes public override returnData; @@ -60,6 +63,7 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil /// @notice Error thrown when attempting to resolve an already resolved promise. error PromiseAlreadyResolved(); + /// @notice Only the local invoker can set then's promise callback error OnlyInvoker(); /// @notice Error thrown when attempting to set an already existing promise @@ -74,16 +78,19 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil } /// @notice Initialize promise states + /// @param payloadId_ The payload id of the promise /// @param invoker_ The address of the local invoker /// @param addressResolver_ The address resolver contract address function initialize( - uint40 requestCount_, + bytes32 payloadId_, address invoker_, - address addressResolver_ + address addressResolver_, + uint256 deadline_ ) public reinitializer(1) { localInvoker = invoker_; - requestCount = requestCount_; + payloadId = payloadId_; _setAddressResolver(addressResolver_); + promiseDeadline = deadline_ + block.timestamp; } /// @notice Marks the promise as resolved and executes the callback if set. @@ -92,11 +99,9 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil function markResolved( PromiseReturnData memory resolvedPromise_ ) external override onlyWatcher returns (bool success) { - if ( - state == AsyncPromiseState.CALLBACK_REVERTING || - state == AsyncPromiseState.ONCHAIN_REVERTING || - state == AsyncPromiseState.RESOLVED - ) revert PromiseAlreadyResolved(); + if (block.timestamp > promiseDeadline) revert DeadlinePassed(); + if (state == AsyncPromiseState.RESOLVED || state == AsyncPromiseState.ONCHAIN_REVERTING) + revert PromiseAlreadyResolved(); state = AsyncPromiseState.RESOLVED; // Call callback to app gateway @@ -124,11 +129,9 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil function markOnchainRevert( PromiseReturnData memory resolvedPromise_ ) external override onlyWatcher { - if ( - state == AsyncPromiseState.CALLBACK_REVERTING || - state == AsyncPromiseState.ONCHAIN_REVERTING || - state == AsyncPromiseState.RESOLVED - ) revert PromiseAlreadyResolved(); + if (block.timestamp > promiseDeadline) revert DeadlinePassed(); + if (state == AsyncPromiseState.RESOLVED || state == AsyncPromiseState.ONCHAIN_REVERTING) + revert PromiseAlreadyResolved(); // to update the state in case selector is bytes(0) but reverting onchain state = AsyncPromiseState.ONCHAIN_REVERTING; @@ -149,7 +152,6 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil (bool success, , ) = localInvoker.tryCall(0, gasleft(), 0, combinedCalldata); if (!success) { - // todo: in this case, promise will stay unresolved revert PromiseRevertFailed(); } } @@ -177,10 +179,10 @@ contract AsyncPromise is AsyncPromiseStorage, Initializable, AddressResolverUtil state = AsyncPromiseState.WAITING_FOR_CALLBACK_EXECUTION; } - function _validate() internal { + function _validate() internal view { if (msg.sender != localInvoker) revert NotInvoker(); if (watcher__().latestAsyncPromise() != address(this)) revert NotLatestPromise(); - if (requestCount != watcher__().getCurrentRequestCount()) revert RequestCountMismatch(); + if (payloadId != watcher__().currentPayloadId()) revert PayloadCountMismatch(); } /** diff --git a/contracts/evmx/helpers/Forwarder.sol b/contracts/evmx/helpers/Forwarder.sol index eff4d8dc..82999448 100644 --- a/contracts/evmx/helpers/Forwarder.sol +++ b/contracts/evmx/helpers/Forwarder.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "solady/utils/Initializable.sol"; import "./AddressResolverUtil.sol"; import "../interfaces/IAppGateway.sol"; import "../interfaces/IForwarder.sol"; -import {QueueParams, OverrideParams, Transaction} from "../../utils/common/Structs.sol"; +import {RawPayload, OverrideParams, Transaction} from "../../utils/common/Structs.sol"; import {AsyncModifierNotSet, WatcherNotSet, InvalidOnChainAddress} from "../../utils/common/Errors.sol"; import "../../utils/RescueFundsLib.sol"; @@ -87,18 +87,16 @@ contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { if (!isAsyncModifierSet) revert AsyncModifierNotSet(); // fetch the override params from app gateway - (OverrideParams memory overrideParams, bytes32 sbType) = IAppGateway(msgSender) - .getOverrideParams(); + OverrideParams memory overrideParams = IAppGateway(msgSender).getOverrideParams(); // Queue the call in the middleware. - QueueParams memory queueParams; - queueParams.overrideParams = overrideParams; - queueParams.transaction = Transaction({ + RawPayload memory rawPayload; + rawPayload.overrideParams = overrideParams; + rawPayload.transaction = Transaction({ chainSlug: chainSlug, target: getOnChainAddress(), payload: msg.data }); - queueParams.switchboardType = sbType; - watcher__().queue(queueParams, msgSender); + watcher__().addPayloadData(rawPayload, msgSender); } } diff --git a/contracts/evmx/helpers/ForwarderSolana.sol b/contracts/evmx/helpers/ForwarderSolana.sol new file mode 100644 index 00000000..f0bec42e --- /dev/null +++ b/contracts/evmx/helpers/ForwarderSolana.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "solady/utils/Initializable.sol"; +import "./AddressResolverUtil.sol"; +import "../interfaces/IAppGateway.sol"; +import "../interfaces/IForwarder.sol"; +import {RawPayload, OverrideParams, Transaction} from "../../utils/common/Structs.sol"; +import {AsyncModifierNotSet, WatcherNotSet, InvalidOnChainAddress} from "../../utils/common/Errors.sol"; +import "../../utils/RescueFundsLib.sol"; +import {toBytes32Format} from "../../utils/common/Converters.sol"; +import {SolanaInstruction} from "../../utils/common/Structs.sol"; +import {CHAIN_SLUG_SOLANA_MAINNET, CHAIN_SLUG_SOLANA_DEVNET} from "../../utils/common/Constants.sol"; +import {ForwarderStorage} from "./Forwarder.sol"; + +/// @title Forwarder Contract +/// @notice This contract acts as a forwarder for async calls to the on-chain contracts. +contract ForwarderSolana is ForwarderStorage, Initializable, AddressResolverUtil { + error InvalidSolanaChainSlug(); + error AddressResolverNotSet(); + error NotUsedForSolana(); + + constructor() { + _disableInitializers(); // disable for implementation + } + + /// @notice Initializer to replace constructor for upgradeable contracts + /// @dev We do not need to store the onChainAddress as Solana forwarder is a singleton + /// that handles calls to multiple Solana contracts. + /// @param chainSlug_ chain slug on which the contract is deployed + /// @param addressResolver_ address resolver contract + function initialize(uint32 chainSlug_, address addressResolver_) public initializer { + if (chainSlug_ == CHAIN_SLUG_SOLANA_MAINNET || chainSlug_ == CHAIN_SLUG_SOLANA_DEVNET) { + chainSlug = chainSlug_; + } else { + revert InvalidSolanaChainSlug(); + } + _setAddressResolver(addressResolver_); + } + + /// @notice Returns the on-chain address associated with this forwarder. + /// @return The on-chain address. + function getOnChainAddress() public pure override returns (bytes32) { + revert NotUsedForSolana(); + } + + /// @dev We do not need to store the onChainAddress as Solana forwarder is a singleton + /// that handles calls to multiple Solana contracts. + function getChainSlug() external view returns (uint32) { + return chainSlug; + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } + + /// @notice Fallback function to process the contract calls to onChainAddress + /// @dev It queues the calls in the middleware and deploys the promise contract + function callSolana(bytes memory solanaPayload, bytes32 targetContract) external { + if (address(watcher__()) == address(0)) { + revert WatcherNotSet(); + } + + // validates if the async modifier is set + address msgSender = msg.sender; + bool isAsyncModifierSet = IAppGateway(msgSender).isAsyncModifierSet(); + if (!isAsyncModifierSet) revert AsyncModifierNotSet(); + + // fetch the override params from app gateway + OverrideParams memory overrideParams = IAppGateway(msgSender).getOverrideParams(); + + // Queue the call in the middleware. + RawPayload memory rawPayload; + rawPayload.overrideParams = overrideParams; + rawPayload.transaction = Transaction({ + chainSlug: chainSlug, + target: targetContract, + payload: solanaPayload + }); + watcher__().addPayloadData(rawPayload, msgSender); + } +} diff --git a/contracts/evmx/helpers/solana-utils/Ed25519.sol b/contracts/evmx/helpers/solana-utils/Ed25519.sol new file mode 100644 index 00000000..75fcd5d2 --- /dev/null +++ b/contracts/evmx/helpers/solana-utils/Ed25519.sol @@ -0,0 +1,906 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.28; + +import "./Sha512.sol"; +import "./Ed25519_pow.sol"; + +library Ed25519 { + function verify(bytes32 k, bytes32 r, bytes32 s, bytes memory m) internal pure returns (bool) { + unchecked { + uint256 hh; + // Step 1: compute SHA-512(R, A, M) + { + bytes memory rs = new bytes(k.length + r.length + m.length); + for (uint256 i = 0; i < r.length; i++) { + rs[i] = r[i]; + } + for (uint256 i = 0; i < k.length; i++) { + rs[i + 32] = k[i]; + } + for (uint256 i = 0; i < m.length; i++) { + rs[i + 64] = m[i]; + } + uint64[8] memory result = Sha512.hash(rs); + + uint256 h0 = uint256(result[0]) | + (uint256(result[1]) << 64) | + (uint256(result[2]) << 128) | + (uint256(result[3]) << 192); + + h0 = + ((h0 & + 0xff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff) << + 8) | + ((h0 & + 0xff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00) >> + 8); + h0 = + ((h0 & 0xffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff) << + 16) | + ((h0 & + 0xffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000) >> + 16); + h0 = + ((h0 & 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff) << + 32) | + ((h0 & + 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff_00000000) >> + 32); + + uint256 h1 = uint256(result[4]) | + (uint256(result[5]) << 64) | + (uint256(result[6]) << 128) | + (uint256(result[7]) << 192); + + h1 = + ((h1 & + 0xff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff) << + 8) | + ((h1 & + 0xff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00) >> + 8); + h1 = + ((h1 & 0xffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff) << + 16) | + ((h1 & + 0xffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000) >> + 16); + h1 = + ((h1 & 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff) << + 32) | + ((h1 & + 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff_00000000) >> + 32); + hh = addmod( + h0, + mulmod( + h1, + 0xfffffff_ffffffff_ffffffff_fffffffe_c6ef5bf4_737dcf70_d6ec3174_8d98951d, + 0x10000000_00000000_00000000_00000000_14def9de_a2f79cd6_5812631a_5cf5d3ed + ), + 0x10000000_00000000_00000000_00000000_14def9de_a2f79cd6_5812631a_5cf5d3ed + ); + } + // Step 2: unpack k + k = bytes32( + ((uint256(k) & + 0xff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff) << 8) | + ((uint256(k) & + 0xff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00) >> + 8) + ); + k = bytes32( + ((uint256(k) & + 0xffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff) << 16) | + ((uint256(k) & + 0xffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000) >> + 16) + ); + k = bytes32( + ((uint256(k) & 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff) << + 32) | + ((uint256(k) & + 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff_00000000) >> + 32) + ); + k = bytes32( + ((uint256(k) & 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff) << 64) | + ((uint256(k) & + 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff_00000000_00000000) >> + 64) + ); + k = bytes32((uint256(k) << 128) | (uint256(k) >> 128)); + uint256 ky = uint256(k) & + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff; + uint256 kx; + { + uint256 ky2 = mulmod( + ky, + ky, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 u = addmod( + ky2, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffec, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 v = mulmod( + ky2, + 0x52036cee_2b6ffe73_8cc74079_7779e898_00700a4d_4141d8ab_75eb4dca_135978a3, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ) + 1; + uint256 t = mulmod( + u, + v, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + (kx, ) = Ed25519_pow.pow22501(t); + kx = mulmod( + kx, + kx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + kx = mulmod( + u, + mulmod( + mulmod( + kx, + kx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + t, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + t = mulmod( + mulmod( + kx, + kx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + v, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + if (t != u) { + if ( + t != + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed - + u + ) { + return false; + } + kx = mulmod( + kx, + 0x2b832480_4fc1df0b_2b4d0099_3dfbd7a7_2f431806_ad2fe478_c4ee1b27_4a0ea0b0, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + } + if ((kx & 1) != uint256(k) >> 255) { + kx = 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed - kx; + } + // Verify s + s = bytes32( + ((uint256(s) & + 0xff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff) << 8) | + ((uint256(s) & + 0xff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00) >> + 8) + ); + s = bytes32( + ((uint256(s) & + 0xffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff) << 16) | + ((uint256(s) & + 0xffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000) >> + 16) + ); + s = bytes32( + ((uint256(s) & 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff) << + 32) | + ((uint256(s) & + 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff_00000000) >> + 32) + ); + s = bytes32( + ((uint256(s) & 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff) << 64) | + ((uint256(s) & + 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff_00000000_00000000) >> + 64) + ); + s = bytes32((uint256(s) << 128) | (uint256(s) >> 128)); + if ( + uint256(s) >= + 0x10000000_00000000_00000000_00000000_14def9de_a2f79cd6_5812631a_5cf5d3ed + ) { + return false; + } + uint256 vx; + uint256 vu; + uint256 vy; + uint256 vv; + // Step 3: compute multiples of k + uint256[8][3][2] memory tables; + { + uint256 ks = ky + kx; + uint256 kd = ky + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed - + kx; + uint256 k2dt = mulmod( + mulmod( + kx, + ky, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + 0x2406d9dc_56dffce7_198e80f2_eef3d130_00e0149a_8283b156_ebd69b94_26b2f159, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 kky = ky; + uint256 kkx = kx; + uint256 kku = 1; + uint256 kkv = 1; + { + uint256 xx = mulmod( + kkx, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy = mulmod( + kky, + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz = mulmod( + kku, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xx2 = mulmod( + xx, + xx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy2 = mulmod( + yy, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xxyy = mulmod( + xx, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz2 = mulmod( + zz, + zz, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + kkx = xxyy + xxyy; + kku = + yy2 - + xx2 + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + kky = xx2 + yy2; + kkv = addmod( + zz2 + zz2, + 0xffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffda - + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + { + uint256 xx = mulmod( + kkx, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy = mulmod( + kky, + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz = mulmod( + kku, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xx2 = mulmod( + xx, + xx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy2 = mulmod( + yy, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xxyy = mulmod( + xx, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz2 = mulmod( + zz, + zz, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + kkx = xxyy + xxyy; + kku = + yy2 - + xx2 + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + kky = xx2 + yy2; + kkv = addmod( + zz2 + zz2, + 0xffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffda - + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + { + uint256 xx = mulmod( + kkx, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy = mulmod( + kky, + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz = mulmod( + kku, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xx2 = mulmod( + xx, + xx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy2 = mulmod( + yy, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xxyy = mulmod( + xx, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz2 = mulmod( + zz, + zz, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + kkx = xxyy + xxyy; + kku = + yy2 - + xx2 + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + kky = xx2 + yy2; + kkv = addmod( + zz2 + zz2, + 0xffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffda - + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + uint256 cprod = 1; + uint256[8][3][2] memory tables_ = tables; + for (uint256 i = 0; ; i++) { + uint256 cs; + uint256 cd; + uint256 ct; + uint256 c2z; + { + uint256 cx = mulmod( + kkx, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 cy = mulmod( + kky, + kku, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 cz = mulmod( + kku, + kkv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + ct = mulmod( + kkx, + kky, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + cs = cy + cx; + cd = + cy - + cx + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + c2z = cz + cz; + } + tables_[1][0][i] = cs; + tables_[1][1][i] = cd; + tables_[1][2][i] = mulmod( + ct, + 0x2406d9dc_56dffce7_198e80f2_eef3d130_00e0149a_8283b156_ebd69b94_26b2f159, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + tables_[0][0][i] = c2z; + tables_[0][1][i] = cprod; + cprod = mulmod( + cprod, + c2z, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + if (i == 7) { + break; + } + uint256 ab = mulmod( + cs, + ks, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 aa = mulmod( + cd, + kd, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ac = mulmod( + ct, + k2dt, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + kkx = + ab - + aa + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + kku = addmod( + c2z, + ac, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + kky = ab + aa; + kkv = addmod( + c2z, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed - + ac, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + uint256 t; + (cprod, t) = Ed25519_pow.pow22501(cprod); + cprod = mulmod( + cprod, + cprod, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + cprod = mulmod( + cprod, + cprod, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + cprod = mulmod( + cprod, + cprod, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + cprod = mulmod( + cprod, + cprod, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + cprod = mulmod( + cprod, + cprod, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + cprod = mulmod( + cprod, + t, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + for (uint256 i = 7; ; i--) { + uint256 cinv = mulmod( + cprod, + tables_[0][1][i], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + tables_[1][0][i] = mulmod( + tables_[1][0][i], + cinv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + tables_[1][1][i] = mulmod( + tables_[1][1][i], + cinv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + tables_[1][2][i] = mulmod( + tables_[1][2][i], + cinv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + if (i == 0) { + break; + } + cprod = mulmod( + cprod, + tables_[0][0][i], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + tables_[0] = [ + [ + 0x43e7ce9d_19ea5d32_9385a44c_321ea161_67c996e3_7dc6070c_97de49e3_7ac61db9, + 0x40cff344_25d8ec30_a3bb74ba_58cd5854_fa1e3818_6ad0d31e_bc8ae251_ceb2c97e, + 0x459bd270_46e8dd45_aea7008d_b87a5a8f_79067792_53d64523_58951859_9fdfbf4b, + 0x69fdd1e2_8c23cc38_94d0c8ff_90e76f6d_5b6e4c2e_620136d0_4dd83c4a_51581ab9, + 0x54dceb34_13ce5cfa_11196dfc_960b6eda_f4b380c6_d4d23784_19cc0279_ba49c5f3, + 0x4e24184d_d71a3d77_eef3729f_7f8cf7c1_7224cf40_aa7b9548_b9942f3c_5084ceed, + 0x5a0e5aab_20262674_ae117576_1cbf5e88_9b52a55f_d7ac5027_c228cebd_c8d2360a, + 0x26239334_073e9b38_c6285955_6d451c3d_cc8d30e8_4b361174_f488eadd_e2cf17d9 + ], + [ + 0x227e97c9_4c7c0933_d2e0c21a_3447c504_fe9ccf82_e8a05f59_ce881c82_eba0489f, + 0x226a3e0e_cc4afec6_fd0d2884_13014a9d_bddecf06_c1a2f0bb_702ba77c_613d8209, + 0x34d7efc8_51d45c5e_71efeb0f_235b7946_91de6228_877569b3_a8d52bf0_58b8a4a0, + 0x3c1f5fb3_ca7166fc_e1471c9b_752b6d28_c56301ad_7b65e845_1b2c8c55_26726e12, + 0x6102416c_f02f02ff_5be75275_f55f28db_89b2a9d2_456b860c_e22fc0e5_031f7cc5, + 0x40adf677_f1bfdae0_57f0fd17_9c126179_18ddaa28_91a6530f_b1a4294f_a8665490, + 0x61936f3c_41560904_6187b8ba_a978cbc9_b4789336_3ae5a3cc_7d909f36_35ae7f48, + 0x562a9662_b6ec47f9_e979d473_c02b51e4_42336823_8c58ddb5_2f0e5c6a_180e6410 + ], + [ + 0x3788bdb4_4f8632d4_2d0dbee5_eea1acc6_136cf411_e655624f_55e48902_c3bd5534, + 0x6190cf2c_2a7b5ad7_69d594a8_2844f23b_4167fa7c_8ac30e51_aa6cfbeb_dcd4b945, + 0x65f77870_96be9204_123a71f3_ac88a87b_e1513217_737d6a1e_2f3a13a4_3d7e3a9a, + 0x23af32d_bfa67975_536479a7_a7ce74a0_2142147f_ac048018_7f1f1334_9cda1f2d, + 0x64fc44b7_fc6841bd_db0ced8b_8b0fe675_9137ef87_ee966512_15fc1dbc_d25c64dc, + 0x1434aa37_48b701d5_b69df3d7_d340c1fe_3f6b9c1e_fc617484_caadb47e_382f4475, + 0x457a6da8_c962ef35_f2b21742_3e5844e9_d2353452_7e8ea429_0d24e3dd_f21720c6, + 0x63b9540c_eb60ccb5_1e4d989d_956e053c_f2511837_efb79089_d2ff4028_4202c53d + ] + ]; + } + // Step 4: compute s*G - h*A + { + uint256 ss = uint256(s) << 3; + uint256 hhh = hh + + 0x80000000_00000000_00000000_00000000_a6f7cef5_17bce6b2_c09318d2_e7ae9f60; + uint256 vvx = 0; + uint256 vvu = 1; + uint256 vvy = 1; + uint256 vvv = 1; + for (uint256 i = 252; ; i--) { + uint256 bit = 8 << i; + if ((ss & bit) != 0) { + uint256 ws; + uint256 wd; + uint256 wz; + uint256 wt; + { + uint256 wx = mulmod( + vvx, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 wy = mulmod( + vvy, + vvu, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + ws = wy + wx; + wd = + wy - + wx + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + wz = mulmod( + vvu, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + wt = mulmod( + vvx, + vvy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + uint256 j = (ss >> i) & 7; + ss &= ~(7 << i); + uint256[8][3][2] memory tables_ = tables; + uint256 aa = mulmod( + wd, + tables_[0][1][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ab = mulmod( + ws, + tables_[0][0][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ac = mulmod( + wt, + tables_[0][2][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vvx = + ab - + aa + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + vvu = wz + ac; + vvy = ab + aa; + vvv = + wz - + ac + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + } + if ((hhh & bit) != 0) { + uint256 ws; + uint256 wd; + uint256 wz; + uint256 wt; + { + uint256 wx = mulmod( + vvx, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 wy = mulmod( + vvy, + vvu, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + ws = wy + wx; + wd = + wy - + wx + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + wz = mulmod( + vvu, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + wt = mulmod( + vvx, + vvy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + uint256 j = (hhh >> i) & 7; + hhh &= ~(7 << i); + uint256[8][3][2] memory tables_ = tables; + uint256 aa = mulmod( + wd, + tables_[1][0][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ab = mulmod( + ws, + tables_[1][1][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ac = mulmod( + wt, + tables_[1][2][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vvx = + ab - + aa + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + vvu = + wz - + ac + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + vvy = ab + aa; + vvv = wz + ac; + } + if (i == 0) { + uint256 ws; + uint256 wd; + uint256 wz; + uint256 wt; + { + uint256 wx = mulmod( + vvx, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 wy = mulmod( + vvy, + vvu, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + ws = wy + wx; + wd = + wy - + wx + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + wz = mulmod( + vvu, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + wt = mulmod( + vvx, + vvy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + uint256 j = hhh & 7; + uint256[8][3][2] memory tables_ = tables; + uint256 aa = mulmod( + wd, + tables_[1][0][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ab = mulmod( + ws, + tables_[1][1][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 ac = mulmod( + wt, + tables_[1][2][j], + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vvx = + ab - + aa + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + vvu = + wz - + ac + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + vvy = ab + aa; + vvv = wz + ac; + break; + } + { + uint256 xx = mulmod( + vvx, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy = mulmod( + vvy, + vvu, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz = mulmod( + vvu, + vvv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xx2 = mulmod( + xx, + xx, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 yy2 = mulmod( + yy, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 xxyy = mulmod( + xx, + yy, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 zz2 = mulmod( + zz, + zz, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vvx = xxyy + xxyy; + vvu = + yy2 - + xx2 + + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + vvy = xx2 + yy2; + vvv = addmod( + zz2 + zz2, + 0xffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffda - + vvu, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } + } + vx = vvx; + vu = vvu; + vy = vvy; + vv = vvv; + } + // Step 5: compare the points + (uint256 vi, uint256 vj) = Ed25519_pow.pow22501( + mulmod( + vu, + vv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ) + ); + vi = mulmod( + vi, + vi, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vi = mulmod( + vi, + vi, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vi = mulmod( + vi, + vi, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vi = mulmod( + vi, + vi, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vi = mulmod( + vi, + vi, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vi = mulmod( + vi, + vj, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vx = mulmod( + vx, + mulmod( + vi, + vv, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + vy = mulmod( + vy, + mulmod( + vi, + vu, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + bytes32 vr = bytes32(vy | (vx << 255)); + vr = bytes32( + ((uint256(vr) & + 0xff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff) << 8) | + ((uint256(vr) & + 0xff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00) >> + 8) + ); + vr = bytes32( + ((uint256(vr) & + 0xffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff) << 16) | + ((uint256(vr) & + 0xffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000) >> + 16) + ); + vr = bytes32( + ((uint256(vr) & 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff) << + 32) | + ((uint256(vr) & + 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff_00000000) >> + 32) + ); + vr = bytes32( + ((uint256(vr) & 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff) << 64) | + ((uint256(vr) & + 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff_00000000_00000000) >> + 64) + ); + vr = bytes32((uint256(vr) << 128) | (uint256(vr) >> 128)); + + return vr == r; + } + } +} diff --git a/contracts/evmx/helpers/solana-utils/Ed25519_pow.sol b/contracts/evmx/helpers/solana-utils/Ed25519_pow.sol new file mode 100644 index 00000000..c17899f6 --- /dev/null +++ b/contracts/evmx/helpers/solana-utils/Ed25519_pow.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.28; + +library Ed25519_pow { + // Computes (v^(2^250-1), v^11) mod p + function pow22501(uint256 v) internal pure returns (uint256 p22501, uint256 p11) { + p11 = mulmod( + v, + v, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + p22501 = mulmod( + p11, + p11, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + p22501 = mulmod( + mulmod( + p22501, + p22501, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + v, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + p11 = mulmod( + p22501, + p11, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + p22501 = mulmod( + mulmod( + p11, + p11, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ), + p22501, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 a = mulmod( + p22501, + p22501, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + p22501 = mulmod( + p22501, + a, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + a = mulmod( + p22501, + p22501, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod( + p22501, + a, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + uint256 b = mulmod( + a, + a, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + p22501 = mulmod( + p22501, + a, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + a = mulmod( + p22501, + p22501, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod( + p22501, + a, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + b = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + b = mulmod(b, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, b, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + a = mulmod(a, a, 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed); + p22501 = mulmod( + p22501, + a, + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed + ); + } +} diff --git a/contracts/evmx/helpers/solana-utils/Sha512.sol b/contracts/evmx/helpers/solana-utils/Sha512.sol new file mode 100644 index 00000000..585abc7b --- /dev/null +++ b/contracts/evmx/helpers/solana-utils/Sha512.sol @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.28; + +// Reference: https://csrc.nist.gov/csrc/media/publications/fips/180/2/archive/2002-08-01/documents/fips180-2.pdf + +library Sha512 { + // @notice: The message, M, shall be padded before hash computation begins. + // The purpose of this padding is to ensure that the padded message is a multiple of 1024 bits. + // @param message input raw message bytes + // @return padded message bytes + function preprocess(bytes memory message) internal pure returns (bytes memory) { + uint256 padding = 128 - (message.length % 128); + if (message.length % 128 >= 112) { + padding = 256 - (message.length % 128); + } + bytes memory result = new bytes(message.length + padding); + + for (uint256 i = 0; i < message.length; i++) { + result[i] = message[i]; + } + result[message.length] = 0x80; + + uint128 bitSize = uint128(message.length * 8); + bytes memory bitlength = abi.encodePacked(bitSize); + for (uint256 index = 0; index < bitlength.length; index++) { + result[result.length - 1 - index] = bitlength[bitlength.length - 1 - index]; + } + return result; + } + + function bytesToBytes8(bytes memory b, uint256 offset) internal pure returns (bytes8) { + bytes8 out; + for (uint256 i = 0; i < 8; i++) { + out |= bytes8(b[offset + i] & 0xFF) >> (i * 8); + } + return out; + } + + function cutBlock( + bytes memory data, + uint256 blockIndex + ) internal pure returns (uint64[16] memory) { + uint64[16] memory result; + for (uint8 r = 0; r < result.length; r++) { + result[r] = uint64(bytesToBytes8(data, blockIndex * 128 + r * 8)); + } + return result; + } + + // This section defines the functions that are used by sha-512. + // https://csrc.nist.gov/csrc/media/publications/fips/180/2/archive/2002-08-01/documents/fips180-2.pdf#page=15 + + // @notice: Thus, ROTR(x, n) is equivalent to a circular shift (rotation) of x by n positions to the right. + // @param x input num + // @param n num of positions to circular shift + // @return uint64 + function ROTR(uint64 x, uint256 n) internal pure returns (uint64) { + return (x << (64 - uint64(n))) + (x >> n); + } + + // @notice: The right shift operation SHR n(x), where x is a w-bit word and n is an integer with 0 <= n < w, is defined by SHR(x, n) = x >> n. + // @param x input num + // @param n num of positions to shift + // @return uint64 + function SHR(uint64 x, uint256 n) internal pure returns (uint64) { + return uint64(x >> n); + } + + // @notice: Ch(x, y, z) = (x ^ y) ⊕ (﹁ x ^ z) + // @param x x + // @param y y + // @param z z + // @return uint64 + function Ch(uint64 x, uint64 y, uint64 z) internal pure returns (uint64) { + return (x & y) ^ ((x ^ 0xffffffffffffffff) & z); + } + + // @notice: Maj(x, y, z) = (x ^ y) ⊕ (x ^ z) ⊕ (y ^ z) + // @param x x + // @param y y + // @param z z + // @return uint64 + function Maj(uint64 x, uint64 y, uint64 z) internal pure returns (uint64) { + return (x & y) ^ (x & z) ^ (y & z); + } + + // @notice: sigma0(x) = ROTR(x, 28) ^ ROTR(x, 34) ^ ROTR(x, 39) + // @param x x + // @return uint64 + function sigma0(uint64 x) internal pure returns (uint64) { + return ROTR(x, 28) ^ ROTR(x, 34) ^ ROTR(x, 39); + } + + // @notice: sigma1(x) = ROTR(x, 14) ^ ROTR(x, 18) ^ ROTR(x, 41) + // @param x x + // @return uint64 + function sigma1(uint64 x) internal pure returns (uint64) { + return ROTR(x, 14) ^ ROTR(x, 18) ^ ROTR(x, 41); + } + + // @notice: gamma0(x) = OTR(x, 1) ^ ROTR(x, 8) ^ SHR(x, 7) + // @param x x + // @return uint64 + function gamma0(uint64 x) internal pure returns (uint64) { + return ROTR(x, 1) ^ ROTR(x, 8) ^ SHR(x, 7); + } + + // @notice: gamma1(x) = ROTR(x, 19) ^ ROTR(x, 61) ^ SHR(x, 6) + // @param x x + // @return uint64 + function gamma1(uint64 x) internal pure returns (uint64) { + return ROTR(x, 19) ^ ROTR(x, 61) ^ SHR(x, 6); + } + + struct FuncVar { + uint64 a; + uint64 b; + uint64 c; + uint64 d; + uint64 e; + uint64 f; + uint64 g; + uint64 h; + } + + // @notice Calculate the SHA512 of input data. + // @param data input data bytes + // @return 512 bits hash result + function hash(bytes memory data) internal pure returns (uint64[8] memory) { + uint64[8] memory H = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179 + ]; + + unchecked { + uint64 T1; + uint64 T2; + + uint64[80] memory W; + FuncVar memory fvar; + + uint64[80] memory K = [ + 0x428a2f98d728ae22, + 0x7137449123ef65cd, + 0xb5c0fbcfec4d3b2f, + 0xe9b5dba58189dbbc, + 0x3956c25bf348b538, + 0x59f111f1b605d019, + 0x923f82a4af194f9b, + 0xab1c5ed5da6d8118, + 0xd807aa98a3030242, + 0x12835b0145706fbe, + 0x243185be4ee4b28c, + 0x550c7dc3d5ffb4e2, + 0x72be5d74f27b896f, + 0x80deb1fe3b1696b1, + 0x9bdc06a725c71235, + 0xc19bf174cf692694, + 0xe49b69c19ef14ad2, + 0xefbe4786384f25e3, + 0x0fc19dc68b8cd5b5, + 0x240ca1cc77ac9c65, + 0x2de92c6f592b0275, + 0x4a7484aa6ea6e483, + 0x5cb0a9dcbd41fbd4, + 0x76f988da831153b5, + 0x983e5152ee66dfab, + 0xa831c66d2db43210, + 0xb00327c898fb213f, + 0xbf597fc7beef0ee4, + 0xc6e00bf33da88fc2, + 0xd5a79147930aa725, + 0x06ca6351e003826f, + 0x142929670a0e6e70, + 0x27b70a8546d22ffc, + 0x2e1b21385c26c926, + 0x4d2c6dfc5ac42aed, + 0x53380d139d95b3df, + 0x650a73548baf63de, + 0x766a0abb3c77b2a8, + 0x81c2c92e47edaee6, + 0x92722c851482353b, + 0xa2bfe8a14cf10364, + 0xa81a664bbc423001, + 0xc24b8b70d0f89791, + 0xc76c51a30654be30, + 0xd192e819d6ef5218, + 0xd69906245565a910, + 0xf40e35855771202a, + 0x106aa07032bbd1b8, + 0x19a4c116b8d2d0c8, + 0x1e376c085141ab53, + 0x2748774cdf8eeb99, + 0x34b0bcb5e19b48a8, + 0x391c0cb3c5c95a63, + 0x4ed8aa4ae3418acb, + 0x5b9cca4f7763e373, + 0x682e6ff3d6b2b8a3, + 0x748f82ee5defb2fc, + 0x78a5636f43172f60, + 0x84c87814a1f0ab72, + 0x8cc702081a6439ec, + 0x90befffa23631e28, + 0xa4506cebde82bde9, + 0xbef9a3f7b2c67915, + 0xc67178f2e372532b, + 0xca273eceea26619c, + 0xd186b8c721c0c207, + 0xeada7dd6cde0eb1e, + 0xf57d4f7fee6ed178, + 0x06f067aa72176fba, + 0x0a637dc5a2c898a6, + 0x113f9804bef90dae, + 0x1b710b35131c471b, + 0x28db77f523047d84, + 0x32caab7b40c72493, + 0x3c9ebe0a15c9bebc, + 0x431d67c49c100d4c, + 0x4cc5d4becb3e42b6, + 0x597f299cfc657e2a, + 0x5fcb6fab3ad6faec, + 0x6c44198c4a475817 + ]; + + bytes memory blocks = preprocess(data); + + for (uint256 j = 0; j < blocks.length / 128; j++) { + uint64[16] memory M = cutBlock(blocks, j); + + fvar.a = H[0]; + fvar.b = H[1]; + fvar.c = H[2]; + fvar.d = H[3]; + fvar.e = H[4]; + fvar.f = H[5]; + fvar.g = H[6]; + fvar.h = H[7]; + + for (uint256 i = 0; i < 80; i++) { + if (i < 16) { + W[i] = M[i]; + } else { + W[i] = gamma1(W[i - 2]) + W[i - 7] + gamma0(W[i - 15]) + W[i - 16]; + } + + T1 = fvar.h + sigma1(fvar.e) + Ch(fvar.e, fvar.f, fvar.g) + K[i] + W[i]; + T2 = sigma0(fvar.a) + Maj(fvar.a, fvar.b, fvar.c); + + fvar.h = fvar.g; + fvar.g = fvar.f; + fvar.f = fvar.e; + fvar.e = fvar.d + T1; + fvar.d = fvar.c; + fvar.c = fvar.b; + fvar.b = fvar.a; + fvar.a = T1 + T2; + } + + H[0] = H[0] + fvar.a; + H[1] = H[1] + fvar.b; + H[2] = H[2] + fvar.c; + H[3] = H[3] + fvar.d; + H[4] = H[4] + fvar.e; + H[5] = H[5] + fvar.f; + H[6] = H[6] + fvar.g; + H[7] = H[7] + fvar.h; + } + } + + return H; + } +} diff --git a/contracts/evmx/helpers/solana-utils/SolanaPda.sol b/contracts/evmx/helpers/solana-utils/SolanaPda.sol new file mode 100644 index 00000000..e4f7b858 --- /dev/null +++ b/contracts/evmx/helpers/solana-utils/SolanaPda.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import "./Ed25519_pow.sol"; + +// TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA +bytes32 constant TOKEN_PROGRAM_ID = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; +// TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb +bytes32 constant TOKEN_2022_PROGRAM_ID = 0x06ddf6e1ee758fde18425dbce46ccddab61afc4d83b90d27febdf928d8a18bfc; +// ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL +bytes32 constant ASSOCIATED_TOKEN_PROGRAM_ID = 0x8c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859; +// 11111111111111111111111111111111 +bytes32 constant SYSTEM_PROGRAM_ID = 0x0000000000000000000000000000000000000000000000000000000000000000; + +/* + * - programId and PDAs are passed as raw 32 bytes (same order as Solana's Pubkey.to_bytes()). + * - Seeds: <= 16, each <= 32 bytes. The bump is the final one-byte seed. + * - PDA derivation: sha256( seeds || bump || programId || "ProgramDerivedAddress" ), + * and the result MUST be OFF the Ed25519 curve (i.e., the 32B value does NOT decode + * to a valid Ed25519 point). + */ + +library Ed25519CurveUtils { + // p = 2^255 - 19 + uint256 internal constant P = + 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed; + + // d = -121665/121666 mod p (RFC 8032 / Edwards25519) + uint256 internal constant D = + 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3; + + // (p-1)/2 for Legendre symbol (quadratic residue test) + uint256 internal constant P_MINUS_1_OVER_2 = + 0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6e; + + // p-2 for modular inverse via Fermat + uint256 internal constant P_MINUS_2 = + 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeb; + + function _toBeBytes(uint256 x) private pure returns (bytes32 b) { + b = bytes32(x); + } + + function _fromBeBytes(bytes memory b) private pure returns (uint256 x) { + require(b.length == 32, "bad size"); + assembly { + x := mload(add(b, 0x20)) + } + } + + /// @dev modular exponentiation via precompile 0x05 + function _modexp(uint256 base, uint256 exp, uint256 modn) private view returns (uint256 r) { + bytes memory input = abi.encodePacked( + uint256(32), + uint256(32), + uint256(32), + _toBeBytes(base), + _toBeBytes(exp), + _toBeBytes(modn) + ); + bytes memory output = new bytes(32); + bool ok; + assembly { + ok := staticcall(gas(), 0x05, add(input, 0x20), mload(input), add(output, 0x20), 32) + } + require(ok, "modexp failed"); + r = _fromBeBytes(output); + } + + // based on Farcaster code + function decodesToValidPoint(bytes32 k) internal pure returns (bool) { + unchecked { + // Byte-swap to little-endian (your code) + k = bytes32( + ((uint256(k) & + 0xff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff_00ff00ff) << 8) | + ((uint256(k) & + 0xff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00_ff00ff00) >> + 8) + ); + k = bytes32( + ((uint256(k) & + 0xffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff_0000ffff) << 16) | + ((uint256(k) & + 0xffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000_ffff0000) >> + 16) + ); + k = bytes32( + ((uint256(k) & 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff) << + 32) | + ((uint256(k) & + 0xffffffff_00000000_ffffffff_00000000_ffffffff_00000000_ffffffff_00000000) >> + 32) + ); + k = bytes32( + ((uint256(k) & 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff) << 64) | + ((uint256(k) & + 0xffffffff_ffffffff_00000000_00000000_ffffffff_ffffffff_00000000_00000000) >> + 64) + ); + k = bytes32((uint256(k) << 128) | (uint256(k) >> 128)); + + // Extract y (clear sign bit) + uint256 ky = uint256(k) & + 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff; + + // --- IMPORTANT: Canonical check (reject non-canonical y) --- + if (ky >= 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed) { + return false; + } + + // Curve math + uint256 p = 0x7fffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffff_ffffffed; + uint256 d = 0x52036cee_2b6ffe73_8cc74079_7779e898_00700a4d_4141d8ab_75eb4dca_135978a3; + uint256 iSqrtM1 = 0x2b832480_4fc1df0b_2b4d0099_3dfbd7a7_2f431806_ad2fe478_c4ee1b27_4a0ea0b0; + + uint256 ky2 = mulmod(ky, ky, p); + uint256 u = addmod(ky2, p - 1, p); + uint256 v = addmod(mulmod(d, ky2, p), 1, p); + + uint256 t = mulmod(u, v, p); + (uint256 kx, ) = Ed25519_pow.pow22501(t); // addition chain sqrt helper + kx = mulmod(kx, kx, p); + kx = mulmod(u, mulmod(mulmod(kx, kx, p), t, p), p); + + t = mulmod(mulmod(kx, kx, p), v, p); + + // Check curve equation (two roots case) + if (t != u) { + if (t != p - u) return false; + kx = mulmod(kx, iSqrtM1, p); + } + + // Sign parity enforcement (not needed for PDA, but harmless) + if ((kx & 1) != (uint256(k) >> 255)) { + kx = p - kx; + } + + return true; + } + } +} + +library SolanaPDA { + bytes constant DOMAIN = "ProgramDerivedAddress"; + + /// Reproduces Solana's sha256(seeds || bump || programId || DOMAIN) + function _compute( + bytes32 programId, + bytes[] memory seeds, + uint8 bump + ) internal pure returns (bytes32) { + require(seeds.length <= 16, "too many seeds"); + uint256 total; + for (uint256 i; i < seeds.length; ++i) { + require(seeds[i].length <= 32, "seed too long"); + total += seeds[i].length; + } + + bytes memory buf = new bytes(total + 1 + 32 + DOMAIN.length); + uint256 p; + + // seeds + for (uint256 i; i < seeds.length; ++i) { + bytes memory s = seeds[i]; + for (uint256 j; j < s.length; ++j) { + buf[p++] = s[j]; + } + } + + // bump (as a single byte) + buf[p++] = bytes1(bump); + + // programId raw 32 bytes (as provided) + bytes32 pid = programId; + for (uint256 k; k < 32; ++k) { + buf[p++] = pid[k]; + } + + // domain separator + for (uint256 d; d < DOMAIN.length; ++d) { + buf[p++] = DOMAIN[d]; + } + + return sha256(buf); + } + + /// @notice Validate PDA like Solana create_program_address (explicit bump). + function validatePDA( + bytes32 programId, + bytes[] memory seeds, + uint8 bump, + bytes32 expectedPDA + ) internal pure returns (bool) { + bytes32 derived = _compute(programId, seeds, bump); + + // Must be OFF-curve + bool onCurve = Ed25519CurveUtils.decodesToValidPoint(derived); + require(!onCurve, "derived is on-curve"); + + require(derived == expectedPDA, "PDA mismatch"); + return true; + } + + /// Returns the canonical (pda, bump) like Solana's findProgramAddressSync: + /// finds the first OFF-curve result scanning from b=255 down to 0. + function findProgramAddress( + bytes32 programId, + bytes[] memory seeds + ) internal pure returns (bytes32 pda, uint8 bump) { + for (uint256 b = 255; ; ) { + bytes32 d = _compute(programId, seeds, uint8(b)); + if (!Ed25519CurveUtils.decodesToValidPoint(d)) { + return (d, uint8(b)); // off-curve => valid PDA + } + if (b == 0) revert("no valid PDA"); + unchecked { + --b; + } + } + } + + function deriveTokenAtaAddress( + bytes32 solanaAddress, + bytes32 mint + ) internal pure returns (bytes32) { + bytes[] memory seeds = new bytes[](3); + // user solanaAddress + seeds[0] = abi.encodePacked(solanaAddress); + // token programId + seeds[1] = abi.encodePacked(TOKEN_PROGRAM_ID); + // token mint + seeds[2] = abi.encodePacked(mint); + + (bytes32 pda /*uint8 bump*/, ) = SolanaPDA.findProgramAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + seeds + ); + return pda; + } + + /// Proves that (expectedPDA, bump) is the *canonical* PDA: + /// - derived(bump) equals expectedPDA and is OFF-curve + /// - for all b in (bump+1 .. 255): derived(b) is ON-curve (so no higher valid PDA exists) + function validatePDAWithCanonicalBump( + bytes32 programId, + bytes[] memory seeds, + uint8 bump, + bytes32 expectedPDA + ) internal pure returns (bool) { + // Must match create_program_address result and be OFF-curve + require(validatePDA(programId, seeds, bump, expectedPDA), "invalid PDA/bump"); + + // Prove canonicality: no valid PDA at any higher bump + if (bump < 255) { + for (uint256 b = 255; b > bump; ) { + bytes32 d = _compute(programId, seeds, uint8(b)); + // Each higher bump must decode as a valid point => ON-curve (invalid PDA) + require( + Ed25519CurveUtils.decodesToValidPoint(d), + "non-canonical: higher bump is off-curve" + ); + unchecked { + --b; + } + } + } + return true; + } +} diff --git a/contracts/evmx/helpers/solana-utils/SolanaSignature.sol b/contracts/evmx/helpers/solana-utils/SolanaSignature.sol new file mode 100644 index 00000000..890f23d1 --- /dev/null +++ b/contracts/evmx/helpers/solana-utils/SolanaSignature.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.28; + +import {Ed25519} from "./Ed25519.sol"; + +contract SolanaSignature { + function verifyMessage( + bytes32 public_key, + bytes32 signature_r, + bytes32 signature_s, + bytes memory message + ) public pure returns (bool) { + bool valid = Ed25519.verify(public_key, signature_r, signature_s, message); + + return valid; + } +} diff --git a/contracts/evmx/helpers/solana-utils/program-pda/GasStationPdas.sol b/contracts/evmx/helpers/solana-utils/program-pda/GasStationPdas.sol new file mode 100644 index 00000000..0e4169b3 --- /dev/null +++ b/contracts/evmx/helpers/solana-utils/program-pda/GasStationPdas.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {SolanaPDA} from "../SolanaPda.sol"; + +library GasStationProgramPda { + // string: "config:" + bytes constant PROGRAM_CONFIG_SEED_PREFIX_HEX = hex"636f6e6669673a"; + // string: "vault_config:" + bytes constant VAULT_CONFIG_SEED_PREFIX_HEX = hex"7661756c745f636f6e6669673a"; + // string: "tmp_data:" + bytes constant TMP_DATA_SEED_PREFIX_HEX = hex"746d705f646174613a"; + // string: "whitelisted_token:" + bytes constant WHITELISTED_TOKEN_SEED_PREFIX_HEX = hex"77686974656c69737465645f746f6b656e3a"; + + function deriveProgramConfigPda(bytes32 programId) internal pure returns (bytes32, uint8) { + bytes[] memory seeds = new bytes[](1); + seeds[0] = PROGRAM_CONFIG_SEED_PREFIX_HEX; + + return SolanaPDA.findProgramAddress(programId, seeds); + } + + function deriveTmpReturnStoragePda(bytes32 programId) internal pure returns (bytes32, uint8) { + bytes[] memory seeds = new bytes[](1); + seeds[0] = TMP_DATA_SEED_PREFIX_HEX; + + return SolanaPDA.findProgramAddress(programId, seeds); + } + + function deriveWhitelistedTokenPda( + bytes32 programId, + bytes32 tokenMint + ) internal pure returns (bytes32, uint8) { + bytes[] memory seeds = new bytes[](2); + seeds[0] = WHITELISTED_TOKEN_SEED_PREFIX_HEX; + seeds[1] = abi.encodePacked(tokenMint); + + return SolanaPDA.findProgramAddress(programId, seeds); + } + + function deriveVaultConfigPda(bytes32 programId) internal pure returns (bytes32, uint8) { + bytes[] memory seeds = new bytes[](1); + seeds[0] = VAULT_CONFIG_SEED_PREFIX_HEX; + + return SolanaPDA.findProgramAddress(programId, seeds); + } +} diff --git a/contracts/evmx/interfaces/IAddressResolver.sol b/contracts/evmx/interfaces/IAddressResolver.sol index 9c226dbf..d1f495b3 100644 --- a/contracts/evmx/interfaces/IAddressResolver.sol +++ b/contracts/evmx/interfaces/IAddressResolver.sol @@ -1,49 +1,57 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "./IWatcher.sol"; -import "./IFeesManager.sol"; +import "./IGasAccountManager.sol"; import "./IAsyncDeployer.sol"; -import "./IDeployForwarder.sol"; +import "./IGasVault.sol"; +import "./IGasEscrow.sol"; +import "./IGasAccountToken.sol"; /// @title IAddressResolver /// @notice Interface for resolving system contract addresses /// @dev Provides address lookup functionality for core system components interface IAddressResolver { - /// @notice Event emitted when the fees manager is updated - event FeesManagerUpdated(address feesManager_); + /// @notice Event emitted when the gas account manager is updated + event GasAccountManagerSet(address gasAccountManager_); + /// @notice Event emitted when the gas vault is updated + event GasVaultSet(address gasVault_); + /// @notice Event emitted when the gas escrow is updated + event GasEscrowSet(address gasEscrow_); + /// @notice Event emitted when the gas account token is updated + event GasAccountTokenSet(address gasAccountToken_); /// @notice Event emitted when the watcher precompile is updated - event WatcherUpdated(address watcher_); + event WatcherSet(address watcher_); /// @notice Event emitted when the async deployer is updated - event AsyncDeployerUpdated(address asyncDeployer_); - /// @notice Event emitted when the default auction manager is updated - event DefaultAuctionManagerUpdated(address defaultAuctionManager_); - /// @notice Event emitted when the deploy forwarder is updated - event DeployForwarderUpdated(address deployForwarder_); + event AsyncDeployerSet(address asyncDeployer_); /// @notice Event emitted when the contract address is updated - event ContractAddressUpdated(bytes32 contractId_, address contractAddress_); + event ContractAddressSet(bytes32 contractId_, address contractAddress_); // System component addresses function watcher__() external view returns (IWatcher); - function feesManager__() external view returns (IFeesManager); + function gasAccountManager__() external view returns (IGasAccountManager); - function asyncDeployer__() external view returns (IAsyncDeployer); + function gasVault__() external view returns (IGasVault); + + function gasEscrow__() external view returns (IGasEscrow); - function defaultAuctionManager() external view returns (address); + function gasAccountToken__() external view returns (IGasAccountToken); - function deployForwarder__() external view returns (IDeployForwarder); + function asyncDeployer__() external view returns (IAsyncDeployer); function contractAddresses(bytes32 contractId_) external view returns (address); function setWatcher(address watcher_) external; - function setFeesManager(address feesManager_) external; + function setGasAccountManager(address gasAccountManager_) external; function setAsyncDeployer(address asyncDeployer_) external; - function setDefaultAuctionManager(address defaultAuctionManager_) external; + function setGasVault(address gasVault_) external; + + function setGasEscrow(address gasEscrow_) external; - function setDeployForwarder(address deployForwarder_) external; + function setGasAccountToken(address gasAccountToken_) external; function setContractAddress(bytes32 contractId_, address contractAddress_) external; } diff --git a/contracts/evmx/interfaces/IAppGateway.sol b/contracts/evmx/interfaces/IAppGateway.sol index ce6fa9e4..cf50f898 100644 --- a/contracts/evmx/interfaces/IAppGateway.sol +++ b/contracts/evmx/interfaces/IAppGateway.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {OverrideParams} from "../../utils/common/Structs.sol"; @@ -12,17 +12,12 @@ interface IAppGateway { /// @notice Gets the override parameters /// @return overrideParams_ The override parameters - /// @return sbType_ The switchboard type - function getOverrideParams() external view returns (OverrideParams memory, bytes32); + function getOverrideParams() external view returns (OverrideParams memory); /// @notice Handles the revert event /// @param payloadId_ The payload id function handleRevert(bytes32 payloadId_) external; - /// @notice initialize the contracts on chain - /// @param chainSlug_ The chain slug - function initializeOnChain(uint32 chainSlug_) external; - /// @notice get the on-chain address of a contract /// @param contractId_ The contract id /// @param chainSlug_ The chain slug @@ -40,8 +35,4 @@ 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/evmx/interfaces/IAsyncDeployer.sol b/contracts/evmx/interfaces/IAsyncDeployer.sol index b0c8d14d..38da381d 100644 --- a/contracts/evmx/interfaces/IAsyncDeployer.sol +++ b/contracts/evmx/interfaces/IAsyncDeployer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; /// @title IAsyncDeployer /// @notice Interface for deploying Forwarder and AsyncPromise contracts @@ -36,12 +36,12 @@ interface IAsyncDeployer { // Async Promise Management function deployAsyncPromiseContract( address invoker_, - uint40 requestCount_ + bytes32 payloadId_ ) external returns (address); function getAsyncPromiseAddress( address invoker_, - uint40 requestCount_ + bytes32 payloadId_ ) external view returns (address); function setAsyncPromiseImplementation(address implementation_) external; diff --git a/contracts/evmx/interfaces/IConfigurations.sol b/contracts/evmx/interfaces/IConfigurations.sol index 227a18f3..443fa9d1 100644 --- a/contracts/evmx/interfaces/IConfigurations.sol +++ b/contracts/evmx/interfaces/IConfigurations.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {AppGatewayConfig} from "../../utils/common/Structs.sol"; +import {AppGatewayConfig, WatcherMultiCallParams} from "../../utils/common/Structs.sol"; /// @title IConfigurations /// @notice Interface for the Watcher Precompile system that handles payload verification and execution @@ -26,7 +26,7 @@ interface IConfigurations { function getPlugConfigs( uint32 chainSlug_, bytes32 plug_ - ) external view returns (bytes32, uint64); + ) external view returns (bytes32, uint32); /// @notice Maps chain slug to their associated socket /// @param chainSlug_ The chain slug @@ -36,21 +36,16 @@ interface IConfigurations { /// @notice Returns the socket for a given chain slug /// @param chainSlug_ The chain slug /// @return The socket - function switchboards(uint32 chainSlug_, bytes32 sbType_) external view returns (uint64); + function switchboards(uint32 chainSlug_, bytes32 sbType_) external view returns (uint32); /// @notice Sets the switchboard for a network - function setSwitchboard(uint32 chainSlug_, bytes32 sbType_, uint64 switchboardId_) external; + function setSwitchboard(uint32 chainSlug_, bytes32 sbType_, uint32 switchboardId_) external; /// @notice Sets valid plugs for each chain slug /// @dev This function is used to verify if a plug deployed on a chain slug is valid connection to the app gateway - function setIsValidPlug( - bool isValid_, - uint32 chainSlug_, - bytes32 plug_, - address appGateway_ - ) external; + function setIsValidPlug(bool isValid_, uint32 chainSlug_, bytes32 plug_) external; - function setAppGatewayConfigs(AppGatewayConfig[] calldata configs_) external; + function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external; /// @notice Sets the socket for a chain slug function setSocket(uint32 chainSlug_, bytes32 socket_) external; diff --git a/contracts/evmx/interfaces/IFeesManager.sol b/contracts/evmx/interfaces/IFeesManager.sol deleted file mode 100644 index 15a77269..00000000 --- a/contracts/evmx/interfaces/IFeesManager.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; -import {WriteFinality, AppGatewayApprovals, OverrideParams, Transaction, QueueParams, RequestParams} from "../../utils/common/Structs.sol"; - -interface IFeesManager { - function deposit( - uint32 chainSlug_, - address token_, - address depositTo_, - uint256 nativeAmount_, - uint256 creditAmount_ - ) external; - - function wrap(address receiver_) external payable; - - function unwrap(uint256 amount_, address receiver_) external; - - function isCreditSpendable( - address consumeFrom_, - address spender_, - uint256 amount_ - ) external view returns (bool); - - function withdrawCredits( - uint32 chainSlug_, - address token_, - uint256 credits_, - uint256 maxFees_, - address receiver_ - ) external; - - function blockCredits(uint40 requestCount_, address consumeFrom_, uint256 credits_) external; - - function unblockAndAssignCredits(uint40 requestCount_, address assignTo_) external; - - function unblockCredits(uint40 requestCount_) external; - - function isApproved(address appGateway_, address user_) external view returns (bool); - - function setMaxFees(uint256 fees_) external; -} diff --git a/contracts/evmx/interfaces/IFeesPlug.sol b/contracts/evmx/interfaces/IFeesPlug.sol deleted file mode 100644 index 24cc719a..00000000 --- a/contracts/evmx/interfaces/IFeesPlug.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -interface IFeesPlug { - /// @notice Event emitted when fees are deposited - event FeesDeposited( - address token, - address receiver, - uint256 creditAmount, - uint256 nativeAmount - ); - /// @notice Event emitted when fees are withdrawn - event FeesWithdrawn(address token, address receiver, uint256 amount); - /// @notice Event emitted when a token is whitelisted - event TokenWhitelisted(address token); - /// @notice Event emitted when a token is removed from whitelist - event TokenRemovedFromWhitelist(address token); - - function depositCredit(address token_, address receiver_, uint256 amount_) external; - - function depositCreditAndNative(address token_, address receiver_, uint256 amount_) external; - - function depositToNative(address token_, address receiver_, uint256 amount_) external; - - function withdrawFees(address token_, address receiver_, uint256 amount_) external; -} diff --git a/contracts/evmx/interfaces/IForwarder.sol b/contracts/evmx/interfaces/IForwarder.sol index cbc2546a..db41b8d2 100644 --- a/contracts/evmx/interfaces/IForwarder.sol +++ b/contracts/evmx/interfaces/IForwarder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; /// @title IForwarder /// @notice Interface for the Forwarder contract that allows contracts to call async promises diff --git a/contracts/evmx/interfaces/IGasAccountManager.sol b/contracts/evmx/interfaces/IGasAccountManager.sol new file mode 100644 index 00000000..d70d0d3a --- /dev/null +++ b/contracts/evmx/interfaces/IGasAccountManager.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; +import {WriteFinality, AppGatewayApprovals, OverrideParams, Transaction, RawPayload, Payload} from "../../utils/common/Structs.sol"; + +interface IGasAccountManager { + /// @notice Emitted when fees deposited are updated + /// @param chainSlug The chain identifier + /// @param token The token address + /// @param depositTo The address to deposit to + /// @param gasAmount The gas amount added + /// @param nativeAmount The native amount transferred + event Deposited( + uint32 indexed chainSlug, + address indexed token, + address indexed depositTo, + uint256 gasAmount, + uint256 nativeAmount + ); + + /// @notice Emitted when fees plug is set + event GasStationSet(uint32 indexed chainSlug, bytes32 indexed gasStation); + + /// @notice Emitted when fees plug solana program id is set + event GasStationSolanaProgramIdSet(bytes32 indexed gasStationSolanaProgramId); + + /// @notice Emitted when forwarder solana is set + event ForwarderSolanaSet(address indexed forwarderSolana); + + /// @notice Emitted when max fees per chain slug is set + /// @param chainSlug The chain slug + /// @param fees The max fees + event MaxGasPerChainSlugSet(uint32 indexed chainSlug, uint256 fees); + + /// @notice Emitted when gas is wrapped + /// @param consumeFrom The address that wrapped the gas + /// @param amount The amount of gas wrapped + event GasWrapped(address indexed consumeFrom, uint256 amount); + + /// @notice Emitted when gas is unwrapped + event GasUnwrapped(address indexed consumeFrom, uint256 amount); + + // ============ GAS ACCOUNT OPERATIONS ============ + + /// @notice Wrap native tokens into SGAS + function wrapToGas(address receiver) external payable; + + /// @notice Unwrap SGAS to native tokens + function unwrapFromGas(uint256 amount, address receiver) external; + + // ============ CROSS-CHAIN OPERATIONS ============ + + /// @notice Deposit tokens from a chain into gas account + /// @dev Called by watcher after detecting GasStation deposit + function depositFromChain( + address token_, + address receiver_, + uint256 gasAmount_, + uint256 nativeAmount_ + ) external; + + /// @notice Withdraw SGAS to tokens on another chain + function withdrawToChain( + uint32 chainSlug, + address token, + uint256 amount, + uint256 bridgeFee, + address receiver + ) external; + + // ============ PAYLOAD LIFECYCLE (Internal) ============ + + /// @notice Escrow gas for a payload + /// @dev Called by W when transmitter assigned + function escrowGas(bytes32 payloadId, address account, uint256 amount) external; + + /// @notice Release escrowed gas back to account + /// @dev Called when transmitter changes or payload cancelled + function releaseEscrow(bytes32 payloadId) external; + + /// @notice Settle escrowed gas to transmitter + /// @dev Called when payload completes successfully + function settleGasPayment( + bytes32 payloadId, + address consumeFrom, + address transmitter, + uint256 amount + ) external; +} diff --git a/contracts/evmx/interfaces/IERC20.sol b/contracts/evmx/interfaces/IGasAccountToken.sol similarity index 69% rename from contracts/evmx/interfaces/IERC20.sol rename to contracts/evmx/interfaces/IGasAccountToken.sol index af96566e..2dd1bd6d 100644 --- a/contracts/evmx/interfaces/IERC20.sol +++ b/contracts/evmx/interfaces/IGasAccountToken.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -interface IERC20 { +interface IGasAccountToken { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); + function totalBalanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); function allowance(address owner, address spender) external view returns (uint256); @@ -16,6 +18,10 @@ interface IERC20 { function decimals() external view returns (uint8); + function mint(address account, uint256 amount) external; + + function burn(address account, uint256 amount) external; + function permit( address spender, uint256 value, @@ -24,6 +30,12 @@ interface IERC20 { bytes memory signature ) external; + function isGasAvailable( + address consumeFrom_, + address spender_, + uint256 amount_ + ) external view returns (bool); + event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); diff --git a/contracts/evmx/interfaces/IGasEscrow.sol b/contracts/evmx/interfaces/IGasEscrow.sol new file mode 100644 index 00000000..572f2cfe --- /dev/null +++ b/contracts/evmx/interfaces/IGasEscrow.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {EscrowEntry, EscrowState} from "../../utils/common/Structs.sol"; + +/// @title Gas Escrow Manager +/// @notice Tracks escrowed gas during payload lifecycle +/// @dev Separates escrow logic from token logic for clarity +interface IGasEscrow { + /// @notice Emitted when fees are blocked for a batch + /// @param payloadId The payload id + /// @param consumeFrom The consume from address + /// @param amount The blocked amount + event GasEscrowed(bytes32 indexed payloadId, address indexed consumeFrom, uint256 amount); + + /// @notice Emitted when fees are unblocked + /// @param payloadId The payload id + /// @param consumeFrom The consume from address + event EscrowReleased(bytes32 indexed payloadId, address indexed consumeFrom); + + /// @notice Emitted when fees are unblocked and assigned to a transmitter + /// @param payloadId The payload id + /// @param consumeFrom The consume from address + /// @param transmitter The transmitter address + /// @param amount The unblocked amount + event EscrowSettled( + bytes32 indexed payloadId, + address indexed consumeFrom, + address indexed transmitter, + uint256 amount + ); + + /// @notice Escrow gas for a payload + function escrowGas(bytes32 payloadId_, address consumeFrom_, uint256 amount_) external; + + /// @notice Release escrow back to account + function releaseEscrow(bytes32 payloadId) external; + + /// @notice Mark escrow as settled (paid to transmitter) + function settleGasPayment(bytes32 payloadId, address transmitter, uint256 amount) external; + + /// @notice Get total escrowed amount for an account + function getEscrowedAmount(address account) external view returns (uint256); + + /// @notice Get payload escrow details + function getPayloadEscrow(bytes32 payloadId) external view returns (EscrowEntry memory); +} diff --git a/contracts/evmx/interfaces/IGasStation.sol b/contracts/evmx/interfaces/IGasStation.sol new file mode 100644 index 00000000..d1a5b4b4 --- /dev/null +++ b/contracts/evmx/interfaces/IGasStation.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +interface IGasStation { + /// @notice Event emitted when fees are deposited + event GasDeposited( + address token, + address receiver, + uint256 gasAmount, + uint256 nativeAmount, + bytes32 payloadId + ); + /// @notice Event emitted when fees are withdrawn + event GasWithdrawn(address token, address receiver, uint256 amount); + /// @notice Event emitted when a token is whitelisted + event TokenWhitelisted(address token); + /// @notice Event emitted when a token is removed from whitelist + event TokenRemovedFromWhitelist(address token); + + function depositGas( + address token_, + address receiver_, + uint256 amount_ + ) external returns (bytes32 payloadId); + + function depositGasTokenAndNative( + address token_, + address receiver_, + uint256 amount_ + ) external returns (bytes32 payloadId); + + function depositNative( + address token_, + address receiver_, + uint256 amount_ + ) external returns (bytes32 payloadId); + + function withdrawTokens(address token_, address receiver_, uint256 amount_) external; +} diff --git a/contracts/evmx/interfaces/IFeesPool.sol b/contracts/evmx/interfaces/IGasVault.sol similarity index 71% rename from contracts/evmx/interfaces/IFeesPool.sol rename to contracts/evmx/interfaces/IGasVault.sol index b2dcf1a1..4e2ca337 100644 --- a/contracts/evmx/interfaces/IFeesPool.sol +++ b/contracts/evmx/interfaces/IGasVault.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -interface IFeesPool { +interface IGasVault { event NativeDeposited(address indexed from, uint256 amount); event NativeWithdrawn(bool success, address indexed to, uint256 amount); function withdraw(address to_, uint256 amount_) external returns (bool success); - function getBalance() external view returns (uint256); + function vaultBalance() external view returns (uint256); } diff --git a/contracts/evmx/interfaces/IPrecompile.sol b/contracts/evmx/interfaces/IPrecompile.sol index 6241434c..00da07a7 100644 --- a/contracts/evmx/interfaces/IPrecompile.sol +++ b/contracts/evmx/interfaces/IPrecompile.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {QueueParams, PayloadParams} from "../../utils/common/Structs.sol"; +import {RawPayload, Payload} from "../../utils/common/Structs.sol"; /// @title IPrecompile /// @notice Interface for precompile functionality @@ -11,27 +11,18 @@ interface IPrecompile { /// @return fees The fees required for processing function getPrecompileFees(bytes memory precompileData_) external view returns (uint256 fees); - /// @notice Gets precompile data and fees for queue parameters - /// @param queueParams_ The queue parameters to process - /// @return precompileData The encoded precompile data - /// @return estimatedFees Estimated fees required for processing - function validateAndGetPrecompileData( - QueueParams calldata queueParams_, - address appGateway_ - ) external view returns (bytes memory precompileData, uint256 estimatedFees); - - /// @notice Handles payload processing and returns fees - /// @param transmitter The address of the transmitter - /// @param payloadParams The payload parameters to handle + /// @notice Handles payload processing and returns processed payload + /// @param rawPayload The payload parameters to handle /// @return fees The fees required for processing /// @return deadline The deadline for processing /// @return precompileData The encoded precompile data function handlePayload( - address transmitter, - PayloadParams calldata payloadParams + RawPayload calldata rawPayload, + address appGateway, + bytes32 payloadId ) external returns (uint256 fees, uint256 deadline, bytes memory precompileData); /// @notice Resolves a payload - /// @param payloadParams The payload parameters to resolve - function resolvePayload(PayloadParams calldata payloadParams) external; + /// @param payload The payload parameters to resolve + function resolvePayload(Payload calldata payload) external; } diff --git a/contracts/evmx/interfaces/IPromise.sol b/contracts/evmx/interfaces/IPromise.sol index c898e16e..f267275b 100644 --- a/contracts/evmx/interfaces/IPromise.sol +++ b/contracts/evmx/interfaces/IPromise.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {AsyncPromiseState, PromiseReturnData} from "../../utils/common/Structs.sol"; @@ -18,8 +18,8 @@ interface IPromise { /// @notice The callback data of the promise function callbackData() external view returns (bytes memory); - /// @notice The request count of the promise - function requestCount() external view returns (uint40); + /// @notice The payload id of the promise + function payloadId() external view returns (bytes32); /// @notice The flag to check if the promise exceeded the max copy limit function exceededMaxCopy() external view returns (bool); diff --git a/contracts/evmx/interfaces/IReceiver.sol b/contracts/evmx/interfaces/IReceiver.sol deleted file mode 100644 index 398ab04b..00000000 --- a/contracts/evmx/interfaces/IReceiver.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -/// @title IReceiver -/// @notice Interface for receiving transfers -interface IReceiver { - function onTransfer( - uint32 chainSlug_, - address token_, - uint256 creditAmount_, - uint256 nativeAmount_, - bytes memory data_ - ) external; -} diff --git a/contracts/evmx/interfaces/IWatcher.sol b/contracts/evmx/interfaces/IWatcher.sol index 7f127de9..53aa5143 100644 --- a/contracts/evmx/interfaces/IWatcher.sol +++ b/contracts/evmx/interfaces/IWatcher.sol @@ -1,93 +1,75 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; -import "../../utils/common/Errors.sol"; -import "../../utils/common/Structs.sol"; +pragma solidity 0.8.28; -import "./IRequestHandler.sol"; -import "./IConfigurations.sol"; -import "./IPromiseResolver.sol"; +import {RawPayload, Payload, PromiseReturnData, TriggerParams, WatcherMultiCallParams} from "../../utils/common/Structs.sol"; +import {IPrecompile} from "./IPrecompile.sol"; +import {IConfigurations} from "./IConfigurations.sol"; -/// @title IWatcher -/// @notice Interface for the Watcher Precompile system that handles payload verification and execution -/// @dev Defines core functionality for payload processing and promise resolution -interface IWatcher { - /// @notice Emitted when a new call is made to an app gateway - /// @param triggerId The unique identifier for the trigger - event CalledAppGateway(bytes32 triggerId); +interface IWatcher is IConfigurations { + event PayloadSubmitted(Payload payload); + event PayloadResolved(bytes32 indexed payloadId); + event PromiseNotResolved(bytes32 indexed payloadId, address asyncPromise); + event PromiseResolved(bytes32 indexed payloadId, address asyncPromise); + event MarkedRevert(bytes32 indexed payloadId, bool isRevertingOnchain); + event TriggerFailed(bytes32 indexed triggerId); + event TriggerSucceeded(bytes32 indexed triggerId); + event FeesIncreased(bytes32 indexed payloadId, uint256 newMaxFees); + event PayloadCancelled(bytes32 indexed payloadId); + event PayloadSettled(bytes32 indexed payloadId); + event TriggerFeesSet(uint256 triggerFees); + event PrecompileSet(bytes4 callType, IPrecompile precompile); - /// @notice Emitted when a call to an app gateway fails - /// @param triggerId The unique identifier for the trigger - event AppGatewayCallFailed(bytes32 triggerId); + function evmxSlug() external view returns (uint32); - function requestHandler__() external view returns (IRequestHandler); + function nextPayloadCount() external view returns (uint64); - function configurations__() external view returns (IConfigurations); + function currentPayloadId() external view returns (bytes32); - function promiseResolver__() external view returns (IPromiseResolver); - - /// @notice Returns the request params for a given request count - /// @param requestCount_ The request count - /// @return The request params - function getRequestParams(uint40 requestCount_) external view returns (RequestParams memory); + function latestAsyncPromise() external view returns (address); - /// @notice Returns the request params for a given request count - /// @param payloadId_ The payload id - /// @return The request params - function getPayloadParams(bytes32 payloadId_) external view returns (PayloadParams memory); + function transmitter() external view returns (address); - /// @notice Returns the current request count - /// @return The current request count - function getCurrentRequestCount() external view returns (uint40); + function transmitterSolana() external view returns (bytes32); - /// @notice Returns the latest async promise deployed for a payload queued - /// @return The latest async promise - function latestAsyncPromise() external view returns (address); + function isNonceUsed(uint256 nonce) external view returns (bool); function triggerFromChainSlug() external view returns (uint32); + function triggerFees() external view returns (uint256); + function triggerFromPlug() external view returns (bytes32); - function isAppGatewayCalled(bytes32 triggerId) external view returns (bool); - - /// @notice Queues a payload for execution - /// @param queueParams_ The parameters for the payload - function queue( - QueueParams calldata queueParams_, - address appGateway_ - ) external returns (address, uint40); - - /// @notice Clears the queue of payloads - function clearQueue() external; - - function submitRequest( - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes calldata onCompleteData - ) external returns (uint40 requestCount, address[] memory promises); - - function queueAndSubmit( - QueueParams memory queue_, - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes calldata onCompleteData - ) external returns (uint40 requestCount, address[] memory promises); - - /// @notice Returns the precompile fees for a given precompile - /// @param precompile_ The precompile - /// @param precompileData_ The precompile data - /// @return The precompile fees - function getPrecompileFees( - bytes4 precompile_, - bytes memory precompileData_ - ) external view returns (uint256); + function addPayloadData(RawPayload calldata rawPayload_, address appGateway_) external; + + function executePayload() external returns (address asyncPromise); + + function resolvePayload(WatcherMultiCallParams memory params_) external; + + function markRevert(WatcherMultiCallParams memory params_) external; - function cancelRequest(uint40 requestCount_) external; + function callAppGateways(WatcherMultiCallParams memory params_) external; - function increaseFees(uint40 requestCount_, uint256 newFees_) external; + function increaseFees(bytes32 payloadId_, uint256 newMaxFees_) external; - function setIsValidPlug(bool isValid_, uint32 chainSlug_, bytes32 onchainAddress_) external; + function cancelExecution(bytes32 payloadId_) external; + + function getCurrentPayloadId( + uint32 chainSlug_, + bytes32 switchboardType_ + ) external view returns (bytes32); + + function getPayload(bytes32 payloadId) external view returns (Payload memory); + + function setTriggerFees(uint256 triggerFees_) external; + + function setPrecompile(bytes4 callType_, IPrecompile precompile_) external; + + function getPrecompileFees( + bytes4 callType_, + bytes memory precompileData_ + ) external view returns (uint256); +} - function isWatcher(address account_) external view returns (bool); +interface IWatcherOwner { + function owner() external view returns (address); } diff --git a/contracts/evmx/mocks/ProxyFactory.sol b/contracts/evmx/mocks/ProxyFactory.sol index a49a9af3..65a4afff 100644 --- a/contracts/evmx/mocks/ProxyFactory.sol +++ b/contracts/evmx/mocks/ProxyFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol"; diff --git a/contracts/evmx/mocks/TestUSDC.sol b/contracts/evmx/mocks/TestUSDC.sol index 02528a07..335b1777 100644 --- a/contracts/evmx/mocks/TestUSDC.sol +++ b/contracts/evmx/mocks/TestUSDC.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "solady/tokens/ERC20.sol"; diff --git a/contracts/evmx/plugs/FeesPlug.sol b/contracts/evmx/plugs/GasStation.sol similarity index 62% rename from contracts/evmx/plugs/FeesPlug.sol rename to contracts/evmx/plugs/GasStation.sol index 8320db52..eb24a21a 100644 --- a/contracts/evmx/plugs/FeesPlug.sol +++ b/contracts/evmx/plugs/GasStation.sol @@ -1,20 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "solady/utils/SafeTransferLib.sol"; import "../../protocol/base/PlugBase.sol"; import "../../utils/AccessControl.sol"; import {RESCUE_ROLE} from "../../utils/common/AccessRoles.sol"; -import {IFeesPlug} from "../interfaces/IFeesPlug.sol"; +import {IGasStation} from "../interfaces/IGasStation.sol"; import "../../utils/RescueFundsLib.sol"; import {InvalidTokenAddress} from "../../utils/common/Errors.sol"; -import "../interfaces/IERC20.sol"; +import "../interfaces/IGasAccountToken.sol"; -/// @title FeesPlug +interface IGasAccountManager { + function depositFromChain( + address token_, + address receiver_, + uint256 gasAmount_, + uint256 nativeAmount_ + ) external returns (bytes memory); +} + +/// @title GasStation /// @notice Contract for managing fees on a network /// @dev The amount deposited here is locked and updated in the EVMx for an app gateway -/// @dev The fees are redeemed by the transmitters executing request or can be withdrawn by the owner -contract FeesPlug is IFeesPlug, PlugBase, AccessControl { +/// @dev The fees are redeemed by the transmitters executing payload or can be withdrawn by the owner +contract GasStation is IGasStation, PlugBase, AccessControl { using SafeTransferLib for address; /// @notice Mapping to store if a token is whitelisted @@ -27,61 +36,80 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { /// @notice Error thrown when token is not whitelisted error TokenNotWhitelisted(address token_); - /// @notice Constructor for the FeesPlug contract + /// @notice Constructor for the GasStation contract /// @param socket_ The socket address /// @param owner_ The owner address constructor(address socket_, address owner_) { _setSocket(socket_); _initializeOwner(owner_); - isSocketInitialized = 1; } /////////////////////// DEPOSIT AND WITHDRAWAL /////////////////////// - function depositCredit(address token_, address receiver_, uint256 amount_) external override { - _deposit(token_, receiver_, amount_, 0); + function depositGas( + address token_, + address receiver_, + uint256 amount_ + ) external override returns (bytes32 payloadId) { + payloadId = _deposit(token_, receiver_, amount_, 0); } - function depositCreditAndNative( + function depositGasTokenAndNative( address token_, address receiver_, uint256 amount_ - ) external override { + ) external override returns (bytes32 payloadId) { uint256 nativeAmount_ = amount_ / 10; - _deposit(token_, receiver_, amount_ - nativeAmount_, nativeAmount_); + payloadId = _deposit(token_, receiver_, amount_ - nativeAmount_, nativeAmount_); } - function depositToNative(address token_, address receiver_, uint256 amount_) external override { - _deposit(token_, receiver_, 0, amount_); + function depositNative( + address token_, + address receiver_, + uint256 amount_ + ) external override returns (bytes32 payloadId) { + payloadId = _deposit(token_, receiver_, 0, amount_); } /// @notice Deposits funds /// @param token_ The token address - /// @param creditAmount_ The amount of fees + /// @param gasAmount_ The amount of fees /// @param nativeAmount_ The amount of native tokens /// @param receiver_ The receiver address function _deposit( address token_, address receiver_, - uint256 creditAmount_, + uint256 gasAmount_, uint256 nativeAmount_ - ) internal { + ) internal returns (bytes32 payloadId) { if (!whitelistedTokens[token_]) revert TokenNotWhitelisted(token_); - token_.safeTransferFrom(msg.sender, address(this), creditAmount_ + nativeAmount_); - emit FeesDeposited(token_, receiver_, creditAmount_, nativeAmount_); + + // Call depositFromChain through interface (goes to Socket's fallback) + bytes memory payloadIdBytes = IGasAccountManager(address(socket__)).depositFromChain( + token_, + receiver_, + gasAmount_, + nativeAmount_ + ); + + // DECODING: Socket's fallback returns abi.encode(abi.encode(payloadId)) + // Using interface call, Solidity auto-decodes the outer ABI layer, payloadIdBytes contains: 32 bytes (the payloadId) + payloadId = abi.decode(payloadIdBytes, (bytes32)); + token_.safeTransferFrom(msg.sender, address(this), gasAmount_ + nativeAmount_); + emit GasDeposited(token_, receiver_, gasAmount_, nativeAmount_, payloadId); } - /// @notice Withdraws fees + /// @notice Withdraws tokens /// @param token_ The token address /// @param amount_ The amount /// @param receiver_ The receiver address - function withdrawFees( + function withdrawTokens( address token_, address receiver_, uint256 amount_ ) external override onlySocket { - uint256 balance = IERC20(token_).balanceOf(address(this)); - uint8 decimals = IERC20(token_).decimals(); + uint256 balance = IGasAccountToken(token_).balanceOf(address(this)); + uint8 decimals = IGasAccountToken(token_).decimals(); if (decimals < 18) { amount_ = amount_ / 10 ** (18 - decimals); @@ -91,7 +119,7 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { if (balance < amount_) revert InsufficientTokenBalance(token_, balance, amount_); token_.safeTransfer(receiver_, amount_); - emit FeesWithdrawn(token_, receiver_, amount_); + emit GasWithdrawn(token_, receiver_, amount_); } /////////////////////// ADMIN FUNCTIONS /////////////////////// @@ -114,7 +142,7 @@ contract FeesPlug is IFeesPlug, PlugBase, AccessControl { function connectSocket( bytes32 appGatewayId_, address socket_, - uint64 switchboardId_ + uint32 switchboardId_ ) external onlyOwner { _connectSocket(appGatewayId_, socket_, switchboardId_); } diff --git a/contracts/evmx/watcher/Configurations.sol b/contracts/evmx/watcher/Configurations.sol index 50bb0999..790f1de9 100644 --- a/contracts/evmx/watcher/Configurations.sol +++ b/contracts/evmx/watcher/Configurations.sol @@ -1,16 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import "solady/utils/Initializable.sol"; import "../interfaces/IConfigurations.sol"; -import {WatcherBase} from "./WatcherBase.sol"; -import {InvalidGateway, InvalidSwitchboard} from "../../utils/common/Errors.sol"; -import "solady/auth/Ownable.sol"; -import "../../utils/RescueFundsLib.sol"; -import {toBytes32Format} from "../../utils/common/Converters.sol"; -import {PlugConfigGeneric} from "../../utils/common/Structs.sol"; - -abstract contract ConfigurationsStorage is IConfigurations { +import "../../utils/common/Errors.sol"; +import "../helpers/AddressResolverUtil.sol"; +import "../../utils/AccessControl.sol"; +import "../../utils/common/Converters.sol"; +import "../../utils/common/Structs.sol"; +import "solady/utils/ECDSA.sol"; + +abstract contract ConfigurationsStorage is IWatcher { // slots [0-49] reserved for gap uint256[50] _gap_before; @@ -22,7 +21,7 @@ abstract contract ConfigurationsStorage is IConfigurations { // slot 51 /// @notice Maps chain slug to their associated switchboard /// @dev chainSlug => sb type => switchboard id - mapping(uint32 => mapping(bytes32 => uint64)) public switchboards; + mapping(uint32 => mapping(bytes32 => uint32)) public switchboards; // slot 52 /// @notice Maps chain slug to their associated socket @@ -34,6 +33,9 @@ abstract contract ConfigurationsStorage is IConfigurations { /// @dev appGateway => chainSlug => plug => isValid mapping(address => mapping(uint32 => mapping(bytes32 => bool))) public isValidPlug; + uint32 public evmxSlug; + mapping(uint256 => bool) public isNonceUsed; + // slots [54-103] reserved for gap uint256[50] _gap_after; @@ -43,7 +45,7 @@ abstract contract ConfigurationsStorage is IConfigurations { /// @title Configurations /// @notice Configuration contract for the Watcher Precompile system /// @dev Handles the mapping between networks, plugs, and app gateways for payload execution -contract Configurations is ConfigurationsStorage, Initializable, Ownable, WatcherBase { +abstract contract Configurations is ConfigurationsStorage, AccessControl, AddressResolverUtil { /// @notice Emitted when a new plug is configured for an app gateway /// @param appGatewayId The id of the app gateway /// @param chainSlug The identifier of the destination network @@ -54,7 +56,7 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche /// @param chainSlug The identifier of the network /// @param sbType The type of switchboard /// @param switchboardId The id of the switchboard - event SwitchboardSet(uint32 chainSlug, bytes32 sbType, uint64 switchboardId); + event SwitchboardSet(uint32 chainSlug, bytes32 sbType, uint32 switchboardId); /// @notice Emitted when socket is set for a network /// @param chainSlug The identifier of the network @@ -68,28 +70,21 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche /// @param isValid Whether the plug is valid event IsValidPlugSet(bool isValid, uint32 chainSlug, bytes32 plug, address appGateway); - constructor() { - _disableInitializers(); // disable for implementation - } - - function initialize(address watcher_, address owner_) external reinitializer(1) { - _initializeOwner(owner_); - _initializeWatcher(watcher_); - } - /// @notice Configures app gateways with their respective plugs and switchboards /// @dev Only callable by the watcher /// @dev This helps in verifying that plugs are called by respective app gateways - /// @param configs_ Array of configurations containing app gateway, network, plug, and switchboard details - function setAppGatewayConfigs(AppGatewayConfig[] calldata configs_) external onlyWatcher { - for (uint256 i = 0; i < configs_.length; i++) { - // Store the plug configuration for this network and plug - _plugConfigs[configs_[i].chainSlug][configs_[i].plug] = configs_[i].plugConfig; + /// @param params_ The parameters containing the data, nonce, and signature + function setAppGatewayConfigs(WatcherMultiCallParams memory params_) external { + _validateSignature(address(this), params_.data, params_.nonce, params_.signature); + AppGatewayConfig[] memory configs = abi.decode(params_.data, (AppGatewayConfig[])); + for (uint256 i = 0; i < configs.length; i++) { + // Store the plug configuration for this network and plug + _plugConfigs[configs[i].chainSlug][configs[i].plug] = configs[i].plugConfig; emit PlugAdded( - configs_[i].plugConfig.appGatewayId, - configs_[i].chainSlug, - configs_[i].plug + configs[i].plugConfig.appGatewayId, + configs[i].chainSlug, + configs[i].plug ); } } @@ -109,7 +104,7 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche function setSwitchboard( uint32 chainSlug_, bytes32 sbType_, - uint64 switchboardId_ + uint32 switchboardId_ ) external onlyOwner { switchboards[chainSlug_][sbType_] = switchboardId_; emit SwitchboardSet(chainSlug_, sbType_, switchboardId_); @@ -121,14 +116,9 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche /// @param chainSlug_ The identifier of the network /// @param plug_ The address of the plug /// @param isValid_ Whether the plug is valid - function setIsValidPlug( - bool isValid_, - uint32 chainSlug_, - bytes32 plug_, - address appGateway_ - ) external onlyWatcher { - isValidPlug[appGateway_][chainSlug_][plug_] = isValid_; - emit IsValidPlugSet(isValid_, chainSlug_, plug_, appGateway_); + function setIsValidPlug(bool isValid_, uint32 chainSlug_, bytes32 plug_) external { + isValidPlug[msg.sender][chainSlug_][plug_] = isValid_; + emit IsValidPlugSet(isValid_, chainSlug_, plug_, msg.sender); } /// @notice Retrieves the configuration for a specific plug on a network @@ -140,7 +130,7 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche function getPlugConfigs( uint32 chainSlug_, bytes32 plug_ - ) public view returns (bytes32, uint64) { + ) public view returns (bytes32, uint32) { return ( _plugConfigs[chainSlug_][plug_].appGatewayId, _plugConfigs[chainSlug_][plug_].switchboardId @@ -159,20 +149,47 @@ contract Configurations is ConfigurationsStorage, Initializable, Ownable, Watche address appGateway_, bytes32 switchboardType_ ) external view { - (bytes32 appGatewayId, uint64 switchboardId) = getPlugConfigs(chainSlug_, target_); + (bytes32 appGatewayId, uint32 switchboardId) = getPlugConfigs(chainSlug_, target_); if (appGatewayId != toBytes32Format(appGateway_)) revert InvalidGateway(); if (switchboardId != switchboards[chainSlug_][switchboardType_]) revert InvalidSwitchboard(); } - /** - * @notice Rescues funds from the contract if they are locked by mistake. This contract does not - * theoretically need this function but it is added for safety. - * @param token_ The address of the token contract. - * @param rescueTo_ The address where rescued tokens need to be sent. - * @param amount_ The amount of tokens to be rescued. - */ - function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { - RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + /// @notice Verifies that a watcher signature is valid + /// @param data_ The data to verify + /// @param nonce_ The nonce of the signature + /// @param signature_ The signature to verify + function _validateSignature( + address contractAddress_, + bytes memory data_, + uint256 nonce_, + bytes memory signature_ + ) internal { + if (contractAddress_ == address(0)) revert InvalidContract(); + if (data_.length == 0) revert InvalidData(); + if (signature_.length == 0) revert InvalidSignature(); + if (isNonceUsed[nonce_]) revert NonceUsed(); + isNonceUsed[nonce_] = true; + + bytes32 digest = keccak256( + abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_) + ); + + // check if signature is valid + if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature(); + } + + /// @notice Recovers the signer of a message + /// @param digest_ The digest of the input data + /// @param signature_ The signature to verify + /// @dev This function verifies that the signature was created by the watcher and that the nonce has not been used before + function _recoverSigner( + bytes32 digest_, + bytes memory signature_ + ) internal view returns (address signer) { + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); + + // recovered signer is checked for the valid roles later + signer = ECDSA.recover(digest, signature_); } } diff --git a/contracts/evmx/watcher/RequestHandler.sol b/contracts/evmx/watcher/RequestHandler.sol deleted file mode 100644 index bde650c7..00000000 --- a/contracts/evmx/watcher/RequestHandler.sol +++ /dev/null @@ -1,445 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "solady/utils/Initializable.sol"; -import "solady/auth/Ownable.sol"; -import "../helpers/AddressResolverUtil.sol"; -import "../../utils/common/Errors.sol"; -import "../../utils/common/Constants.sol"; -import "../../utils/common/IdUtils.sol"; -import "../interfaces/IAppGateway.sol"; -import "../interfaces/IPromise.sol"; -import "../interfaces/IRequestHandler.sol"; -import "../../utils/RescueFundsLib.sol"; -import "solady/utils/LibCall.sol"; -import "../interfaces/IERC20.sol"; - -abstract contract RequestHandlerStorage is IRequestHandler { - // slots [0-49] reserved for gap - uint256[50] _gap_before; - - // slot 50 (40 + 40 + 40 + 128) - /// @notice Counter for tracking request counts - uint40 public nextRequestCount = 1; - - /// @notice Counter for tracking payload _requests - uint40 public payloadCounter; - - /// @notice Counter for tracking batch counts - uint40 public nextBatchCount; - - /// @notice max number of payloads in single request - uint128 requestPayloadCountLimit; - - // slot 51 - /// @notice Mapping to store the precompiles for each call type - mapping(bytes4 => IPrecompile) public precompiles; - - // slot 52 - /// @notice Mapping to store the list of payload IDs for each batch - mapping(uint40 => bytes32[]) internal _batchPayloadIds; - - // slot 53 - /// @notice Mapping to store the batch IDs for each request - mapping(uint40 => uint40[]) internal _requestBatchIds; - - // queue => update to payloadParams, assign id, store in payloadParams map - // slot 54 - /// @notice Mapping to store the payload parameters for each payload ID - mapping(bytes32 => PayloadParams) internal _payloads; - - // slot 55 - /// @notice The metadata for a request - mapping(uint40 => RequestParams) internal _requests; - - // slots [56-105] reserved for gap - uint256[50] _gap_after; - - // slots [106-155] 50 slots reserved for address resolver util -} - -/// @title RequestHandler -/// @notice Contract that handles request processing and management, including request submission, batch processing, and request lifecycle management -/// @dev Handles request submission, batch processing, transmitter assignment, request cancellation and settlement -contract RequestHandler is RequestHandlerStorage, Initializable, Ownable, AddressResolverUtil { - using LibCall for address; - - error InsufficientMaxFees(); - - event RequestSubmitted( - bool hasWrite, - uint40 requestCount, - uint256 totalEstimatedWatcherFees, - RequestParams requestParams, - PayloadParams[] payloadParamsArray - ); - - event FeesIncreased(uint40 requestCount, uint256 newMaxFees); - event RequestSettled(uint40 requestCount, address winner); - event RequestCompletedWithErrors(uint40 requestCount); - event RequestCancelled(uint40 requestCount); - event PrecompileSet(bytes4 callType, IPrecompile precompile); - event RequestPayloadCountLimitSet(uint128 requestPayloadCountLimit); - - modifier isRequestCancelled(uint40 requestCount_) { - if (_requests[requestCount_].requestTrackingParams.isRequestCancelled) - revert RequestAlreadyCancelled(); - _; - } - - modifier onlyPromiseResolver() { - if (msg.sender != address(watcher__().promiseResolver__())) revert NotPromiseResolver(); - _; - } - - constructor() { - _disableInitializers(); // disable for implementation - } - - function initialize(address owner_, address addressResolver_) external reinitializer(1) { - requestPayloadCountLimit = 100; - _initializeOwner(owner_); - _setAddressResolver(addressResolver_); - } - - function setPrecompile(bytes4 callType_, IPrecompile precompile_) external onlyOwner { - precompiles[callType_] = precompile_; - emit PrecompileSet(callType_, precompile_); - } - - function setRequestPayloadCountLimit(uint128 requestPayloadCountLimit_) external onlyOwner { - requestPayloadCountLimit = requestPayloadCountLimit_; - emit RequestPayloadCountLimitSet(requestPayloadCountLimit_); - } - - function getPrecompileFees( - bytes4 callType_, - bytes memory precompileData_ - ) external view returns (uint256) { - return precompiles[callType_].getPrecompileFees(precompileData_); - } - - function getRequestBatchIds(uint40 requestCount_) external view returns (uint40[] memory) { - return _requestBatchIds[requestCount_]; - } - - function getBatchPayloadIds(uint40 batchCount_) external view returns (bytes32[] memory) { - return _batchPayloadIds[batchCount_]; - } - - function getRequest(uint40 requestCount_) external view returns (RequestParams memory) { - return _requests[requestCount_]; - } - - function getPayload(bytes32 payloadId_) external view returns (PayloadParams memory) { - return _payloads[payloadId_]; - } - - function submitRequest( - uint256 maxFees_, - address auctionManager_, - address consumeFrom_, - address appGateway_, - QueueParams[] calldata queueParams_, - bytes memory onCompleteData_ - ) external onlyWatcher returns (uint40 requestCount, address[] memory promiseList) { - if (queueParams_.length == 0) return (0, new address[](0)); - if (queueParams_.length > requestPayloadCountLimit) - revert RequestPayloadCountLimitExceeded(); - - if (!feesManager__().isCreditSpendable(consumeFrom_, appGateway_, maxFees_)) - revert InsufficientFees(); - - requestCount = nextRequestCount++; - uint40 currentBatch = nextBatchCount; - - RequestParams storage r = _requests[requestCount]; - r.requestTrackingParams.payloadsRemaining = queueParams_.length; - r.requestFeesDetails.maxFees = maxFees_; - r.requestFeesDetails.consumeFrom = consumeFrom_; - r.auctionManager = _getAuctionManager(auctionManager_); - r.appGateway = appGateway_; - r.onCompleteData = onCompleteData_; - - CreateRequestResult memory result = _createRequest(queueParams_, appGateway_, requestCount); - - // initialize tracking params - r.requestTrackingParams.currentBatch = currentBatch; - r.requestTrackingParams.currentBatchPayloadsLeft = _batchPayloadIds[currentBatch].length; - - r.writeCount = result.writeCount; - promiseList = result.promiseList; - - if (result.totalEstimatedWatcherFees > maxFees_) revert InsufficientMaxFees(); - if (r.writeCount == 0) _processBatch(currentBatch, r); - - emit RequestSubmitted( - r.writeCount > 0, - requestCount, - result.totalEstimatedWatcherFees, - r, - result.payloadParams - ); - } - - // called by auction manager when a auction ends or a new transmitter is assigned (bid expiry) - function assignTransmitter( - uint40 requestCount_, - Bid memory bid_ - ) external isRequestCancelled(requestCount_) { - RequestParams storage r = _requests[requestCount_]; - if (r.auctionManager != msg.sender) revert InvalidCaller(); - if (r.requestTrackingParams.isRequestExecuted) revert RequestAlreadySettled(); - - if (r.writeCount == 0) revert NoWriteRequest(); - - // If same transmitter is reassigned, revert - // todo: remove after game - // also this overrides a payload deadline hence an unexecuted payload can - // be executed by new added transmitters. need to fix this by marking req deadline or something. - if (r.requestFeesDetails.winningBid.transmitter == bid_.transmitter) - revert AlreadyAssigned(); - - // If a transmitter was already assigned previously, unblock the credits - if (r.requestFeesDetails.winningBid.transmitter != address(0)) { - feesManager__().unblockCredits(requestCount_); - } - - r.requestFeesDetails.winningBid = bid_; - - // If a transmitter changed to address(0), return after unblocking the credits - if (bid_.transmitter == address(0)) return; - - // Block the credits for the new transmitter - feesManager__().blockCredits(requestCount_, r.requestFeesDetails.consumeFrom, bid_.fee); - - // re-process current batch again or process the batch for the first time - _processBatch(r.requestTrackingParams.currentBatch, r); - } - - function _createRequest( - QueueParams[] calldata queueParams_, - address appGateway_, - uint40 requestCount_ - ) internal returns (CreateRequestResult memory result) { - // push first batch count - _requestBatchIds[requestCount_].push(nextBatchCount); - - result.promiseList = new address[](queueParams_.length); - result.payloadParams = new PayloadParams[](queueParams_.length); - for (uint256 i = 0; i < queueParams_.length; i++) { - QueueParams calldata queuePayloadParam = queueParams_[i]; - bytes4 callType = queuePayloadParam.overrideParams.callType; - if (callType == WRITE) result.writeCount++; - - // decide batch count - if (i > 0 && queueParams_[i].overrideParams.isParallelCall != Parallel.ON) { - nextBatchCount++; - _requestBatchIds[requestCount_].push(nextBatchCount); - } - - uint64 switchboardId = watcher__().configurations__().switchboards( - queuePayloadParam.transaction.chainSlug, - queuePayloadParam.switchboardType - ); - - // process payload data and store - (bytes memory precompileData, uint256 estimatedFees) = _validateAndGetPrecompileData( - queuePayloadParam, - appGateway_, - callType - ); - result.totalEstimatedWatcherFees += estimatedFees; - - // create payload id - uint160 payloadPointer = (uint160(requestCount_) << 120) | - (uint160(nextBatchCount) << 80) | - uint160(payloadCounter++); - - bytes32 payloadId = createPayloadId( - payloadPointer, - switchboardId, - queuePayloadParam.transaction.chainSlug - ); - _batchPayloadIds[nextBatchCount].push(payloadId); - - // create prev digest hash - PayloadParams memory p; - p.payloadPointer = payloadPointer; - p.callType = callType; - p.asyncPromise = queueParams_[i].asyncPromise; - p.appGateway = appGateway_; - p.payloadId = payloadId; - p.precompileData = precompileData; - - result.promiseList[i] = queueParams_[i].asyncPromise; - result.payloadParams[i] = p; - _payloads[payloadId] = p; - } - - nextBatchCount++; - } - - function _validateAndGetPrecompileData( - QueueParams calldata payloadParams_, - address appGateway_, - bytes4 callType_ - ) internal view returns (bytes memory precompileData, uint256 estimatedFees) { - if (address(precompiles[callType_]) == address(0)) revert InvalidCallType(); - return - IPrecompile(precompiles[callType_]).validateAndGetPrecompileData( - payloadParams_, - appGateway_ - ); - } - - function _getAuctionManager(address auctionManager_) internal view returns (address) { - return - auctionManager_ == address(0) - ? addressResolver__.defaultAuctionManager() - : auctionManager_; - } - - // called when processing batch first time or being retried - function _processBatch(uint40 batchCount_, RequestParams storage r) internal { - bytes32[] memory payloadIds = _batchPayloadIds[batchCount_]; - - uint256 totalFees = 0; - for (uint40 i = 0; i < payloadIds.length; i++) { - bytes32 payloadId = payloadIds[i]; - - // check needed for re-process, in case a payload is already executed by last transmitter - if (_isPromiseResolved(_payloads[payloadId].asyncPromise)) continue; - PayloadParams storage payloadParams = _payloads[payloadId]; - - (uint256 fees, uint256 deadline, bytes memory precompileData) = IPrecompile( - precompiles[payloadParams.callType] - ).handlePayload(r.requestFeesDetails.winningBid.transmitter, payloadParams); - - totalFees += fees; - payloadParams.deadline = deadline; - payloadParams.precompileData = precompileData; - } - - address watcherFeesPayer = r.requestFeesDetails.winningBid.transmitter == address(0) - ? r.requestFeesDetails.consumeFrom - : r.requestFeesDetails.winningBid.transmitter; - IERC20(address(feesManager__())).transferFrom(watcherFeesPayer, address(this), totalFees); - } - - /// @notice Increases the fees for a request if no bid is placed - /// @param requestCount_ The ID of the request - /// @param newMaxFees_ The new maximum fees - function increaseFees( - uint40 requestCount_, - uint256 newMaxFees_, - address appGateway_ - ) external onlyWatcher isRequestCancelled(requestCount_) { - RequestParams storage r = _requests[requestCount_]; - if (r.requestTrackingParams.isRequestExecuted) revert RequestAlreadySettled(); - - if (appGateway_ != r.appGateway) revert OnlyAppGateway(); - if (r.requestFeesDetails.maxFees >= newMaxFees_) - revert NewMaxFeesLowerThanCurrent(r.requestFeesDetails.maxFees, newMaxFees_); - - if ( - !IFeesManager(feesManager__()).isCreditSpendable( - r.requestFeesDetails.consumeFrom, - appGateway_, - newMaxFees_ - ) - ) revert InsufficientFees(); - - r.requestFeesDetails.maxFees = newMaxFees_; - - // indexed by transmitter and watcher to start bidding or re-processing the request - emit FeesIncreased(requestCount_, newMaxFees_); - } - - function updateRequestAndProcessBatch( - uint40 requestCount_, - bytes32 payloadId_ - ) external onlyPromiseResolver isRequestCancelled(requestCount_) { - RequestParams storage r = _requests[requestCount_]; - - PayloadParams storage payloadParams = _payloads[payloadId_]; - payloadParams.resolvedAt = block.timestamp; - - RequestTrackingParams storage trackingParams = r.requestTrackingParams; - trackingParams.currentBatchPayloadsLeft--; - trackingParams.payloadsRemaining--; - - IPrecompile(precompiles[payloadParams.callType]).resolvePayload(payloadParams); - - if (trackingParams.currentBatchPayloadsLeft != 0) return; - if (trackingParams.payloadsRemaining == 0) { - trackingParams.isRequestExecuted = true; - _settleRequest(requestCount_, r); - } else { - uint40 currentBatch = ++trackingParams.currentBatch; - trackingParams.currentBatchPayloadsLeft = _batchPayloadIds[currentBatch].length; - _processBatch(currentBatch, r); - } - } - - function _isPromiseResolved(address promise_) internal view returns (bool) { - return IPromise(promise_).state() == AsyncPromiseState.RESOLVED; - } - - /// @notice Cancels a request - /// @param requestCount The request count to cancel - /// @dev This function cancels a request - /// @dev It verifies that the caller is the middleware and that the request hasn't been cancelled yet - function cancelRequestForReverts(uint40 requestCount) external onlyPromiseResolver { - _cancelRequest(requestCount, _requests[requestCount]); - } - - /// @notice Cancels a request - /// @param requestCount The request count to cancel - /// @dev This function cancels a request - /// @dev It verifies that the caller is the middleware and that the request hasn't been cancelled yet - function cancelRequest(uint40 requestCount, address appGateway_) external onlyWatcher { - RequestParams storage r = _requests[requestCount]; - if (appGateway_ != r.appGateway) revert InvalidCaller(); - _cancelRequest(requestCount, r); - } - - function handleRevert(uint40 requestCount) external onlyPromiseResolver { - _cancelRequest(requestCount, _requests[requestCount]); - } - - function _cancelRequest( - uint40 requestCount_, - RequestParams storage r - ) internal isRequestCancelled(requestCount_) { - if (r.requestTrackingParams.isRequestExecuted) revert RequestAlreadySettled(); - - r.requestTrackingParams.isRequestCancelled = true; - _settleRequest(requestCount_, r); - emit RequestCancelled(requestCount_); - } - - function _settleRequest(uint40 requestCount_, RequestParams storage r) internal { - feesManager__().unblockAndAssignCredits( - requestCount_, - r.requestFeesDetails.winningBid.transmitter - ); - - if (r.onCompleteData.length > 0) { - (bool success, , ) = r.appGateway.tryCall(0, gasleft(), 0, r.onCompleteData); - if (!success) emit RequestCompletedWithErrors(requestCount_); - } - emit RequestSettled(requestCount_, r.requestFeesDetails.winningBid.transmitter); - } - - /** - * @notice Rescues funds from the contract if they are locked by mistake. This contract does not - * theoretically need this function but it is added for safety. - * @param token_ The address of the token contract. - * @param rescueTo_ The address where rescued tokens need to be sent. - * @param amount_ The amount of tokens to be rescued. - */ - function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { - RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); - } -} diff --git a/contracts/evmx/watcher/Watcher.sol b/contracts/evmx/watcher/Watcher.sol index 0c714c3f..99249b9d 100644 --- a/contracts/evmx/watcher/Watcher.sol +++ b/contracts/evmx/watcher/Watcher.sol @@ -1,132 +1,224 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; + +import "solady/utils/Initializable.sol"; +import "./Configurations.sol"; +import {IPrecompile} from "../interfaces/IPrecompile.sol"; +import {IGasAccountManager} from "../interfaces/IGasAccountManager.sol"; +import {IPromise} from "../interfaces/IPromise.sol"; +import {IGasAccountToken} from "../interfaces/IGasAccountToken.sol"; +import "../../utils/common/IdUtils.sol"; +import "../../utils/Pausable.sol"; +import {PAUSER_ROLE, GOVERNANCE_ROLE} from "../../utils/common/AccessRoles.sol"; +import "solady/utils/LibCall.sol"; + +/// @title Watcher +/// @notice Minimal payload → payloads container with no batch/auction logic. +/// @dev Lives alongside existing Watcher without modifying current code. +contract Watcher is Initializable, Configurations, Pausable { + using LibCall for address; -import "./Trigger.sol"; -import "../interfaces/IPromise.sol"; + uint64 public nextPayloadCount; -contract Watcher is Trigger { - using LibCall for address; + mapping(bytes32 => Payload) internal _payloads; + mapping(bytes4 => IPrecompile) public precompiles; + // trigger + uint32 public triggerFromChainSlug; + uint256 public triggerFees; + bytes32 public triggerFromPlug; + mapping(bytes32 => bool) public isAppGatewayCalled; - event CoreContractsSet(address requestHandler, address configManager, address promiseResolver); + bytes32 public currentPayloadId; + address public latestAsyncPromise; + address public latestAppGateway; + RawPayload public payloadData; + address public transmitter; + bytes32 public transmitterSolana; + + error PayloadAlreadyCancelled(); + error PayloadAlreadySettled(); + error PayloadAlreadySet(); + error AppGatewayMismatch(); constructor() { - _disableInitializers(); // disable for implementation + _disableInitializers(); } function initialize( uint32 evmxSlug_, - uint256 triggerFees_, address owner_, - address addressResolver_ - ) public reinitializer(1) { + address addressResolver_, + address transmitter_, + bytes32 transmitterSolana_, + uint256 triggerFees_ + ) external reinitializer(1) { evmxSlug = evmxSlug_; + transmitter = transmitter_; + transmitterSolana = transmitterSolana_; triggerFees = triggerFees_; _initializeOwner(owner_); _setAddressResolver(addressResolver_); + if (nextPayloadCount == 0) nextPayloadCount = 1; } - function setCoreContracts( - address requestHandler_, - address configManager_, - address promiseResolver_ - ) external onlyOwner { - requestHandler__ = IRequestHandler(requestHandler_); - configurations__ = IConfigurations(configManager_); - promiseResolver__ = IPromiseResolver(promiseResolver_); + function addPayloadData(RawPayload calldata rawPayload_, address appGateway_) external { + if (payloadData.asyncPromise != address(0)) revert PayloadAlreadySet(); - emit CoreContractsSet(requestHandler_, configManager_, promiseResolver_); - } + payloadData = rawPayload_; - function isWatcher(address account_) public view override returns (bool) { - return - account_ == address(requestHandler__) || - account_ == address(configurations__) || - account_ == address(promiseResolver__); + // todo: what happens in read and schedule? + currentPayloadId = getCurrentPayloadId( + payloadData.transaction.chainSlug, + payloadData.overrideParams.switchboardType + ); + latestAsyncPromise = asyncDeployer__().deployAsyncPromiseContract( + appGateway_, + currentPayloadId + ); + latestAppGateway = appGateway_; } - // can be called to submit single payload request without any callback - function queueAndSubmit( - QueueParams memory queue_, - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes memory onCompleteData - ) external returns (uint40 requestCount, address[] memory promises) { - _queue(queue_, msg.sender); - return _submitRequest(maxFees, auctionManager, consumeFrom, onCompleteData); + /// @notice Submit a payload containing a single payload. No batches/auctions. + /// @dev Deploys promise via asyncDeployer and stores payload directly. + function executePayload() external whenNotPaused returns (address asyncPromise) { + if (latestAppGateway != msg.sender) revert AppGatewayMismatch(); + if ( + !gasAccountToken__().isGasAvailable( + payloadData.overrideParams.consumeFrom, + latestAppGateway, + payloadData.overrideParams.maxFees + ) + ) revert InsufficientFees(); + + IPrecompile precompile = IPrecompile(precompiles[payloadData.overrideParams.callType]); + if (address(precompile) == address(0)) revert InvalidCallType(); + + gasAccountManager__().escrowGas( + currentPayloadId, + payloadData.overrideParams.consumeFrom, + payloadData.overrideParams.maxFees + ); + asyncPromise = latestAsyncPromise; + + (uint256 fees, uint256 deadline, bytes memory precompileData) = IPrecompile(precompile) + .handlePayload(payloadData, latestAppGateway, currentPayloadId); + if (fees > payloadData.overrideParams.maxFees) revert InsufficientFees(); + + _payloads[currentPayloadId] = Payload({ + callType: payloadData.overrideParams.callType, + isPayloadCancelled: false, + isPayloadExecuted: false, + isTransmitterFeesSettled: false, + payloadPointer: nextPayloadCount++, + asyncPromise: asyncPromise, + appGateway: latestAppGateway, + consumeFrom: payloadData.overrideParams.consumeFrom, + payloadId: currentPayloadId, + watcherFees: fees, + maxFees: payloadData.overrideParams.maxFees, + resolvedAt: 0, + deadline: deadline, + precompileData: precompileData + }); + + emit PayloadSubmitted(_payloads[currentPayloadId]); } - /// @notice Queues a new payload - /// @param queue_ The call parameters - function queue( - QueueParams memory queue_, - address appGateway_ - ) external returns (address, uint40) { - return _queue(queue_, appGateway_); + function resolvePayload(WatcherMultiCallParams memory params_) external whenNotPaused { + _validateSignature(address(this), params_.data, params_.nonce, params_.signature); + (PromiseReturnData memory resolvedPromise, uint256 feesUsed) = abi.decode( + params_.data, + (PromiseReturnData, uint256) + ); + _resolvePayload(resolvedPromise, feesUsed); } - function _queue( - QueueParams memory queue_, - address appGateway_ - ) internal returns (address, uint40) { - // checks if app gateway passed by forwarder is coming from same core app gateway group - if (appGatewayTemp != address(0)) - if (appGatewayTemp != appGateway_ || appGateway_ == address(0)) - revert InvalidAppGateway(); - - uint40 requestCount = getCurrentRequestCount(); - // Deploy a new async promise contract. - latestAsyncPromise = asyncDeployer__().deployAsyncPromiseContract( - appGateway_, - requestCount - ); - appGatewayTemp = appGateway_; - queue_.asyncPromise = latestAsyncPromise; + /// @notice Mark a payload as resolved and complete its parent payload when all are done. + function _resolvePayload( + PromiseReturnData memory resolvedPromise_, + uint256 feesUsed_ + ) internal { + Payload storage p = _payloads[resolvedPromise_.payloadId]; + if (p.isPayloadExecuted) return; + if (p.isPayloadCancelled) return; + + if (!p.isTransmitterFeesSettled) { + p.isTransmitterFeesSettled = true; + gasAccountManager__().settleGasPayment( + p.payloadId, + p.consumeFrom, + transmitter, + feesUsed_ + ); + } + + p.isPayloadExecuted = true; + p.resolvedAt = block.timestamp; + + IPrecompile(precompiles[p.callType]).resolvePayload(p); + bool success = _markResolved(resolvedPromise_); + if (!success) return; - // Add the promise to the queue. - payloadQueue.push(queue_); - // return the promise and request count - return (latestAsyncPromise, requestCount); + gasAccountManager__().settleGasPayment( + p.payloadId, + p.consumeFrom, + address(this), + p.watcherFees + ); + gasAccountManager__().releaseEscrow(p.payloadId); + emit PayloadSettled(p.payloadId); // todo: both settle and resolve events needed? + emit PayloadResolved(resolvedPromise_.payloadId); } - function submitRequest( - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes memory onCompleteData - ) external returns (uint40, address[] memory) { - return _submitRequest(maxFees, auctionManager, consumeFrom, onCompleteData); + function _markResolved( + PromiseReturnData memory resolvedPromise_ + ) internal returns (bool success) { + Payload storage payloadParams = _payloads[resolvedPromise_.payloadId]; + + address asyncPromise = payloadParams.asyncPromise; + if (asyncPromise != address(0)) { + success = IPromise(asyncPromise).markResolved(resolvedPromise_); + if (!success) { + emit PromiseNotResolved(resolvedPromise_.payloadId, asyncPromise); + return false; + } + } + emit PromiseResolved(resolvedPromise_.payloadId, asyncPromise); + return true; } - function _submitRequest( - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes memory onCompleteData - ) internal returns (uint40 requestCount, address[] memory promiseList) { - if (payloadQueue.length == 0) return (0, new address[](0)); - address appGateway = msg.sender; - - // this check is to verify that msg.sender (app gateway base) belongs to correct app gateway - if (appGateway != appGatewayTemp) revert InvalidAppGateway(); - latestAsyncPromise = address(0); - appGatewayTemp = address(0); - - (requestCount, promiseList) = requestHandler__.submitRequest( - maxFees, - auctionManager, - consumeFrom, - appGateway, - payloadQueue, - onCompleteData + function markRevert(WatcherMultiCallParams memory params_) external { + _validateSignature(address(this), params_.data, params_.nonce, params_.signature); + (PromiseReturnData memory resolvedPromise, bool isRevertingOnchain) = abi.decode( + params_.data, + (PromiseReturnData, bool) ); - clearQueue(); + _markRevert(resolvedPromise, isRevertingOnchain); } - /// @notice Clears the call parameters array - function clearQueue() public { - delete payloadQueue; + /// @notice Marks a payload as reverting + /// @param isRevertingOnchain_ Whether the payload is reverting onchain + /// @param resolvedPromise_ The resolved promise + /// @dev This function marks a payload as reverting + /// @dev It cancels the payload and marks the promise as onchain reverting if the payload is reverting onchain + function _markRevert( + PromiseReturnData memory resolvedPromise_, + bool isRevertingOnchain_ + ) internal { + // Get payload params from Watcher + bytes32 payloadId = resolvedPromise_.payloadId; + Payload memory payloadParams = _payloads[payloadId]; + if (payloadParams.deadline > block.timestamp) revert DeadlineNotPassedForOnChainRevert(); + + // marks the payload as cancelled and settles the fees + cancelExecution(payloadId); + + // marks the promise as onchain reverting if the payload is reverting onchain + if (isRevertingOnchain_ && payloadParams.asyncPromise != address(0)) + IPromise(payloadParams.asyncPromise).markOnchainRevert(resolvedPromise_); + + emit MarkedRevert(payloadId, isRevertingOnchain_); } function callAppGateways(WatcherMultiCallParams memory params_) external { @@ -138,48 +230,81 @@ contract Watcher is Trigger { } } - function setTriggerFees( - uint256 triggerFees_, - uint256 nonce_, - bytes memory signature_ - ) external { - _validateSignature(address(this), abi.encode(triggerFees_), nonce_, signature_); - _setTriggerFees(triggerFees_); - } + function _callAppGateways(TriggerParams memory params_) internal { + if (isAppGatewayCalled[params_.triggerId]) revert AppGatewayAlreadyCalled(); - function getCurrentRequestCount() public view returns (uint40) { - return requestHandler__.nextRequestCount(); - } + address appGateway = fromBytes32Format(params_.appGatewayId); + if (!isValidPlug[appGateway][params_.chainSlug][params_.plug]) + revert InvalidCallerTriggered(); - function getRequestParams(uint40 requestCount_) external view returns (RequestParams memory) { - return requestHandler__.getRequest(requestCount_); - } + uint256 deadline = abi.decode(params_.overrides, (uint256)); + if (deadline < block.timestamp) revert DeadlinePassed(); - function getPayloadParams(bytes32 payloadId_) external view returns (PayloadParams memory) { - return requestHandler__.getPayload(payloadId_); - } + gasAccountToken__().transferFrom(appGateway, address(this), triggerFees); + triggerFromChainSlug = params_.chainSlug; + triggerFromPlug = params_.plug; + (bool success, , ) = appGateway.tryCall( + 0, + gasleft(), + 0, // setting max_copy_bytes to 0 as not using returnData right now + params_.payload + ); - function setIsValidPlug(bool isValid_, uint32 chainSlug_, bytes32 plug_) external override { - configurations__.setIsValidPlug(isValid_, chainSlug_, plug_, msg.sender); - } + if (!success) { + emit TriggerFailed(params_.triggerId); + } else { + isAppGatewayCalled[params_.triggerId] = true; + emit TriggerSucceeded(params_.triggerId); + } - function cancelRequest(uint40 requestCount_) external override { - requestHandler__.cancelRequest(requestCount_, msg.sender); + triggerFromChainSlug = 0; + triggerFromPlug = bytes32(0); } - function increaseFees(uint40 requestCount_, uint256 newFees_) external override { - requestHandler__.increaseFees(requestCount_, newFees_, msg.sender); + /// @notice Increases the fees for a payload if no bid is placed + /// @param payloadId_ The ID of the payload + /// @param newMaxFees_ The new maximum fees + function increaseFees(bytes32 payloadId_, uint256 newMaxFees_) external { + Payload storage r = _payloads[payloadId_]; + if (msg.sender != r.appGateway) revert OnlyAppGateway(); + if (r.isPayloadCancelled) revert PayloadAlreadyCancelled(); + if (r.isPayloadExecuted) revert PayloadAlreadySettled(); + if (r.maxFees >= newMaxFees_) revert NewMaxFeesLowerThanCurrent(r.maxFees, newMaxFees_); + gasAccountManager__().releaseEscrow(payloadId_); + r.maxFees = newMaxFees_; + + // reblock new fees + if (!gasAccountToken__().isGasAvailable(r.consumeFrom, msg.sender, newMaxFees_)) + revert InsufficientFees(); + gasAccountManager__().escrowGas(payloadId_, r.consumeFrom, newMaxFees_); + + // indexed by transmitter and watcher to start bidding or re-processing the payload + emit FeesIncreased(payloadId_, newMaxFees_); } - function getPrecompileFees( - bytes4 precompile_, - bytes memory precompileData_ - ) external view returns (uint256) { - return requestHandler__.getPrecompileFees(precompile_, precompileData_); + function cancelExecution(bytes32 payloadId_) public { + Payload storage r = _payloads[payloadId_]; + if (r.isPayloadExecuted) revert PayloadAlreadySettled(); + if (r.isPayloadCancelled) revert PayloadAlreadyCancelled(); + + r.isPayloadCancelled = true; + r.isTransmitterFeesSettled = true; + + gasAccountManager__().settleGasPayment( + payloadId_, + r.consumeFrom, + transmitter, + r.maxFees - r.watcherFees + ); + gasAccountManager__().settleGasPayment( + payloadId_, + r.consumeFrom, + address(this), + r.watcherFees + ); + emit PayloadCancelled(payloadId_); } - // all function from watcher requiring signature - // can be also used to do msg.sender check related function in other contracts like withdraw credits from fees manager and set core app-gateways in configurations function watcherMultiCall(WatcherMultiCallParams[] memory params_) external payable { for (uint40 i = 0; i < params_.length; i++) { _validateSignature( @@ -200,64 +325,56 @@ contract Watcher is Trigger { } } - /// @notice Verifies that a watcher signature is valid - /// @param data_ The data to verify - /// @param nonce_ The nonce of the signature - /// @param signature_ The signature to verify - function _validateSignature( - address contractAddress_, - bytes memory data_, - uint256 nonce_, - bytes memory signature_ - ) internal { - if (contractAddress_ == address(0)) revert InvalidContract(); - if (data_.length == 0) revert InvalidData(); - if (signature_.length == 0) revert InvalidSignature(); - if (isNonceUsed[nonce_]) revert NonceUsed(); - isNonceUsed[nonce_] = true; - - bytes32 digest = keccak256( - abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_) - ); + function getCurrentPayloadId( + uint32 chainSlug_, + bytes32 switchboardType_ + ) public view returns (bytes32) { + uint32 switchboardId = switchboards[chainSlug_][switchboardType_]; + // Write payload: source = (evmxChainSlug, watcherId), verification = (dstChainSlug, dstSwitchboardId) + // watcherId hardcoded as 1 for now + return + createPayloadId( + evmxSlug, // source chain slug (evmx) + 1, // source id (watcher id, hardcoded) + chainSlug_, // verification chain slug (destination) + switchboardId, // verification id (destination switchboard) + nextPayloadCount // pointer (counter) + ); + } - // check if signature is valid - if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature(); + /// @notice Read a simple payload by id. + function getPayload(bytes32 payloadId) external view returns (Payload memory) { + return _payloads[payloadId]; } - /// @notice Recovers the signer of a message - /// @param digest_ The digest of the input data - /// @param signature_ The signature to verify - /// @dev This function verifies that the signature was created by the watcher and that the nonce has not been used before - function _recoverSigner( - bytes32 digest_, - bytes memory signature_ - ) internal view returns (address signer) { - bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); - - // recovered signer is checked for the valid roles later - signer = ECDSA.recover(digest, signature_); + function setTriggerFees(uint256 triggerFees_) external onlyWatcher { + triggerFees = triggerFees_; + emit TriggerFeesSet(triggerFees_); } - /** - * @notice Rescues funds from the contract if they are locked by mistake. This contract does not - * theoretically need this function but it is added for safety. - * @param token_ The address of the token contract. - * @param rescueTo_ The address where rescued tokens need to be sent. - * @param amount_ The amount of tokens to be rescued. - */ - function rescueFunds( - address token_, - address rescueTo_, - uint256 amount_, - uint256 nonce_, - bytes memory signature_ - ) external { - _validateSignature( - address(this), - abi.encode(token_, rescueTo_, amount_), - nonce_, - signature_ - ); - RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + function setPrecompile(bytes4 callType_, IPrecompile precompile_) external onlyOwner { + precompiles[callType_] = precompile_; + emit PrecompileSet(callType_, precompile_); + } + + function getPrecompileFees( + bytes4 callType_, + bytes memory precompileData_ + ) external view returns (uint256) { + return precompiles[callType_].getPrecompileFees(precompileData_); + } + + //////////////////////////////////////////////////////// + ////////////////////// Pausable //////////////////////// + //////////////////////////////////////////////////////// + + /// @notice Pause the contract (only pauser role) + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @notice Unpause the contract (only unpauser role) + function unpause() external onlyRole(GOVERNANCE_ROLE) { + _unpause(); } } diff --git a/contracts/evmx/watcher/borsh-serde/BorshDecoder.sol b/contracts/evmx/watcher/borsh-serde/BorshDecoder.sol new file mode 100644 index 00000000..77344401 --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshDecoder.sol @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Based on Aurora bridge repo: https://github.com/aurora-is-near/aurora-contracts-sdk/blob/main/aurora-solidity-sdk +pragma solidity 0.8.28; + +import "../../../utils/common/Structs.sol"; +import "./BorshUtils.sol"; + +library BorshDecoder { + /// Decodes the borsh schema into abi.encode(value) list of params + /// Handles decoding of: + /// 1. u8/u16/u32/u64 Rust types + /// 2. "String" Rust type + /// 3. array/Vec and String numeric Rust types (mentioned in 1) and 2)) + function decodeGenericSchema( + GenericSchema memory schema, + bytes memory encodedData + ) internal pure returns (bytes[] memory) { + bytes[] memory decodedParams = new bytes[](schema.valuesTypeNames.length); + Data memory data = from(encodedData); + + for (uint256 i = 0; i < schema.valuesTypeNames.length; i++) { + string memory typeName = schema.valuesTypeNames[i]; + + if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { + uint8 value = data.decodeU8(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { + uint16 value = data.decodeU16(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { + uint32 value = data.decodeU32(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { + uint64 value = data.decodeU64(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { + uint128 value = data.decodeU128(); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { + string memory value = data.decodeString(); + decodedParams[i] = abi.encode(value); + } + // Handle Vector types with variable length + else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint8[] memory value; + (length, value) = decodeUint8Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint16[] memory value; + (length, value) = decodeUint16Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint32[] memory value; + (length, value) = decodeUint32Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint64[] memory value; + (length, value) = decodeUint64Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + uint128[] memory value; + (length, value) = decodeUint128Vec(data); + decodedParams[i] = abi.encode(value); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32 length; + string[] memory value; + (length, value) = decodeStringVec(data); + decodedParams[i] = abi.encode(value); + } + // Handle Array types with fixed length + else if (BorshUtils.startsWith(typeName, "[u8;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint8[] memory value = decodeUint8Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u16;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint16[] memory value = decodeUint16Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u32;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint32[] memory value = decodeUint32Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u64;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint64[] memory value = decodeUint64Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[u128;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + uint128[] memory value = decodeUint128Array(data, length); + decodedParams[i] = abi.encode(value); + } else if (BorshUtils.startsWith(typeName, "[String;")) { + uint256 length = BorshUtils.extractArrayLength(typeName); + string[] memory value = decodeStringArray(data, length); + decodedParams[i] = abi.encode(value); + } else { + revert("Unsupported type"); + } + } + + return decodedParams; + } + + /********* Decode primitive types *********/ + + using BorshDecoder for Data; + + struct Data { + uint256 ptr; + uint256 end; + } + + /********* Helper to manage data pointer *********/ + + function from(bytes memory data) internal pure returns (Data memory res) { + uint256 ptr; + assembly { + ptr := data + } + unchecked { + res.ptr = ptr + 32; + res.end = res.ptr + BorshUtils.readMemory(ptr); + } + } + + // This function assumes that length is reasonably small, so that data.ptr + length will not overflow. In the current code, length is always less than 2^32. + function requireSpace(Data memory data, uint256 length) internal pure { + unchecked { + require(data.ptr + length <= data.end, "Parse error: unexpected EOI"); + } + } + + function read(Data memory data, uint256 length) internal pure returns (bytes32 res) { + data.requireSpace(length); + res = bytes32(BorshUtils.readMemory(data.ptr)); + unchecked { + data.ptr += length; + } + return res; + } + + function done(Data memory data) internal pure { + require(data.ptr == data.end, "Parse error: EOI expected"); + } + + /********* Decoders for primitive types *********/ + + function decodeU8(Data memory data) internal pure returns (uint8) { + return uint8(bytes1(data.read(1))); + } + + function decodeU16(Data memory data) internal pure returns (uint16) { + return BorshUtils.swapBytes2(uint16(bytes2(data.read(2)))); + } + + function decodeU32(Data memory data) internal pure returns (uint32) { + return BorshUtils.swapBytes4(uint32(bytes4(data.read(4)))); + } + + function decodeU64(Data memory data) internal pure returns (uint64) { + return BorshUtils.swapBytes8(uint64(bytes8(data.read(8)))); + } + + function decodeU128(Data memory data) internal pure returns (uint128) { + return BorshUtils.swapBytes16(uint128(bytes16(data.read(16)))); + } + + function decodeU256(Data memory data) internal pure returns (uint256) { + return BorshUtils.swapBytes32(uint256(data.read(32))); + } + + function decodeBytes20(Data memory data) internal pure returns (bytes20) { + return bytes20(data.read(20)); + } + + function decodeBytes32(Data memory data) internal pure returns (bytes32) { + return data.read(32); + } + + function decodeBool(Data memory data) internal pure returns (bool) { + uint8 res = data.decodeU8(); + require(res <= 1, "Parse error: invalid bool"); + return res != 0; + } + + function skipBytes(Data memory data) internal pure { + uint256 length = data.decodeU32(); + data.requireSpace(length); + unchecked { + data.ptr += length; + } + } + + function decodeBytes(Data memory data) internal pure returns (bytes memory res) { + uint256 length = data.decodeU32(); + data.requireSpace(length); + res = BorshUtils.memoryToBytes(data.ptr, length); + unchecked { + data.ptr += length; + } + } + + function decodeString(Data memory data) internal pure returns (string memory) { + bytes memory stringBytes = data.decodeBytes(); + return string(stringBytes); + } + + /********* Decode Vector types with variable length *********/ + + function decodeUint8Vec(Data memory data) internal pure returns (uint32, uint8[] memory) { + uint32 length = data.decodeU32(); + uint8[] memory values = new uint8[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU8(); + } + + return (length, values); + } + + function decodeUint16Vec(Data memory data) internal pure returns (uint32, uint16[] memory) { + uint32 length = data.decodeU32(); + uint16[] memory values = new uint16[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU16(); + } + + return (length, values); + } + + function decodeUint32Vec(Data memory data) internal pure returns (uint32, uint32[] memory) { + uint32 length = data.decodeU32(); + uint32[] memory values = new uint32[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU32(); + } + + return (length, values); + } + + function decodeUint64Vec(Data memory data) internal pure returns (uint32, uint64[] memory) { + uint32 length = data.decodeU32(); + uint64[] memory values = new uint64[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU64(); + } + + return (length, values); + } + + function decodeUint128Vec(Data memory data) internal pure returns (uint32, uint128[] memory) { + uint32 length = data.decodeU32(); + uint128[] memory values = new uint128[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU128(); + } + + return (length, values); + } + + function decodeStringVec(Data memory data) internal pure returns (uint32, string[] memory) { + uint32 length = data.decodeU32(); + string[] memory values = new string[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeString(); + } + + return (length, values); + } + + /********* Decode array types with fixed length *********/ + + function decodeUint8Array( + Data memory data, + uint256 length + ) internal pure returns (uint8[] memory) { + uint8[] memory values = new uint8[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU8(); + } + + return values; + } + + function decodeUint16Array( + Data memory data, + uint256 length + ) internal pure returns (uint16[] memory) { + uint16[] memory values = new uint16[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU16(); + } + + return values; + } + + function decodeUint32Array( + Data memory data, + uint256 length + ) internal pure returns (uint32[] memory) { + uint32[] memory values = new uint32[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU32(); + } + + return values; + } + + function decodeUint64Array( + Data memory data, + uint256 length + ) internal pure returns (uint64[] memory) { + uint64[] memory values = new uint64[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU64(); + } + + return values; + } + + function decodeUint128Array( + Data memory data, + uint256 length + ) internal pure returns (uint128[] memory) { + uint128[] memory values = new uint128[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeU128(); + } + + return values; + } + + function decodeStringArray( + Data memory data, + uint256 length + ) internal pure returns (string[] memory) { + string[] memory values = new string[](length); + + for (uint256 i = 0; i < length; i++) { + values[i] = data.decodeString(); + } + + return values; + } +} diff --git a/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol b/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol new file mode 100644 index 00000000..41214ba8 --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshEncoder.sol @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Based on Aurora bridge repo: https://github.com/aurora-is-near/aurora-contracts-sdk/blob/main/aurora-solidity-sdk +pragma solidity 0.8.28; + +import "../../../utils/common/Structs.sol"; +import "./BorshUtils.sol"; + +library BorshEncoder { + function encodeFunctionArgs( + SolanaInstruction memory instruction + ) internal pure returns (bytes memory) { + bytes memory functionArgsPacked; + for (uint256 i = 0; i < instruction.data.functionArguments.length; i++) { + string memory typeName = instruction.description.functionArgumentTypeNames[i]; + bytes memory data = instruction.data.functionArguments[i]; + + if (keccak256(bytes(typeName)) == keccak256(bytes("u8"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint8 arg = uint8(abiDecodedArg); + bytes1 borshEncodedArg = encodeU8(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint16 arg = uint16(abiDecodedArg); + bytes2 borshEncodedArg = encodeU16(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint32 arg = uint32(abiDecodedArg); + bytes4 borshEncodedArg = encodeU32(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint64 arg = uint64(abiDecodedArg); + bytes8 borshEncodedArg = encodeU64(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128"))) { + uint256 abiDecodedArg = abi.decode(data, (uint256)); + uint128 arg = uint128(abiDecodedArg); + bytes16 borshEncodedArg = encodeU128(arg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("String"))) { + string memory abiDecodedArg = abi.decode(data, (string)); + bytes memory borshEncodedArg = encodeString(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + // Handle array types with fixed length + else if (BorshUtils.startsWith(typeName, "u8[]")) { + uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + bytes memory borshEncodedArg = encodeUint8Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u16[]"))) { + uint16[] memory abiDecodedArg = abi.decode(data, (uint16[])); + bytes memory borshEncodedArg = encodeUint16Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u32[]"))) { + uint32[] memory abiDecodedArg = abi.decode(data, (uint32[])); + bytes memory borshEncodedArg = encodeUint32Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u64[]"))) { + uint64[] memory abiDecodedArg = abi.decode(data, (uint64[])); + bytes memory borshEncodedArg = encodeUint64Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("u128[]"))) { + uint128[] memory abiDecodedArg = abi.decode(data, (uint128[])); + bytes memory borshEncodedArg = encodeUint128Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("String[]"))) { + string[] memory abiDecodedArg = abi.decode(data, (string[])); + bytes memory borshEncodedArg = encodeStringArray(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + // Handle Vector types with that can have variable length - length prefix is added + else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + bytes memory borshEncodedArg = encodeUint8Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint16[] memory abiDecodedArg = abi.decode(data, (uint16[])); + bytes memory borshEncodedArg = encodeUint16Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint32[] memory abiDecodedArg = abi.decode(data, (uint32[])); + bytes memory borshEncodedArg = encodeUint32Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint64[] memory abiDecodedArg = abi.decode(data, (uint64[])); + bytes memory borshEncodedArg = encodeUint64Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + uint128[] memory abiDecodedArg = abi.decode(data, (uint128[])); + bytes memory borshEncodedArg = encodeUint128Vec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (keccak256(bytes(typeName)) == keccak256(bytes("Vec"))) { + string[] memory abiDecodedArg = abi.decode(data, (string[])); + bytes memory borshEncodedArg = encodeStringVec(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + // Handle array types with fixed length - no length prefix, just the bytes + else if (BorshUtils.startsWith(typeName, "[u8;")) { + // Old code to be fixed: + // uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + // bytes memory borshEncodedArg = encodeUint8Array(abiDecodedArg); + // functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + + // TODO: this is a tmp fix, later we must fix this to use GenericSchema for decoding the arrays + // like in test: BorshEncoderTest.testEncodeBytes32Account() + + uint256 expectedLength = BorshUtils.extractArrayLength(typeName); + if (data.length == expectedLength) { + // payload already provided as tightly packed bytes + functionArgsPacked = abi.encodePacked(functionArgsPacked, data); + } else if (data.length == expectedLength * 32) { + // payload encoded as abi.encode(uint8[N]) -> each element padded to 32 bytes + uint8[] memory abiDecodedArg = new uint8[](expectedLength); + for (uint256 j = 0; j < expectedLength; j++) { + abiDecodedArg[j] = uint8(data[j * 32 + 31]); + } + bytes memory borshEncodedArg = encodeUint8Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else { + uint8[] memory abiDecodedArg = abi.decode(data, (uint8[])); + require(abiDecodedArg.length == expectedLength, "[u8;N] length mismatch"); + bytes memory borshEncodedArg = encodeUint8Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } + } else if (BorshUtils.startsWith(typeName, "[u16;")) { + uint16[] memory abiDecodedArg = abi.decode(data, (uint16[])); + bytes memory borshEncodedArg = encodeUint16Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u32;")) { + uint32[] memory abiDecodedArg = abi.decode(data, (uint32[])); + bytes memory borshEncodedArg = encodeUint32Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u64;")) { + uint64[] memory abiDecodedArg = abi.decode(data, (uint64[])); + bytes memory borshEncodedArg = encodeUint64Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[u128;")) { + uint128[] memory abiDecodedArg = abi.decode(data, (uint128[])); + bytes memory borshEncodedArg = encodeUint128Array(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else if (BorshUtils.startsWith(typeName, "[String;")) { + string[] memory abiDecodedArg = abi.decode(data, (string[])); + bytes memory borshEncodedArg = encodeStringArray(abiDecodedArg); + functionArgsPacked = abi.encodePacked(functionArgsPacked, borshEncodedArg); + } else { + revert("Unsupported type"); + } + } + return functionArgsPacked; + } + + /********* Encode functions *********/ + + /** Encode primitive types **/ + + function encodeU8(uint8 v) internal pure returns (bytes1) { + return bytes1(v); + } + + function encodeU16(uint16 v) internal pure returns (bytes2) { + return bytes2(BorshUtils.swapBytes2(v)); + } + + function encodeU32(uint32 v) internal pure returns (bytes4) { + return bytes4(BorshUtils.swapBytes4(v)); + } + + function encodeU64(uint64 v) internal pure returns (bytes8) { + return bytes8(BorshUtils.swapBytes8(v)); + } + + function encodeU128(uint128 v) internal pure returns (bytes16) { + return bytes16(BorshUtils.swapBytes16(v)); + } + + /// Encode bytes vector into borsh. Use this method to encode strings as well. + function encodeBytes(bytes memory value) internal pure returns (bytes memory) { + require(value.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + return abi.encodePacked(encodeU32(uint32(value.length)), bytes(value)); + } + + function encodeString(string memory value) internal pure returns (bytes memory) { + bytes memory strBytes = bytes(value); + return bytes.concat(encodeU32(uint32(strBytes.length)), strBytes); + } + + /** Encode Vector types with that can have variable length **/ + + function encodeUint8Vec(uint8[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint8Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint16Vec(uint16[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint16Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint32Vec(uint32[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint32Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint64Vec(uint64[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint64Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeUint128Vec(uint128[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "vector length overflow (must fit in uint32)"); + bytes memory packed = packUint128Array(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + function encodeStringVec(string[] memory arr) internal pure returns (bytes memory) { + require(arr.length <= type(uint32).max, "string length overflow (must fit in uint32)"); + bytes memory packed = packStringArray(arr); + return bytes.concat(encodeU32(uint32(arr.length)), packed); + } + + /** Encode array types with fixed length - no length prefix, just the bytes **/ + + function encodeUint8Array(uint8[] memory arr) internal pure returns (bytes memory) { + return packUint8Array(arr); + } + + function encodeUint16Array(uint16[] memory arr) internal pure returns (bytes memory) { + return packUint16Array(arr); + } + + function encodeUint32Array(uint32[] memory arr) internal pure returns (bytes memory) { + return packUint32Array(arr); + } + + function encodeUint64Array(uint64[] memory arr) internal pure returns (bytes memory) { + return packUint64Array(arr); + } + + function encodeUint128Array(uint128[] memory arr) internal pure returns (bytes memory) { + return packUint128Array(arr); + } + + function encodeStringArray(string[] memory arr) internal pure returns (bytes memory) { + return packStringArray(arr); + } + + /********* Packing functions *********/ + + // NOTE: + // When you use abi.encodePacked() on a dynamic array (uint8[]), Solidity applies ABI encoding rules where each array element gets padded to 32 bytes: + // this is why when you have: + //uint8[] memory value = new uint8[](3); + // value[0] = 1; + // value[1] = 2; + // value[2] = 3; + // bytes memory encoded = abi.encodePacked(value); + // console.logBytes(encoded); + // you get: + // 0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003 + // cause each element is padded to 32 bytes + + // --> Below function packs the array into elements without the padding + + function packUint8Array(uint8[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU8(arr[i])); + } + return out; + } + + function packUint16Array(uint16[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU16(arr[i])); + } + return out; + } + + function packUint32Array(uint32[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU32(arr[i])); + } + return out; + } + + function packUint64Array(uint64[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU64(arr[i])); + } + return out; + } + + function packUint128Array(uint128[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeU128(arr[i])); + } + return out; + } + + function packStringArray(string[] memory arr) internal pure returns (bytes memory) { + bytes memory out; + for (uint256 i = 0; i < arr.length; i++) { + out = bytes.concat(out, encodeString(arr[i])); + } + return out; + } +} diff --git a/contracts/evmx/watcher/borsh-serde/BorshUtils.sol b/contracts/evmx/watcher/borsh-serde/BorshUtils.sol new file mode 100644 index 00000000..23affb77 --- /dev/null +++ b/contracts/evmx/watcher/borsh-serde/BorshUtils.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Based on Aurora bridge repo: https://github.com/aurora-is-near/aurora-contracts-sdk/blob/main/aurora-solidity-sdk +pragma solidity 0.8.28; + +library BorshUtils { + function readMemory(uint256 ptr) internal pure returns (uint256 res) { + assembly { + res := mload(ptr) + } + } + + function writeMemory(uint256 ptr, uint256 value) internal pure { + assembly { + mstore(ptr, value) + } + } + + function memoryToBytes(uint256 ptr, uint256 length) internal pure returns (bytes memory res) { + if (length != 0) { + assembly { + // 0x40 is the address of free memory pointer. + res := mload(0x40) + let end := add( + res, + and( + add(length, 63), + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 + ) + ) + // end = res + 32 + 32 * ceil(length / 32). + mstore(0x40, end) + mstore(res, length) + let destPtr := add(res, 32) + // prettier-ignore + for {} 1 {} { + mstore(destPtr, mload(ptr)) + destPtr := add(destPtr, 32) + if eq(destPtr, end) { break } + ptr := add(ptr, 32) + } + } + } + } + + function swapBytes2(uint16 v) internal pure returns (uint16) { + return (v << 8) | (v >> 8); + } + + function swapBytes4(uint32 v) internal pure returns (uint32) { + v = ((v & 0x00ff00ff) << 8) | ((v & 0xff00ff00) >> 8); + return (v << 16) | (v >> 16); + } + + function swapBytes8(uint64 v) internal pure returns (uint64) { + v = ((v & 0x00ff00ff00ff00ff) << 8) | ((v & 0xff00ff00ff00ff00) >> 8); + v = ((v & 0x0000ffff0000ffff) << 16) | ((v & 0xffff0000ffff0000) >> 16); + return (v << 32) | (v >> 32); + } + + function swapBytes16(uint128 v) internal pure returns (uint128) { + v = + ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff) << 8) | + ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00) >> 8); + v = + ((v & 0x0000ffff0000ffff0000ffff0000ffff) << 16) | + ((v & 0xffff0000ffff0000ffff0000ffff0000) >> 16); + v = + ((v & 0x00000000ffffffff00000000ffffffff) << 32) | + ((v & 0xffffffff00000000ffffffff00000000) >> 32); + return (v << 64) | (v >> 64); + } + + function swapBytes32(uint256 v) internal pure returns (uint256) { + v = + ((v & 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff) << 8) | + ((v & 0xff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00) >> 8); + v = + ((v & 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff) << 16) | + ((v & 0xffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000) >> 16); + v = + ((v & 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff) << 32) | + ((v & 0xffffffff00000000ffffffff00000000ffffffff00000000ffffffff00000000) >> 32); + v = + ((v & 0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff) << 64) | + ((v & 0xffffffffffffffff0000000000000000ffffffffffffffff0000000000000000) >> 64); + return (v << 128) | (v >> 128); + } + + function startsWith(string memory str, string memory prefix) internal pure returns (bool) { + bytes memory strBytes = bytes(str); + bytes memory prefixBytes = bytes(prefix); + + if (prefixBytes.length > strBytes.length) return false; + + for (uint256 i = 0; i < prefixBytes.length; i++) { + if (strBytes[i] != prefixBytes[i]) return false; + } + return true; + } + + function extractArrayLength(string memory typeName) internal pure returns (uint256) { + bytes memory typeBytes = bytes(typeName); + uint256 length = 0; + bool foundSemicolon = false; + bool foundDigit = false; + + // Parse patterns like "[u8; 32]" + for (uint256 i = 0; i < typeBytes.length; i++) { + bytes1 char = typeBytes[i]; + + if (char == 0x3B) { + // ';' + foundSemicolon = true; + } else if (foundSemicolon && char >= 0x30 && char <= 0x39) { + // '0' to '9' + foundDigit = true; + length = length * 10 + uint256(uint8(char)) - 48; // Convert ASCII to number + } else if (foundSemicolon && foundDigit && char == 0x5D) { + // ']' + break; // End of array type declaration + } else if (foundSemicolon && foundDigit && char != 0x20) { + // Not a space + // If we found digits but hit a non-digit non-space, invalid format + revert("Invalid array length format"); + } + // Skip spaces and other characters before semicolon + } + + require(foundSemicolon && foundDigit && length > 0, "Could not extract array length"); + return length; + } +} diff --git a/contracts/evmx/watcher/precompiles/ReadPrecompile.sol b/contracts/evmx/watcher/precompiles/ReadPrecompile.sol index 175fb4d0..f08582ad 100644 --- a/contracts/evmx/watcher/precompiles/ReadPrecompile.sol +++ b/contracts/evmx/watcher/precompiles/ReadPrecompile.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "../../interfaces/IPrecompile.sol"; +import "../../interfaces/IWatcher.sol"; + import "../../../utils/common/Structs.sol"; import "../../../utils/common/Errors.sol"; import "../../../utils/RescueFundsLib.sol"; -import "../WatcherBase.sol"; /// @title Read /// @notice Handles read precompile logic -contract ReadPrecompile is IPrecompile, WatcherBase { +contract ReadPrecompile is IPrecompile { /// @notice Emitted when a new read is requested event ReadRequested(Transaction transaction, uint256 readAtBlockNumber, bytes32 payloadId); event ExpiryTimeSet(uint256 expiryTime); @@ -19,59 +20,53 @@ contract ReadPrecompile is IPrecompile, WatcherBase { uint256 public readFees; uint256 public expiryTime; + IWatcher public watcher__; + + modifier onlyWatcher() { + if (msg.sender != address(watcher__)) revert OnlyWatcherAllowed(); + _; + } + constructor(address watcher_, uint256 readFees_, uint256 expiryTime_) { readFees = readFees_; expiryTime = expiryTime_; - _initializeWatcher(watcher_); + watcher__ = IWatcher(watcher_); } function getPrecompileFees(bytes memory) public view returns (uint256) { return readFees; } - /// @notice Gets precompile data and fees for queue parameters - /// @param queueParams_ The queue parameters to process - /// @return precompileData The encoded precompile data - /// @return estimatedFees Estimated fees required for processing - function validateAndGetPrecompileData( - QueueParams calldata queueParams_, - address - ) external view returns (bytes memory precompileData, uint256 estimatedFees) { - if (queueParams_.transaction.target == bytes32(0)) revert InvalidTarget(); - if (queueParams_.transaction.payload.length == 0) revert InvalidPayloadSize(); - - // For read precompile, encode the payload parameters - precompileData = abi.encode( - queueParams_.transaction, - queueParams_.overrideParams.readAtBlockNumber - ); - estimatedFees = getPrecompileFees(precompileData); - } - /// @notice Handles payload processing and returns fees - /// @param payloadParams The payload parameters to handle + /// @param rawPayload The payload parameters to handle /// @return fees The fees required for processing /// @return deadline The deadline for the payload function handlePayload( + RawPayload calldata rawPayload, address, - PayloadParams calldata payloadParams - ) - external - onlyRequestHandler - returns (uint256 fees, uint256 deadline, bytes memory precompileData) - { + bytes32 payloadId + ) external onlyWatcher returns (uint256 fees, uint256 deadline, bytes memory precompileData) { + if (rawPayload.transaction.target == bytes32(0)) revert InvalidTarget(); + if (rawPayload.transaction.payload.length == 0) revert InvalidPayloadSize(); + + // For read precompile, encode the payload parameters + precompileData = abi.encode( + rawPayload.transaction, + rawPayload.overrideParams.readAtBlockNumber + ); deadline = block.timestamp + expiryTime; - precompileData = payloadParams.precompileData; - fees = getPrecompileFees(payloadParams.precompileData); + fees = getPrecompileFees(precompileData); - (Transaction memory transaction, uint256 readAtBlockNumber) = abi.decode( - payloadParams.precompileData, - (Transaction, uint256) + emit ReadRequested( + rawPayload.transaction, + rawPayload.overrideParams.readAtBlockNumber, + payloadId ); - emit ReadRequested(transaction, readAtBlockNumber, payloadParams.payloadId); } - function resolvePayload(PayloadParams calldata payloadParams_) external onlyRequestHandler {} + function resolvePayload(Payload calldata payload) external view { + if (block.timestamp > payload.deadline) revert DeadlinePassed(); + } function setFees(uint256 readFees_) external onlyWatcher { readFees = readFees_; diff --git a/contracts/evmx/watcher/precompiles/SchedulePrecompile.sol b/contracts/evmx/watcher/precompiles/SchedulePrecompile.sol index 186c9b42..b7c5b3a9 100644 --- a/contracts/evmx/watcher/precompiles/SchedulePrecompile.sol +++ b/contracts/evmx/watcher/precompiles/SchedulePrecompile.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "../../interfaces/IPrecompile.sol"; import "../../interfaces/IPromise.sol"; +import "../../interfaces/IWatcher.sol"; import "../../../utils/common/Structs.sol"; -import {InvalidScheduleDelay, ResolvingScheduleTooEarly} from "../../../utils/common/Errors.sol"; +import {InvalidScheduleDelay, ResolvingScheduleTooEarly, OnlyWatcherAllowed} from "../../../utils/common/Errors.sol"; import "../../../utils/RescueFundsLib.sol"; -import "../WatcherBase.sol"; /// @title SchedulePrecompile /// @notice Library that handles schedule logic for the WatcherPrecompile system /// @dev This library contains pure functions for schedule operations -contract SchedulePrecompile is IPrecompile, WatcherBase { +contract SchedulePrecompile is IPrecompile { // slot 52 /// @notice The maximum delay for a schedule /// @dev Maximum schedule delay in seconds @@ -25,6 +25,15 @@ contract SchedulePrecompile is IPrecompile, WatcherBase { /// @notice The expiry time for a schedule uint256 public expiryTime; + IWatcher public watcher__; + + error DeadlinePassed(); + + modifier onlyWatcher() { + if (msg.sender != address(watcher__)) revert OnlyWatcherAllowed(); + _; + } + /// @notice Emitted when the maximum schedule delay in seconds is set event MaxScheduleDelayInSecondsSet(uint256 maxScheduleDelayInSeconds_); /// @notice Emitted when the fees per second for a schedule is set @@ -58,7 +67,7 @@ contract SchedulePrecompile is IPrecompile, WatcherBase { if (maxScheduleDelayInSeconds < expiryTime) revert InvalidScheduleDelay(); expiryTime = expiryTime_; - _initializeWatcher(watcher_); + watcher__ = IWatcher(watcher_); } function getPrecompileFees(bytes memory precompileData_) public view returns (uint256) { @@ -104,44 +113,27 @@ contract SchedulePrecompile is IPrecompile, WatcherBase { emit ExpiryTimeSet(expiryTime_); } - /// @notice Validates schedule parameters and return data with fees - /// @dev assuming that tx is executed on EVMx chain - function validateAndGetPrecompileData( - QueueParams calldata queueParams_, - address - ) external view returns (bytes memory precompileData, uint256 estimatedFees) { - if (queueParams_.overrideParams.delayInSeconds > maxScheduleDelayInSeconds) - revert InvalidScheduleDelay(); - - // For schedule precompile, encode the payload parameters - precompileData = abi.encode(queueParams_.overrideParams.delayInSeconds, 0); - estimatedFees = getPrecompileFees(precompileData); - } - /// @notice Handles payload processing and returns fees - /// @param payloadParams The payload parameters to handle + /// @param rawPayload The payload parameters to handle /// @return fees The fees required for processing function handlePayload( + RawPayload calldata rawPayload, address, - PayloadParams calldata payloadParams - ) - external - onlyRequestHandler - returns (uint256 fees, uint256 deadline, bytes memory precompileData) - { - (uint256 delayInSeconds, ) = abi.decode(payloadParams.precompileData, (uint256, uint256)); - - // expiryTime is very low, to account for infra delay - uint256 executeAfter = block.timestamp + delayInSeconds; - deadline = executeAfter + expiryTime; - precompileData = abi.encode(delayInSeconds, executeAfter); - fees = getPrecompileFees(precompileData); + bytes32 payloadId + ) external onlyWatcher returns (uint256 fees, uint256 deadline, bytes memory precompileData) { + if (rawPayload.overrideParams.delayInSeconds > maxScheduleDelayInSeconds) + revert InvalidScheduleDelay(); - IPromise promise_ = IPromise(payloadParams.asyncPromise); + // For schedule precompile, encode the payload parameters + uint256 executeAfter = block.timestamp + rawPayload.overrideParams.delayInSeconds; + precompileData = abi.encode(rawPayload.overrideParams.delayInSeconds, executeAfter); + fees = getPrecompileFees(precompileData); + deadline = executeAfter + expiryTime; + IPromise promise_ = IPromise(watcher__.latestAsyncPromise()); // emits event for watcher to track schedule and resolve when deadline is reached emit ScheduleRequested( - payloadParams.payloadId, + payloadId, executeAfter, deadline, promise_.localInvoker(), @@ -150,10 +142,10 @@ contract SchedulePrecompile is IPrecompile, WatcherBase { ); } - function resolvePayload(PayloadParams calldata payloadParams_) external onlyRequestHandler { + function resolvePayload(Payload calldata payloadParams_) external onlyWatcher { (, uint256 executeAfter) = abi.decode(payloadParams_.precompileData, (uint256, uint256)); - if (executeAfter > block.timestamp) revert ResolvingScheduleTooEarly(); + if (block.timestamp > payloadParams_.deadline) revert DeadlinePassed(); emit ScheduleResolved(payloadParams_.payloadId); } diff --git a/contracts/evmx/watcher/precompiles/WritePrecompile.sol b/contracts/evmx/watcher/precompiles/WritePrecompile.sol index 49409261..c3978a21 100644 --- a/contracts/evmx/watcher/precompiles/WritePrecompile.sol +++ b/contracts/evmx/watcher/precompiles/WritePrecompile.sol @@ -1,15 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "solady/utils/Initializable.sol"; import "solady/auth/Ownable.sol"; import "../../interfaces/IPrecompile.sol"; +import "../../interfaces/IWatcher.sol"; + +import {DigestParams, Payload} from "../../../utils/common/Structs.sol"; + import {WRITE, PAYLOAD_SIZE_LIMIT, CHAIN_SLUG_SOLANA_MAINNET, CHAIN_SLUG_SOLANA_DEVNET} from "../../../utils/common/Constants.sol"; -import {InvalidIndex, MaxMsgValueLimitExceeded, InvalidPayloadSize} from "../../../utils/common/Errors.sol"; +import {InvalidIndex, MaxMsgValueLimitExceeded, InvalidPayloadSize, OnlyWatcherAllowed, InvalidTarget} from "../../../utils/common/Errors.sol"; import "../../../utils/RescueFundsLib.sol"; -import "../WatcherBase.sol"; import {toBytes32Format} from "../../../utils/common/Converters.sol"; +import "../borsh-serde/BorshEncoder.sol"; abstract contract WritePrecompileStorage is IPrecompile { // slots [0-49] reserved for gap @@ -39,33 +43,42 @@ abstract contract WritePrecompileStorage is IPrecompile { // slot 55 mapping(uint32 => bytes32) public contractFactoryPlugs; + IWatcher public watcher__; + // slots [56-105] reserved for gap uint256[50] _gap_after; // 1 slot reserved for watcher base } + /// @title WritePrecompile /// @notice Handles write precompile logic -contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, WatcherBase { +contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable { /// @notice Emitted when fees are set event FeesSet(uint256 writeFees); event ChainMaxMsgValueLimitsUpdated(uint32 chainSlug, uint256 maxMsgValueLimit); - event ContractFactoryPlugSet(uint32 chainSlug, bytes32 contractFactoryPlug); /// @notice Emitted when a proof upload request is made - event WriteProofRequested( - address transmitter, - bytes32 digest, - bytes32 prevBatchDigestHash, - uint256 deadline, - PayloadParams payloadParams - ); + event WriteProofRequested(bytes32 digest, uint256 deadline, RawPayload rawPayload); /// @notice Emitted when a proof is uploaded - /// @param payloadId The unique identifier for the request + /// @param payloadId The unique identifier for the payload /// @param proof The proof from the watcher event WriteProofUploaded(bytes32 indexed payloadId, bytes proof); event ExpiryTimeSet(uint256 expiryTime); + // TODO: remove after debugging + event SolanaInstructionInput(bytes payload); + event SolanaDecodedInstruction(SolanaInstruction instruction); + event SolanaFunctionArgsPacked(bytes functionArgsPacked); + + modifier onlyWatcher() { + if ( + msg.sender != IWatcherOwner(address(watcher__)).owner() && + msg.sender != address(watcher__) + ) revert OnlyWatcherAllowed(); + _; + } + constructor() { _disableInitializers(); // disable for implementation } @@ -79,189 +92,218 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc writeFees = writeFees_; expiryTime = expiryTime_; _initializeOwner(owner_); - _initializeWatcher(watcher_); + + watcher__ = IWatcher(watcher_); + // _initializeWatcher(watcher_); } function getPrecompileFees(bytes memory) public view returns (uint256) { return writeFees; } + /// @notice Handles payload processing and returns fees + /// @param rawPayload The payload parameters to handle + /// @return fees The fees required for processing + /// @return deadline The deadline for the payload + function handlePayload( + RawPayload calldata rawPayload, + address appGateway, + bytes32 payloadId + ) external onlyWatcher returns (uint256 fees, uint256 deadline, bytes memory precompileData) { + uint256 gasLimit = _validate(rawPayload, appGateway); + deadline = block.timestamp + expiryTime; + + // For write precompile, encode the payload parameters + precompileData = abi.encode( + appGateway, + rawPayload.transaction, + rawPayload.overrideParams.writeFinality, + gasLimit, + rawPayload.overrideParams.value, + watcher__.switchboards( + rawPayload.transaction.chainSlug, + rawPayload.overrideParams.switchboardType + ) + ); + fees = getPrecompileFees(precompileData); + + // create digest + DigestParams memory digestParams_; + if (_isSolanaChainSlug(rawPayload.transaction.chainSlug)) { + digestParams_ = _createSolanaDigestParams( + rawPayload, + payloadId, + appGateway, + deadline, + gasLimit + ); + } else { + digestParams_ = _createEvmDigestParams( + rawPayload, + payloadId, + appGateway, + deadline, + gasLimit + ); + } + + // Calculate and store digest from payload parameters + bytes32 digest = getDigest(digestParams_); + digestHashes[payloadId] = digest; + + emit WriteProofRequested(digest, deadline, rawPayload); + } + /// @notice Gets precompile data and fees for queue parameters - /// @param queueParams_ The queue parameters to process - /// @return precompileData The encoded precompile data - /// @return estimatedFees Estimated fees required for processing - function validateAndGetPrecompileData( - QueueParams memory queueParams_, + // @param rawPayload_ The queue parameters to process + /// @return gasLimit + function _validate( + RawPayload calldata rawPayload_, address appGateway_ - ) external view override returns (bytes memory precompileData, uint256 estimatedFees) { + ) internal view returns (uint256 gasLimit) { if ( - queueParams_.overrideParams.value > - chainMaxMsgValueLimit[queueParams_.transaction.chainSlug] + rawPayload_.overrideParams.value > + chainMaxMsgValueLimit[rawPayload_.transaction.chainSlug] ) revert MaxMsgValueLimitExceeded(); if ( - queueParams_.transaction.payload.length == 0 || - queueParams_.transaction.payload.length > PAYLOAD_SIZE_LIMIT + rawPayload_.transaction.payload.length == 0 || + rawPayload_.transaction.payload.length > PAYLOAD_SIZE_LIMIT ) { revert InvalidPayloadSize(); } - if (queueParams_.transaction.target == bytes32(0)) { - queueParams_.transaction.target = contractFactoryPlugs[ - queueParams_.transaction.chainSlug - ]; - appGateway_ = address(this); - } else { - configurations__().verifyConnections( - queueParams_.transaction.chainSlug, - queueParams_.transaction.target, - appGateway_, - queueParams_.switchboardType - ); - } + if (rawPayload_.transaction.target == bytes32(0)) revert InvalidTarget(); + + watcher__.verifyConnections( + rawPayload_.transaction.chainSlug, + rawPayload_.transaction.target, + appGateway_, + rawPayload_.overrideParams.switchboardType + ); // todo: can be changed to set the default gas limit for each chain - if (queueParams_.overrideParams.gasLimit == 0) { - if (queueParams_.transaction.chainSlug == 5000) { + if (rawPayload_.overrideParams.gasLimit == 0) { + if (rawPayload_.transaction.chainSlug == 5000) { // Mantle default gas limit - queueParams_.overrideParams.gasLimit = 8_000_000_000; - } else if (queueParams_.transaction.chainSlug == 1329) { + gasLimit = 8_000_000_000; + } else if (rawPayload_.transaction.chainSlug == 1329) { // Sei default gas limit - queueParams_.overrideParams.gasLimit = 8_000_000; - } else if (queueParams_.transaction.chainSlug == 999) { + gasLimit = 8_000_000; + } else if (rawPayload_.transaction.chainSlug == 999) { // HyperEVM default gas limit - queueParams_.overrideParams.gasLimit = 1_500_000; + gasLimit = 1_500_000; } else { - queueParams_.overrideParams.gasLimit = 10_000_000; // other chains default gas limit + gasLimit = 10_000_000; // other chains default gas limit } } + } - // For write precompile, encode the payload parameters - precompileData = abi.encode( - appGateway_, - queueParams_.transaction, - queueParams_.overrideParams.writeFinality, - queueParams_.overrideParams.gasLimit, - queueParams_.overrideParams.value, - configurations__().switchboards( - queueParams_.transaction.chainSlug, - queueParams_.switchboardType - ) + /// @notice Calculates the digest hash of payload parameters + /// @dev extraData is empty for now, not needed for this EVMx + /// @param params_ The payload parameters to calculate the digest for + /// @return digest The calculated digest hash + /// @dev This function creates a keccak256 hash of the payload parameters + /// @dev Uses length prefixes for variable-length fields to prevent collision attacks + function getDigest(DigestParams memory params_) public pure returns (bytes32 digest) { + bytes memory fixedPart = abi.encodePacked( + // Fixed-size fields + params_.socket, + params_.transmitter, + params_.payloadId, + params_.deadline, + params_.callType, + params_.gasLimit, + params_.value, + params_.target, + params_.prevBatchDigestHash ); - estimatedFees = getPrecompileFees(precompileData); + digest = keccak256( + abi.encodePacked( + fixedPart, + // Variable-length fields with length prefixes + uint32(params_.payload.length), + params_.payload, + uint32(params_.source.length), + params_.source, + uint32(params_.extraData.length), + params_.extraData + ) + ); } - /// @notice Handles payload processing and returns fees - /// @param payloadParams The payload parameters to handle - /// @return fees The fees required for processing - /// @return deadline The deadline for the payload - function handlePayload( - address transmitter_, - PayloadParams memory payloadParams - ) - external - onlyRequestHandler - returns (uint256 fees, uint256 deadline, bytes memory precompileData) - { - ( - address appGateway, - Transaction memory transaction, - , - uint256 gasLimit, - uint256 value, - - ) = abi.decode( - payloadParams.precompileData, - (address, Transaction, WriteFinality, uint256, uint256, uint64) + function _createEvmDigestParams( + RawPayload memory rawPayload_, + bytes32 payloadId_, + address appGateway_, + uint256 deadline_, + uint256 gasLimit_ + ) internal view returns (DigestParams memory) { + return + DigestParams( + deadline_, + gasLimit_, + rawPayload_.overrideParams.callType, + watcher__.sockets(rawPayload_.transaction.chainSlug), + rawPayload_.overrideParams.value, + toBytes32Format(watcher__.transmitter()), + payloadId_, + rawPayload_.transaction.target, + bytes32(0), + rawPayload_.transaction.payload, + abi.encodePacked(toBytes32Format(appGateway_)), + bytes("") ); + } - precompileData = payloadParams.precompileData; - deadline = block.timestamp + expiryTime; - fees = getPrecompileFees(payloadParams.precompileData); - - bytes32 prevBatchDigestHash = getPrevBatchDigestHash( - uint40(payloadParams.payloadPointer >> 120), - uint40(payloadParams.payloadPointer >> 80) + function _createSolanaDigestParams( + RawPayload memory rawPayload_, + bytes32 payloadId_, + address appGateway_, + uint256 deadline_, + uint256 gasLimit_ + ) internal returns (DigestParams memory) { + emit SolanaInstructionInput(rawPayload_.transaction.payload); + SolanaInstruction memory instruction = abi.decode( + rawPayload_.transaction.payload, + (SolanaInstruction) ); + emit SolanaDecodedInstruction(instruction); - // create digest - DigestParams memory digestParams_ = DigestParams( - configurations__().sockets(transaction.chainSlug), - toBytes32Format(transmitter_), - payloadParams.payloadId, - deadline, - payloadParams.callType, - gasLimit, - value, - transaction.payload, - transaction.target, - toBytes32Format(appGateway), - prevBatchDigestHash, - bytes("") - ); + bytes memory functionArgsPacked = BorshEncoder.encodeFunctionArgs(instruction); + emit SolanaFunctionArgsPacked(functionArgsPacked); - // Calculate and store digest from payload parameters - bytes32 digest = getDigest(digestParams_); - digestHashes[payloadParams.payloadId] = digest; - - emit WriteProofRequested( - transmitter_, - digest, - prevBatchDigestHash, - deadline, - payloadParams + bytes memory payloadPacked = abi.encodePacked( + instruction.data.programId, + instruction.data.accounts, + instruction.data.instructionDiscriminator, + functionArgsPacked ); - } - - function getPrevBatchDigestHash( - uint40 requestCount_, - uint40 batchCount_ - ) public view returns (bytes32) { - if (batchCount_ == 0) return bytes32(0); - // if first batch, return bytes32(0) - uint40[] memory requestBatchIds = requestHandler__().getRequestBatchIds(requestCount_); - if (requestBatchIds[0] == batchCount_) return bytes32(0); - - uint40 prevBatchCount = batchCount_ - 1; - - bytes32[] memory payloadIds = requestHandler__().getBatchPayloadIds(prevBatchCount); - bytes32 prevBatchDigestHash = bytes32(0); - for (uint40 i = 0; i < payloadIds.length; i++) { - prevBatchDigestHash = keccak256( - abi.encodePacked(prevBatchDigestHash, digestHashes[payloadIds[i]]) + return + DigestParams( + deadline_, + gasLimit_, + rawPayload_.overrideParams.callType, + watcher__.sockets(rawPayload_.transaction.chainSlug), + rawPayload_.overrideParams.value, + watcher__.transmitterSolana(), + payloadId_, + rawPayload_.transaction.target, + bytes32(0), + payloadPacked, + abi.encodePacked(toBytes32Format(appGateway_)), + bytes("") ); - } - return prevBatchDigestHash; } - /// @notice Calculates the digest hash of payload parameters - /// @dev extraData is empty for now, not needed for this EVMx - /// @param params_ The payload parameters to calculate the digest for - /// @return digest The calculated digest hash - /// @dev This function creates a keccak256 hash of the payload parameters - function getDigest(DigestParams memory params_) public pure returns (bytes32 digest) { - digest = keccak256( - abi.encodePacked( - params_.socket, - params_.transmitter, - params_.payloadId, - params_.deadline, - params_.callType, - params_.gasLimit, - params_.value, - params_.payload, - params_.target, - params_.appGatewayId, - params_.prevBatchDigestHash, - params_.extraData - ) - ); + function _isSolanaChainSlug(uint32 chainSlug_) internal pure returns (bool) { + return chainSlug_ == CHAIN_SLUG_SOLANA_MAINNET || chainSlug_ == CHAIN_SLUG_SOLANA_DEVNET; } - /// @notice Marks a write request with a proof on digest - /// @param payloadId_ The unique identifier of the request + /// @notice Marks a write payload with a proof on digest + /// @param payloadId_ The unique identifier of the payload /// @param proof_ The watcher's proof function uploadProof(bytes32 payloadId_, bytes memory proof_) public onlyWatcher { watcherProofs[payloadId_] = proof_; @@ -284,14 +326,6 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc emit FeesSet(writeFees_); } - function setContractFactoryPlugs( - uint32 chainSlug_, - bytes32 contractFactoryPlug_ - ) external onlyOwner { - contractFactoryPlugs[chainSlug_] = contractFactoryPlug_; - emit ContractFactoryPlugSet(chainSlug_, contractFactoryPlug_); - } - /// @notice Sets the expiry time for payload execution /// @param expiryTime_ The expiry time in seconds /// @dev This function sets the expiry time for payload execution @@ -301,9 +335,7 @@ contract WritePrecompile is WritePrecompileStorage, Initializable, Ownable, Watc emit ExpiryTimeSet(expiryTime_); } - function resolvePayload( - PayloadParams calldata payloadParams_ - ) external override onlyRequestHandler {} + function resolvePayload(Payload calldata payloadParams_) external override onlyWatcher {} /** * @notice Rescues funds from the contract if they are locked by mistake. This contract does not diff --git a/contracts/protocol/NetworkFeeCollector.sol b/contracts/protocol/NetworkFeeCollector.sol new file mode 100644 index 00000000..3c1e453a --- /dev/null +++ b/contracts/protocol/NetworkFeeCollector.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "./interfaces/INetworkFeeCollector.sol"; +import "../utils/AccessControl.sol"; +import {GOVERNANCE_ROLE, RESCUE_ROLE, SOCKET_ROLE} from "../utils/common/AccessRoles.sol"; +import "../utils/common/Errors.sol"; +import "../utils/RescueFundsLib.sol"; +import {ExecutionParams, TransmissionParams} from "../utils/common/Structs.sol"; + +/** + * @title NetworkFeeCollector + * @notice Contract responsible for collecting and managing network fees for socket executions + * @dev Collects fees from successful payload executions and allows governance to update fee amounts + */ +contract NetworkFeeCollector is INetworkFeeCollector, AccessControl { + // --- State Variables --- + + /// @notice Current network fee amount in native tokens + uint256 public networkFee; + + // --- Events --- + + /** + * @notice Emitted when the socket fees are updated + * @param oldFees The old socket fees + * @param newFees The new socket fees + */ + event NetworkFeeUpdated(uint256 oldFees, uint256 newFees); + + /** + * @notice Emitted when the network fees are collected + * @param amount The amount of network fees collected + * @param params The execution parameters + * @param transmissionParams The transmission parameters + */ + event NetworkFeeCollected( + uint256 amount, + ExecutionParams params, + TransmissionParams transmissionParams + ); + + // --- Constructor --- + + constructor(address owner_, address socket_, uint256 networkFee_) { + _grantRole(GOVERNANCE_ROLE, owner_); + _grantRole(RESCUE_ROLE, owner_); + _grantRole(SOCKET_ROLE, socket_); + + networkFee = networkFee_; + emit NetworkFeeUpdated(0, networkFee_); + } + + // --- External Functions --- + + /** + * @notice Collects and validates network fees for a payload execution + * @param executionParams_ The execution parameters + * @param transmissionParams_ The transmission parameters + * @dev Only callable by SOCKET_ROLE. Reverts if msg.value is less than networkFee. + * Emits NetworkFeeCollected event with fee amount and execution details. + */ + function collectNetworkFee( + ExecutionParams calldata executionParams_, + TransmissionParams calldata transmissionParams_ + ) external payable onlyRole(SOCKET_ROLE) { + // todo: optimise gas cost (266k) + if (msg.value < networkFee) revert InsufficientFees(); + emit NetworkFeeCollected(msg.value, executionParams_, transmissionParams_); + } + + /** + * @notice Returns the current network fee amount + * @return networkFee The minimum network fees required for execution + * @dev View function that returns the current network fee setting. + */ + function getNetworkFee() external view returns (uint256) { + return networkFee; + } + + /** + * @notice Sets the network fee amount + * @param networkFee_ The new network fee amount in native tokens + * @dev Only callable by GOVERNANCE_ROLE. Emits NetworkFeeUpdated event with old and new fee values. + */ + function setNetworkFee(uint256 networkFee_) external onlyRole(GOVERNANCE_ROLE) { + emit NetworkFeeUpdated(networkFee, networkFee_); + networkFee = networkFee_; + } + + // --- Rescue Functions --- + + /** + * @notice Rescues stuck funds from the contract + * @param token_ Token address (address(0) for native tokens) + * @param to_ Address to send rescued funds to + * @param amount_ Amount of tokens to rescue (0 for all) + * @dev Only callable by RESCUE_ROLE. Safety mechanism for recovering stuck funds. + */ + function rescueFunds( + address token_, + address to_, + uint256 amount_ + ) external onlyRole(RESCUE_ROLE) { + RescueFundsLib._rescueFunds(token_, to_, amount_); + } +} diff --git a/contracts/protocol/Socket.sol b/contracts/protocol/Socket.sol index 4d4b2257..2df3668d 100644 --- a/contracts/protocol/Socket.sol +++ b/contracts/protocol/Socket.sol @@ -1,256 +1,262 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "./SocketUtils.sol"; - import {WRITE} from "../utils/common/Constants.sol"; -import {createPayloadId} from "../utils/common/IdUtils.sol"; +import "../utils/common/Errors.sol"; +using LibCall for address; /** * @title Socket - * @dev Socket is an abstract contract that inherits from SocketUtils and SocketConfig and - * provides functionality for payload execution, verification, and management of payload execution status + * @notice Core contract for executing cross-chain payloads and managing payload lifecycle + * @dev Inherits from SocketUtils and provides functionality for payload execution, verification, + * and management of payload execution status. Handles both inbound (execute) and outbound (sendPayload) flows. */ contract Socket is SocketUtils { - using LibCall for address; - - // mapping of payload id to execution status - mapping(bytes32 => ExecutionStatus) public payloadExecuted; - - // mapping of payload id to digest - mapping(bytes32 => bytes32) public payloadIdToDigest; + // --- State Variables --- + /// @notice Mapping of payload id to execution status (Executed/Reverted) + mapping(bytes32 => ExecutionStatus) public executionStatus; - //////////////////////////////////////////////////////// - ////////////////////// ERRORS ////////////////////////// - //////////////////////////////////////////////////////// - /// @notice Thrown when a payload has already been executed - error PayloadAlreadyExecuted(ExecutionStatus status); - /// @notice Thrown when verification fails - error VerificationFailed(); - /// @notice Thrown when less gas limit is provided for execution than expected - error LowGasLimit(); - /// @notice Thrown when the message value is insufficient - error InsufficientMsgValue(); + // --- Constructor --- /** * @notice Constructor for the Socket contract - * @param chainSlug_ The chain slug - * @param owner_ The owner of the contract - * @param version_ The version of the contract + * @param chainSlug_ The unique chain identifier where this socket is deployed + * @param owner_ The owner address with governance permissions + * @param gasLimitBuffer_ The gas limit buffer percentage (e.g., 105 = 5% buffer) + * @param maxCopyBytes_ The maximum bytes to copy from return data (default: 2048 = 2KB) */ constructor( uint32 chainSlug_, address owner_, - string memory version_ - ) SocketUtils(chainSlug_, owner_, version_) { - gasLimitBuffer = 105; - } + uint256 gasLimitBuffer_, + uint16 maxCopyBytes_ + ) SocketUtils(chainSlug_, owner_, gasLimitBuffer_, maxCopyBytes_) {} /** * @notice Executes a payload that has been delivered by transmitters and authenticated by switchboards - * @param executeParams_ The execution parameters - * @param transmissionParams_ The transmission parameters + * @param executionParams_ The execution parameters (target, payload, value, gasLimit, etc.) + * @param transmissionParams_ The transmission parameters (socketFees, transmitterProof, refundAddress) * @return success True if the payload was executed successfully - * @return returnData The return data from the execution + * @return returnData The return data from the execution (truncated to maxCopyBytes if exceeded) + * @dev Validates deadline, call type, plug connection, msg.value, payload ID, execution status, and digest + * @dev Reverts if any validation fails or if contract is paused + * @dev NOTE: This is the main entry point for executing cross-chain payloads */ function execute( - ExecuteParams calldata executeParams_, + ExecutionParams memory executionParams_, TransmissionParams calldata transmissionParams_ - ) external payable returns (bool, bytes memory) { - // check if the deadline has passed - if (executeParams_.deadline < block.timestamp) revert DeadlinePassed(); + ) external payable whenNotPaused returns (bool, bytes memory) { + if (executionParams_.deadline < block.timestamp) revert DeadlinePassed(); - // check if the call type is valid - if (executeParams_.callType != WRITE) revert InvalidCallType(); + if (executionParams_.callType != WRITE) revert InvalidCallType(); - // check if the plug is connected - PlugConfigEvm storage plugConfig = _plugConfigs[executeParams_.target]; - if (plugConfig.appGatewayId == bytes32(0)) revert PlugNotFound(); + // Verify plug is connected and get its switchboard address + address switchboardAddress = _verifyPlugSwitchboard(executionParams_.target); - // check if the message value is sufficient - if (msg.value < executeParams_.value + transmissionParams_.socketFees) + // Validate msg.value covers both execution value and socket fees + if (msg.value < executionParams_.value + transmissionParams_.socketFees) revert InsufficientMsgValue(); - bytes32 payloadId = createPayloadId( - executeParams_.payloadPointer, - plugConfig.switchboardId, - chainSlug - ); + // Verify payload ID matches expected verification chain and switchboard + _verifyPayloadId(executionParams_.payloadId, switchboardAddress); - // validate the execution status - _validateExecutionStatus(payloadId); + // Prevent double execution by checking and updating execution status + _validateExecutionStatus(executionParams_.payloadId); - // verify the digest - _verify(payloadId, plugConfig, executeParams_, transmissionParams_.transmitterProof); + // Verify digest matches switchboard attestation + _verify(switchboardAddress, executionParams_, transmissionParams_.transmitterProof); - // execute the payload - return _execute(payloadId, executeParams_, transmissionParams_); + // Execute the payload on target plug + return _execute(executionParams_, transmissionParams_); } - //////////////////////////////////////////////////////// - ////////////////// INTERNAL FUNCS ////////////////////// - //////////////////////////////////////////////////////// + // --- Internal Functions --- /** - * @notice Verifies the digest of the payload - * @param payloadId_ The id of the payload - * @param plugConfig_ The plug configuration - * @param executeParams_ The execution parameters (appGatewayId, value, payloadPointer, callType, gasLimit) - * @param transmitterProof_ The transmitter proof + * @notice Verifies the digest of the payload against switchboard attestation + * @param switchboardAddress The switchboard address that attested the payload + * @param executionParams_ The execution parameters containing payload details + * @param transmitterProof_ The transmitter signature proof + * @dev NOTE: This is the first untrusted external call in the execution flow */ function _verify( - bytes32 payloadId_, - PlugConfigEvm memory plugConfig_, - ExecuteParams calldata executeParams_, - bytes calldata transmitterProof_ - ) internal { - if (isValidSwitchboard[plugConfig_.switchboardId] != SwitchboardStatus.REGISTERED) - revert InvalidSwitchboard(); - + address switchboardAddress, + ExecutionParams memory executionParams_, + bytes memory transmitterProof_ + ) internal view { // NOTE: the first un-trusted call in the system - address transmitter = ISwitchboard(switchboardAddresses[plugConfig_.switchboardId]) - .getTransmitter(msg.sender, payloadId_, transmitterProof_); - - // create the digest - // transmitter, payloadId, appGateway, executeParams_ and there contents are validated using digest verification from switchboard - bytes32 digest = _createDigest( - transmitter, - payloadId_, - plugConfig_.appGatewayId, - executeParams_ + address transmitter = ISwitchboard(switchboardAddress).getTransmitter( + msg.sender, + executionParams_.payloadId, + transmitterProof_ ); - payloadIdToDigest[payloadId_] = digest; + bytes32 digest = _createDigest(transmitter, executionParams_); if ( - !ISwitchboard(switchboardAddresses[plugConfig_.switchboardId]).allowPayload( + !ISwitchboard(switchboardAddress).allowPayload( digest, - payloadId_ + executionParams_.payloadId, + executionParams_.target, + executionParams_.source ) ) revert VerificationFailed(); } /** - * @notice Executes the payload - * @param payloadId_ The id of the payload - * @param executeParams_ The execution parameters (appGatewayId, value, payloadPointer, callType, gasLimit) + * @notice Executes the payload on the target plug + * @param executionParams_ The execution parameters (target, payload, value, gasLimit, etc.) * @param transmissionParams_ The transmission parameters (socketFees, transmitterProof, refundAddress) + * @return success True if execution succeeded, false if it reverted + * @return returnData The return data from execution (truncated to maxCopyBytes) + * @dev NOTE: This performs an external untrusted call to the target plug */ function _execute( - bytes32 payloadId_, - ExecuteParams calldata executeParams_, + ExecutionParams memory executionParams_, TransmissionParams calldata transmissionParams_ ) internal returns (bool success, bytes memory returnData) { - // check if the gas limit is sufficient - // bump by 5% to account for gas used by current contract execution - if (gasleft() < (executeParams_.gasLimit * gasLimitBuffer) / 100) revert LowGasLimit(); + // Gas buffer (105) accounts for ~5% overhead from current contract execution + if (gasleft() < (executionParams_.gasLimit * gasLimitBuffer) / 100) revert LowGasLimit(); - // NOTE: external un-trusted call + // NOTE: external un-trusted call to target plug bool exceededMaxCopy; - (success, exceededMaxCopy, returnData) = executeParams_.target.tryCall( - executeParams_.value, - executeParams_.gasLimit, + (success, exceededMaxCopy, returnData) = executionParams_.target.tryCall( + executionParams_.value, + executionParams_.gasLimit, maxCopyBytes, - executeParams_.payload + executionParams_.payload ); + // Handle execution result if (success) { - emit ExecutionSuccess(payloadId_, exceededMaxCopy, returnData); - - // pay and check fees - if (address(socketFeeManager) != address(0)) { - socketFeeManager.payAndCheckFees{value: transmissionParams_.socketFees}( - executeParams_, - transmissionParams_ - ); - } + _handleSuccessfulExecution( + exceededMaxCopy, + returnData, + executionParams_, + transmissionParams_ + ); } else { - payloadExecuted[payloadId_] = ExecutionStatus.Reverted; - - // refund the fees - address receiver = transmissionParams_.refundAddress; - if (receiver == address(0)) receiver = msg.sender; - SafeTransferLib.forceSafeTransferETH(receiver, msg.value); - emit ExecutionFailed(payloadId_, exceededMaxCopy, returnData); + _handleFailedExecution( + executionParams_.payloadId, + exceededMaxCopy, + returnData, + transmissionParams_.refundAddress + ); } return (success, returnData); } + /** + * @notice Handles successful payload execution + * @param exceededMaxCopy_ Whether return data exceeded maxCopyBytes limit + * @param returnData_ The return data from execution + * @param executionParams_ The execution parameters + * @param transmissionParams_ The transmission parameters + */ + function _handleSuccessfulExecution( + bool exceededMaxCopy_, + bytes memory returnData_, + ExecutionParams memory executionParams_, + TransmissionParams calldata transmissionParams_ + ) internal { + emit ExecutionSuccess(executionParams_.payloadId, exceededMaxCopy_, returnData_); + + if (address(networkFeeCollector) != address(0)) { + networkFeeCollector.collectNetworkFee{value: transmissionParams_.socketFees}( + executionParams_, + transmissionParams_ + ); + } + } + + /** + * @notice Handles failed payload execution + * @param payloadId_ The payload ID that failed + * @param exceededMaxCopy_ Whether return data exceeded maxCopyBytes limit + * @param returnData_ The revert data from execution + * @param refundAddress_ Address to refund msg.value to (uses msg.sender if zero) + */ + function _handleFailedExecution( + bytes32 payloadId_, + bool exceededMaxCopy_, + bytes memory returnData_, + address refundAddress_ + ) internal { + executionStatus[payloadId_] = ExecutionStatus.Reverted; + + address receiver = refundAddress_; + if (receiver == address(0)) receiver = msg.sender; + SafeTransferLib.safeTransferETH(receiver, msg.value); + + emit ExecutionFailed(payloadId_, exceededMaxCopy_, returnData_); + } + /** * @notice Validates the execution status of a payload - * @dev This function can be retried till execution status is executed - * @param payloadId_ The id of the payload + * @param payloadId_ The payload ID to validate + * @dev Marks payload as executed to prevent double execution. This function can be retried until execution status is executed. */ function _validateExecutionStatus(bytes32 payloadId_) internal { - if (payloadExecuted[payloadId_] == ExecutionStatus.Executed) - revert PayloadAlreadyExecuted(payloadExecuted[payloadId_]); + if (executionStatus[payloadId_] == ExecutionStatus.Executed) + revert PayloadAlreadyExecuted(); - payloadExecuted[payloadId_] = ExecutionStatus.Executed; + executionStatus[payloadId_] = ExecutionStatus.Executed; } - //////////////////////////////////////////////////////// - ////////////////////// Trigger ////////////////////// - //////////////////////////////////////////////////////// + // --- Outbound Payload Functions --- /** - * @notice To trigger to a connected remote chain. Should only be called by a plug. - * @param data_ The data to trigger the app gateway - * @return triggerId The id of the trigger + * @notice Sends a payload to a connected remote chain + * @param callData_ The payload data to execute on the destination chain (encoded with function selector) + * @return payloadId The created payload ID from the switchboard + * @dev Should only be called by a plug. The switchboard will create the payload Id and emit PayloadRequested event. */ - function triggerAppGateway(bytes calldata data_) external payable returns (bytes32 triggerId) { - triggerId = _triggerAppGateway(msg.sender, msg.value, data_); + function sendPayload(bytes calldata callData_) external payable returns (bytes32 payloadId) { + payloadId = _sendPayload(msg.sender, msg.value, callData_); } /** - * @notice To trigger to a connected remote chain. Should only be called by a plug. - * @param plug_ The address of the plug - * @param value_ The value to trigger the app gateway - * @param data_ The data to trigger the app gateway - * @return triggerId The id of the trigger + * @notice Internal function to send a payload to a connected remote chain + * @param plug_ The address of the plug sending the payload + * @param value_ The native value to send with the payload + * @param callData_ The payload data to execute on destination + * @return payloadId The created payload ID from the switchboard */ - function _triggerAppGateway( + function _sendPayload( address plug_, uint256 value_, - bytes calldata data_ - ) internal returns (bytes32 triggerId) { - PlugConfigEvm memory plugConfig = _plugConfigs[plug_]; - - // if no sibling plug is found for the given chain slug, revert - if (plugConfig.appGatewayId == bytes32(0)) revert PlugNotFound(); - if (isValidSwitchboard[plugConfig.switchboardId] != SwitchboardStatus.REGISTERED) - revert InvalidSwitchboard(); + bytes calldata callData_ + ) internal whenNotPaused returns (bytes32 payloadId) { + address switchboardAddress = _verifyPlugSwitchboard(plug_); bytes memory plugOverrides = IPlug(plug_).overrides(); - triggerId = _encodeTriggerId(); - - // todo: need gas limit? - ISwitchboard(switchboardAddresses[plugConfig.switchboardId]).processTrigger{value: value_}( + payloadId = ISwitchboard(switchboardAddress).processPayload{value: value_}( plug_, - triggerId, - data_, + callData_, plugOverrides ); - - emit AppGatewayCallRequested( - triggerId, - plugConfig.appGatewayId, - plugConfig.switchboardId, - toBytes32Format(plug_), - plugOverrides, - data_ - ); } /** - * @notice Fallback function that forwards all calls to Socket's callAppGateway - * @dev The calldata is passed as-is to the gateways - * @return The trigger id + * @notice Fallback function that forwards all calls to Socket's sendPayload + * @return ABI encoded payload ID as bytes + * @dev The calldata is passed as-is to the switchboard. + * Solidity does not ABI-encode dynamic returns in fallback functions. The fallback return is raw returndata, so we must manually wrap a `bytes32` into ABI-encoded `bytes` (offset + length + data). + * We use double encoding: `abi.encode(abi.encode(payloadId))` to create proper ABI structure. + * @dev If using .call() ((bool success, bytes memory returnData) = address(socket).call(payload)), returnData will be raw returndata, so we need to decode twice to get the payloadId. + * @dev if using interface call bytes memory data = (IContract(address(socket)).someFunc(args)), data will be already ABI decoded once by solidity, so we need to decode once to get the payloadId. */ fallback(bytes calldata) external payable returns (bytes memory) { - // return the trigger id - return abi.encode(_triggerAppGateway(msg.sender, msg.value, msg.data)); + bytes32 payloadId = _sendPayload(msg.sender, msg.value, msg.data); + + // same as return abi.encode(abi.encode(payloadId)); uses less gas + uint256 offset = 0x20; // Points to length field (32 bytes from start) + uint256 length = 0x20; // Length of bytes32 is 32 bytes + return abi.encodePacked(offset, length, payloadId); } /** - * @notice Sending ETH to the socket will revert + * @notice Reverts when ETH is sent directly to the contract + * @dev Prevents accidental ETH deposits. Use execute() or sendPayload() with msg.value instead. */ receive() external payable { revert("Socket does not accept ETH"); diff --git a/contracts/protocol/SocketBatcher.sol b/contracts/protocol/SocketBatcher.sol index 1ed5a359..31f89053 100644 --- a/contracts/protocol/SocketBatcher.sol +++ b/contracts/protocol/SocketBatcher.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; 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, CCTPBatchParams, CCTPExecutionParams} from "../utils/common/Structs.sol"; -import {createPayloadId} from "../utils/common/IdUtils.sol"; +import {ExecutionParams, TransmissionParams} from "../utils/common/Structs.sol"; /** * @title IFastSwitchboard @@ -20,94 +18,95 @@ interface IFastSwitchboard is ISwitchboard { /** * @title SocketBatcher - * @notice The SocketBatcher contract is responsible for batching payloads and executing them on the socket + * @notice Contract for batching payload attestation and execution in a single transaction + * @dev Allows combining switchboard attestation with socket execution to reduce transaction count */ contract SocketBatcher is ISocketBatcher, Ownable { - // socket contract + // --- State Variables --- + + /// @notice Immutable reference to the socket contract ISocket public immutable socket__; - /** - * @notice Initializes the SocketBatcher contract - * @param owner_ The owner of the contract with GOVERNANCE_ROLE - * @param socket_ The address of socket contract - */ + // --- Constructor --- + constructor(address owner_, ISocket socket_) { socket__ = socket_; _initializeOwner(owner_); } + // --- External Functions --- + /** - * @notice Attests a payload and executes it - * @param executeParams_ The execution parameters - * @param digest_ The digest of the payload - * @param proof_ The proof of the payload - * @param transmitterProof_ The signature of the transmitter - * @return The return data after execution + * @notice Attests a payload on switchboard and executes it on socket in a single transaction + * @param executionParams_ The execution parameters for socket.execute() + * @param transmissionParams_ The transmission parameters for socket.execute() + * @param switchboardId_ The switchboard ID to attest on + * @param digest_ The digest of the payload to attest + * @param proof_ The watcher proof for attestation + * @return success True if execution succeeded + * @return returnData The return data from execution + * @dev First attests the digest on FastSwitchboard, then executes on socket. + * Reduces transaction count by combining attestation and execution. */ function attestAndExecute( - ExecuteParams calldata executeParams_, - uint64 switchboardId_, + ExecutionParams calldata executionParams_, + TransmissionParams calldata transmissionParams_, + uint32 switchboardId_, bytes32 digest_, - bytes calldata proof_, - bytes calldata transmitterProof_, - address refundAddress_ + bytes calldata proof_ ) external payable returns (bool, bytes memory) { + // Attest digest on FastSwitchboard IFastSwitchboard(socket__.switchboardAddresses(switchboardId_)).attest(digest_, proof_); - return - socket__.execute{value: msg.value}( - executeParams_, - TransmissionParams({ - transmitterProof: transmitterProof_, - socketFees: 0, - extraData: executeParams_.extraData, - refundAddress: refundAddress_ - }) - ); + // Execute payload on socket + return socket__.execute{value: msg.value}(executionParams_, transmissionParams_); } - /** - * @notice Attests a CCTP payload and proves and executes it - * @param execParams_ The execution parameters - * @param cctpParams_ The CCTP parameters - * @param switchboardId_ The switchboard id - * @return success True if the payload was executed successfully - * @return returnData The return data from the execution - */ - function attestCCTPAndProveAndExecute( - CCTPExecutionParams calldata execParams_, - CCTPBatchParams calldata cctpParams_, - uint64 switchboardId_ - ) external payable returns (bool, bytes memory) { - address switchboard = socket__.switchboardAddresses(switchboardId_); - bytes32 payloadId = createPayloadId( - execParams_.executeParams.payloadPointer, - switchboardId_, - socket__.chainSlug() - ); - ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions( - execParams_, - cctpParams_, - payloadId - ); - (bool success, bytes memory returnData) = socket__.execute{value: msg.value}( - execParams_.executeParams, - TransmissionParams({ - transmitterProof: execParams_.transmitterSignature, - socketFees: 0, - extraData: execParams_.executeParams.extraData, - refundAddress: execParams_.refundAddress - }) - ); + // /** + // * @notice Attests a CCTP payload and proves and executes it + // * @param execParams_ The execution parameters + // * @param cctpParams_ The CCTP parameters + // * @param switchboardId_ The switchboard id + // * @return success True if the payload was executed successfully + // * @return returnData The return data from the execution + // */ + // function attestCCTPAndProveAndExecute( + // CCTPExecutionParams calldata execParams_, + // CCTPBatchParams calldata cctpParams_, + // uint32 switchboardId_ + // ) external payable returns (bool, bytes memory) { + // address switchboard = socket__.switchboardAddresses(switchboardId_); + // bytes32 payloadId = createPayloadId( + // execParams_.executionParams.payloadPointer, + // switchboardId_, + // socket__.chainSlug() + // ); + // ICCTPSwitchboard(switchboard).attestVerifyAndProveExecutions( + // execParams_, + // cctpParams_, + // payloadId + // ); + // (bool success, bytes memory returnData) = socket__.execute{value: msg.value}( + // execParams_.executionParams, + // TransmissionParams({ + // transmitterProof: execParams_.transmitterSignature, + // socketFees: 0, + // extraData: execParams_.executionParams.extraData, + // refundAddress: execParams_.refundAddress + // }) + // ); - ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs); - return (success, returnData); - } + // ICCTPSwitchboard(switchboard).syncOut(payloadId, cctpParams_.nextBatchRemoteChainSlugs); + // return (success, returnData); + // } + + // --- Rescue Functions --- /** - * @notice Rescues funds from the contract - * @param token_ The address of the token to rescue - * @param to_ The address to rescue the funds to - * @param amount_ The amount of funds to rescue + * @notice Rescues stuck funds from the contract + * @param token_ The address of the token to rescue (address(0) for native tokens) + * @param to_ The address to send rescued funds to + * @param amount_ The amount of funds to rescue (0 for all) + * @dev Only callable by owner. Safety mechanism for recovering stuck funds. */ function rescueFunds(address token_, address to_, uint256 amount_) external onlyOwner { RescueFundsLib._rescueFunds(token_, to_, amount_); diff --git a/contracts/protocol/SocketConfig.sol b/contracts/protocol/SocketConfig.sol index 990003f4..2cee9a81 100644 --- a/contracts/protocol/SocketConfig.sol +++ b/contracts/protocol/SocketConfig.sol @@ -1,179 +1,165 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; +import "./interfaces/INetworkFeeCollector.sol"; +import {IPlug} from "./interfaces/IPlug.sol"; import "./interfaces/ISocket.sol"; import "./interfaces/ISwitchboard.sol"; -import {IPlug} from "./interfaces/IPlug.sol"; -import "./interfaces/ISocketFeeManager.sol"; import "../utils/AccessControl.sol"; -import {GOVERNANCE_ROLE, RESCUE_ROLE, SWITCHBOARD_DISABLER_ROLE} from "../utils/common/AccessRoles.sol"; -import {PlugConfigEvm, SwitchboardStatus, ExecutionStatus} from "../utils/common/Structs.sol"; -import "../utils/common/Errors.sol"; +import {GOVERNANCE_ROLE, RESCUE_ROLE, SWITCHBOARD_DISABLER_ROLE, PAUSER_ROLE} from "../utils/common/AccessRoles.sol"; import {MAX_COPY_BYTES} from "../utils/common/Constants.sol"; +import "../utils/common/Errors.sol"; +import "../utils/common/IdUtils.sol"; +import "../utils/Pausable.sol"; +import {PlugConfigEvm, SwitchboardStatus, ExecutionStatus, SimulationResult, SimulateParams} from "../utils/common/Structs.sol"; /** * @title SocketConfig - * @notice An abstract contract for configuring socket connections for plugs, - * manages plug configs and switchboard registrations - * @dev This contract is meant to be inherited by other contracts that require socket configuration functionality + * @notice Abstract contract for configuring socket connections, managing plug configs and switchboard registrations + * @dev Provides configuration management for plugs, switchboards, and socket parameters. + * Inherited by SocketUtils and ultimately by Socket contract. */ -abstract contract SocketConfig is ISocket, AccessControl { - // socket fee manager - ISocketFeeManager public socketFeeManager; +abstract contract SocketConfig is ISocket, AccessControl, Pausable { + // --- State Variables --- - // @notice mapping of switchboard address to its status, helps socket to block invalid switchboards - mapping(uint64 => SwitchboardStatus) public isValidSwitchboard; + /// @notice Counter for generating unique switchboard IDs (starts at 1) + uint32 public switchboardIdCounter = 1; - // @notice mapping of plug address to its config - mapping(address => PlugConfigEvm) internal _plugConfigs; + /// @notice Network fee collector contract for collecting socket execution fees + INetworkFeeCollector public networkFeeCollector; - // @notice max copy bytes for socket - uint16 public maxCopyBytes = 2048; // 2KB + /// @notice Mapping of switchboard ID to its status (NOT_REGISTERED/REGISTERED/DISABLED) + /// @dev Helps socket block invalid or disabled switchboards + mapping(uint32 => SwitchboardStatus) public switchboardStatus; - // @notice counter for switchboard ids - uint64 public switchboardIdCounter = 1; + /// @notice Mapping of plug address to its connected switchboard ID + mapping(address => uint32) public plugSwitchboardIds; - // @notice mapping of switchboard id to its address - mapping(uint64 => address) public switchboardAddresses; + /// @notice Mapping of switchboard ID to its address + mapping(uint32 => address) public switchboardAddresses; - // @notice mapping of switchboard address to its id - mapping(address => uint64) public switchboardIds; + /// @notice Mapping of switchboard address to its ID + mapping(address => uint32) public switchboardAddressToId; - // @notice buffer to account for gas used by current contract execution - uint256 public gasLimitBuffer; - - // @notice error triggered when a switchboard already exists - error SwitchboardExists(); - // @notice error triggered when a plug is not connected - error PlugNotConnected(); - - // @notice event triggered when a new switchboard is added - event SwitchboardAdded(address switchboard, uint64 switchboardId); - // @notice event triggered when a switchboard is disabled - event SwitchboardDisabled(uint64 switchboardId); - // @notice event triggered when a switchboard is enabled - event SwitchboardEnabled(uint64 switchboardId); - // @notice event triggered when a socket fee manager is updated - event SocketFeeManagerUpdated(address oldSocketFeeManager, address newSocketFeeManager); - // @notice event triggered when the gas limit buffer is updated - event GasLimitBufferUpdated(uint256 gasLimitBuffer); - // @notice event triggered when the max copy bytes is updated - event MaxCopyBytesUpdated(uint16 maxCopyBytes); + // --- External Functions --- /** * @notice Registers a switchboard on the socket - * @dev This function is called by the switchboard to register itself on the socket - * @dev This function will revert if the switchboard already exists - * @return switchboardId The id of the switchboard + * @return switchboardId The assigned switchboard ID + * @dev Called by switchboard contract to register itself. Assigns a unique ID and sets status to REGISTERED. + * Reverts if switchboard is already registered (non-zero ID). */ - function registerSwitchboard() external returns (uint64 switchboardId) { - switchboardId = switchboardIds[msg.sender]; + function registerSwitchboard() external returns (uint32 switchboardId) { + switchboardId = switchboardAddressToId[msg.sender]; if (switchboardId != 0) revert SwitchboardExists(); - // increment the switchboard id counter switchboardId = switchboardIdCounter++; - - // set the switchboard id and address - switchboardIds[msg.sender] = switchboardId; + switchboardAddressToId[msg.sender] = switchboardId; switchboardAddresses[switchboardId] = msg.sender; - // set the switchboard status to registered - isValidSwitchboard[switchboardId] = SwitchboardStatus.REGISTERED; + switchboardStatus[switchboardId] = SwitchboardStatus.REGISTERED; emit SwitchboardAdded(msg.sender, switchboardId); } /** - * @notice Disables a switchboard - * @dev This function is called by the governance role to disable a switchboard - * @param switchboardId_ The id of the switchboard to disable + * @notice Disables a switchboard, preventing new payloads from using it + * @param switchboardId_ The ID of the switchboard to disable + * @dev Only callable by SWITCHBOARD_DISABLER_ROLE. Existing payloads are unaffected. */ function disableSwitchboard( - uint64 switchboardId_ + uint32 switchboardId_ ) external onlyRole(SWITCHBOARD_DISABLER_ROLE) { - isValidSwitchboard[switchboardId_] = SwitchboardStatus.DISABLED; + switchboardStatus[switchboardId_] = SwitchboardStatus.DISABLED; emit SwitchboardDisabled(switchboardId_); } /** - * @notice Enables a switchboard if disabled - * @dev This function is called by the governance role to enable a switchboard - * @param switchboardId_ The id of the switchboard to enable + * @notice Re-enables a previously disabled switchboard + * @param switchboardId_ The ID of the switchboard to enable + * @dev Only callable by GOVERNANCE_ROLE. Sets status back to REGISTERED. */ - function enableSwitchboard(uint64 switchboardId_) external onlyRole(GOVERNANCE_ROLE) { - isValidSwitchboard[switchboardId_] = SwitchboardStatus.REGISTERED; + function enableSwitchboard(uint32 switchboardId_) external onlyRole(GOVERNANCE_ROLE) { + switchboardStatus[switchboardId_] = SwitchboardStatus.REGISTERED; emit SwitchboardEnabled(switchboardId_); } /** - * @notice Sets the socket fee manager - * @dev This function is called by the governance role to set the socket fee manager - * @param socketFeeManager_ The address of the socket fee manager + * @notice Sets the network fee collector contract address + * @param networkFeeCollector_ The address of the network fee collector contract + * @dev Only callable by GOVERNANCE_ROLE. Can be set to address(0) to disable fee collection. */ - function setSocketFeeManager(address socketFeeManager_) external onlyRole(GOVERNANCE_ROLE) { - socketFeeManager = ISocketFeeManager(socketFeeManager_); - emit SocketFeeManagerUpdated(address(socketFeeManager), socketFeeManager_); + function setNetworkFeeCollector( + address networkFeeCollector_ + ) external onlyRole(GOVERNANCE_ROLE) { + emit NetworkFeeCollectorUpdated(address(networkFeeCollector), networkFeeCollector_); + networkFeeCollector = INetworkFeeCollector(networkFeeCollector_); } /** - * @notice Connects a plug to socket - * @dev This function is called by the plug to connect itself to the socket - * @param appGatewayId_ The app gateway id - * @param switchboardId_ The switchboard id + * @notice Connects a plug to socket with a switchboard and configuration + * @notice NOTE: switchboard should be verified by plugs before calling this function + * @param switchboardId_ The switchboard ID to connect to + * @param plugConfig_ The configuration data for the switchboard (can be empty) + * @dev Called by plug contract to register itself. Validates switchboard is registered. + * @dev If plugConfig_ is non-empty, forwards it to switchboard for processing. */ - function connect(bytes32 appGatewayId_, uint64 switchboardId_) external override { - if (isValidSwitchboard[switchboardId_] != SwitchboardStatus.REGISTERED) - revert InvalidSwitchboard(); - - PlugConfigEvm storage _plugConfig = _plugConfigs[msg.sender]; - _plugConfig.appGatewayId = appGatewayId_; - _plugConfig.switchboardId = switchboardId_; - - emit PlugConnected(msg.sender, appGatewayId_, switchboardId_); + function connect(uint32 switchboardId_, bytes memory plugConfig_) external override { + if ( + switchboardId_ == 0 || switchboardStatus[switchboardId_] != SwitchboardStatus.REGISTERED + ) revert InvalidSwitchboard(); + + plugSwitchboardIds[msg.sender] = switchboardId_; + if (plugConfig_.length > 0) { + ISwitchboard(switchboardAddresses[switchboardId_]).updatePlugConfig( + msg.sender, + plugConfig_ + ); + } + emit PlugConnected(msg.sender, switchboardId_, plugConfig_); } /** * @notice Disconnects a plug from socket - * @dev This function is called by the plug to disconnect itself from the socket + * @notice External calls to switchboard verifies the connection hence no need for a disconnect hook on switchboard, they can read if needed. + * @dev Called by plug to unregister itself. Clears plug-to-switchboard mapping. Reverts if plug is not currently connected. */ function disconnect() external override { - PlugConfigEvm storage _plugConfig = _plugConfigs[msg.sender]; - if (_plugConfig.appGatewayId == bytes32(0)) revert PlugNotConnected(); + if (plugSwitchboardIds[msg.sender] == 0) revert PlugNotConnected(); - _plugConfig.appGatewayId = bytes32(0); - _plugConfig.switchboardId = 0; + // Clear connection by setting ID to 0 + plugSwitchboardIds[msg.sender] = 0; emit PlugDisconnected(msg.sender); } /** - * @notice Sets the gas limit buffer for socket - * @dev This function is called by the governance role to set the gas limit buffer for socket - * @param gasLimitBuffer_ The gas limit buffer for socket + * @notice Returns the configuration for a given plug + * @param plugAddress_ The address of the plug + * @param extraData_ Extra data passed to switchboard for config retrieval + * @return switchboardId The switchboard ID the plug is connected to + * @return plugConfig The plug configuration from the switchboard */ - function setGasLimitBuffer(uint256 gasLimitBuffer_) external onlyRole(GOVERNANCE_ROLE) { - gasLimitBuffer = gasLimitBuffer_; - emit GasLimitBufferUpdated(gasLimitBuffer_); - } - - /** - * @notice Sets the max copy bytes for socket - * @dev This function is called by the governance role to set the max copy bytes for socket - * @param maxCopyBytes_ The max copy bytes for socket - */ - function setMaxCopyBytes(uint16 maxCopyBytes_) external onlyRole(GOVERNANCE_ROLE) { - maxCopyBytes = maxCopyBytes_; - emit MaxCopyBytesUpdated(maxCopyBytes_); + function getPlugConfig( + address plugAddress_, + bytes memory extraData_ + ) external view returns (uint32 switchboardId, bytes memory plugConfig) { + switchboardId = plugSwitchboardIds[plugAddress_]; + if (switchboardId == 0) return (0, bytes("")); + plugConfig = ISwitchboard(switchboardAddresses[switchboardId]).getPlugConfig( + plugAddress_, + extraData_ + ); } /** - * @notice Returns the config for given `plugAddress_` - * @param plugAddress_ The address of the plug present at current chain - * @return appGatewayId The app gateway id - * @return switchboardId The switchboard id + * @notice Returns the switchboard ID and address for a given plug + * @param plugAddress_ The address of the plug + * @return switchboardId The switchboard ID (0 if not connected) + * @return switchboardAddress The switchboard address (address(0) if not connected) */ - function getPlugConfig( + function getPlugSwitchboard( address plugAddress_ - ) external view returns (bytes32 appGatewayId, uint64 switchboardId) { - PlugConfigEvm memory _plugConfig = _plugConfigs[plugAddress_]; - return (_plugConfig.appGatewayId, _plugConfig.switchboardId); + ) external view returns (uint32 switchboardId, address switchboardAddress) { + switchboardId = plugSwitchboardIds[plugAddress_]; + switchboardAddress = switchboardAddresses[switchboardId]; } } diff --git a/contracts/protocol/SocketFeeManager.sol b/contracts/protocol/SocketFeeManager.sol deleted file mode 100644 index c0b7b946..00000000 --- a/contracts/protocol/SocketFeeManager.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "../utils/AccessControl.sol"; -import {GOVERNANCE_ROLE, RESCUE_ROLE} from "../utils/common/AccessRoles.sol"; -import {ExecuteParams, TransmissionParams} from "../utils/common/Structs.sol"; -import "./interfaces/ISocketFeeManager.sol"; -import "../utils/RescueFundsLib.sol"; - -/** - * @title SocketFeeManager - * @notice The SocketFeeManager contract is responsible for managing socket fees - */ -contract SocketFeeManager is ISocketFeeManager, AccessControl { - // current socket fees in native tokens - uint256 public socketFees; - - //////////////////////////////////////////////////////////// - ////////////////////// ERRORS ////////////////////////// - //////////////////////////////////////////////////////////// - - /// @notice Thrown when the fees are insufficient - error InsufficientFees(); - - /// @notice Thrown when the fees are too low - error FeeTooLow(); - - //////////////////////////////////////////////////////////// - ////////////////////// EVENTS ////////////////////////// - //////////////////////////////////////////////////////////// - - /** - * @notice Emitted when the socket fees are updated - * @param oldFees The old socket fees - * @param newFees The new socket fees - */ - event SocketFeesUpdated(uint256 oldFees, uint256 newFees); - - /** - * @notice Initializes the SocketFeeManager contract - * @param owner_ The owner of the contract with GOVERNANCE_ROLE - * @param socketFees_ Initial socket fees amount - */ - constructor(address owner_, uint256 socketFees_) { - _grantRole(GOVERNANCE_ROLE, owner_); - _grantRole(RESCUE_ROLE, owner_); - - socketFees = socketFees_; - emit SocketFeesUpdated(0, socketFees_); - } - - /** - * @notice Pays and validates fees for execution - * @dev This function is payable and will revert if the fees are insufficient - */ - function payAndCheckFees(ExecuteParams memory, TransmissionParams memory) external payable { - if (msg.value < socketFees) revert InsufficientFees(); - } - - /** - * @notice Gets minimum fees required for execution - * @return nativeFees Minimum native token fees required - */ - function getMinSocketFees() external view returns (uint256 nativeFees) { - return socketFees; - } - - /** - * @notice Sets socket fees - * @param socketFees_ New socket fees amount - */ - function setSocketFees(uint256 socketFees_) external onlyRole(GOVERNANCE_ROLE) { - emit SocketFeesUpdated(socketFees, socketFees_); - socketFees = socketFees_; - } - - /** - * @notice Allows owner to rescue stuck funds - * @param token_ Token address (address(0) for native tokens) - * @param to_ Address to send funds to - * @param amount_ Amount of tokens to rescue - */ - function rescueFunds( - address token_, - address to_, - uint256 amount_ - ) external onlyRole(RESCUE_ROLE) { - RescueFundsLib._rescueFunds(token_, to_, amount_); - } -} diff --git a/contracts/protocol/SocketUtils.sol b/contracts/protocol/SocketUtils.sol index 303d67ab..491f55bb 100644 --- a/contracts/protocol/SocketUtils.sol +++ b/contracts/protocol/SocketUtils.sol @@ -1,134 +1,102 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {ECDSA} from "solady/utils/ECDSA.sol"; -import "../utils/RescueFundsLib.sol"; -import "./SocketConfig.sol"; import {LibCall} from "solady/utils/LibCall.sol"; +import "./SocketConfig.sol"; import {toBytes32Format} from "../utils/common/Converters.sol"; +import {DigestParams} from "../utils/common/Structs.sol"; +import {createDigest} from "../utils/common/DigestUtils.sol"; +import "../utils/RescueFundsLib.sol"; +using LibCall for address; /** * @title SocketUtils - * @notice Utility functions for socket + * @notice Utility functions for socket including digest creation, simulation, and verification + * @dev Provides helper functions for payload processing, verification, and off-chain simulation */ abstract contract SocketUtils is SocketConfig { - using LibCall for address; - - //////////////////////////////////////////////////////////// - ////////////////////// State Vars ////////////////////////// - //////////////////////////////////////////////////////////// + // --- State Variables --- - struct SimulateParams { - address target; - uint256 value; - uint256 gasLimit; - bytes payload; - } - - // address of the off-chain caller + /// @notice Special address used to identify off-chain simulation calls address public constant OFF_CHAIN_CALLER = address(0xDEAD); - // prefix for trigger ID containing chain slug and address bits - uint256 private immutable triggerPrefix; - // version string for this socket instance - bytes32 public immutable version; - // chain slug for this deployed socket instance + /// @notice Chain slug identifier for this socket deployment uint32 public immutable chainSlug; - // counter for trigger id - uint64 public triggerCounter; - /// @notice Thrown when the caller is not off-chain - error OnlyOffChain(); + /// @notice Maximum bytes to copy from return data (default: 2048 = 2KB) + /// @dev Prevents unbounded return data attacks by limiting copied bytes + uint16 public immutable maxCopyBytes; + + /// @notice Gas limit buffer percentage (e.g., 105 = 5% buffer) + /// @dev Accounts for gas used by current contract execution overhead + uint256 public immutable gasLimitBuffer; - /// @notice Thrown when the simulation fails - error SimulationFailed(); + // --- Modifiers --- - /// @notice Modifier to check if the caller is off-chain + /// @notice Modifier to restrict function calls to off-chain simulation only modifier onlyOffChain() { if (msg.sender != OFF_CHAIN_CALLER) revert OnlyOffChain(); _; } - /** - * @notice constructor for creating a new Socket contract instance - * @param chainSlug_ The unique identifier of the chain this socket is deployed on - * @param owner_ The address of the owner who has the initial admin role - * @param version_ The version string which is hashed and stored in socket - */ - constructor(uint32 chainSlug_, address owner_, string memory version_) { - chainSlug = chainSlug_; - version = keccak256(bytes(version_)); - triggerPrefix = (uint256(chainSlug_) << 224) | (uint256(uint160(address(this))) << 64); + // --- Constructor --- + constructor(uint32 chainSlug_, address owner_, uint256 gasLimitBuffer_, uint16 maxCopyBytes_) { + if (chainSlug_ == 0) revert InvalidChainSlug(); + if (owner_ == address(0)) revert InvalidOwner(); + if (gasLimitBuffer_ < 100) revert GasLimitBufferTooLow(); + chainSlug = chainSlug_; _initializeOwner(owner_); + gasLimitBuffer = gasLimitBuffer_; + maxCopyBytes = maxCopyBytes_; } + // --- Internal Functions --- + /** - * @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 - * @dev This function is used to create the digest for the payload + * @notice Creates the digest for the payload execution + * @param transmitter_ The address of the transmitter that delivered the payload + * @param executionParams_ The execution parameters containing payload details + * @return The keccak256 hash of the encoded payload + * @dev Creates a deterministic digest from all execution parameters. Uses length prefixes for variable-length fields + * (payload, source, extraData) to prevent collision attacks. Fixed-size fields are packed directly, + * variable fields are prefixed with their length. using encodePacked instead of encode for bytes fields + * to make it cross-chain compatible. */ function _createDigest( address transmitter_, - bytes32 payloadId_, - bytes32 appGatewayId_, - ExecuteParams calldata executeParams_ + ExecutionParams memory executionParams_ ) internal view returns (bytes32) { - return - keccak256( - abi.encodePacked( - toBytes32Format(address(this)), - toBytes32Format(transmitter_), - payloadId_, - executeParams_.deadline, - executeParams_.callType, - executeParams_.gasLimit, - executeParams_.value, - executeParams_.payload, - toBytes32Format(executeParams_.target), - appGatewayId_, - executeParams_.prevBatchDigestHash, - executeParams_.extraData - ) - ); - } - - /** - * @notice Encodes the trigger ID with the chain slug, socket address and nonce - * @return The trigger ID - * @dev This function is used to encode the trigger ID with the chain slug, socket address and nonce - */ - function _encodeTriggerId() internal returns (bytes32) { - return bytes32(triggerPrefix | triggerCounter++); + DigestParams memory digestParams = DigestParams({ + socket: toBytes32Format(address(this)), + transmitter: toBytes32Format(transmitter_), + payloadId: executionParams_.payloadId, + deadline: executionParams_.deadline, + callType: executionParams_.callType, + gasLimit: executionParams_.gasLimit, + value: executionParams_.value, + target: toBytes32Format(executionParams_.target), + prevBatchDigestHash: executionParams_.prevBatchDigestHash, + payload: executionParams_.payload, + source: executionParams_.source, + extraData: executionParams_.extraData + }); + return createDigest(digestParams); } /** - * @notice Simulation result - * @param success True if the simulation was successful - * @param returnData The return data from the simulation - * @param exceededMaxCopy True if the simulation exceeded the max copy bytes - */ - struct SimulationResult { - bool success; - bytes returnData; - bool exceededMaxCopy; - } - - /** - * @notice Simulates the payload - * @dev This function is used to simulate the payload offchain for gas estimation and checking reverts - * @param params The parameters of the simulation - * @return The simulation results + * @notice Simulates payload execution off-chain for gas estimation and revert checking + * @param params Array of simulation parameters to test + * @return Array of simulation results corresponding to input params + * @dev Only callable by OFF_CHAIN_CALLER address. Used by off-chain services for gas estimation. + * Each simulation uses tryCall with maxCopyBytes limit to prevent unbounded return data. */ function simulate( SimulateParams[] calldata params ) external payable onlyOffChain returns (SimulationResult[] memory) { SimulationResult[] memory results = new SimulationResult[](params.length); + // Simulate each payload execution for (uint256 i = 0; i < params.length; i++) { (bool success, bool exceededMaxCopy, bytes memory returnData) = params[i] .target @@ -139,16 +107,64 @@ abstract contract SocketUtils is SocketConfig { return results; } - ////////////////////////////////////////////// - //////////// Rescue role actions //////////// - ///////////////////////////////////////////// + /** + * @notice Verifies plug is connected and returns its switchboard address + * @param plug_ The address of the plug to verify + * @return switchboardAddress The address of the switchboard the plug is connected to + * @dev Reverts if plug is not connected or switchboard is not registered + */ + function _verifyPlugSwitchboard( + address plug_ + ) internal view returns (address switchboardAddress) { + uint32 switchboardId = plugSwitchboardIds[plug_]; + if (switchboardId == 0) revert PlugNotConnected(); + if (switchboardStatus[switchboardId] != SwitchboardStatus.REGISTERED) + revert InvalidSwitchboard(); + switchboardAddress = switchboardAddresses[switchboardId]; + } + + /** + * @notice Verifies payload ID matches expected verification chain and switchboard + * @param payloadId_ The payload ID to verify + * @param switchboardAddress_ The expected switchboard address + * @dev Reverts if verification chain slug or switchboard ID don't match + */ + function _verifyPayloadId(bytes32 payloadId_, address switchboardAddress_) internal view { + (uint32 verificationChainSlug, uint32 verificationSwitchboardId) = getVerificationInfo( + payloadId_ + ); + if (verificationChainSlug != chainSlug) revert InvalidVerificationChainSlug(); + if (switchboardAddresses[verificationSwitchboardId] != switchboardAddress_) + revert InvalidVerificationSwitchboardId(); + } + + // --- External Functions --- /** - * @notice Rescues funds from the contract if they are locked by mistake. This contract does not - * theoretically need this function but it is added for safety. - * @param token_ The address of the token contract + * @notice Increases fees for a pending payload + * @param payloadId_ The payload ID to increase fees for + * @param feesData_ Encoded fees data (type + data) - format depends on switchboard implementation + * @dev Verifies caller is a connected plug, then forwards to switchboard for processing. + * Used to top up fees for payloads that haven't been executed yet. + * @dev NOTE: payloadId belongs to a plug is assumed to be verified in switchboards + */ + function increaseFeesForPayload(bytes32 payloadId_, bytes calldata feesData_) external payable { + address switchboardAddress = _verifyPlugSwitchboard(msg.sender); + ISwitchboard(switchboardAddress).increaseFeesForPayload{value: msg.value}( + payloadId_, + msg.sender, + feesData_ + ); + } + + // --- Rescue Functions --- + + /** + * @notice Rescues funds from the contract if they are locked by mistake + * @param token_ The address of the token contract (address(0) for native tokens) * @param rescueTo_ The address where rescued tokens need to be sent - * @param amount_ The amount of tokens to be rescued + * @param amount_ The amount of tokens to be rescued (0 for all) + * @dev Only callable by RESCUE_ROLE. Added as safety mechanism for stuck funds. */ function rescueFunds( address token_, @@ -157,4 +173,15 @@ abstract contract SocketUtils is SocketConfig { ) external onlyRole(RESCUE_ROLE) { RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); } + + // --- Pausable Functions --- + + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); + } + + /// @notice Unpauses the contract, re-enabling execute() and sendPayload() calls + function unpause() external onlyRole(GOVERNANCE_ROLE) { + _unpause(); + } } diff --git a/contracts/protocol/base/MessagePlugBase.sol b/contracts/protocol/base/MessagePlugBase.sol index 768a374e..680fc317 100644 --- a/contracts/protocol/base/MessagePlugBase.sol +++ b/contracts/protocol/base/MessagePlugBase.sol @@ -1,59 +1,60 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {PlugBase} from "./PlugBase.sol"; import {ISwitchboard} from "../interfaces/ISwitchboard.sol"; -import {APP_GATEWAY_ID} from "../../utils/common/Constants.sol"; +import {PlugBase} from "./PlugBase.sol"; import {toBytes32Format} from "../../utils/common/Converters.sol"; - -interface IMessageSwitchboard is ISwitchboard { - function registerSibling(uint32 chainSlug_, bytes32 siblingPlug_) external; - - function getSwitchboardFees(uint32 chainSlug_) external view returns (uint256); -} +import {ArrayLengthMismatch, InvalidSocket, InvalidSwitchboardId} from "../../utils/common/Errors.sol"; /// @title MessagePlugBase -/// @notice Abstract contract for message plugs in the updated protocol -/// @dev This contract contains helpers for socket connection, disconnection, and overrides -/// Uses constant appGatewayId (0xaaaaa) for all chains +/// @notice Abstract base contract for message-based plugs +/// @dev Extends PlugBase with message-specific functionality for registering sibling plugs. +/// Uses constant appGatewayId for all chains in message-based flows. abstract contract MessagePlugBase is PlugBase { + // --- State Variables --- + + /// @notice The switchboard address this plug is connected to address public switchboard; - uint64 public switchboardId; - uint256 public triggerPrefix; - error NotSupported(); - constructor(address socket_, uint64 switchboardId_) { + /// @notice The switchboard ID this plug is connected to + uint32 public switchboardId; + + // --- Constructor --- + + constructor(address socket_, uint32 switchboardId_) { + if (socket_ == address(0)) revert InvalidSocket(); + if (switchboardId_ == 0) revert InvalidSwitchboardId(); + _setSocket(socket_); switchboardId = switchboardId_; switchboard = socket__.switchboardAddresses(switchboardId_); - socket__.connect(APP_GATEWAY_ID, switchboardId_); - - triggerPrefix = (uint256(socket__.chainSlug()) << 224) | (uint256(uint160(socket_)) << 64); + // Connect with empty config (siblings registered separately) + socket__.connect(switchboardId_, ""); } - /// @notice Initializes the socket with the new protocol - function initSocket(bytes32, address, uint64) external override socketInitializer { - revert("Not Supported"); - } + // --- Internal Functions --- - /// @notice Registers a sibling plug for a specific chain - /// @param chainSlug_ Chain slug of the sibling chain + /// @notice Registers a sibling plug for a specific destination chain + /// @param chainSlug_ Chain slug of the destination chain /// @param siblingPlug_ Address of the sibling plug on the destination chain - function registerSibling(uint32 chainSlug_, address siblingPlug_) public { + /// @dev Updates plug config via socket.connect() with chain slug and sibling plug address + function _registerSibling(uint32 chainSlug_, address siblingPlug_) internal { // Call the switchboard to register the sibling - IMessageSwitchboard(switchboard).registerSibling(chainSlug_, toBytes32Format(siblingPlug_)); - } - - function getSocketFees(uint32 chainSlug_) public view returns (uint256) { - return IMessageSwitchboard(switchboard).getSwitchboardFees(chainSlug_); + // Using abi.encodePacked to match digest encoding (needed for Solana compatibility) + socket__.connect(switchboardId, abi.encode(chainSlug_, toBytes32Format(siblingPlug_))); } - function getNextTriggerId(uint32 chainSlug_) public view returns (bytes32) { - return - bytes32( - (uint256(chainSlug_) << 224) | - (uint256(uint160(address(socket__))) << 64) | - (uint256(socket__.triggerCounter()) << 16) - ); + /// @notice Registers multiple sibling plugs in batch + /// @param chainSlugs_ Array of destination chain slugs + /// @param siblingPlugs_ Array of sibling plug addresses (must match chainSlugs_ length) + /// @dev Reverts if array lengths don't match. Registers each sibling sequentially. + function _registerSiblings( + uint32[] memory chainSlugs_, + address[] memory siblingPlugs_ + ) internal { + if (chainSlugs_.length != siblingPlugs_.length) revert ArrayLengthMismatch(); + for (uint256 i = 0; i < chainSlugs_.length; i++) { + _registerSibling(chainSlugs_[i], siblingPlugs_[i]); + } } } diff --git a/contracts/protocol/base/PlugBase.sol b/contracts/protocol/base/PlugBase.sol index a6eda992..b22e36c0 100644 --- a/contracts/protocol/base/PlugBase.sol +++ b/contracts/protocol/base/PlugBase.sol @@ -1,88 +1,101 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {ISocket} from "../interfaces/ISocket.sol"; import {IPlug} from "../interfaces/IPlug.sol"; +import {ISocket} from "../interfaces/ISocket.sol"; import {NotSocket, SocketAlreadyInitialized} from "../../utils/common/Errors.sol"; /// @title PlugBase -/// @notice Abstract contract for plugs -/// @dev This contract contains helpers for socket connection, disconnection, and overrides +/// @notice Abstract base contract for plug implementations +/// @dev Provides helpers for socket connection, disconnection, and override management. +/// All plugs must inherit from this contract to interact with the socket protocol. abstract contract PlugBase is IPlug { - // socket instance + // --- State Variables --- + + /// @notice The socket contract instance this plug is connected to ISocket public socket__; - // app gateway id connected to this plug + /// @notice The app gateway ID this plug is associated with bytes32 public appGatewayId; - // tracks if socket is initialized + /// @notice Tracks if socket has been initialized (1 = initialized, 0 = not initialized) uint256 public isSocketInitialized; - // overrides encoded in bytes + /// @notice Override parameters encoded in bytes (format depends on switchboard) bytes public overrides; - // event emitted when plug is disconnected + // --- Events --- + + /// @notice Emitted when plug disconnects from socket event ConnectorPlugDisconnected(); - /// @notice Modifier to ensure only the socket can call the function - /// @dev only the socket can call the function + // --- Modifiers --- + + /// @notice Modifier to restrict function calls to socket contract only modifier onlySocket() { if (msg.sender != address(socket__)) revert NotSocket(); _; } - /// @notice Modifier to ensure the socket is initialized and if not already initialized, it will be initialized + /// @notice Modifier to ensure socket initialization happens only once + /// @dev Sets isSocketInitialized to 1 atomically to prevent re-initialization modifier socketInitializer() { if (isSocketInitialized == 1) revert SocketAlreadyInitialized(); isSocketInitialized = 1; _; } - /// @notice Connects the plug to the app gateway and switchboard - /// @param appGatewayId_ The app gateway id - /// @param socket_ The socket address - /// @param switchboardId_ The switchboard id + // --- Internal Functions --- + + /// @notice Connects the plug to socket with app gateway and switchboard + /// @param appGatewayId_ The app gateway ID to associate with this plug + /// @param socket_ The socket contract address + /// @param switchboardId_ The switchboard ID to connect to + /// @dev Sets socket reference, stores appGatewayId, and calls socket.connect() function _connectSocket( bytes32 appGatewayId_, address socket_, - uint64 switchboardId_ + uint32 switchboardId_ ) internal { _setSocket(socket_); appGatewayId = appGatewayId_; - // connect to the app gateway and switchboard - socket__.connect(appGatewayId_, switchboardId_); + // Connect to socket with switchboard and encode appGatewayId as config + socket__.connect(switchboardId_, abi.encode(appGatewayId_)); } /// @notice Disconnects the plug from the socket + /// @dev Calls socket.disconnect() and emits ConnectorPlugDisconnected event function _disconnectSocket() internal { socket__.disconnect(); emit ConnectorPlugDisconnected(); } - /// @notice Sets the socket - /// @param socket_ The socket address + /// @notice Sets the socket contract reference + /// @param socket_ The socket contract address function _setSocket(address socket_) internal { socket__ = ISocket(socket_); } - /// @notice Sets the overrides needed for the trigger - /// @dev encoding format depends on the watcher system - /// @param overrides_ The overrides + /// @notice Sets the override parameters for payload execution + /// @param overrides_ The override parameters (encoding format depends on switchboard) + /// @dev Overrides are used to specify destination chain, gas limit, fees, etc. function _setOverrides(bytes memory overrides_) internal { overrides = overrides_; } - /// @notice Initializes the socket - /// @dev this function should be called even if deployed independently - /// to avoid ownership and permission exploit - /// @param appGatewayId_ The app gateway id - /// @param socket_ The socket address - /// @param switchboardId_ The switchboard id + // --- External Functions --- + + /// @notice Initializes the socket connection (one-time setup) + /// @param appGatewayId_ The app gateway ID + /// @param socket_ The socket contract address + /// @param switchboardId_ The switchboard ID to connect to + /// @dev Must be called even if plug is deployed independently to prevent ownership/permission exploits. + /// Uses socketInitializer modifier to ensure single initialization. function initSocket( bytes32 appGatewayId_, address socket_, - uint64 switchboardId_ + uint32 switchboardId_ ) external virtual socketInitializer { _connectSocket(appGatewayId_, socket_, switchboardId_); } diff --git a/contracts/protocol/interfaces/IMessageHandler.sol b/contracts/protocol/interfaces/IMessageHandler.sol index cdc4764e..9a50f97c 100644 --- a/contracts/protocol/interfaces/IMessageHandler.sol +++ b/contracts/protocol/interfaces/IMessageHandler.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; /** * @title IMessageHandler diff --git a/contracts/protocol/interfaces/IMessageTransmitter.sol b/contracts/protocol/interfaces/IMessageTransmitter.sol index a00626f0..df854816 100644 --- a/contracts/protocol/interfaces/IMessageTransmitter.sol +++ b/contracts/protocol/interfaces/IMessageTransmitter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; /** * @title IMessageTransmitter diff --git a/contracts/protocol/interfaces/INetworkFeeCollector.sol b/contracts/protocol/interfaces/INetworkFeeCollector.sol new file mode 100644 index 00000000..4908a738 --- /dev/null +++ b/contracts/protocol/interfaces/INetworkFeeCollector.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {ExecutionParams, TransmissionParams} from "../../utils/common/Structs.sol"; + +/** + * @title INetworkFeeCollector + * @notice Interface for the network fee collector contract + * @dev Responsible for collecting and managing network fees for socket executions + */ +interface INetworkFeeCollector { + /** + * @notice Collects and validates network fees for a payload execution + * @param executionParams_ The execution parameters + * @param transmissionParams_ The transmission parameters + * @dev Only callable by SOCKET_ROLE. Reverts if msg.value is less than networkFee. + * @dev Emits NetworkFeeCollected event with fee amount and execution details. + */ + function collectNetworkFee( + ExecutionParams memory executionParams_, + TransmissionParams memory transmissionParams_ + ) external payable; + + /** + * @notice Returns the current network fee amount + * @return networkFee The minimum network fees required for execution + */ + function getNetworkFee() external view returns (uint256); + + /** + * @notice Sets the network fee amount + * @param networkFee_ The new network fee amount in native tokens + * @dev Only callable by GOVERNANCE_ROLE. Emits NetworkFeeUpdated event. + */ + function setNetworkFee(uint256 networkFee_) external; +} diff --git a/contracts/protocol/interfaces/IPlug.sol b/contracts/protocol/interfaces/IPlug.sol index f67160a4..6fc0917e 100644 --- a/contracts/protocol/interfaces/IPlug.sol +++ b/contracts/protocol/interfaces/IPlug.sol @@ -1,19 +1,26 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; /** * @title IPlug - * @notice Interface for a plug contract that executes the payload received from a source chain. + * @notice Interface for a plug contract that executes payloads received from source chains + * @dev Plugs are contracts that connect to socket and handle cross-chain payload execution */ interface IPlug { - /// @notice Initializes the socket - /// @param appGatewayId_ The app gateway id - /// @param socket_ The socket address - /// @param switchboardId_ The switchboard id - function initSocket(bytes32 appGatewayId_, address socket_, uint64 switchboardId_) external; + /** + * @notice Initializes the socket connection (one-time setup) + * @param appGatewayId_ The app gateway ID + * @param socket_ The socket contract address + * @param switchboardId_ The switchboard ID to connect to + * @dev Must be called even if plug is deployed independently to prevent ownership/permission exploits + * @dev Uses socketInitializer modifier to ensure single initialization + */ + function initSocket(bytes32 appGatewayId_, address socket_, uint32 switchboardId_) external; - /// @notice Gets the overrides - /// @dev encoding format depends on the watcher system - /// @return overrides_ The overrides + /** + * @notice Gets the override parameters for payload execution + * @return overrides_ The override parameters (encoding format depends on switchboard) + * @dev Overrides are used to specify destination chain, gas limit, fees, etc. + */ function overrides() external view returns (bytes memory overrides_); } diff --git a/contracts/protocol/interfaces/ISocket.sol b/contracts/protocol/interfaces/ISocket.sol index 166c0f88..eecc719b 100644 --- a/contracts/protocol/interfaces/ISocket.sol +++ b/contracts/protocol/interfaces/ISocket.sol @@ -1,130 +1,151 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {ExecuteParams, TransmissionParams, ExecutionStatus} from "../../utils/common/Structs.sol"; +import {ExecutionParams, TransmissionParams, ExecutionStatus} from "../../utils/common/Structs.sol"; /** * @title ISocket - * @notice An interface for a Chain Abstraction contract - * @dev This interface provides methods for transmitting and executing payloads, - * connecting a plug to a remote chain and setting up switchboards for the payload transmission - * This interface also emits events for important operations such as payload transmission, execution status, - * and plug connection + * @notice Interface for a Chain Abstraction contract + * @dev Provides methods for transmitting and executing payloads, connecting plugs to remote chains, + * and setting up switchboards for payload transmission. Emits events for payload transmission, + * execution status, and plug connection. */ interface ISocket { - /** - * @notice emits the status of payload after external call - * @param payloadId payload id which is executed - */ - event ExecutionSuccess(bytes32 payloadId, bool exceededMaxCopy, bytes returnData); + /// @notice Emitted when payload execution succeeds + /// @param payloadId The payload ID that was executed + /// @param exceededMaxCopy Whether return data exceeded maxCopyBytes limit + /// @param returnData The return data from execution (truncated if exceeded maxCopyBytes) + event ExecutionSuccess(bytes32 indexed payloadId, bool exceededMaxCopy, bytes returnData); + + /// @notice Emitted when payload execution fails + /// @param payloadId The payload ID that failed + /// @param exceededMaxCopy Whether return data exceeded maxCopyBytes limit + /// @param returnData The revert data from execution + event ExecutionFailed(bytes32 indexed payloadId, bool exceededMaxCopy, bytes returnData); + + /// @notice Emitted when a plug connects to socket + /// @param plug The address of the plug on current chain + /// @param switchboardId The switchboard ID the plug connected to + /// @param plugConfig The configuration data for the plug + event PlugConnected(address indexed plug, uint32 indexed switchboardId, bytes plugConfig); + + /// @notice Emitted when a plug disconnects from socket + /// @param plug The address of the plug that disconnected + event PlugDisconnected(address indexed plug); + + /// @notice Emitted when a payload is requested for transmission + /// @param payloadId The created payload ID + /// @param plug The source plug address + /// @param switchboardId The switchboard ID processing the payload + /// @param overrides The override parameters (destination chain, gas limit, fees, etc.) + /// @param payload The payload data to execute on destination + event PayloadRequested( + bytes32 indexed payloadId, + address indexed plug, + uint32 indexed switchboardId, + bytes overrides, + bytes payload + ); - /** - * @notice emits the status of payload after external call - * @param payloadId payload id which is executed - */ - event ExecutionFailed(bytes32 payloadId, bool exceededMaxCopy, bytes returnData); + /// @notice Emitted when a new switchboard is registered + event SwitchboardAdded(address switchboard, uint32 switchboardId); - /** - * @notice emits the config set by a plug for a remoteChainSlug - * @param plug The address of plug on current chain - * @param appGatewayId The address of plug on sibling chain - * @param switchboardId The outbound switchboard (select from registered options) - */ - event PlugConnected(address plug, bytes32 appGatewayId, uint64 switchboardId); + /// @notice Emitted when a switchboard is disabled by governance + event SwitchboardDisabled(uint32 switchboardId); - /** - * @notice emits the config set by a plug for a remoteChainSlug - * @param plug The address of plug on current chain - */ - event PlugDisconnected(address plug); + /// @notice Emitted when a switchboard is re-enabled by governance + event SwitchboardEnabled(uint32 switchboardId); - /** - * @notice emits the payload details when a new payload arrives at outbound - * @param triggerId trigger id - * @param switchboardId switchboard id - * @param plug local plug address - * @param overrides params, for specifying details like fee pool chain, fee pool token and max fees if required - * @param payload the data which will be used by contracts on chain - */ - event AppGatewayCallRequested( - bytes32 triggerId, - bytes32 appGatewayId, - uint64 switchboardId, - bytes32 plug, - bytes overrides, - bytes payload + /// @notice Emitted when the network fee collector address is updated + event NetworkFeeCollectorUpdated( + address oldNetworkFeeCollector, + address newNetworkFeeCollector ); /** - * @notice Executes a payload - * @param executeParams_ The execution parameters - * @param transmissionParams_ The transmission parameters + * @notice Executes a payload that has been delivered by transmitters and authenticated by switchboards + * @param executionParams_ The execution parameters (target, payload, value, gasLimit, etc.) + * @param transmissionParams_ The transmission parameters (socketFees, transmitterProof, refundAddress) * @return success True if the payload was executed successfully - * @return returnData The return data from the execution + * @return returnData The return data from the execution (truncated to maxCopyBytes if exceeded) + * @dev Validates deadline, call type, plug connection, msg.value, payload ID, execution status, and digest + * @dev Reverts if any validation fails or if contract is paused */ function execute( - ExecuteParams calldata executeParams_, + ExecutionParams calldata executionParams_, TransmissionParams calldata transmissionParams_ ) external payable returns (bool, bytes memory); /** - * @notice sets the config specific to the plug - * @param appGatewayId_ The address of plug present at sibling chain - * @param switchboardId_ The id of switchboard to use for executing payloads + * @notice Connects a plug to socket with a switchboard and configuration + * @param switchboardId_ The switchboard ID to connect to + * @param plugConfig_ The configuration data for the switchboard (can be empty) + * @dev Called by plug contract to register itself. Validates switchboard is registered. + * @dev If plugConfig_ is non-empty, forwards it to switchboard for processing. */ - function connect(bytes32 appGatewayId_, uint64 switchboardId_) external; + function connect(uint32 switchboardId_, bytes memory plugConfig_) external; /** - * @notice Disconnects Plug from Socket + * @notice Disconnects a plug from socket + * @dev Called by plug to unregister itself. Clears plug-to-switchboard mapping. + * @dev Reverts if plug is not currently connected. */ function disconnect() external; /** - * @notice Registers a switchboard for the socket - * @return switchboardId The id of the switchboard + * @notice Registers a switchboard on the socket + * @return switchboardId The assigned switchboard ID + * @dev Called by switchboard contract to register itself. Assigns a unique ID and sets status to REGISTERED. + * @dev Reverts if switchboard is already registered (non-zero ID) */ - function registerSwitchboard() external returns (uint64); + function registerSwitchboard() external returns (uint32); /** - * @notice Returns the config for given `plugAddress_` and `siblingChainSlug_` - * @param plugAddress_ The address of plug present at current chain - * @return appGatewayId The address of plug on sibling chain - * @return switchboardId The id of the switchboard + * @notice Returns the configuration for a given plug + * @param plugAddress_ The address of the plug + * @param extraData_ Extra data passed to switchboard for config retrieval + * @return switchboardId The switchboard ID the plug is connected to + * @return plugConfig The plug configuration from the switchboard */ function getPlugConfig( - address plugAddress_ - ) external view returns (bytes32 appGatewayId, uint64 switchboardId); + address plugAddress_, + bytes memory extraData_ + ) external view returns (uint32 switchboardId, bytes memory plugConfig); /** * @notice Returns the execution status of a payload - * @param payloadId_ The payload id - * @return executionStatus The execution status + * @param payloadId_ The payload ID + * @return executionStatus The execution status (Executed/Reverted) */ - function payloadExecuted(bytes32 payloadId_) external view returns (ExecutionStatus); + function executionStatus(bytes32 payloadId_) external view returns (ExecutionStatus); /** - * @notice Returns the chain slug - * @return chainSlug The chain slug + * @notice Returns the chain slug identifier + * @return chainSlug The chain slug where this socket is deployed */ function chainSlug() external view returns (uint32); /** - * @notice Returns the digest of a payload - * @param payloadId_ The payload id - * @return digest The digest + * @notice Returns the switchboard address for a given switchboard ID + * @param switchboardId_ The switchboard ID + * @return switchboardAddress The switchboard address (address(0) if ID doesn't exist) */ - function payloadIdToDigest(bytes32 payloadId_) external view returns (bytes32); + function switchboardAddresses(uint32 switchboardId_) external view returns (address); /** - * @notice Returns the current trigger counter - * @return triggerCounter The trigger counter + * @notice Sends a payload to a connected remote chain + * @param callData_ The payload data + * @return payloadId The created payload ID + * @dev Should only be called by a plug. The switchboard will create the payload ID. */ - function triggerCounter() external view returns (uint64); + function sendPayload(bytes calldata callData_) external payable returns (bytes32 payloadId); /** - * @notice Returns the switchboard address for a given switchboard id - * @param switchboardId_ The switchboard id - * @return switchboardAddress The switchboard address + * @notice Increases fees for a pending payload + * @param payloadId_ The payload ID to increase fees for + * @param feesData_ Encoded fees data (type + data) - format depends on switchboard implementation + * @dev Verifies caller is a connected plug, then forwards to switchboard for processing + * @dev Used to top up fees for payloads that haven't been executed yet */ - function switchboardAddresses(uint64 switchboardId_) external view returns (address); + function increaseFeesForPayload(bytes32 payloadId_, bytes calldata feesData_) external payable; } diff --git a/contracts/protocol/interfaces/ISocketBatcher.sol b/contracts/protocol/interfaces/ISocketBatcher.sol index f31782b1..10b90c65 100644 --- a/contracts/protocol/interfaces/ISocketBatcher.sol +++ b/contracts/protocol/interfaces/ISocketBatcher.sol @@ -1,29 +1,31 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {ExecuteParams} from "../../utils/common/Structs.sol"; +import {ExecutionParams, TransmissionParams} from "../../utils/common/Structs.sol"; /** * @title ISocketBatcher - * @notice Interface for a helper contract for socket which batches attest (on sb) - * and execute calls (on socket) + * @notice Interface for a helper contract that batches attestation and execution + * @dev Allows combining switchboard attestation with socket execution to reduce transaction count */ interface ISocketBatcher { /** - * @notice Attests a payload and executes it - * @param executeParams_ The execution parameters - * @param digest_ The digest of the payload - * @param proof_ The proof of the payload - * @param transmitterSignature_ The signature of the transmitter - * @param refundAddress_ The address to refund the fees to - * @return The return data after execution + * @notice Attests a payload on switchboard and executes it on socket in a single transaction + * @param executionParams_ The execution parameters for socket.execute() + * @param transmissionParams_ The transmission parameters for socket.execute() + * @param switchboardId_ The switchboard ID to attest on + * @param digest_ The digest of the payload to attest + * @param proof_ The watcher proof for attestation + * @return success True if execution succeeded + * @return returnData The return data from execution + * @dev First attests the digest on FastSwitchboard, then executes on socket. + * @dev Reduces transaction count by combining attestation and execution. */ function attestAndExecute( - ExecuteParams calldata executeParams_, - uint64 switchboardId_, + ExecutionParams memory executionParams_, + TransmissionParams memory transmissionParams_, + uint32 switchboardId_, bytes32 digest_, - bytes calldata proof_, - bytes calldata transmitterSignature_, - address refundAddress_ - ) external payable returns (bool, bytes memory); + bytes calldata proof_ + ) external payable returns (bool success, bytes memory returnData); } diff --git a/contracts/protocol/interfaces/ISocketFeeManager.sol b/contracts/protocol/interfaces/ISocketFeeManager.sol deleted file mode 100644 index 029379e5..00000000 --- a/contracts/protocol/interfaces/ISocketFeeManager.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import {ExecuteParams, TransmissionParams} from "../../utils/common/Structs.sol"; - -/** - * @title ISocketFeeManager - * @notice Interface for the socket fee manager - */ -interface ISocketFeeManager { - /** - * @notice Pays and validates fees for execution - * @param executeParams_ The execution parameters - * @param transmissionParams_ The transmission parameters - */ - function payAndCheckFees( - ExecuteParams memory executeParams_, - TransmissionParams memory transmissionParams_ - ) external payable; - - /** - * @notice Gets minimum fees required for execution - * @return nativeFees The minimum native token fees required - */ - function getMinSocketFees() external view returns (uint256 nativeFees); - - /** - * @notice Sets socket fees - * @param socketFees_ The new socket fees amount - */ - function setSocketFees(uint256 socketFees_) external; - - /** - * @notice Gets current socket fees - * @return socketFees The current socket fees amount - */ - function socketFees() external view returns (uint256); -} diff --git a/contracts/protocol/interfaces/ISwitchboard.sol b/contracts/protocol/interfaces/ISwitchboard.sol index f462cd18..c659d233 100644 --- a/contracts/protocol/interfaces/ISwitchboard.sol +++ b/contracts/protocol/interfaces/ISwitchboard.sol @@ -1,47 +1,88 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; /** * @title ISwitchboard - * @dev The interface for a switchboard contract that is responsible for verification of payloads if the correct - * digest is executed. + * @notice Interface for a switchboard contract responsible for payload verification + * @dev Switchboards verify payloads by checking if the correct digest is executed. + * They handle payload processing, attestation, and fee management. */ interface ISwitchboard { /** - * @notice Checks if a payloads can be allowed to go through the switchboard. - * @param digest_ the payloads digest. - * @param payloadId_ The unique identifier for the payloads. - * @return A boolean indicating whether the payloads is allowed to go through the switchboard or not. + * @notice Checks if a payload is allowed for execution + * @param digest_ The payload digest + * @param payloadId_ The unique identifier for the payload + * @param target_ The target plug address + * @param source_ The source of the payload (encoded chainSlug and plug address) + * @return True if payload is allowed to execute, false otherwise + * @dev Validates digest attestation and source/target matching based on switchboard implementation */ - function allowPayload(bytes32 digest_, bytes32 payloadId_) external view returns (bool); + function allowPayload( + bytes32 digest_, + bytes32 payloadId_, + address target_, + bytes memory source_ + ) external view returns (bool); /** - * @notice Processes a trigger and creates payload - * @dev This function is called by the socket to process a trigger - * @dev sb can override this function to add additional logic - * @param triggerId_ Trigger ID from socket + * @notice Processes a payload request and creates payload ID * @param plug_ Source plug address - * @param payload_ Payload data - * @param overrides_ Overrides for the trigger + * @param payload_ Payload data to execute on destination + * @param overrides_ Override parameters (format depends on switchboard implementation) + * @return payloadId_ The created payload ID + * @dev Called by socket to process payload requests. Switchboards can override to add custom logic. + * @dev Creates unique payload ID and emits PayloadRequested event for off-chain watchers. */ - function processTrigger( + function processPayload( address plug_, - bytes32 triggerId_, bytes calldata payload_, bytes calldata overrides_ - ) external payable; + ) external payable returns (bytes32 payloadId_); /** - * @notice Gets the transmitter for a given payload - * @notice Switchboard are required to implement this function to allow for the verification of the transmitters - * @param sender_ The sender of the payload + * @notice Returns the transmitter address for a given payload + * @param sender_ The sender of the payload (unused in base implementation) * @param payloadId_ The payload ID - * @param transmitterSignature_ The transmitter signature - * @return The transmitter address + * @param transmitterSignature_ The transmitter signature (optional, empty bytes if not provided) + * @return transmitter The transmitter address (address(0) if no signature provided) + * @dev If signature is provided, recovers signer from signature. Otherwise returns address(0). + * @dev Recovered signer should be validated for valid roles by caller. */ function getTransmitter( address sender_, bytes32 payloadId_, bytes calldata transmitterSignature_ - ) external view returns (address); + ) external view returns (address transmitter); + + /** + * @notice Increases fees for a pending payload + * @param payloadId_ The payload ID to increase fees for + * @param plug_ The address of the plug + * @param feesData_ Encoded fees data (type + data) - format depends on switchboard implementation + * @dev Only callable by socket. Used to top up fees for payloads that haven't been executed yet. + */ + function increaseFeesForPayload( + bytes32 payloadId_, + address plug_, + bytes calldata feesData_ + ) external payable; + + /** + * @notice Updates plug configuration + * @param plug_ The address of the plug + * @param plugConfig_ The configuration data for the plug (format depends on switchboard) + * @dev Only callable by socket. Stores configuration for source validation and routing. + */ + function updatePlugConfig(address plug_, bytes memory plugConfig_) external; + + /** + * @notice Gets the plug configuration + * @param plug_ The address of the plug + * @param extraData_ Extra data for config retrieval (e.g., destination chain slug) + * @return plugConfig_ The configuration data for the plug (encoded format) + */ + function getPlugConfig( + address plug_, + bytes memory extraData_ + ) external view returns (bytes memory plugConfig_); } diff --git a/contracts/protocol/switchboard/EVMxSwitchboard.sol b/contracts/protocol/switchboard/EVMxSwitchboard.sol new file mode 100644 index 00000000..a939265b --- /dev/null +++ b/contracts/protocol/switchboard/EVMxSwitchboard.sol @@ -0,0 +1,329 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "./SwitchboardBase.sol"; +import {EVMxOverrides} from "../../utils/common/Structs.sol"; + +/** + * @title EVMxSwitchboard + * @notice Fast switchboard implementation that enables payload attestations from watchers + * @dev Allows watchers to attest payloads for fast execution. Uses EVMX for verification. + */ +contract EVMxSwitchboard is SwitchboardBase { + // --- State Variables --- + + /// @notice EVMX chain slug for payload verification + uint32 public immutable evmxChainSlug; + + /// @notice EVMX watcher ID for payload verification + uint32 public immutable evmxWatcherId; + + /// @notice Transmitter address for payload execution + address public transmitter; + + /// @notice TotalWatchers registered which are responsible for attesting payloads from evmx + uint256 public totalWatchers; + + /// @notice Mapping of watcher address to digest to attestation status (true if attested by watcher) + mapping(address => mapping(bytes32 => bool)) public isAttestedByWatcher; + + /// @notice Mapping of digest to attestation count + mapping(bytes32 => uint256) public attestations; + + /// @notice Mapping of digest to validity status (true if digest is attested by enough watchers) + mapping(bytes32 => bool) public isValid; + + /// @notice Mapping of plug address to app gateway ID + mapping(address => bytes32) public plugAppGatewayIds; + + // @notice Mapping of payload ID to plug address + mapping(bytes32 => address) public payloadIdToPlug; + + // --- Events --- + + /// @notice Event emitted when transmitter is assigned + event TransmitterAssigned(bytes32 indexed payloadId, address indexed transmitter); + + /// @notice Event emitted when transmitter is set + event TransmitterSet(address indexed transmitter); + + /// @notice Event emitted when fees are increased + event FeesIncreased(bytes32 indexed payloadId, address indexed plug, bytes feesData); + + /// @notice Event emitted when plug configuration is updated + event PlugConfigUpdated(address indexed plug, bytes32 appGatewayId); + + /// @notice Event emitted when EVMX config is set + event EvmxConfigSet(uint32 evmxChainSlug, uint32 evmxWatcherId); + + /// @notice Event emitted when payload is requested + event PayloadRequested( + bytes32 indexed payloadId, + address indexed plug, + uint32 indexed switchboardId, + bytes overrides, + bytes payload + ); + + // --- Constructor --- + + constructor( + uint32 chainSlug_, + ISocket socket_, + address owner_, + address transmitter_, + uint32 evmxChainSlug_, + uint32 evmxWatcherId_ + ) SwitchboardBase(chainSlug_, socket_, owner_) { + transmitter = transmitter_; + evmxChainSlug = evmxChainSlug_; + evmxWatcherId = evmxWatcherId_; + } + + // --- External Functions --- + + /** + * @notice Attests a payload digest with watcher signature + * @param digest_ The digest of the payload to be executed + * @param proof_ The watcher signature proof + * @dev Reverts if digest already attested or watcher is not authorized. + * Payload is uniquely identified by digest. Once attested, payload can be executed. + */ + function attest(bytes32 digest_, bytes calldata proof_) public virtual { + address watcher = _recoverSigner( + keccak256(abi.encodePacked(toBytes32Format(address(this)), chainSlug, digest_)), + proof_ + ); + if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + + // Prevent double attestation + if (isAttestedByWatcher[watcher][digest_]) revert AlreadyAttested(); + isAttestedByWatcher[watcher][digest_] = true; + attestations[digest_]++; + + // Mark digest as valid if enough attestations are reached + if (attestations[digest_] >= totalWatchers) isValid[digest_] = true; + + emit Attested(digest_, watcher); + } + + /** + * @inheritdoc ISwitchboard + * @notice Checks if payload is allowed for execution + * @param digest_ The payload digest + * @param target_ The target plug address + * @param source_ The source app gateway ID (encoded as bytes32) + * @return True if digest is attested and source matches plug's app gateway ID + */ + function allowPayload( + bytes32 digest_, + bytes32, + address target_, + bytes memory source_ + ) external view returns (bool) { + bytes32 appGatewayId = abi.decode(source_, (bytes32)); + if (plugAppGatewayIds[target_] != appGatewayId) revert InvalidSource(); + return isValid[digest_]; + } + + /** + * @inheritdoc ISwitchboard + * @notice Processes a payload request and creates payload ID + * @param plug_ The source plug address + * @param payload_ The payload data + * @param overrides_ The override parameters (deadline encoded as uint256, empty for default) + * @return payloadId The created payload ID + */ + function processPayload( + address plug_, + bytes calldata payload_, + bytes calldata overrides_ + ) external payable override onlySocket returns (bytes32 payloadId) { + if (evmxChainSlug == 0 || evmxWatcherId == 0) revert EvmxConfigNotSet(); + EVMxOverrides memory overridesParams = abi.decode(overrides_, (EVMxOverrides)); + + payloadId = createPayloadId( + chainSlug, + switchboardId, + evmxChainSlug, + evmxWatcherId, + payloadCounter++ + ); + DigestParams memory digestParams = DigestParams({ + socket: toBytes32Format(address(this)), + transmitter: toBytes32Format(transmitter), + payloadId: payloadId, + deadline: block.timestamp + (overridesParams.deadline > 0 ? overridesParams.deadline : defaultDeadline), + callType: WRITE, + gasLimit: overridesParams.gasLimit, + value: msg.value, + payload: payload_, + target: plugAppGatewayIds[plug_], + source: abi.encodePacked(chainSlug, toBytes32Format(plug_)), + prevBatchDigestHash: bytes32(0), + extraData: bytes("") + }); + bytes32 digest = createDigest(digestParams); + payloadIdToPlug[payloadId] = plug_; + + emit PayloadRequested(payloadId, plug_, switchboardId, overrides_, payload_); + } + + /** + * @notice Increases fees for a pending payload + * @param payloadId_ The payload ID to increase fees for + * @param plug_ The address of the plug + * @param feesData_ Encoded fees data (type + data) + * @dev Currently we don't support increasing fees for payloads in EVMxSwitchboard, but we will in the future. + * Currently only emitting the event. Verifications happen off-chain on evmx. + */ + function increaseFeesForPayload( + bytes32 payloadId_, + address plug_, + bytes calldata feesData_ + ) external payable override onlySocket { + if (payloadIdToPlug[payloadId_] != plug_) revert InvalidSource(); + emit FeesIncreased(payloadId_, plug_, feesData_); + } + + /** + * @inheritdoc ISwitchboard + * @notice Updates plug configuration with app gateway ID + * @param plug_ The plug address + * @param plugConfig_ The configuration (app gateway ID encoded as bytes32) + * @dev Only callable by socket. Stores app gateway ID for source validation. + */ + function updatePlugConfig(address plug_, bytes memory plugConfig_) external virtual onlySocket { + bytes32 appGatewayId_ = abi.decode(plugConfig_, (bytes32)); + plugAppGatewayIds[plug_] = appGatewayId_; + emit PlugConfigUpdated(plug_, appGatewayId_); + } + + /** + * @notice Sets reverting status for a payload + * @param payloadId_ The payload ID to mark + * @param isReverting_ True if payload should be marked as reverting + * @dev Only callable by owner. Used to mark payloads that are known to revert. + */ + function setRevertingPayload( + bytes32 payloadId_, + bool isReverting_, + uint256 nonce_, + bytes calldata signature_ + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(this)), + chainSlug, + payloadId_, + isReverting_, + nonce_ + ) + ); + + address watcher = _recoverSigner(digest, signature_); + if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + if (usedNonces[watcher][nonce_]) revert NonceAlreadyUsed(); + usedNonces[watcher][nonce_] = true; + + revertingPayloadIds[payloadId_] = isReverting_; + emit RevertingPayloadIdset(payloadId_, isReverting_); + } + + /** + * @notice Gets the transmitter address for payload execution + * @param digestParams_ The digest parameters + * @param signature_ The watcher signature + * @dev Only callable by watcher. Used to assign the transmitter address for payload execution. + */ + function assignTransmitter( + uint256 nonce_, + DigestParams memory digestParams_, + bytes calldata signature_ + ) external { + bytes32 digest = createDigest(digestParams_); + address watcher = _recoverSigner( + keccak256(abi.encodePacked(toBytes32Format(address(this)), chainSlug, digest)), + signature_ + ); + if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + if (usedNonces[watcher][nonce_]) revert NonceAlreadyUsed(); + usedNonces[watcher][nonce_] = true; + + payloadIdToDigest[digestParams_.payloadId] = digest; + emit TransmitterAssigned(digestParams_.payloadId, transmitter); + } + + /** + * @notice Sets the transmitter address for payload execution + * @param transmitter_ The new transmitter address + * @dev Only callable by owner. Used to set the transmitter address for payload execution. + */ + function setTransmitter(address transmitter_) external onlyOwner { + transmitter = transmitter_; + emit TransmitterSet(transmitter_); + } + + /** + * @notice Sets the default deadline for payload execution + * @param defaultDeadline_ The new default deadline in seconds + * @dev Only callable by owner. Used when overrides don't specify a deadline. + */ + function setDefaultDeadline(uint256 defaultDeadline_) external onlyOwner { + defaultDeadline = defaultDeadline_; + emit DefaultDeadlineSet(defaultDeadline_); + } + + /** + * @inheritdoc ISwitchboard + * @notice Returns the plug configuration (app gateway ID) + * @param plug_ The plug address + * @return plugConfig_ The app gateway ID encoded as bytes + */ + function getPlugConfig( + address plug_, + bytes memory + ) external view override returns (bytes memory plugConfig_) { + plugConfig_ = abi.encode(plugAppGatewayIds[plug_]); + } + + /** + * @notice adds a watcher + * @param watcher_ watcher address + */ + function grantWatcherRole(address watcher_) external onlyRole(GOVERNANCE_ROLE) { + if (_hasRole(WATCHER_ROLE, watcher_)) revert WatcherFound(); + _grantRole(WATCHER_ROLE, watcher_); + + ++totalWatchers; + } + + /** + * @notice removes a watcher + * @param watcher_ watcher address + */ + function revokeWatcherRole(address watcher_) external onlyRole(GOVERNANCE_ROLE) { + if (!_hasRole(WATCHER_ROLE, watcher_)) revert WatcherNotFound(); + _revokeRole(WATCHER_ROLE, watcher_); + + --totalWatchers; + } + + /** + * @dev Overriding this function from AccessControl to make sure owner can't grant Watcher Role directly, and should + * only use grantWatcherRole function instead. This is to make sure watcher count remains correct + */ + function grantRole(bytes32 role_, address grantee_) external override onlyOwner { + if (role_ == WATCHER_ROLE) revert InvalidRole(); + _grantRole(role_, grantee_); + } + + /** + * @dev Overriding this function from AccessControl to make sure owner can't revoke Watcher Role directly, and should + * only use revokeWatcherRole function instead. This is to make sure watcher count remains correct + */ + function revokeRole(bytes32 role_, address grantee_) external override onlyOwner { + if (role_ == WATCHER_ROLE) revert InvalidRole(); + _revokeRole(role_, grantee_); + } +} diff --git a/contracts/protocol/switchboard/FastSwitchboard.sol b/contracts/protocol/switchboard/FastSwitchboard.sol deleted file mode 100644 index 3995a380..00000000 --- a/contracts/protocol/switchboard/FastSwitchboard.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "./SwitchboardBase.sol"; -import {WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; -import {toBytes32Format} from "../../utils/common/Converters.sol"; - -/** - * @title FastSwitchboard contract - * @dev This contract implements a fast version of the SwitchboardBase contract - * that enables payload attestations from watchers - */ -contract FastSwitchboard is SwitchboardBase { - // used to track if watcher have attested a payload - // payloadId => isAttested - mapping(bytes32 => bool) public isAttested; - - // Error emitted when a payload is already attested by watcher. - error AlreadyAttested(); - // Error emitted when watcher is not valid - error WatcherNotFound(); - // Event emitted when watcher attests a payload - event Attested(bytes32 payloadId_, address watcher); - - /** - * @dev Constructor function for the FastSwitchboard contract - * @param chainSlug_ Chain slug of the chain where the contract is deployed - * @param socket_ Socket contract address - * @param owner_ Owner of the contract - */ - constructor( - uint32 chainSlug_, - ISocket socket_, - address owner_ - ) SwitchboardBase(chainSlug_, socket_, owner_) {} - - /** - * @dev Function to attest a payload - * @param digest_ digest of the payload to be executed - * @param proof_ proof from watcher - * @notice we are attesting a payload uniquely identified with digest. - */ - function attest(bytes32 digest_, bytes calldata proof_) public virtual { - if (isAttested[digest_]) revert AlreadyAttested(); - - address watcher = _recoverSigner( - keccak256(abi.encodePacked(toBytes32Format(address(this)), chainSlug, digest_)), - proof_ - ); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); - - isAttested[digest_] = true; - emit Attested(digest_, watcher); - } - - /** - * @inheritdoc ISwitchboard - */ - function allowPayload(bytes32 digest_, bytes32) external view returns (bool) { - // digest has enough attestations - return isAttested[digest_]; - } - - /** - * @inheritdoc ISwitchboard - */ - function processTrigger( - address plug_, - bytes32 triggerId_, - bytes calldata payload_, - bytes calldata overrides_ - ) external payable virtual {} -} diff --git a/contracts/protocol/switchboard/MessageSwitchboard.sol b/contracts/protocol/switchboard/MessageSwitchboard.sol index 549634eb..bec5a967 100644 --- a/contracts/protocol/switchboard/MessageSwitchboard.sol +++ b/contracts/protocol/switchboard/MessageSwitchboard.sol @@ -1,143 +1,307 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; +import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol"; import "./SwitchboardBase.sol"; -import {WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; -import {toBytes32Format} from "../../utils/common/Converters.sol"; -import {createPayloadId} from "../../utils/common/IdUtils.sol"; -import {DigestParams} from "../../utils/common/Structs.sol"; -import {WRITE, APP_GATEWAY_ID} from "../../utils/common/Constants.sol"; +import {FEE_UPDATER_ROLE} from "../../utils/common/AccessRoles.sol"; +import {MessageOverrides, PayloadFees, SponsoredPayloadFees} from "../../utils/common/Structs.sol"; /** - * @title MessageSwitchboard contract - * @dev This contract implements a message switchboard that enables payload attestations from watchers + * @title MessageSwitchboard + * @notice Message-based switchboard implementation with watcher attestations and fee management + * @dev Supports both native token fees and sponsored fees. Enables payload attestations from watchers. */ -contract MessageSwitchboard is SwitchboardBase { - // used to track if watcher have attested a payload - // payloadId => isAttested - mapping(bytes32 => bool) public isAttested; +contract MessageSwitchboard is SwitchboardBase, ReentrancyGuard { + // --- State Variables --- - // sibling mappings for outbound journey - // chainSlug => siblingSocket + /// @notice Mapping of sibling chain slug to totalWatchers registered + mapping(uint32 => uint256) public totalWatchers; + + /// @notice Mapping of watcher address to digest to attestation status (true if attested by watcher) + mapping(address => mapping(bytes32 => bool)) public isAttestedByWatcher; + + /// @notice Mapping of digest to attestation count + mapping(bytes32 => uint256) public attestations; + + /// @notice Mapping of digest to validity status (true if digest is attested by enough watchers) + mapping(bytes32 => bool) public isValid; + + /// @notice Mapping of destination chain slug to sibling socket address (bytes32 format) mapping(uint32 => bytes32) public siblingSockets; - // chainSlug => siblingSwitchboard + + /// @notice Mapping of destination chain slug to sibling switchboard address (bytes32 format) mapping(uint32 => bytes32) public siblingSwitchboards; - // chainSlug => address => siblingPlug + + /// @notice Mapping of destination chain slug to sibling switchboard ID + mapping(uint32 => uint32) public siblingSwitchboardIds; + + /// @notice Mapping of sibling chain slug and plug address to sibling plug address (bytes32 format) mapping(uint32 => mapping(address => bytes32)) public siblingPlugs; - // payload counter for generating unique payload IDs - uint40 public payloadCounter; - - // switchboard fees mapping: chainSlug => fee amount - mapping(uint32 => uint256) public switchboardFees; - - // Error emitted when a payload is already attested by watcher. - error AlreadyAttested(); - // Error emitted when watcher is not valid - error WatcherNotFound(); - // Error emitted when sibling not found - error SiblingNotFound(); - // Error emitted when invalid target verification - error InvalidTargetVerification(); - // Error emitted when msg.value is not equal to switchboard fees + value - error InvalidMsgValue(); - - // Event emitted when watcher attests a payload - event Attested(bytes32 payloadId, bytes32 digest, address watcher); - // Event emitted when trigger is processed - event TriggerProcessed( - uint32 dstChainSlug, - uint256 switchboardFees, + /// @notice Minimum message value fees per destination chain + mapping(uint32 => uint256) public minMsgValueFees; + + /// @notice Mapping of payload ID to fee information (for native token flow) + mapping(bytes32 => PayloadFees) public payloadFees; + + /// @notice Mapping of payload ID to sponsored fee information + mapping(bytes32 => SponsoredPayloadFees) public sponsoredPayloadFees; + + /// @notice Mapping of sponsor address to plug address to approval status + mapping(address => mapping(address => bool)) public sponsorApprovals; + + // --- Events --- + + /// @notice Event emitted when message is sent outbound + event MessageOutbound( + bytes32 indexed payloadId, + uint32 indexed dstChainSlug, bytes32 digest, - DigestParams digestParams + DigestParams digestParams, + bool isSponsored, + uint256 nativeFees, + uint256 maxFees, + address indexed sponsor ); - // Event emitted when sibling is registered - event SiblingRegistered(uint32 chainSlug, address plugAddress, bytes32 siblingPlug); - // Event emitted when sibling config is set - event SiblingConfigSet(uint32 chainSlug, uint256 fee, bytes32 socket, bytes32 switchboard); - // Event emitted when switchboard fees are set - event SwitchboardFeesSet(uint32 chainSlug, uint256 feeAmount); + /// @notice Event emitted when sibling config is set + event SiblingConfigSet(uint32 indexed chainSlug, bytes32 socket, bytes32 switchboard); + + /// @notice Event emitted when sponsor approves a plug + event PlugApproved(address indexed sponsor, address indexed plug); + + /// @notice Event emitted when sponsor revokes a plug + event PlugRevoked(address indexed sponsor, address indexed plug); + + /// @notice Event emitted when plug configuration is updated + event PlugConfigUpdated(address indexed plug, uint32 indexed chainSlug, bytes32 siblingPlug); + + /// @notice Event emitted when refund eligibility is marked by watcher + event RefundEligibilityMarked(bytes32 indexed payloadId, address indexed watcher); + + /// @notice Event emitted when refund is issued + event Refunded(bytes32 indexed payloadId, address indexed refundAddress, uint256 amount); + + /// @notice Event emitted when fees are increased for a payload + event NativeFeesIncreased( + bytes32 indexed payloadId, + uint256 additionalNativeFees, + bytes feesData + ); + + /// @notice Event emitted when minimum message value fees are set + event MinMsgValueFeesSet(uint32 indexed chainSlug, uint256 minFees, address indexed updater); + + /// @notice Event emitted when sponsored fees are increased + event SponsoredFeesIncreased( + bytes32 indexed payloadId, + uint256 newMaxFees, + address indexed plug + ); + + /// @notice Event emitted when payload is requested + event PayloadRequested( + bytes32 indexed payloadId, + address indexed plug, + uint32 indexed switchboardId, + bytes overrides, + bytes payload + ); + + // --- Constructor --- - /** - * @dev Constructor function for the MessageSwitchboard contract - * @param chainSlug_ Chain slug of the chain where the contract is deployed - * @param socket_ Socket contract address - * @param owner_ Owner of the contract - */ constructor( uint32 chainSlug_, ISocket socket_, address owner_ ) SwitchboardBase(chainSlug_, socket_, owner_) {} + // --- External Functions --- /** - * @dev Function to register sibling addresses for a chain (admin only) - * @param chainSlug_ Chain slug of the sibling chain - * @param socket_ Sibling socket address - * @param switchboard_ Sibling switchboard address + * @notice Attests a payload with enhanced verification + * @param digest_ Full digest parameters (un-hashed) + * @param proof_ Watcher signature proof + * @dev Creates digest from parameters, recovers watcher, and marks digest as attested. + * @dev Enhanced attestation verifies target with sibling chain slug and sibling plug. */ - function setSiblingConfig( - uint32 chainSlug_, - uint256 fee_, - bytes32 socket_, - bytes32 switchboard_ - ) external onlyOwner { - siblingSockets[chainSlug_] = socket_; - siblingSwitchboards[chainSlug_] = switchboard_; - switchboardFees[chainSlug_] = fee_; + function attest(bytes32 digest_, bytes calldata proof_) public { + // Recover watcher from signature + address watcher = _recoverSigner( + keccak256(abi.encodePacked(toBytes32Format(address(this)), chainSlug, digest_)), + proof_ + ); + // Verify watcher has WATCHER_ROLE + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, chainSlug)); + if (!_hasRole(role, watcher)) revert WatcherNotFound(); + + // Prevent double attestation + if (isAttestedByWatcher[watcher][digest_]) revert AlreadyAttested(); + isAttestedByWatcher[watcher][digest_] = true; + attestations[digest_]++; - emit SiblingConfigSet(chainSlug_, fee_, socket_, switchboard_); + // Mark digest_ as valid if enough attestations are reached + if (attestations[digest_] >= totalWatchers[chainSlug]) isValid[digest_] = true; + + emit Attested(digest_, watcher); } /** - * @dev Function for plugs to register their own siblings - * @param chainSlug_ Chain slug of the sibling chain - * @param siblingPlug_ Sibling plug address + * @inheritdoc ISwitchboard */ - function registerSibling(uint32 chainSlug_, bytes32 siblingPlug_) external { - if ( - siblingSockets[chainSlug_] == bytes32(0) || - siblingSwitchboards[chainSlug_] == bytes32(0) - ) { - revert SiblingNotFound(); - } - - // Register the sibling for the calling plug - siblingPlugs[chainSlug_][msg.sender] = siblingPlug_; - emit SiblingRegistered(chainSlug_, msg.sender, siblingPlug_); + function allowPayload( + bytes32 digest_, + bytes32, + address target_, + bytes memory sibling_ + ) external view override returns (bool) { + (uint32 siblingChainSlug, bytes32 siblingPlug) = _decodePackedSource(sibling_); + if (siblingPlugs[siblingChainSlug][target_] != siblingPlug) revert InvalidSource(); + // digest has enough attestations + return isValid[digest_]; } /** - * @dev Function to process trigger and create payload + * @notice Processes a payload request and creates payload ID * @param plug_ Source plug address - * @param triggerId_ Trigger ID from socket - * @param payload_ Payload data - * @param overrides_ Override parameters including dstChainSlug and gasLimit + * @param payload_ Payload data to execute on destination + * @param overrides_ Override parameters (version, dstChainSlug, gasLimit, value, fees, etc.) + * @return payloadId The created payload ID + * @dev Supports both native token fees and sponsored fees flows. + * @dev Validates sibling configuration, creates digest, and tracks fees for refund eligibility. */ - function processTrigger( + function processPayload( address plug_, - bytes32 triggerId_, bytes calldata payload_, bytes calldata overrides_ - ) external payable override { - (uint32 dstChainSlug, uint256 gasLimit, uint256 value) = abi.decode( - overrides_, - (uint32, uint256, uint256) - ); - _validateSibling(dstChainSlug, plug_); - if (switchboardFees[dstChainSlug] + value < msg.value) revert InvalidMsgValue(); - - (DigestParams memory digestParams, bytes32 digest) = _createDigestAndPayloadId( - dstChainSlug, - plug_, - gasLimit, - value, - triggerId_, - payload_ - ); + ) external payable override onlySocket returns (bytes32 payloadId) { + // Decode and validate overrides based on version + MessageOverrides memory overrides = _decodeOverrides(overrides_); + overrides.deadline = + block.timestamp + + (overrides.deadline > 0 ? overrides.deadline : defaultDeadline); + + _validateSibling(overrides.dstChainSlug, plug_); + + // Create digest and payload ID (common for both native and sponsored flows) + ( + DigestParams memory digestParams, + bytes32 digest, + bytes32 payloadId_ + ) = _createDigestAndPayloadId(plug_, overrides, payload_); + payloadId = payloadId_; + payloadIdToDigest[payloadId] = digest; + + if (overrides.isSponsored) { + // Sponsored flow - validate sponsor has approved this plug + if (!sponsorApprovals[overrides.sponsor][plug_]) revert PlugNotApprovedBySponsor(); - emit TriggerProcessed(dstChainSlug, switchboardFees[dstChainSlug], digest, digestParams); + // Store sponsored fee information + sponsoredPayloadFees[payloadId] = SponsoredPayloadFees({ + maxFees: overrides.maxFees, + plug: plug_ + }); + + emit MessageOutbound( + payloadId, + overrides.dstChainSlug, + digest, + digestParams, + true, // isSponsored + 0, // nativeFees (not used in sponsored flow) + overrides.maxFees, + overrides.sponsor + ); + } else { + // Native token flow - validate fees and track for potential refund + if (msg.value < minMsgValueFees[overrides.dstChainSlug] + overrides.value) + revert InsufficientMsgValue(); + + payloadFees[payloadId] = PayloadFees({ + nativeFees: msg.value, + refundAddress: overrides.refundAddress, + isRefundEligible: false, + isRefunded: false, + plug: plug_ + }); + + emit MessageOutbound( + payloadId, + overrides.dstChainSlug, + digest, + digestParams, + false, // isSponsored + msg.value, // nativeFees + 0, // maxFees (not used in native flow) + address(0) // No sponsor for native flow + ); + } + + emit PayloadRequested(payloadId, plug_, switchboardId, overrides_, payload_); + } + + /** + * @notice Decodes override parameters based on version + * @param overrides_ The encoded override parameters + * @return Decoded MessageOverrides struct + * @dev Version 1: Native token flow (with refundAddress) + * @dev Version 2: Sponsored flow (with sponsor and maxFees) + */ + function _decodeOverrides( + bytes calldata overrides_ + ) internal view returns (MessageOverrides memory) { + uint8 version = abi.decode(overrides_, (uint8)); + + if (version == 1) { + // Version 1: Native token flow + ( + , + uint32 dstChainSlug, + uint256 gasLimit, + uint256 value, + address refundAddress, + uint256 deadline + ) = abi.decode(overrides_, (uint8, uint32, uint256, uint256, address, uint256)); + + return + MessageOverrides({ + isSponsored: false, + dstChainSlug: dstChainSlug, + gasLimit: gasLimit, + deadline: deadline, + refundAddress: refundAddress, + sponsor: address(0), + value: value, + maxFees: 0 + }); + } else if (version == 2) { + // Version 2: Sponsored flow + ( + , + uint32 dstChainSlug, + uint256 gasLimit, + uint256 value, + uint256 maxFees, + address sponsor, + uint256 deadline + ) = abi.decode( + overrides_, + (uint8, uint32, uint256, uint256, uint256, address, uint256) + ); + + return + MessageOverrides({ + isSponsored: true, + dstChainSlug: dstChainSlug, + gasLimit: gasLimit, + deadline: deadline, + refundAddress: address(0), + sponsor: sponsor, + value: value, + maxFees: maxFees + }); + } else { + revert UnsupportedOverrideVersion(); + } } function _validateSibling(uint32 dstChainSlug_, address plug_) internal view { @@ -146,112 +310,368 @@ contract MessageSwitchboard is SwitchboardBase { bytes32 dstPlug = siblingPlugs[dstChainSlug_][plug_]; if (dstSocket == bytes32(0) || dstSwitchboard == bytes32(0) || dstPlug == bytes32(0)) { - revert SiblingNotFound(); + revert SiblingSocketNotFound(); } } function _createDigestAndPayloadId( - uint32 dstChainSlug_, address plug_, - uint256 gasLimit_, - uint256 value_, - bytes32 triggerId_, + MessageOverrides memory overrides_, bytes calldata payload_ - ) internal returns (DigestParams memory digestParams, bytes32 digest) { - uint160 payloadPointer = (uint160(chainSlug) << 120) | - (uint160(uint64(uint256(triggerId_))) << 80) | - payloadCounter++; + ) internal returns (DigestParams memory digestParams, bytes32 digest, bytes32 payloadId) { + uint32 dstSwitchboardId = siblingSwitchboardIds[overrides_.dstChainSlug]; + if (dstSwitchboardId == 0) revert SiblingSocketNotFound(); - bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, dstChainSlug_); + // Message payload: sibling = (srcChainSlug, srcSwitchboardId), verification = (dstChainSlug, dstSwitchboardId) + payloadId = createPayloadId( + chainSlug, // sibling chain slug (sibling) + switchboardId, // sibling id (sibling switchboard) + overrides_.dstChainSlug, // verification chain slug (destination) + dstSwitchboardId, // verification id (destination switchboard) + payloadCounter++ // pointer (counter) + ); digestParams = DigestParams({ - socket: siblingSockets[dstChainSlug_], + socket: siblingSockets[overrides_.dstChainSlug], transmitter: bytes32(0), payloadId: payloadId, - deadline: block.timestamp + 3600, + deadline: overrides_.deadline, callType: WRITE, - gasLimit: gasLimit_, - value: value_, + gasLimit: overrides_.gasLimit, + value: overrides_.value, payload: payload_, - target: siblingPlugs[dstChainSlug_][plug_], - appGatewayId: APP_GATEWAY_ID, - prevBatchDigestHash: triggerId_, - extraData: abi.encode(chainSlug, toBytes32Format(plug_)) + target: siblingPlugs[overrides_.dstChainSlug][plug_], + source: abi.encodePacked(chainSlug, toBytes32Format(plug_)), + prevBatchDigestHash: bytes32(0), + extraData: bytes("") }); - digest = _createDigest(digestParams); + digest = createDigest(digestParams); } /** - * @dev Function to attest a payload with enhanced verification - * @param digest_ Full un-hashed digest parameters - * @param proof_ proof from watcher - * @notice Enhanced attestation that verifies target with srcChainSlug and srcPlug + * @dev Increase fees for a pending payload + * @param payloadId_ Payload ID to increase fees for + * @param plug_ The address of the plug + * @param feesData_ Encoded fees data (type + data) */ - function attest(DigestParams calldata digest_, bytes calldata proof_) public { - (uint32 srcChainSlug, bytes32 srcPlug) = abi.decode(digest_.extraData, (uint32, bytes32)); - if (siblingPlugs[srcChainSlug][address(uint160(uint256(digest_.target)))] != srcPlug) { - revert InvalidTargetVerification(); + function increaseFeesForPayload( + bytes32 payloadId_, + address plug_, + bytes calldata feesData_ + ) external payable override onlySocket { + // Decode the fees type from feesData + uint8 feesType = abi.decode(feesData_, (uint8)); + + if (feesType == 1) { + _increaseNativeFees(payloadId_, plug_, feesData_); + } else if (feesType == 2) { + _increaseSponsoredFees(payloadId_, plug_, feesData_); + } else { + revert InvalidFeesType(); } - bytes32 digest = _createDigest(digest_); - address watcher = _recoverSigner( - keccak256(abi.encodePacked(toBytes32Format(address(this)), chainSlug, digest)), - proof_ + } + + /** + * @dev Internal function to increase native fees + */ + function _increaseNativeFees( + bytes32 payloadId_, + address plug_, + bytes calldata feesData_ + ) internal { + PayloadFees storage fees = payloadFees[payloadId_]; + + // Validation: Only the plug that created this payload can increase fees + if (fees.plug != plug_) revert UnauthorizedFeeIncrease(); + + // Update native fees if msg.value is provided + if (msg.value > 0) { + fees.nativeFees += msg.value; + } + + emit NativeFeesIncreased(payloadId_, msg.value, feesData_); + } + + /** + * @dev Internal function to increase sponsored fees + */ + function _increaseSponsoredFees( + bytes32 payloadId_, + address plug_, + bytes calldata feesData_ + ) internal { + SponsoredPayloadFees storage fees = sponsoredPayloadFees[payloadId_]; + + // Validation: Only the plug that created this payload can increase fees + if (fees.plug != plug_) revert UnauthorizedFeeIncrease(); + + // Decode new maxFees (skip first byte which is feesType) + (, uint256 newMaxFees) = abi.decode(feesData_, (uint8, uint256)); + fees.maxFees = newMaxFees; + + emit SponsoredFeesIncreased(payloadId_, newMaxFees, plug_); + } + + /** + * @notice Decodes packed sibling bytes to extract chain slug and plug address + * @param packed The packed bytes from abi.encodePacked(chainSlug, bytes32Plug) + * @return siblingChainSlug The decoded chain slug (uint32) + * @return siblingPlug The decoded plug address in bytes32 format + * @dev not using abi.encode/decode as we want solana compatibility. + */ + function _decodePackedSource( + bytes memory packed + ) internal pure returns (uint32 siblingChainSlug, bytes32 siblingPlug) { + require(packed.length == 36, "Invalid packed length"); + + assembly { + // Read first 32 bytes of data (contains uint32 in rightmost 4 bytes) + let firstWord := mload(add(packed, 32)) + // Extract uint32 from rightmost 4 bytes (shift right by 224 bits = 28 bytes) + siblingChainSlug := shr(224, firstWord) + + // Read next 32 bytes starting at offset 36 (skip 4 bytes for uint32) + siblingPlug := mload(add(packed, 36)) + } + } + + /** + * @dev Mark a payload as eligible for refund (called with watcher signature) + * @param payloadId_ Payload ID to mark as refund eligible + * @param nonce_ Nonce to prevent replay attacks + * @param signature_ Watcher signature + */ + function markRefundEligible( + bytes32 payloadId_, + uint256 nonce_, + bytes calldata signature_ + ) external { + PayloadFees storage fees = payloadFees[payloadId_]; + if (fees.isRefundEligible) revert AlreadyMarkedRefundEligible(); + if (fees.nativeFees == 0) revert NoFeesToRefund(); + bytes32 digest = keccak256( + abi.encodePacked(toBytes32Format(address(this)), chainSlug, payloadId_, nonce_) ); - if (!_hasRole(WATCHER_ROLE, watcher)) revert WatcherNotFound(); + address watcher = _recoverSigner(digest, signature_); + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, chainSlug)); + if (!_hasRole(role, watcher)) revert WatcherNotFound(); - if (isAttested[digest]) revert AlreadyAttested(); - isAttested[digest] = true; + _validateAndUseNonce(this.markRefundEligible.selector, watcher, nonce_); + + fees.isRefundEligible = true; + emit RefundEligibilityMarked(payloadId_, watcher); + } + + /** + * @dev Claim refund for a payload + * @param payloadId_ Payload ID to refund + */ + function refund(bytes32 payloadId_) external nonReentrant { + PayloadFees storage fees = payloadFees[payloadId_]; + if (!fees.isRefundEligible) revert RefundNotEligible(); + if (fees.isRefunded) revert AlreadyRefunded(); - emit Attested(digest_.payloadId, digest, watcher); + uint256 feesToRefund = fees.nativeFees; + fees.isRefunded = true; + fees.nativeFees = 0; + + emit Refunded(payloadId_, fees.refundAddress, feesToRefund); + SafeTransferLib.safeTransferETH(fees.refundAddress, feesToRefund); + } + + /** + * @dev Approve multiple plugs at once + * @param plugs_ Array of plug addresses to approve + */ + function approvePlugs(address[] calldata plugs_) external { + for (uint256 i = 0; i < plugs_.length; i++) { + sponsorApprovals[msg.sender][plugs_[i]] = true; + emit PlugApproved(msg.sender, plugs_[i]); + } + } + + /** + * @dev Revoke multiple plug approvals at once + * @param plugs_ Array of plug addresses to revoke + */ + function revokePlugs(address[] calldata plugs_) external { + for (uint256 i = 0; i < plugs_.length; i++) { + sponsorApprovals[msg.sender][plugs_[i]] = false; + emit PlugRevoked(msg.sender, plugs_[i]); + } + } + + /** + * @dev Batch update minimum fees using oracle signature + * @param siblingChainSlugs_ Array of sibling chain slugs + * @param minFees_ Array of minimum fees + * @param nonce_ Nonce to prevent replay attacks + * @param signature_ Signature from authorized fee updater + * @dev Uses length prefixes for array fields to prevent collision attacks + */ + function setMinMsgValueFeesBatch( + uint32[] calldata siblingChainSlugs_, + uint256[] calldata minFees_, + uint256 nonce_, + bytes calldata signature_ + ) external { + if (siblingChainSlugs_.length != minFees_.length) revert ArrayLengthMismatch(); + + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(this)), + chainSlug, + uint32(siblingChainSlugs_.length), // Length prefix for array + siblingChainSlugs_, + uint32(minFees_.length), // Length prefix for array + minFees_, + nonce_ + ) + ); + + address feeUpdater = _recoverSigner(digest, signature_); + if (!_hasRole(FEE_UPDATER_ROLE, feeUpdater)) revert UnauthorizedFeeUpdater(); + + _validateAndUseNonce(this.setMinMsgValueFeesBatch.selector, feeUpdater, nonce_); + + for (uint256 i = 0; i < siblingChainSlugs_.length; i++) { + minMsgValueFees[siblingChainSlugs_[i]] = minFees_[i]; + emit MinMsgValueFeesSet(siblingChainSlugs_[i], minFees_[i], feeUpdater); + } + } + + /** + * @dev Function to register sibling addresses for a chain (admin only) + * @param chainSlug_ Chain slug of the sibling chain + * @param socket_ Sibling socket address + * @param switchboard_ Sibling switchboard address + */ + function setSiblingConfig( + uint32 chainSlug_, + bytes32 socket_, + bytes32 switchboard_, + uint32 switchboardId_ + ) external onlyOwner { + siblingSockets[chainSlug_] = socket_; + siblingSwitchboards[chainSlug_] = switchboard_; + siblingSwitchboardIds[chainSlug_] = switchboardId_; + + emit SiblingConfigSet(chainSlug_, socket_, switchboard_); + } + + function setRevertingPayload( + bytes32 payloadId_, + bool isReverting_, + uint256 nonce_, + bytes calldata signature_ + ) external { + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(this)), + chainSlug, + payloadId_, + isReverting_, + nonce_ + ) + ); + + address watcher = _recoverSigner(digest, signature_); + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, chainSlug)); + if (!_hasRole(role, watcher)) revert WatcherNotFound(); + _validateAndUseNonce(this.setRevertingPayload.selector, watcher, nonce_); + + revertingPayloadIds[payloadId_] = isReverting_; + emit RevertingPayloadIdset(payloadId_, isReverting_); + } + + /** + * @notice Updates plug configuration + * @param plugConfig_ The configuration data for the plug + */ + function updatePlugConfig( + address plug_, + bytes memory plugConfig_ + ) external override onlySocket { + (uint32 siblingChainSlug, bytes32 siblingPlug) = abi.decode(plugConfig_, (uint32, bytes32)); + if ( + siblingSockets[siblingChainSlug] == bytes32(0) || + siblingSwitchboards[siblingChainSlug] == bytes32(0) + ) { + revert SiblingSocketNotFound(); + } + + siblingPlugs[siblingChainSlug][plug_] = siblingPlug; + emit PlugConfigUpdated(plug_, siblingChainSlug, siblingPlug); + } + + /** + * @notice Sets the default deadline for payload execution + * @param defaultDeadline_ The new default deadline in seconds + * @dev Only callable by owner. Used when overrides don't specify a deadline. + */ + function setDefaultDeadline(uint256 defaultDeadline_) external onlyOwner { + defaultDeadline = defaultDeadline_; + emit DefaultDeadlineSet(defaultDeadline_); } /** * @inheritdoc ISwitchboard */ - function allowPayload(bytes32 digest_, bytes32) external view override returns (bool) { - // digest has enough attestations - return isAttested[digest_]; + function getPlugConfig( + address plug_, + bytes memory extraData_ + ) external view override returns (bytes memory plugConfig_) { + uint32 chainSlug_ = abi.decode(extraData_, (uint32)); + plugConfig_ = abi.encode(siblingPlugs[chainSlug_][plug_]); + } + + /** + * @notice adds a watcher + * @param watcher_ watcher address + */ + function grantWatcherRole( + uint32 siblingChainSlug_, + address watcher_ + ) external onlyRole(GOVERNANCE_ROLE) { + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, siblingChainSlug_)); + if (_hasRole(role, watcher_)) revert WatcherFound(); + _grantRole(role, watcher_); + + ++totalWatchers[siblingChainSlug_]; } /** - * @dev Function to set switchboard fees for a specific chain (admin only) - * @param chainSlug_ Chain slug for which to set the fee - * @param feeAmount_ Fee amount in wei + * @notice removes a watcher + * @param watcher_ watcher address */ - function setSwitchboardFees(uint32 chainSlug_, uint256 feeAmount_) external onlyOwner { - switchboardFees[chainSlug_] = feeAmount_; - emit SwitchboardFeesSet(chainSlug_, feeAmount_); + function revokeWatcherRole( + uint32 siblingChainSlug_, + address watcher_ + ) external onlyRole(GOVERNANCE_ROLE) { + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, siblingChainSlug_)); + if (!_hasRole(role, watcher_)) revert WatcherNotFound(); + _revokeRole(role, watcher_); + + --totalWatchers[siblingChainSlug_]; } /** - * @dev Function to get switchboard fees for a specific chain - * @param chainSlug_ Chain slug for which to get the fee - * @return feeAmount Fee amount in wei + * @dev Overriding this function from AccessControl to make sure owner can't grant Watcher Role directly, and should + * only use grantWatcherRole function instead. This is to make sure watcher count remains correct */ - function getSwitchboardFees(uint32 chainSlug_) external view returns (uint256 feeAmount) { - return switchboardFees[chainSlug_]; + function grantRole(bytes32 role_, address grantee_) external override onlyOwner { + if (role_ != GOVERNANCE_ROLE && role_ != RESCUE_ROLE && role_ != FEE_UPDATER_ROLE) + revert InvalidRole(); + _grantRole(role_, grantee_); } /** - * @dev Internal function to create digest from parameters + * @dev Overriding this function from AccessControl to make sure owner can't revoke Watcher Role directly, and should + * only use revokeWatcherRole function instead. This is to make sure watcher count remains correct */ - function _createDigest(DigestParams memory digest_) internal pure returns (bytes32) { - return - keccak256( - abi.encodePacked( - digest_.socket, - digest_.transmitter, - digest_.payloadId, - digest_.deadline, - digest_.callType, - digest_.gasLimit, - digest_.value, - digest_.payload, - digest_.target, - digest_.appGatewayId, - digest_.prevBatchDigestHash, - digest_.extraData - ) - ); + function revokeRole(bytes32 role_, address grantee_) external override onlyOwner { + if (role_ != GOVERNANCE_ROLE && role_ != RESCUE_ROLE && role_ != FEE_UPDATER_ROLE) + revert InvalidRole(); + _revokeRole(role_, grantee_); } } diff --git a/contracts/protocol/switchboard/SwitchboardBase.sol b/contracts/protocol/switchboard/SwitchboardBase.sol index 43d5cf52..81eaa1d3 100644 --- a/contracts/protocol/switchboard/SwitchboardBase.sol +++ b/contracts/protocol/switchboard/SwitchboardBase.sol @@ -1,50 +1,105 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {ECDSA} from "solady/utils/ECDSA.sol"; -import "../interfaces/ISwitchboard.sol"; import "../interfaces/ISocket.sol"; +import "../interfaces/ISwitchboard.sol"; import "../../utils/AccessControl.sol"; +import {RESCUE_ROLE, GOVERNANCE_ROLE, WATCHER_ROLE} from "../../utils/common/AccessRoles.sol"; +import {WRITE} from "../../utils/common/Constants.sol"; +import {toBytes32Format} from "../../utils/common/Converters.sol"; +import {createDigest} from "../../utils/common/DigestUtils.sol"; +import "../../utils/common/Errors.sol"; +import {createPayloadId} from "../../utils/common/IdUtils.sol"; + +import {DigestParams} from "../../utils/common/Structs.sol"; import "../../utils/RescueFundsLib.sol"; -import {RESCUE_ROLE} from "../../utils/common/AccessRoles.sol"; /// @title SwitchboardBase -/// @notice Base contract for switchboards, contains common and util functions for all switchboards +/// @notice Base contract for switchboard implementations +/// @dev Provides common functionality for all switchboards including registration, transmitter recovery, and rescue functions abstract contract SwitchboardBase is ISwitchboard, AccessControl { - // socket contract + // --- State Variables --- + + /// @notice Immutable reference to the socket contract ISocket public immutable socket__; - // chain slug of deployed chain + /// @notice Chain slug of the chain where this switchboard is deployed uint32 public immutable chainSlug; - // switchboard id - uint64 public switchboardId; + /// @notice The switchboard ID assigned by socket (0 until registered) + uint32 public immutable switchboardId; + + /// @notice Counter for generating unique payload IDs + /// @dev If we deploy a new set of Socket contracts, we need to start counter from last value to avoid + /// replay attacks. + uint64 public payloadCounter; + + /// @notice Default deadline for payload execution (1 day) + uint256 public defaultDeadline = 1 days; + + /// @notice Mapping of payload ID to revert status (used by plugs to mark payloads as reverting) + mapping(bytes32 => bool) public revertingPayloadIds; + + /// @notice Mapping of payload id to its digest for verification + mapping(bytes32 => bytes32) public payloadIdToDigest; + + /// @notice Mapping of fee updater address to nonce to usage status (prevents replay attacks) + mapping(address => mapping(uint256 => bool)) public usedNonces; + + // --- Events --- + + /// @notice Event emitted when watcher attests a payload + event Attested(bytes32 indexed digest, address indexed watcher); + + /// @notice Event emitted when reverting payload is set + event RevertingPayloadIdset(bytes32 payloadId, bool isReverting); + + /// @notice Event emitted when default deadline is set + event DefaultDeadlineSet(uint256 defaultDeadline); + + // --- Modifiers --- + + /// @notice Modifier to restrict function calls to socket contract only + modifier onlySocket() { + if (msg.sender != address(socket__)) revert NotSocket(); + _; + } + + // --- Constructor --- - /** - * @dev Constructor of SwitchboardBase - * @param chainSlug_ Chain slug of deployment chain - * @param socket_ socket_ contract - */ constructor(uint32 chainSlug_, ISocket socket_, address owner_) { + if (chainSlug_ == 0) revert InvalidChainSlug(); + if (address(socket_) == address(0)) revert InvalidSocket(); + if (owner_ == address(0)) revert InvalidOwner(); + + _initializeOwner(owner_); chainSlug = chainSlug_; socket__ = socket_; - _initializeOwner(owner_); + switchboardId = socket__.registerSwitchboard(); } + // --- External Functions --- + /** - * @notice Registers a switchboard on the socket - * @dev This function is called by the owner of the switchboard + * @dev Internal function to validate and mark nonce as used with namespace isolation + * @param selector_ The function selector to isolate nonce usage by function type + * @param signer_ The address of the signer + * @param nonce_ The nonce to validate and mark as used */ - function registerSwitchboard() external onlyOwner { - switchboardId = socket__.registerSwitchboard(); + function _validateAndUseNonce(bytes4 selector_, address signer_, uint256 nonce_) internal { + uint256 namespacedNonce = uint256(keccak256(abi.encodePacked(selector_, nonce_))); + if (usedNonces[signer_][namespacedNonce]) revert NonceAlreadyUsed(); + usedNonces[signer_][namespacedNonce] = true; } /** - * @notice Returns the transmitter for a given payload - * @dev If the transmitter signature is provided, the function will return the signer of the signature - * @param payloadId_ The payload id - * @param transmitterSignature_ The transmitter signature (optional) - * @return transmitter The transmitter address + * @notice Returns the transmitter address for a given payload + * @param payloadId_ The payload ID + * @param transmitterSignature_ The transmitter signature (optional, empty bytes if not provided) + * @return transmitter The transmitter address (address(0) if no signature provided) + * @dev If signature is provided, recovers signer from signature. Otherwise returns address(0). + * Recovered signer should be validated for valid roles by caller. */ function getTransmitter( address, @@ -53,35 +108,36 @@ abstract contract SwitchboardBase is ISwitchboard, AccessControl { ) external view returns (address transmitter) { transmitter = transmitterSignature_.length > 0 ? _recoverSigner( - keccak256(abi.encode(address(socket__), payloadId_)), + keccak256(abi.encodePacked(address(socket__), payloadId_)), transmitterSignature_ ) : address(0); } - /// @notice Recovers the signer from the signature - /// @param digest_ The digest of the payload - /// @param signature_ The signature of the watcher + // --- Internal Functions --- + + /// @notice Recovers the signer address from a signature + /// @param digest_ The message digest that was signed + /// @param signature_ The signature bytes /// @return signer The address of the signer + /// @dev Uses Ethereum signed message format (\x19Ethereum Signed Message:\n32). + /// Recovered signer should be validated for valid roles by caller. function _recoverSigner( bytes32 digest_, bytes memory signature_ ) internal view returns (address signer) { bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); - // recovered signer is checked for the valid roles later signer = ECDSA.recover(digest, signature_); } - ////////////////////////////////////////////// - //////////// Rescue role actions //////////// - ///////////////////////////////////////////// + // --- Rescue Functions --- /** - * @notice Rescues funds from the contract if they are locked by mistake. This contract does not - * theoretically need this function but it is added for safety. - * @param token_ The address of the token contract. - * @param rescueTo_ The address where rescued tokens need to be sent. - * @param amount_ The amount of tokens to be rescued. + * @notice Rescues stuck funds from the contract + * @param token_ The address of the token contract (address(0) for native tokens) + * @param rescueTo_ The address where rescued tokens need to be sent + * @param amount_ The amount of tokens to rescue (0 for all) + * @dev Only callable by RESCUE_ROLE. Safety mechanism for recovering stuck funds. */ function rescueFunds( address token_, diff --git a/contracts/utils/AccessControl.sol b/contracts/utils/AccessControl.sol index 29225404..870fb575 100644 --- a/contracts/utils/AccessControl.sol +++ b/contracts/utils/AccessControl.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "solady/auth/Ownable.sol"; diff --git a/contracts/utils/OverrideParamsLib.sol b/contracts/utils/OverrideParamsLib.sol new file mode 100644 index 00000000..bee7f753 --- /dev/null +++ b/contracts/utils/OverrideParamsLib.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "../utils/common/Structs.sol"; +import "../utils/common/Constants.sol"; + +/// @title OverrideParamsLib +/// @notice Library providing fluent builder pattern methods for OverrideParams +library OverrideParamsLib { + /// @notice Clears the OverrideParams with default values + /// @return A new OverrideParams instance + function clear(bytes32 switchboardType_) internal pure returns (OverrideParams memory) { + return + OverrideParams({ + callType: WRITE, + isParallelCall: false, + writeFinality: WriteFinality.LOW, + gasLimit: 0, + value: 0, + readAtBlockNumber: 0, + delayInSeconds: 0, + maxFees: 0, + consumeFrom: address(0), + switchboardType: switchboardType_ + }); + } + + /// @notice Sets the read call type + /// @param self The OverrideParams instance + /// @param isReadCall_ The read call flag + /// @return The OverrideParams instance for chaining + function setRead( + OverrideParams memory self, + bool isReadCall_ + ) internal pure returns (OverrideParams memory) { + self.callType = isReadCall_ ? READ : WRITE; + return self; + } + + /// @notice Sets the parallel call flag + /// @param self The OverrideParams instance + /// @param isParallel_ The parallel call flag + /// @return The OverrideParams instance for chaining + function setParallel( + OverrideParams memory self, + bool isParallel_ + ) internal pure returns (OverrideParams memory) { + self.isParallelCall = isParallel_; + return self; + } + + /// @notice Sets the write finality + /// @param self The OverrideParams instance + /// @param finality_ The write finality + /// @return The OverrideParams instance for chaining + function setWriteFinality( + OverrideParams memory self, + WriteFinality finality_ + ) internal pure returns (OverrideParams memory) { + self.writeFinality = finality_; + return self; + } + + /// @notice Sets the gas limit + /// @param self The OverrideParams instance + /// @param gasLimit_ The gas limit + /// @return The OverrideParams instance for chaining + function setGasLimit( + OverrideParams memory self, + uint256 gasLimit_ + ) internal pure returns (OverrideParams memory) { + self.gasLimit = gasLimit_; + return self; + } + + /// @notice Sets the value + /// @param self The OverrideParams instance + /// @param value_ The value + /// @return The OverrideParams instance for chaining + function setValue( + OverrideParams memory self, + uint256 value_ + ) internal pure returns (OverrideParams memory) { + self.value = value_; + return self; + } + + /// @notice Sets the read at block number + /// @param self The OverrideParams instance + /// @param blockNumber_ The block number + /// @return The OverrideParams instance for chaining + function setReadAtBlock( + OverrideParams memory self, + uint64 blockNumber_ + ) internal pure returns (OverrideParams memory) { + self.readAtBlockNumber = blockNumber_; + return self; + } + + /// @notice Sets the delay in seconds + /// @param self The OverrideParams instance + /// @param delayInSeconds_ The delay in seconds + /// @return The OverrideParams instance for chaining + function setDelay( + OverrideParams memory self, + uint256 delayInSeconds_ + ) internal pure returns (OverrideParams memory) { + self.delayInSeconds = delayInSeconds_; + return self; + } + + /// @notice Sets the max fees + /// @param self The OverrideParams instance + /// @param fees_ The max fees + /// @return The OverrideParams instance for chaining + function setMaxFees( + OverrideParams memory self, + uint256 fees_ + ) internal pure returns (OverrideParams memory) { + self.maxFees = fees_; + return self; + } + + /// @notice Sets the consume from address + /// @param self The OverrideParams instance + /// @param consumeFrom_ The consume from address + /// @return The OverrideParams instance for chaining + function setConsumeFrom( + OverrideParams memory self, + address consumeFrom_ + ) internal pure returns (OverrideParams memory) { + self.consumeFrom = consumeFrom_; + return self; + } + + /// @notice Sets the switchboard type + /// @param self The OverrideParams instance + /// @param switchboardType_ The switchboard type + /// @return The OverrideParams instance for chaining + function setSwitchboardType( + OverrideParams memory self, + bytes32 switchboardType_ + ) internal pure returns (OverrideParams memory) { + self.switchboardType = switchboardType_; + return self; + } +} diff --git a/contracts/utils/Pausable.sol b/contracts/utils/Pausable.sol new file mode 100644 index 00000000..76fe03fd --- /dev/null +++ b/contracts/utils/Pausable.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +/** + * @title Pausable + * @dev Base contract that provides pausable functionality + * @notice This contract can be inherited to add pause/unpause capabilities + * @dev Uses a dedicated storage slot to avoid storage collisions + */ +abstract contract Pausable { + /// @notice Storage slot for pausable state + bytes32 private constant STORAGE_SLOT = keccak256("socket.storage.Pausable"); + + /// @notice Thrown when the contract is paused + error ContractPaused(); + + /// @notice Event emitted when contract is paused + event Paused(); + + /// @notice Event emitted when contract is unpaused + event Unpaused(); + + /// @notice Returns the paused state of the contract + function paused() public view returns (bool) { + bytes32 slot = STORAGE_SLOT; + bool result; + assembly { + result := sload(slot) + } + return result; + } + + /// @notice Modifier to check if contract is not paused + modifier whenNotPaused() { + if (paused()) revert ContractPaused(); + _; + } + + /// @notice Internal function to pause the contract + function _pause() internal { + bytes32 slot = STORAGE_SLOT; + bool current; + assembly { + current := sload(slot) + } + if (current) return; + assembly { + sstore(slot, 1) + } + emit Paused(); + } + + /// @notice Internal function to unpause the contract + function _unpause() internal { + bytes32 slot = STORAGE_SLOT; + bool current; + assembly { + current := sload(slot) + } + if (!current) return; + assembly { + sstore(slot, 0) + } + emit Unpaused(); + } +} diff --git a/contracts/utils/RescueFundsLib.sol b/contracts/utils/RescueFundsLib.sol index d2921df9..a1abc990 100644 --- a/contracts/utils/RescueFundsLib.sol +++ b/contracts/utils/RescueFundsLib.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "solady/utils/SafeTransferLib.sol"; import {ZeroAddress, InvalidTokenAddress} from "./common/Errors.sol"; @@ -33,7 +33,7 @@ library RescueFundsLib { if (rescueTo_ == address(0)) revert ZeroAddress(); if (token_ == ETH_ADDRESS) { - SafeTransferLib.forceSafeTransferETH(rescueTo_, amount_); + SafeTransferLib.safeTransferETH(rescueTo_, amount_); } else { if (token_.code.length == 0) revert InvalidTokenAddress(); diff --git a/contracts/utils/common/AccessRoles.sol b/contracts/utils/common/AccessRoles.sol index 1ca5ab61..5dbd022f 100644 --- a/contracts/utils/common/AccessRoles.sol +++ b/contracts/utils/common/AccessRoles.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; // contains role hashes used in socket for various different operation // used to rescue funds @@ -12,5 +12,11 @@ bytes32 constant TRANSMITTER_ROLE = keccak256("TRANSMITTER_ROLE"); bytes32 constant WATCHER_ROLE = keccak256("WATCHER_ROLE"); // used to disable switchboard bytes32 constant SWITCHBOARD_DISABLER_ROLE = keccak256("SWITCHBOARD_DISABLER_ROLE"); -// used by fees manager to withdraw native tokens -bytes32 constant FEE_MANAGER_ROLE = keccak256("FEE_MANAGER_ROLE"); +// used by gas manager to withdraw native tokens +bytes32 constant GAS_MANAGER_ROLE = keccak256("GAS_MANAGER_ROLE"); +// used by oracle to update minimum message value fees +bytes32 constant FEE_UPDATER_ROLE = keccak256("FEE_UPDATER_ROLE"); + +bytes32 constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + +bytes32 constant SOCKET_ROLE = keccak256("SOCKET_ROLE"); diff --git a/contracts/utils/common/Constants.sol b/contracts/utils/common/Constants.sol index 9b1ee6f5..747def8b 100644 --- a/contracts/utils/common/Constants.sol +++ b/contracts/utils/common/Constants.sol @@ -1,25 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; address constant ETH_ADDRESS = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); -bytes32 constant FORWARD_CALL = keccak256("FORWARD_CALL"); -bytes32 constant DISTRIBUTE_FEE = keccak256("DISTRIBUTE_FEE"); -bytes32 constant DEPLOY = keccak256("DEPLOY"); - bytes4 constant READ = bytes4(keccak256("READ")); bytes4 constant WRITE = bytes4(keccak256("WRITE")); 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 +uint256 constant GAS_LIMIT_BUFFER = 105; // 5% buffer uint32 constant CHAIN_SLUG_SOLANA_MAINNET = 10000001; uint32 constant CHAIN_SLUG_SOLANA_DEVNET = 10000002; - -// Constant appGatewayId used on all chains -bytes32 constant APP_GATEWAY_ID = 0xdeadbeefcafebabe1234567890abcdef1234567890abcdef1234567890abcdef; diff --git a/contracts/utils/common/Converters.sol b/contracts/utils/common/Converters.sol index 4f290e4b..4596f2e5 100644 --- a/contracts/utils/common/Converters.sol +++ b/contracts/utils/common/Converters.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache 2 -pragma solidity ^0.8.21; +pragma solidity 0.8.28; error NotAnEvmAddress(bytes32 bytes32FormatAddress); @@ -13,3 +13,10 @@ function fromBytes32Format(bytes32 bytes32FormatAddress) pure returns (address) } return address(uint160(uint256(bytes32FormatAddress))); } + +// convert EVM uint256 18 decimals to Solana uint64 6 decimals +function convertToSolanaUint64(uint256 amount) pure returns (uint64) { + uint256 scaledAmount = amount / 10 ** 12; + require(scaledAmount <= type(uint64).max, "Amount exceeds uint64 max"); + return uint64(scaledAmount); +} diff --git a/contracts/utils/common/DigestUtils.sol b/contracts/utils/common/DigestUtils.sol new file mode 100644 index 00000000..3306df32 --- /dev/null +++ b/contracts/utils/common/DigestUtils.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import { DigestParams } from "./Structs.sol"; +import { toBytes32Format } from "./Converters.sol"; + +/// @notice Creates the digest for the payload execution +/// @param digestParams_ The digest parameters +/// @return The keccak256 hash of the encoded payload +function createDigest(DigestParams memory digestParams_) pure returns (bytes32) { + // Fixed-size fields + bytes memory encoded = abi.encodePacked( + digestParams_.deadline, + digestParams_.gasLimit, + digestParams_.callType, + digestParams_.socket, + digestParams_.value, + digestParams_.transmitter, + digestParams_.payloadId, + digestParams_.target, + digestParams_.prevBatchDigestHash + ); + + // Hash with variable-length fields (with length prefixes to prevent collisions) + return + keccak256( + abi.encodePacked( + encoded, + uint32(digestParams_.payload.length), + digestParams_.payload, + uint32(digestParams_.source.length), + digestParams_.source, + uint32(digestParams_.extraData.length), + digestParams_.extraData + ) + ); +} diff --git a/contracts/utils/common/Errors.sol b/contracts/utils/common/Errors.sol index 6b286d08..e9c9068a 100644 --- a/contracts/utils/common/Errors.sol +++ b/contracts/utils/common/Errors.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; error ZeroAddress(); error InvalidTransmitter(); @@ -9,7 +9,6 @@ error SocketAlreadyInitialized(); // Socket error NotSocket(); -error PlugNotFound(); // EVMx error ResolvingScheduleTooEarly(); @@ -36,19 +35,17 @@ error AuctionNotOpen(); error BidExceedsMaxFees(); /// @notice Error thrown if a lower bid already exists error LowerBidAlreadyExists(); -/// @notice Error thrown when request count mismatch -error RequestCountMismatch(); +/// @notice Error thrown when payload count mismatch +error PayloadCountMismatch(); error InvalidAmount(); -error InsufficientCreditsAvailable(); error InsufficientBalance(); /// @notice Error thrown when a caller is invalid error InvalidCaller(); /// @notice Error thrown when a gateway is invalid error InvalidGateway(); -/// @notice Error thrown when a request is already cancelled -error RequestAlreadyCancelled(); +/// @notice Error thrown when a payload is already cancelled error DeadlineNotPassedForOnChainRevert(); error InvalidBid(); @@ -58,12 +55,9 @@ error MaxMsgValueLimitExceeded(); error OnlyWatcherAllowed(); error InvalidPrecompileData(); error InvalidCallType(); -error NotRequestHandler(); error NotInvoker(); error NotPromiseResolver(); -error RequestPayloadCountLimitExceeded(); error InsufficientFees(); -error RequestAlreadySettled(); error NoWriteRequest(); error AlreadyAssigned(); @@ -75,6 +69,99 @@ error InvalidSignature(); error DeadlinePassed(); // Only Watcher can call functions -error OnlyRequestHandlerAllowed(); -error OnlyPromiseResolverAllowed(); error InvalidReceiver(); + +/// @notice Thrown when the caller is not off-chain +error OnlyOffChain(); + +/// @notice Thrown when the simulation fails +error SimulationFailed(); +/// @notice Thrown when the verification chain slug is invalid +error InvalidVerificationChainSlug(); +/// @notice Thrown when the verification switchboard id is invalid +error InvalidVerificationSwitchboardId(); + +/// @notice Thrown when verification fails +error VerificationFailed(); +/// @notice Thrown when less gas limit is provided for execution than expected +error LowGasLimit(); +/// @notice Thrown when the message value is insufficient +error InsufficientMsgValue(); +error InsufficientGasAvailable(); + +// Socket Protocol Errors +/// @notice Thrown when a payload has already been executed +error PayloadAlreadyExecuted(); + +/// @notice Thrown when attempting to register an already registered switchboard +error SwitchboardExists(); + +/// @notice Thrown when a plug is not connected to any switchboard +error PlugNotConnected(); + +/// @notice Thrown when a payload is already attested by watcher +error AlreadyAttested(); + +/// @notice Thrown when watcher is not valid +error WatcherNotFound(); + +/// @notice Thrown when source is invalid +error InvalidSource(); + +/// @notice Thrown when EVMX config not set +error EvmxConfigNotSet(); + +/// @notice Thrown when msg.value is not allowed +error MsgValueNotAllowed(); + +/// @notice Thrown when sibling not found +error SiblingSocketNotFound(); + +/// @notice Thrown when msg.value is not equal to minimum fees + value +error InvalidMsgValue(); + +/// @notice Thrown when fee updater is not authorized +error UnauthorizedFeeUpdater(); + +/// @notice Thrown when nonce is already used +error NonceAlreadyUsed(); + +/// @notice Thrown when array lengths mismatch +error ArrayLengthMismatch(); + +/// @notice Thrown when plug is not approved by sponsor +error PlugNotApprovedBySponsor(); + +/// @notice Thrown when refund is not eligible +error RefundNotEligible(); + +/// @notice Thrown when refund already issued +error AlreadyRefunded(); + +/// @notice Thrown when no fees to refund +error NoFeesToRefund(); + +/// @notice Thrown when override version is not supported +error UnsupportedOverrideVersion(); + +/// @notice Thrown when unauthorized fee increase attempt +error UnauthorizedFeeIncrease(); + +/// @notice Thrown when invalid fees type +error InvalidFeesType(); + +/// @notice Thrown when refund eligibility already marked +error AlreadyMarkedRefundEligible(); + +/// @notice Thrown when gas limit buffer is too low +error GasLimitBufferTooLow(); +/// @notice Thrown when owner is invalid +error InvalidOwner(); +/// @notice Thrown when socket is invalid +error InvalidSocket(); +/// @notice Thrown when switchboard id is invalid +error InvalidSwitchboardId(); +/// @notice Thrown when role is invalid +error InvalidRole(); +/// @notice Thrown when watcher is already found +error WatcherFound(); diff --git a/contracts/utils/common/IdUtils.sol b/contracts/utils/common/IdUtils.sol index 8482df03..62ef2252 100644 --- a/contracts/utils/common/IdUtils.sol +++ b/contracts/utils/common/IdUtils.sol @@ -1,20 +1,74 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -/// @notice Creates a payload ID from the given parameters -/// @param payloadPointer_ The payload pointer -/// @param switchboardId_ The switchboard id -/// @param chainSlug_ The chain slug +/// @notice Payload ID structure: +/// [Source: 64 bits][Verification: 64 bits][Pointer: 64 bits][Reserved: 64 bits] +/// Source = chainSlug (32 bits) | switchboardId/watcherId (32 bits) +/// Verification = chainSlug (32 bits) | switchboardId/watcherId (32 bits) +/// Pointer = counter (64 bits) +/// Reserved = 64 bits for future use + +/// @notice Creates a payload ID from source, verification, and pointer components +/// @param sourceChainSlug_ Chain slug for source (32 bits) +/// @param sourceId_ Switchboard ID or watcher ID for source (32 bits) +/// @param verificationChainSlug_ Chain slug for verification (32 bits) +/// @param verificationId_ Switchboard ID or watcher ID for verification (32 bits) +/// @param pointer_ Counter/pointer value (64 bits) /// @return The created payload ID function createPayloadId( - uint160 payloadPointer_, - uint64 switchboardId_, - uint32 chainSlug_ + uint32 sourceChainSlug_, + uint32 sourceId_, + uint32 verificationChainSlug_, + uint32 verificationId_, + uint64 pointer_ ) pure returns (bytes32) { - return - bytes32( - (uint256(chainSlug_) << 224) | - (uint256(switchboardId_) << 160) | - uint256(payloadPointer_) - ); + uint256 source = (uint256(sourceChainSlug_) << 32) | uint256(sourceId_); + uint256 verification = (uint256(verificationChainSlug_) << 32) | uint256(verificationId_); + return bytes32((source << 192) | (verification << 128) | (uint256(pointer_) << 64)); +} + +/// @notice Decodes payload ID into its components +/// @param payloadId_ The payload ID to decode +/// @return sourceChainSlug Chain slug for source +/// @return sourceId Switchboard ID or watcher ID for source +/// @return verificationChainSlug Chain slug for verification +/// @return verificationId Switchboard ID or watcher ID for verification +/// @return pointer Counter/pointer value +function decodePayloadId( + bytes32 payloadId_ +) + pure + returns ( + uint32 sourceChainSlug, + uint32 sourceId, + uint32 verificationChainSlug, + uint32 verificationId, + uint64 pointer + ) +{ + sourceChainSlug = uint32(uint256(payloadId_) >> 224); + sourceId = uint32(uint256(payloadId_) >> 192); + verificationChainSlug = uint32(uint256(payloadId_) >> 160); + verificationId = uint32(uint256(payloadId_) >> 128); + pointer = uint64(uint256(payloadId_) >> 64); +} + +/// @notice Gets verification chain slug and switchboard ID from payload ID +/// @param payloadId_ The payload ID to decode +/// @return chainSlug Verification chain slug +/// @return switchboardId Verification switchboard ID +function getVerificationInfo( + bytes32 payloadId_ +) pure returns (uint32 chainSlug, uint32 switchboardId) { + chainSlug = uint32(uint256(payloadId_) >> 160); + switchboardId = uint32(uint256(payloadId_) >> 128); +} + +/// @notice Gets source chain slug and switchboard ID from payload ID +/// @param payloadId_ The payload ID to decode +/// @return chainSlug Source chain slug +/// @return switchboardId Source switchboard ID or watcher ID +function getSourceInfo(bytes32 payloadId_) pure returns (uint32 chainSlug, uint32 switchboardId) { + chainSlug = uint32(uint256(payloadId_) >> 224); + switchboardId = uint32(uint256(payloadId_) >> 192); } diff --git a/contracts/utils/common/Structs.sol b/contracts/utils/common/Structs.sol index 2636db28..82d5971d 100644 --- a/contracts/utils/common/Structs.sol +++ b/contracts/utils/common/Structs.sol @@ -1,22 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; //// ENUMS //// -enum IsPlug { - YES, - NO -} - -enum Parallel { - OFF, - ON -} - -enum Read { - OFF, - ON -} - enum WriteFinality { LOW, MEDIUM, @@ -44,6 +29,19 @@ enum ExecutionStatus { Reverted } +enum EscrowState { + None, // No escrow + Active, // Escrowed, payload in progress + Released, // Returned to account + Settled // Paid to transmitter +} + +struct EscrowEntry { + address account; // Who's paying + uint256 amount; // How much is escrowed + EscrowState state; // Current state +} + struct AppGatewayApprovals { address appGateway; uint256 approval; @@ -58,13 +56,13 @@ struct AppGatewayConfig { // Plug config: struct PlugConfigGeneric { bytes32 appGatewayId; - uint64 switchboardId; + uint32 switchboardId; } // Plug config: struct PlugConfigEvm { bytes32 appGatewayId; - uint64 switchboardId; + uint32 switchboardId; } //trigger: @@ -82,15 +80,15 @@ struct PromiseReturnData { bytes32 payloadId; bytes returnData; } -// AM -struct ExecuteParams { +struct ExecutionParams { bytes4 callType; - uint160 payloadPointer; uint256 deadline; uint256 gasLimit; + address target; uint256 value; + bytes32 payloadId; bytes32 prevBatchDigestHash; - address target; + bytes source; bytes payload; bytes extraData; } @@ -109,49 +107,34 @@ struct WatcherMultiCallParams { bytes signature; } -struct CreateRequestResult { - uint256 totalEstimatedWatcherFees; - uint256 writeCount; - address[] promiseList; - PayloadParams[] payloadParams; -} - -struct Bid { - uint256 fee; - address transmitter; - bytes extraData; -} - -struct UserCredits { - uint256 totalCredits; - uint256 blockedCredits; -} - // digest: struct DigestParams { - bytes32 socket; - bytes32 transmitter; - bytes32 payloadId; uint256 deadline; - bytes4 callType; uint256 gasLimit; + bytes4 callType; + bytes32 socket; uint256 value; - bytes payload; + bytes32 transmitter; + bytes32 payloadId; bytes32 target; - bytes32 appGatewayId; bytes32 prevBatchDigestHash; + bytes payload; + bytes source; bytes extraData; } // App gateway base: struct OverrideParams { bytes4 callType; - Parallel isParallelCall; - WriteFinality writeFinality; + bool isParallelCall; uint256 gasLimit; - uint256 value; uint256 readAtBlockNumber; + WriteFinality writeFinality; + address consumeFrom; + bytes32 switchboardType; + uint256 value; uint256 delayInSeconds; + uint256 maxFees; } struct Transaction { @@ -160,50 +143,31 @@ struct Transaction { bytes payload; } -struct QueueParams { +struct RawPayload { OverrideParams overrideParams; Transaction transaction; address asyncPromise; - bytes32 switchboardType; } -struct PayloadParams { +struct Payload { bytes4 callType; - uint160 payloadPointer; + bool isPayloadCancelled; + bool isPayloadExecuted; + bool isTransmitterFeesSettled; + uint256 payloadPointer; address asyncPromise; address appGateway; + address consumeFrom; bytes32 payloadId; + uint256 watcherFees; + uint256 maxFees; uint256 resolvedAt; uint256 deadline; bytes precompileData; } -// request -struct RequestTrackingParams { - bool isRequestCancelled; - bool isRequestExecuted; - uint40 currentBatch; - uint256 currentBatchPayloadsLeft; - uint256 payloadsRemaining; -} - -struct RequestFeesDetails { - uint256 maxFees; - address consumeFrom; - Bid winningBid; -} - -struct RequestParams { - RequestTrackingParams requestTrackingParams; - RequestFeesDetails requestFeesDetails; - address appGateway; - address auctionManager; - uint256 writeCount; - bytes onCompleteData; -} - struct CCTPExecutionParams { - ExecuteParams executeParams; + ExecutionParams executionParams; bytes32 digest; bytes proof; bytes transmitterSignature; @@ -237,3 +201,73 @@ struct SolanaInstructionDataDescription { // names for function argument types used later in data decoding in watcher and transmitter string[] functionArgumentTypeNames; } + +/** Solana read payload - SolanaReadInstruction **/ + +enum SolanaReadSchemaType { + PREDEFINED, + GENERIC +} + +struct SolanaReadRequest { + bytes32 accountToRead; + SolanaReadSchemaType schemaType; + // keccak256("schema-name") + bytes32 predefinedSchemaNameHash; +} + +// this is only used after getting the data from Solana account +struct GenericSchema { + // list of types recognizable by BorshEncoder that we expect to read from Solana account (data model) + string[] valuesTypeNames; +} + +// payload fee tracking for refunds (native token flow only) +struct PayloadFees { + uint256 nativeFees; + address refundAddress; + bool isRefundEligible; + bool isRefunded; + address plug; +} + +// sponsored payload fee tracking +struct SponsoredPayloadFees { + uint256 maxFees; + address plug; +} + +/** + * @dev Internal struct for decoded overrides + */ +struct MessageOverrides { + bool isSponsored; + uint32 dstChainSlug; + uint256 gasLimit; + uint256 deadline; + address refundAddress; + address sponsor; + uint256 value; + uint256 maxFees; +} + +struct EVMxOverrides { + uint256 gasLimit; + uint256 deadline; + uint256 maxFees; +} + +/// @notice Parameters for simulating payload execution +struct SimulateParams { + address target; + uint256 value; + uint256 gasLimit; + bytes payload; +} + +/// @notice Result of a payload simulation +struct SimulationResult { + bool success; + bytes returnData; + bool exceededMaxCopy; +} diff --git a/contracts/evmx/AuctionManager.sol b/deprecated/AuctionManager.sol similarity index 98% rename from contracts/evmx/AuctionManager.sol rename to deprecated/AuctionManager.sol index 2694b821..839c3a39 100644 --- a/contracts/evmx/AuctionManager.sol +++ b/deprecated/AuctionManager.sol @@ -14,6 +14,12 @@ import {TRANSMITTER_ROLE} from "../utils/common/AccessRoles.sol"; import {AppGatewayBase} from "./base/AppGatewayBase.sol"; import "./interfaces/IERC20.sol"; +struct Bid { + uint256 fee; + address transmitter; + bytes extraData; +} + /// @title AuctionManagerStorage /// @notice Storage for the AuctionManager contract abstract contract AuctionManagerStorage is IAuctionManager { @@ -227,7 +233,7 @@ contract AuctionManager is AuctionManagerStorage, Initializable, AppGatewayBase, watcher__().requestHandler__().assignTransmitter( requestCount, - Bid({fee: 0, transmitter: address(0), extraData: ""}) + Bid({fee: 0, transmitter: address(0), extraData: bytes("")}) ); emit AuctionRestarted(requestCount); } diff --git a/contracts/protocol/switchboard/CCTPSwitchboard.sol b/deprecated/CCTPSwitchboard.sol similarity index 100% rename from contracts/protocol/switchboard/CCTPSwitchboard.sol rename to deprecated/CCTPSwitchboard.sol diff --git a/deprecated/Configurations.sol b/deprecated/Configurations.sol new file mode 100644 index 00000000..50bb0999 --- /dev/null +++ b/deprecated/Configurations.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "solady/utils/Initializable.sol"; +import "../interfaces/IConfigurations.sol"; +import {WatcherBase} from "./WatcherBase.sol"; +import {InvalidGateway, InvalidSwitchboard} from "../../utils/common/Errors.sol"; +import "solady/auth/Ownable.sol"; +import "../../utils/RescueFundsLib.sol"; +import {toBytes32Format} from "../../utils/common/Converters.sol"; +import {PlugConfigGeneric} from "../../utils/common/Structs.sol"; + +abstract contract ConfigurationsStorage is IConfigurations { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + // slot 50 + /// @notice Maps network and plug to their configuration + /// @dev chainSlug => plug => PlugConfig + mapping(uint32 => mapping(bytes32 => PlugConfigGeneric)) internal _plugConfigs; + + // slot 51 + /// @notice Maps chain slug to their associated switchboard + /// @dev chainSlug => sb type => switchboard id + mapping(uint32 => mapping(bytes32 => uint64)) public switchboards; + + // slot 52 + /// @notice Maps chain slug to their associated socket + /// @dev chainSlug => socket address + mapping(uint32 => bytes32) public sockets; + + // slot 53 + /// @notice Maps app gateway, chain slug, and plug to whether it is valid + /// @dev appGateway => chainSlug => plug => isValid + mapping(address => mapping(uint32 => mapping(bytes32 => bool))) public isValidPlug; + + // slots [54-103] reserved for gap + uint256[50] _gap_after; + + // 1 slot reserved for watcher base +} + +/// @title Configurations +/// @notice Configuration contract for the Watcher Precompile system +/// @dev Handles the mapping between networks, plugs, and app gateways for payload execution +contract Configurations is ConfigurationsStorage, Initializable, Ownable, WatcherBase { + /// @notice Emitted when a new plug is configured for an app gateway + /// @param appGatewayId The id of the app gateway + /// @param chainSlug The identifier of the destination network + /// @param plug The address of the plug + event PlugAdded(bytes32 appGatewayId, uint32 chainSlug, bytes32 plug); + + /// @notice Emitted when a switchboard is set for a network + /// @param chainSlug The identifier of the network + /// @param sbType The type of switchboard + /// @param switchboardId The id of the switchboard + event SwitchboardSet(uint32 chainSlug, bytes32 sbType, uint64 switchboardId); + + /// @notice Emitted when socket is set for a network + /// @param chainSlug The identifier of the network + /// @param socket The address of the socket + event SocketSet(uint32 chainSlug, bytes32 socket); + + /// @notice Emitted when a valid plug is set for an app gateway + /// @param appGateway The address of the app gateway + /// @param chainSlug The identifier of the network + /// @param plug The address of the plug + /// @param isValid Whether the plug is valid + event IsValidPlugSet(bool isValid, uint32 chainSlug, bytes32 plug, address appGateway); + + constructor() { + _disableInitializers(); // disable for implementation + } + + function initialize(address watcher_, address owner_) external reinitializer(1) { + _initializeOwner(owner_); + _initializeWatcher(watcher_); + } + + /// @notice Configures app gateways with their respective plugs and switchboards + /// @dev Only callable by the watcher + /// @dev This helps in verifying that plugs are called by respective app gateways + /// @param configs_ Array of configurations containing app gateway, network, plug, and switchboard details + function setAppGatewayConfigs(AppGatewayConfig[] calldata configs_) external onlyWatcher { + for (uint256 i = 0; i < configs_.length; i++) { + // Store the plug configuration for this network and plug + _plugConfigs[configs_[i].chainSlug][configs_[i].plug] = configs_[i].plugConfig; + + emit PlugAdded( + configs_[i].plugConfig.appGatewayId, + configs_[i].chainSlug, + configs_[i].plug + ); + } + } + + /// @notice Sets the socket for a network + /// @param chainSlug_ The identifier of the network + /// @param socket_ The address of the socket + function setSocket(uint32 chainSlug_, bytes32 socket_) external onlyOwner { + sockets[chainSlug_] = socket_; + emit SocketSet(chainSlug_, socket_); + } + + /// @notice Sets the switchboard for a network + /// @param chainSlug_ The identifier of the network + /// @param sbType_ The type of switchboard, hash of a string + /// @param switchboardId_ The id of the switchboard + function setSwitchboard( + uint32 chainSlug_, + bytes32 sbType_, + uint64 switchboardId_ + ) external onlyOwner { + switchboards[chainSlug_][sbType_] = switchboardId_; + emit SwitchboardSet(chainSlug_, sbType_, switchboardId_); + } + + /// @notice Sets the valid plugs for an app gateway + /// @dev Only callable by the app gateway + /// @dev This helps in verifying that app gateways are called by respective plugs + /// @param chainSlug_ The identifier of the network + /// @param plug_ The address of the plug + /// @param isValid_ Whether the plug is valid + function setIsValidPlug( + bool isValid_, + uint32 chainSlug_, + bytes32 plug_, + address appGateway_ + ) external onlyWatcher { + isValidPlug[appGateway_][chainSlug_][plug_] = isValid_; + emit IsValidPlugSet(isValid_, chainSlug_, plug_, appGateway_); + } + + /// @notice Retrieves the configuration for a specific plug on a network + /// @dev Returns zero addresses if configuration doesn't exist + /// @param chainSlug_ The identifier of the network + /// @param plug_ The address of the plug + /// @return The app gateway id and switchboard id for the plug + /// @dev Returns zero addresses if configuration doesn't exist + function getPlugConfigs( + uint32 chainSlug_, + bytes32 plug_ + ) public view returns (bytes32, uint64) { + return ( + _plugConfigs[chainSlug_][plug_].appGatewayId, + _plugConfigs[chainSlug_][plug_].switchboardId + ); + } + + /// @notice Verifies the connections between the target, app gateway, and switchboard + /// @dev Only callable by the watcher + /// @param chainSlug_ The identifier of the network + /// @param target_ The address of the target + /// @param appGateway_ The address of the app gateway + /// @param switchboardType_ The type of switchboard + function verifyConnections( + uint32 chainSlug_, + bytes32 target_, + address appGateway_, + bytes32 switchboardType_ + ) external view { + (bytes32 appGatewayId, uint64 switchboardId) = getPlugConfigs(chainSlug_, target_); + if (appGatewayId != toBytes32Format(appGateway_)) revert InvalidGateway(); + if (switchboardId != switchboards[chainSlug_][switchboardType_]) + revert InvalidSwitchboard(); + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } +} diff --git a/contracts/evmx/plugs/ContractFactoryPlug.sol b/deprecated/ContractFactoryPlug.sol similarity index 100% rename from contracts/evmx/plugs/ContractFactoryPlug.sol rename to deprecated/ContractFactoryPlug.sol diff --git a/contracts/evmx/helpers/DeployForwarder.sol b/deprecated/DeployForwarder.sol similarity index 100% rename from contracts/evmx/helpers/DeployForwarder.sol rename to deprecated/DeployForwarder.sol diff --git a/deprecated/ForwarderV2.sol b/deprecated/ForwarderV2.sol new file mode 100644 index 00000000..df652388 --- /dev/null +++ b/deprecated/ForwarderV2.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "solady/utils/Initializable.sol"; +import "./AddressResolverUtil.sol"; +import "../interfaces/IAppGateway.sol"; +import "../interfaces/IForwarder.sol"; +import {QueueParams, OverrideParams, Transaction} from "../../utils/common/Structs.sol"; +import {AsyncModifierNotSet, WatcherNotSet, InvalidOnChainAddress} from "../../utils/common/Errors.sol"; +import "../../utils/RescueFundsLib.sol"; + +/// @title Forwarder Storage +/// @notice Storage contract for the Forwarder contract that contains the state variables +abstract contract ForwarderStorage is IForwarder { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + // slot 50 + /// @notice chain slug on which the contract is deployed + uint32 public chainSlug; + + // slot 51 + /// @notice on-chain address associated with this forwarder + bytes32 public onChainAddress; + + // slots [52-100] reserved for gap + uint256[50] _gap_after; + + // slots [101-150] 50 slots reserved for address resolver util +} + +/// @title Forwarder Contract +/// @notice This contract acts as a forwarder for async calls to the on-chain contracts. +contract Forwarder is ForwarderStorage, Initializable, AddressResolverUtil { + constructor() { + _disableInitializers(); // disable for implementation + } + + /// @notice Initializer to replace constructor for upgradeable contracts + /// @param chainSlug_ chain slug on which the contract is deployed + /// @param onChainAddress_ on-chain address associated with this forwarder + /// @param addressResolver_ address resolver contract + function initialize( + uint32 chainSlug_, + bytes32 onChainAddress_, + address addressResolver_ + ) public reinitializer(1) { + if (onChainAddress_ == bytes32(0)) revert InvalidOnChainAddress(); + chainSlug = chainSlug_; + onChainAddress = onChainAddress_; + _setAddressResolver(addressResolver_); + } + + /// @notice Returns the on-chain address associated with this forwarder. + /// @return The on-chain address. + function getOnChainAddress() public view override returns (bytes32) { + return onChainAddress; + } + + /// @notice Returns the chain slug on which the contract is deployed. + /// @return chain slug + function getChainSlug() external view override returns (uint32) { + return chainSlug; + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } + + /// @notice Fallback function to process the contract calls to onChainAddress + /// @dev It queues the calls in the middleware and deploys the promise contract + fallback() external { + if (address(watcher__()) == address(0)) { + revert WatcherNotSet(); + } + + // validates if the async modifier is set + address msgSender = msg.sender; + bool isAsyncModifierSet = IAppGateway(msgSender).isAsyncModifierSet(); + if (!isAsyncModifierSet) revert AsyncModifierNotSet(); + + // fetch the override params from app gateway + (OverrideParams memory overrideParams, bytes32 sbType) = IAppGateway(msgSender) + .getOverrideParams(); + + // Queue the call in the middleware. + QueueParams memory queueParams; + queueParams.overrideParams = overrideParams; + queueParams.transaction = Transaction({ + chainSlug: chainSlug, + target: getOnChainAddress(), + payload: msg.data + }); + queueParams.switchboardType = sbType; + watcher__().queueAndSubmit( + queueParams, + overrideParams.maxFees, + address(0), + overrideParams.consumeFrom, + bytes("") + ); + } +} diff --git a/contracts/evmx/interfaces/IAuctionManager.sol b/deprecated/IAuctionManager.sol similarity index 92% rename from contracts/evmx/interfaces/IAuctionManager.sol rename to deprecated/IAuctionManager.sol index af867f81..3a86a3a0 100644 --- a/contracts/evmx/interfaces/IAuctionManager.sol +++ b/deprecated/IAuctionManager.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.21; -import {QueueParams, OverrideParams, Transaction, Bid, RequestParams} from "../../utils/common/Structs.sol"; +import {RawPayload, OverrideParams, Transaction, Bid, RequestParams} from "../../utils/common/Structs.sol"; interface IAuctionManager { enum AuctionStatus { diff --git a/contracts/protocol/interfaces/ICCTPSwitchboard.sol b/deprecated/ICCTPSwitchboard.sol similarity index 100% rename from contracts/protocol/interfaces/ICCTPSwitchboard.sol rename to deprecated/ICCTPSwitchboard.sol diff --git a/contracts/evmx/interfaces/IContractFactoryPlug.sol b/deprecated/IContractFactoryPlug.sol similarity index 100% rename from contracts/evmx/interfaces/IContractFactoryPlug.sol rename to deprecated/IContractFactoryPlug.sol diff --git a/contracts/evmx/interfaces/IDeployForwarder.sol b/deprecated/IDeployForwarder.sol similarity index 100% rename from contracts/evmx/interfaces/IDeployForwarder.sol rename to deprecated/IDeployForwarder.sol diff --git a/contracts/evmx/interfaces/IPromiseResolver.sol b/deprecated/IPromiseResolver.sol similarity index 100% rename from contracts/evmx/interfaces/IPromiseResolver.sol rename to deprecated/IPromiseResolver.sol diff --git a/contracts/evmx/interfaces/IRequestHandler.sol b/deprecated/IRequestHandler.sol similarity index 69% rename from contracts/evmx/interfaces/IRequestHandler.sol rename to deprecated/IRequestHandler.sol index e2871ef1..01d1c28b 100644 --- a/contracts/evmx/interfaces/IRequestHandler.sol +++ b/deprecated/IRequestHandler.sol @@ -5,10 +5,6 @@ import "../../utils/common/Structs.sol"; import "../interfaces/IPrecompile.sol"; interface IRequestHandler { - function getRequestBatchIds(uint40 requestCount_) external view returns (uint40[] memory); - - function getBatchPayloadIds(uint40 batchCount_) external view returns (bytes32[] memory); - function getRequest(uint40 requestCount_) external view returns (RequestParams memory); function getPayload(bytes32 payloadId_) external view returns (PayloadParams memory); @@ -24,22 +20,17 @@ interface IRequestHandler { function submitRequest( uint256 maxFees_, - address auctionManager_, address consumeFrom_, address appGateway_, - QueueParams[] calldata queueParams_, + RawPayload calldata RawPayload_, bytes memory onCompleteData_ ) external returns (uint40 requestCount, address[] memory promiseList); - function assignTransmitter(uint40 requestCount_, Bid memory bid_) external; - - function updateRequestAndProcessBatch(uint40 requestCount_, bytes32 payloadId_) external; + function updateRequest(uint40 requestCount_, uint256 feesUsed_) external; function cancelRequestForReverts(uint40 requestCount) external; function cancelRequest(uint40 requestCount, address appGateway_) external; - function handleRevert(uint40 requestCount) external; - function increaseFees(uint40 requestCount_, uint256 newMaxFees_, address appGateway_) external; } diff --git a/deprecated/IWatcher.sol b/deprecated/IWatcher.sol new file mode 100644 index 00000000..427f6a9b --- /dev/null +++ b/deprecated/IWatcher.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; +import "../../utils/common/Errors.sol"; +import "../../utils/common/Structs.sol"; + +import "./IRequestHandler.sol"; +import "./IConfigurations.sol"; +import "./IPromiseResolver.sol"; + +/// @title IWatcher +/// @notice Interface for the Watcher Precompile system that handles payload verification and execution +/// @dev Defines core functionality for payload processing and promise resolution +interface IWatcher { + /// @notice Emitted when a new call is made to an app gateway + /// @param triggerId The unique identifier for the trigger + event CalledAppGateway(bytes32 triggerId); + + /// @notice Emitted when a call to an app gateway fails + /// @param triggerId The unique identifier for the trigger + event AppGatewayCallFailed(bytes32 triggerId); + + function requestHandler__() external view returns (IRequestHandler); + + function configurations__() external view returns (IConfigurations); + + function promiseResolver__() external view returns (IPromiseResolver); + + /// @notice Returns the request params for a given request count + /// @param requestCount_ The request count + /// @return The request params + function getRequestParams(uint40 requestCount_) external view returns (RequestParams memory); + + /// @notice Returns the request params for a given request count + /// @param payloadId_ The payload id + /// @return The request params + function getPayloadParams(bytes32 payloadId_) external view returns (PayloadParams memory); + + /// @notice Returns the current request count + /// @return The current request count + function getCurrentRequestCount() external view returns (uint40); + + /// @notice Returns the latest async promise deployed for a payload queued + /// @return The latest async promise + function latestAsyncPromise() external view returns (address); + + function triggerFromChainSlug() external view returns (uint32); + + function triggerFromPlug() external view returns (bytes32); + + function isAppGatewayCalled(bytes32 triggerId) external view returns (bool); + + /// @notice Queues a payload for execution + /// @param RawPayload_ The parameters for the payload + function queue( + RawPayload calldata RawPayload_, + address appGateway_ + ) external returns (address, uint40); + + /// @notice Clears the queue of payloads + function clearQueue() external; + + function submitRequest( + ForwarderParams calldata forwarderParams, + bytes calldata onCompleteData + ) external returns (uint40 requestCount, address[] memory promises); + + function queueAndSubmit( + RawPayload memory queue_, + ForwarderParams calldata forwarderParams, + bytes calldata onCompleteData + ) external returns (uint40 requestCount, address[] memory promises); + + /// @notice Returns the precompile fees for a given precompile + /// @param precompile_ The precompile + /// @param precompileData_ The precompile data + /// @return The precompile fees + function getPrecompileFees( + bytes4 precompile_, + bytes memory precompileData_ + ) external view returns (uint256); + + function cancelRequest(uint40 requestCount_) external; + + function increaseFees(uint40 requestCount_, uint256 newFees_) external; + + function setIsValidPlug(bool isValid_, uint32 chainSlug_, bytes32 onchainAddress_) external; + + function isWatcher(address account_) external view returns (bool); +} diff --git a/contracts/evmx/watcher/PromiseResolver.sol b/deprecated/PromiseResolver.sol similarity index 96% rename from contracts/evmx/watcher/PromiseResolver.sol rename to deprecated/PromiseResolver.sol index 796054ea..31ce2be9 100644 --- a/contracts/evmx/watcher/PromiseResolver.sol +++ b/deprecated/PromiseResolver.sol @@ -46,10 +46,7 @@ contract PromiseResolver is IPromiseResolver, WatcherBase, Initializable { for (uint256 i = 0; i < promiseReturnData_.length; i++) { (uint40 requestCount, bool success) = _processPromiseResolution(promiseReturnData_[i]); if (success) { - requestHandler__().updateRequestAndProcessBatch( - requestCount, - promiseReturnData_[i].payloadId - ); + requestHandler__().updateRequest(requestCount); } } } diff --git a/deprecated/RequestHandler.sol b/deprecated/RequestHandler.sol new file mode 100644 index 00000000..84161fd3 --- /dev/null +++ b/deprecated/RequestHandler.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "solady/utils/Initializable.sol"; +import "solady/auth/Ownable.sol"; +import "../helpers/AddressResolverUtil.sol"; +import "../../utils/common/Errors.sol"; +import "../../utils/common/Constants.sol"; +import "../../utils/common/IdUtils.sol"; +import "../interfaces/IAppGateway.sol"; +import "../interfaces/IPromise.sol"; +import "../interfaces/IRequestHandler.sol"; +import "../../utils/RescueFundsLib.sol"; +import "solady/utils/LibCall.sol"; +import "../interfaces/IERC20.sol"; + +abstract contract RequestHandlerStorage is IRequestHandler { + // slots [0-49] reserved for gap + uint256[50] _gap_before; + + // slot 50 (40 + 40 + 40 + 128) + /// @notice Counter for tracking request counts + uint40 public nextRequestCount = 1; + + /// @notice Counter for tracking payload _requests + uint40 public payloadCounter; + + // slot 51 + /// @notice Mapping to store the precompiles for each call type + mapping(bytes4 => IPrecompile) public precompiles; + + // queue => update to payloadParams, assign id, store in payloadParams map + // slot 54 + /// @notice Mapping to store the payload parameters for each payload ID + mapping(bytes32 => PayloadParams) internal _payloads; + + // slot 55 + /// @notice The metadata for a request + mapping(uint40 => RequestParams) internal _requests; + + // slots [56-105] reserved for gap + uint256[50] _gap_after; + + // slots [106-155] 50 slots reserved for address resolver util +} + +/// @title RequestHandler +/// @notice Contract that handles request processing and management, including request submission, batch processing, and request lifecycle management +/// @dev Handles request submission, batch processing, transmitter assignment, request cancellation and settlement +contract RequestHandler is RequestHandlerStorage, Initializable, Ownable, AddressResolverUtil { + using LibCall for address; + + error InsufficientMaxFees(); + event RequestSubmitted( + bool hasWrite, + uint40 requestCount, + uint256 estimatedFees, + RequestParams requestParams, + PayloadParams payloadParams + ); + + event FeesIncreased(uint40 requestCount, uint256 newMaxFees); + event RequestSettled(uint40 requestCount, address winner); + event RequestCompletedWithErrors(uint40 requestCount); + event RequestCancelled(uint40 requestCount); + event PrecompileSet(bytes4 callType, IPrecompile precompile); + + modifier isRequestCancelled(uint40 requestCount_) { + if (_requests[requestCount_].isRequestCancelled) revert RequestAlreadyCancelled(); + _; + } + + modifier onlyPromiseResolver() { + if (msg.sender != address(watcher__().promiseResolver__())) revert NotPromiseResolver(); + _; + } + + constructor() { + _disableInitializers(); // disable for implementation + } + + function initialize(address owner_, address addressResolver_) external reinitializer(1) { + _initializeOwner(owner_); + _setAddressResolver(addressResolver_); + } + + function setPrecompile(bytes4 callType_, IPrecompile precompile_) external onlyOwner { + precompiles[callType_] = precompile_; + emit PrecompileSet(callType_, precompile_); + } + + function getPrecompileFees( + bytes4 callType_, + bytes memory precompileData_ + ) external view returns (uint256) { + return precompiles[callType_].getPrecompileFees(precompileData_); + } + + function getRequest(uint40 requestCount_) external view returns (RequestParams memory) { + return _requests[requestCount_]; + } + + function getPayload(bytes32 payloadId_) external view returns (PayloadParams memory) { + return _payloads[payloadId_]; + } + + function submitRequest( + uint256 maxFees_, + address consumeFrom_, + address appGateway_, + QueueParams calldata queueParams_, + bytes memory onCompleteData_ + ) external onlyWatcher returns (uint40 requestCount, address[] memory promiseList) { + if (!feesManager__().isCreditSpendable(consumeFrom_, appGateway_, maxFees_)) + revert InsufficientFees(); + + RequestParams storage r = _requests[requestCount]; + r.requestFeesDetails.maxFees = maxFees_; + r.requestFeesDetails.consumeFrom = consumeFrom_; + requestCount = nextRequestCount++; + + bytes4 callType = queueParams_.overrideParams.callType; + + (PayloadParams memory p, uint256 estimatedFees) = _createRequest( + queueParams_, + appGateway_, + requestCount + ); + + if (estimatedFees > maxFees_) revert InsufficientMaxFees(); + feesManager__().blockCredits(requestCount, r.requestFeesDetails.consumeFrom, estimatedFees); + r.payloadId = p.payloadId; + _processBatch(r); + + emit RequestSubmitted(callType == WRITE, requestCount, estimatedFees, r, p); + } + + function _createRequest( + QueueParams calldata queueParams_, + address appGateway_, + uint40 requestCount_ + ) internal returns (PayloadParams memory p, uint256 estimatedFees) { + QueueParams calldata queuePayloadParam = queueParams_; + uint64 switchboardId = watcher__().configurations__().switchboards( + queuePayloadParam.transaction.chainSlug, + queuePayloadParam.switchboardType + ); + + // process payload data and store + bytes4 callType = queuePayloadParam.overrideParams.callType; + (bytes memory precompileData, uint256 estFees) = _validateAndGetPrecompileData( + queuePayloadParam, + appGateway_, + callType + ); + estimatedFees = estFees; + + // create payload id + bytes32 payloadId = createPayloadId( + uint160(payloadCounter++), + switchboardId, + queuePayloadParam.transaction.chainSlug + ); + + // encode requestCount into pointer upper bits as done elsewhere + uint160 payloadPointer = uint160((uint256(requestCount_) << 120)); + p.payloadPointer = payloadPointer; + p.callType = callType; + p.asyncPromise = queueParams_.asyncPromise; + p.appGateway = appGateway_; + p.payloadId = payloadId; + p.precompileData = precompileData; + _payloads[payloadId] = p; + } + + function _validateAndGetPrecompileData( + QueueParams calldata payloadParams_, + address appGateway_, + bytes4 callType_ + ) internal view returns (bytes memory precompileData, uint256 estimatedFees) { + if (address(precompiles[callType_]) == address(0)) revert InvalidCallType(); + return + IPrecompile(precompiles[callType_]).validateAndGetPrecompileData( + payloadParams_, + appGateway_ + ); + } + + // called when processing batch first time or being retried + function _processBatch(RequestParams storage r) internal { + bytes32 payloadId = r.payloadId; + + // check needed for re-process, in case a payload is already executed by last transmitter + if (_isPromiseResolved(_payloads[payloadId].asyncPromise)) return; + PayloadParams storage payloadParams = _payloads[payloadId]; + + (uint256 fees, uint256 deadline, bytes memory precompileData) = IPrecompile( + precompiles[payloadParams.callType] + ).handlePayload(r.requestFeesDetails.consumeFrom, payloadParams); + + payloadParams.deadline = deadline; + payloadParams.precompileData = precompileData; + } + + /// @notice Increases the fees for a request if no bid is placed + /// @param requestCount_ The ID of the request + /// @param newMaxFees_ The new maximum fees + function increaseFees( + uint40 requestCount_, + uint256 newMaxFees_, + address appGateway_ + ) external onlyWatcher isRequestCancelled(requestCount_) { + RequestParams storage r = _requests[requestCount_]; + if (r.isRequestExecuted) revert RequestAlreadySettled(); + + if (appGateway_ != r.appGateway) revert OnlyAppGateway(); + if (r.requestFeesDetails.maxFees >= newMaxFees_) + revert NewMaxFeesLowerThanCurrent(r.requestFeesDetails.maxFees, newMaxFees_); + + // reblock new fees + if ( + !IFeesManager(feesManager__()).isCreditSpendable( + r.requestFeesDetails.consumeFrom, + appGateway_, + newMaxFees_ + ) + ) revert InsufficientFees(); + r.requestFeesDetails.maxFees = newMaxFees_; + + // indexed by transmitter and watcher to start bidding or re-processing the request + emit FeesIncreased(requestCount_, newMaxFees_); + } + + function updateRequest( + uint40 requestCount_, + uint256 feesUsed_ + ) external onlyPromiseResolver isRequestCancelled(requestCount_) { + RequestParams storage r = _requests[requestCount_]; + PayloadParams storage payloadParams = _payloads[r.payloadId]; + payloadParams.resolvedAt = block.timestamp; + + IPrecompile(precompiles[payloadParams.callType]).resolvePayload(payloadParams); + r.isRequestExecuted = true; + _settleRequest(requestCount_, r); + } + + function _isPromiseResolved(address promise_) internal view returns (bool) { + return IPromise(promise_).state() == AsyncPromiseState.RESOLVED; + } + + /// @notice Cancels a request + /// @param requestCount The request count to cancel + /// @dev This function cancels a request + /// @dev It verifies that the caller is the middleware and that the request hasn't been cancelled yet + function cancelRequestForReverts(uint40 requestCount) external onlyPromiseResolver { + _cancelRequest(requestCount, _requests[requestCount]); + } + + /// @notice Cancels a request + /// @param requestCount The request count to cancel + /// @dev This function cancels a request + /// @dev It verifies that the caller is the middleware and that the request hasn't been cancelled yet + function cancelRequest(uint40 requestCount, address appGateway_) external onlyWatcher { + RequestParams storage r = _requests[requestCount]; + if (appGateway_ != r.appGateway) revert InvalidCaller(); + _cancelRequest(requestCount, r); + } + + function handleRevert(uint40 requestCount) external onlyPromiseResolver { + _cancelRequest(requestCount, _requests[requestCount]); + } + + function _cancelRequest( + uint40 requestCount_, + RequestParams storage r + ) internal isRequestCancelled(requestCount_) { + if (r.isRequestExecuted) revert RequestAlreadySettled(); + + r.isRequestCancelled = true; + _settleRequest(requestCount_, r); + emit RequestCancelled(requestCount_); + } + + function _settleRequest(uint40 requestCount_, RequestParams storage r) internal { + feesManager__().unblockAndAssignCredits(requestCount_, address(feesManager__())); + emit RequestSettled(requestCount_); + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds(address token_, address rescueTo_, uint256 amount_) external onlyWatcher { + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } +} diff --git a/contracts/evmx/watcher/Trigger.sol b/deprecated/Trigger.sol similarity index 100% rename from contracts/evmx/watcher/Trigger.sol rename to deprecated/Trigger.sol diff --git a/deprecated/Watcher.sol b/deprecated/Watcher.sol new file mode 100644 index 00000000..1160e0a9 --- /dev/null +++ b/deprecated/Watcher.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "./Trigger.sol"; +import "../interfaces/IPromise.sol"; + +contract Watcher is Trigger { + using LibCall for address; + + event CoreContractsSet(address requestHandler, address configManager, address promiseResolver); + + constructor() { + _disableInitializers(); // disable for implementation + } + + function initialize( + uint32 evmxSlug_, + uint256 triggerFees_, + address owner_, + address addressResolver_ + ) public reinitializer(1) { + evmxSlug = evmxSlug_; + triggerFees = triggerFees_; + _initializeOwner(owner_); + _setAddressResolver(addressResolver_); + } + + function setCoreContracts( + address requestHandler_, + address configManager_, + address promiseResolver_ + ) external onlyOwner { + requestHandler__ = IRequestHandler(requestHandler_); + configurations__ = IConfigurations(configManager_); + promiseResolver__ = IPromiseResolver(promiseResolver_); + + emit CoreContractsSet(requestHandler_, configManager_, promiseResolver_); + } + + function isWatcher(address account_) public view override returns (bool) { + return + account_ == address(requestHandler__) || + account_ == address(configurations__) || + account_ == address(promiseResolver__); + } + + // can be called to submit single payload request without any callback + function queueAndSubmit( + QueueParams memory queue_, + uint256 maxFees, + address auctionManager, + address consumeFrom, + bytes memory onCompleteData + ) external returns (uint40 requestCount, address[] memory promises) { + _queue(queue_, msg.sender); + return _submitRequest(maxFees, auctionManager, consumeFrom, onCompleteData); + } + + /// @notice Queues a new payload + /// @param queue_ The call parameters + function queue( + QueueParams memory queue_, + address appGateway_ + ) external returns (address, uint40) { + return _queue(queue_, appGateway_); + } + + function _queue( + QueueParams memory queue_, + address appGateway_ + ) internal returns (address, uint40) { + // checks if app gateway passed by forwarder is coming from same core app gateway group + if (appGatewayTemp != address(0)) + if (appGatewayTemp != appGateway_ || appGateway_ == address(0)) + revert InvalidAppGateway(); + + uint40 requestCount = getCurrentRequestCount(); + // Deploy a new async promise contract. + latestAsyncPromise = asyncDeployer__().deployAsyncPromiseContract( + appGateway_, + requestCount + ); + appGatewayTemp = appGateway_; + queue_.asyncPromise = latestAsyncPromise; + + // Add the promise to the queue. + payloadQueue.push(queue_); + // return the promise and request count + return (latestAsyncPromise, requestCount); + } + + function submitRequest( + uint256 maxFees, + address auctionManager, + address consumeFrom, + bytes memory onCompleteData + ) external returns (uint40, address[] memory) { + return _submitRequest(maxFees, auctionManager, consumeFrom, onCompleteData); + } + + function _submitRequest( + uint256 maxFees, + address, + address consumeFrom, + bytes memory onCompleteData + ) internal returns (uint40 requestCount, address[] memory promiseList) { + if (payloadQueue.length == 0) return (0, new address[](0)); + address appGateway = msg.sender; + + // this check is to verify that msg.sender (app gateway base) belongs to correct app gateway + if (appGateway != appGatewayTemp) revert InvalidAppGateway(); + latestAsyncPromise = address(0); + appGatewayTemp = address(0); + + (requestCount, promiseList) = requestHandler__.submitRequest( + maxFees, + consumeFrom, + appGateway, + payloadQueue[0], + onCompleteData + ); + + clearQueue(); + } + + /// @notice Clears the call parameters array + function clearQueue() public { + delete payloadQueue; + } + + function callAppGateways(WatcherMultiCallParams memory params_) external { + _validateSignature(address(this), params_.data, params_.nonce, params_.signature); + TriggerParams[] memory params = abi.decode(params_.data, (TriggerParams[])); + + for (uint40 i = 0; i < params.length; i++) { + _callAppGateways(params[i]); + } + } + + function setTriggerFees( + uint256 triggerFees_, + uint256 nonce_, + bytes memory signature_ + ) external { + _validateSignature(address(this), abi.encode(triggerFees_), nonce_, signature_); + _setTriggerFees(triggerFees_); + } + + function getCurrentRequestCount() public view returns (uint40) { + return requestHandler__.nextRequestCount(); + } + + function getRequestParams(uint40 requestCount_) external view returns (RequestParams memory) { + return requestHandler__.getRequest(requestCount_); + } + + function getPayloadParams(bytes32 payloadId_) external view returns (PayloadParams memory) { + return requestHandler__.getPayload(payloadId_); + } + + function setIsValidPlug(bool isValid_, uint32 chainSlug_, bytes32 plug_) external override { + configurations__.setIsValidPlug(isValid_, chainSlug_, plug_, msg.sender); + } + + function cancelRequest(uint40 requestCount_) external override { + requestHandler__.cancelRequest(requestCount_, msg.sender); + } + + function increaseFees(uint40 requestCount_, uint256 newFees_) external override { + requestHandler__.increaseFees(requestCount_, newFees_, msg.sender); + } + + function getPrecompileFees( + bytes4 precompile_, + bytes memory precompileData_ + ) external view returns (uint256) { + return requestHandler__.getPrecompileFees(precompile_, precompileData_); + } + + // all function from watcher requiring signature + // can be also used to do msg.sender check related function in other contracts like withdraw credits from fees manager and set core app-gateways in configurations + function watcherMultiCall(WatcherMultiCallParams[] memory params_) external payable { + for (uint40 i = 0; i < params_.length; i++) { + _validateSignature( + params_[i].contractAddress, + params_[i].data, + params_[i].nonce, + params_[i].signature + ); + + // call the contract + (bool success, , ) = params_[i].contractAddress.tryCall( + 0, + gasleft(), + 0, + params_[i].data + ); + if (!success) revert CallFailed(); + } + } + + /// @notice Verifies that a watcher signature is valid + /// @param data_ The data to verify + /// @param nonce_ The nonce of the signature + /// @param signature_ The signature to verify + function _validateSignature( + address contractAddress_, + bytes memory data_, + uint256 nonce_, + bytes memory signature_ + ) internal { + if (contractAddress_ == address(0)) revert InvalidContract(); + if (data_.length == 0) revert InvalidData(); + if (signature_.length == 0) revert InvalidSignature(); + if (isNonceUsed[nonce_]) revert NonceUsed(); + isNonceUsed[nonce_] = true; + + bytes32 digest = keccak256( + abi.encode(address(this), evmxSlug, nonce_, contractAddress_, data_) + ); + + // check if signature is valid + if (_recoverSigner(digest, signature_) != owner()) revert InvalidSignature(); + } + + /// @notice Recovers the signer of a message + /// @param digest_ The digest of the input data + /// @param signature_ The signature to verify + /// @dev This function verifies that the signature was created by the watcher and that the nonce has not been used before + function _recoverSigner( + bytes32 digest_, + bytes memory signature_ + ) internal view returns (address signer) { + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); + + // recovered signer is checked for the valid roles later + signer = ECDSA.recover(digest, signature_); + } + + /** + * @notice Rescues funds from the contract if they are locked by mistake. This contract does not + * theoretically need this function but it is added for safety. + * @param token_ The address of the token contract. + * @param rescueTo_ The address where rescued tokens need to be sent. + * @param amount_ The amount of tokens to be rescued. + */ + function rescueFunds( + address token_, + address rescueTo_, + uint256 amount_, + uint256 nonce_, + bytes memory signature_ + ) external { + _validateSignature( + address(this), + abi.encode(token_, rescueTo_, amount_), + nonce_, + signature_ + ); + RescueFundsLib._rescueFunds(token_, rescueTo_, amount_); + } +} diff --git a/contracts/evmx/watcher/WatcherBase.sol b/deprecated/WatcherBase.sol similarity index 100% rename from contracts/evmx/watcher/WatcherBase.sol rename to deprecated/WatcherBase.sol diff --git a/contracts/evmx/watcher/WatcherStorage.sol b/deprecated/WatcherStorage.sol similarity index 100% rename from contracts/evmx/watcher/WatcherStorage.sol rename to deprecated/WatcherStorage.sol diff --git a/script/admin/RescueFunds.s.sol b/deprecated/script/admin/RescueFunds.s.sol similarity index 100% rename from script/admin/RescueFunds.s.sol rename to deprecated/script/admin/RescueFunds.s.sol diff --git a/script/admin/mock/DeployEVMx.s.sol b/deprecated/script/admin/mock/DeployEVMx.s.sol similarity index 100% rename from script/admin/mock/DeployEVMx.s.sol rename to deprecated/script/admin/mock/DeployEVMx.s.sol diff --git a/script/admin/mock/DeploySocket.s.sol b/deprecated/script/admin/mock/DeploySocket.s.sol similarity index 100% rename from script/admin/mock/DeploySocket.s.sol rename to deprecated/script/admin/mock/DeploySocket.s.sol diff --git a/deprecated/script/counter/DeployEVMxCounterApp.s.sol b/deprecated/script/counter/DeployEVMxCounterApp.s.sol new file mode 100644 index 00000000..5bf183ca --- /dev/null +++ b/deprecated/script/counter/DeployEVMxCounterApp.s.sol @@ -0,0 +1,28 @@ +// 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 {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; + +// source .env && forge script script/counter/deployEVMxCounterApp.s.sol --broadcast --skip-simulation +contract CounterDeploy 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; + + CounterAppGateway gateway = new CounterAppGateway(addressResolver, fees); + + console.log("Contracts deployed:"); + console.log("CounterAppGateway:", address(gateway)); + console.log("counterId:"); + console.logBytes32(gateway.counter()); + } +} diff --git a/script/counter/DeployOnchainCounters.s.sol b/deprecated/script/counter/DeployOnchainCounters.s.sol similarity index 100% rename from script/counter/DeployOnchainCounters.s.sol rename to deprecated/script/counter/DeployOnchainCounters.s.sol diff --git a/deprecated/script/counter/IncrementCountersFromApp.s.sol b/deprecated/script/counter/IncrementCountersFromApp.s.sol new file mode 100644 index 00000000..92c688c7 --- /dev/null +++ b/deprecated/script/counter/IncrementCountersFromApp.s.sol @@ -0,0 +1,59 @@ +// 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 {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; + +// source .env && forge script script/counter/IncrementCountersFromApp.s.sol --broadcast --skip-simulation --legacy --gas-price 0 +// source .env && cast send $APP_GATEWAY "incrementCounters(address[])" '[0xdA908E7491499d64944Ea5Dc967135a0F22d2057]' --private-key $PRIVATE_KEY --legacy --gas-price 0 +contract IncrementCounters is Script { + function run() external { + string memory socketRPC = vm.envString("EVMX_RPC"); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + vm.createSelectFork(socketRPC); + + CounterAppGateway gateway = CounterAppGateway(vm.envAddress("APP_GATEWAY")); + + address counterForwarderArbitrumSepolia = gateway.forwarderAddresses( + gateway.counter(), + 421614 + ); + address counterForwarderOptimismSepolia = gateway.forwarderAddresses( + gateway.counter(), + 11155420 + ); + address counterForwarderBaseSepolia = gateway.forwarderAddresses(gateway.counter(), 84532); + + // Count non-zero addresses + uint256 nonZeroCount = 0; + if (counterForwarderArbitrumSepolia != address(0)) nonZeroCount++; + if (counterForwarderOptimismSepolia != address(0)) nonZeroCount++; + if (counterForwarderBaseSepolia != address(0)) nonZeroCount++; + + address[] memory instances = new address[](nonZeroCount); + uint256 index = 0; + if (counterForwarderArbitrumSepolia != address(0)) { + instances[index] = counterForwarderArbitrumSepolia; + index++; + } else { + console.log("Arbitrum Sepolia forwarder not yet deployed"); + } + if (counterForwarderOptimismSepolia != address(0)) { + instances[index] = counterForwarderOptimismSepolia; + index++; + } else { + console.log("Optimism Sepolia forwarder not yet deployed"); + } + if (counterForwarderBaseSepolia != address(0)) { + instances[index] = counterForwarderBaseSepolia; + index++; + } else { + console.log("Base Sepolia forwarder not yet deployed"); + } + + vm.startBroadcast(deployerPrivateKey); + gateway.incrementCounters(instances); + } +} diff --git a/deprecated/script/counter/ReadOnchainCounters.s.sol b/deprecated/script/counter/ReadOnchainCounters.s.sol new file mode 100644 index 00000000..4a041e95 --- /dev/null +++ b/deprecated/script/counter/ReadOnchainCounters.s.sol @@ -0,0 +1,49 @@ +// 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 {Counter} from "../../test/apps/app-gateways/counter/Counter.sol"; +import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; +import {fromBytes32Format} from "../../contracts/utils/common/Converters.sol"; + +contract CheckCounters is Script { + function run() external { + CounterAppGateway gateway = CounterAppGateway(vm.envAddress("APP_GATEWAY")); + + vm.createSelectFork(vm.envString("EVMX_RPC")); + address counterInstanceArbitrumSepolia = fromBytes32Format( + gateway.getOnChainAddress(gateway.counter(), 421614) + ); + address counterInstanceOptimismSepolia = fromBytes32Format( + gateway.getOnChainAddress(gateway.counter(), 11155420) + ); + address counterInstanceBaseSepolia = fromBytes32Format( + gateway.getOnChainAddress(gateway.counter(), 84532) + ); + + if (counterInstanceArbitrumSepolia != address(0)) { + vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC")); + uint256 counterValueArbitrumSepolia = Counter(counterInstanceArbitrumSepolia).counter(); + console.log("Counter value on Arbitrum Sepolia: ", counterValueArbitrumSepolia); + } else { + console.log("Counter not yet deployed on Arbitrum Sepolia"); + } + + if (counterInstanceOptimismSepolia != address(0)) { + vm.createSelectFork(vm.envString("OPTIMISM_SEPOLIA_RPC")); + uint256 counterValueOptimismSepolia = Counter(counterInstanceOptimismSepolia).counter(); + console.log("Counter value on Optimism Sepolia: ", counterValueOptimismSepolia); + } else { + console.log("Counter not yet deployed on Optimism Sepolia"); + } + + if (counterInstanceBaseSepolia != address(0)) { + vm.createSelectFork(vm.envString("BASE_SEPOLIA_RPC")); + uint256 counterValueBaseSepolia = Counter(counterInstanceBaseSepolia).counter(); + console.log("Counter value on Base Sepolia: ", counterValueBaseSepolia); + } else { + console.log("Counter not yet deployed on Base Sepolia"); + } + } +} diff --git a/deprecated/script/counter/SetFees.s.sol b/deprecated/script/counter/SetFees.s.sol new file mode 100644 index 00000000..758ff54a --- /dev/null +++ b/deprecated/script/counter/SetFees.s.sol @@ -0,0 +1,26 @@ +// 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 {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; + +// source .env && forge script script/counter/DeployCounterOnchain.s.sol --broadcast --skip-simulation --legacy --gas-price 0 +contract CounterSetFees is Script { + function run() external { + string memory rpc = vm.envString("EVMX_RPC"); + console.log(rpc); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + CounterAppGateway appGateway = CounterAppGateway(vm.envAddress("APP_GATEWAY")); + console.log("Counter Gateway:", address(appGateway)); + + console.log("Setting fees..."); + // Setting fee payment on Arbitrum Sepolia + // uint256 fees = 0.00001 ether; + // appGateway.setFees(fees); + } +} diff --git a/deprecated/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol b/deprecated/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol new file mode 100644 index 00000000..3783000b --- /dev/null +++ b/deprecated/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol @@ -0,0 +1,59 @@ +// 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 {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; + +// @notice This script is used to withdraw fees from EVMX to Arbitrum Sepolia +// @dev Make sure your app has withdrawFeeTokens() function implemented. You can check its implementation in CounterAppGateway.sol +contract WithdrawFees is Script { + function run() external { + // EVMX Check available fees + vm.createSelectFork(vm.envString("EVMX_RPC")); + FeesManager feesManager = FeesManager(payable(vm.envAddress("FEES_MANAGER"))); + address appGatewayAddress = vm.envAddress("APP_GATEWAY"); + address token = vm.envAddress("USDC"); + + CounterAppGateway appGateway = CounterAppGateway(appGatewayAddress); + uint256 availableFees = feesManager.balanceOf(appGatewayAddress); + console.log("Available fees:", availableFees); + + if (availableFees > 0) { + // Switch to Arbitrum Sepolia to get gas price + vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC")); + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + address sender = vm.addr(privateKey); + + // Gas price from Arbitrum + uint256 arbitrumGasPrice = block.basefee + 0.1 gwei; // With buffer + uint256 gasLimit = 5_000_000; // Estimate + uint256 estimatedGasCost = gasLimit * arbitrumGasPrice; + + console.log("Arbitrum gas price (wei):", arbitrumGasPrice); + console.log("Gas limit:", gasLimit); + console.log("Estimated gas cost:", estimatedGasCost); + + // Calculate amount to withdraw + uint256 amountToWithdraw = availableFees > estimatedGasCost + ? availableFees - estimatedGasCost + : 0; + + if (amountToWithdraw > 0) { + // Switch back to EVMX to perform withdrawal + vm.createSelectFork(vm.envString("EVMX_RPC")); + vm.startBroadcast(privateKey); + console.log("Withdrawing amount:", amountToWithdraw); + appGateway.withdrawCredits(421614, token, amountToWithdraw, sender); + vm.stopBroadcast(); + + // Switch back to Arbitrum Sepolia to check final balance + vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC")); + console.log("Final sender balance:", sender.balance); + } else { + console.log("Available fees less than estimated gas cost"); + } + } + } +} diff --git a/script/helpers/CheckDepositedCredits.s.sol b/deprecated/script/helpers/CheckDepositedCredits.s.sol similarity index 100% rename from script/helpers/CheckDepositedCredits.s.sol rename to deprecated/script/helpers/CheckDepositedCredits.s.sol diff --git a/script/helpers/DepositCredit.s.sol b/deprecated/script/helpers/DepositCredit.s.sol similarity index 96% rename from script/helpers/DepositCredit.s.sol rename to deprecated/script/helpers/DepositCredit.s.sol index b8d432f8..58cce51e 100644 --- a/script/helpers/DepositCredit.s.sol +++ b/deprecated/script/helpers/DepositCredit.s.sol @@ -9,7 +9,7 @@ import {TestUSDC} from "../../contracts/evmx/mocks/TestUSDC.sol"; // source .env && forge script script/helpers/DepositCreditAndNative.s.sol --broadcast --skip-simulation contract DepositCredit is Script { function run() external { - uint256 feesAmount = 2000000; // 2 USDC + uint256 feesAmount = 10000000; // 10 USDC vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC")); uint256 privateKey = vm.envUint("PRIVATE_KEY"); diff --git a/script/helpers/DepositCreditAndNative.s.sol b/deprecated/script/helpers/DepositCreditAndNative.s.sol similarity index 100% rename from script/helpers/DepositCreditAndNative.s.sol rename to deprecated/script/helpers/DepositCreditAndNative.s.sol diff --git a/script/helpers/DepositCreditMainnet.s.sol b/deprecated/script/helpers/DepositCreditMainnet.s.sol similarity index 100% rename from script/helpers/DepositCreditMainnet.s.sol rename to deprecated/script/helpers/DepositCreditMainnet.s.sol diff --git a/script/helpers/TransferRemainingCredits.s.sol b/deprecated/script/helpers/TransferRemainingCredits.s.sol similarity index 100% rename from script/helpers/TransferRemainingCredits.s.sol rename to deprecated/script/helpers/TransferRemainingCredits.s.sol diff --git a/script/helpers/WithdrawRemainingCredits.s.sol b/deprecated/script/helpers/WithdrawRemainingCredits.s.sol similarity index 100% rename from script/helpers/WithdrawRemainingCredits.s.sol rename to deprecated/script/helpers/WithdrawRemainingCredits.s.sol diff --git a/script/supertoken/DeployEVMxSuperTokenApp.s.sol b/deprecated/script/supertoken/DeployEVMxSuperTokenApp.s.sol similarity index 100% rename from script/supertoken/DeployEVMxSuperTokenApp.s.sol rename to deprecated/script/supertoken/DeployEVMxSuperTokenApp.s.sol diff --git a/script/supertoken/TransferSuperToken.s.sol b/deprecated/script/supertoken/TransferSuperToken.s.sol similarity index 100% rename from script/supertoken/TransferSuperToken.s.sol rename to deprecated/script/supertoken/TransferSuperToken.s.sol diff --git a/deprecated/test/SetupTest.t.sol b/deprecated/test/SetupTest.t.sol new file mode 100644 index 00000000..2578dffa --- /dev/null +++ b/deprecated/test/SetupTest.t.sol @@ -0,0 +1,1509 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "../contracts/utils/common/Structs.sol"; +import "../contracts/utils/common/Errors.sol"; +import "../contracts/utils/common/Constants.sol"; +import "../contracts/utils/common/AccessRoles.sol"; +import "../contracts/utils/common/IdUtils.sol"; + +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/switchboard/MessageSwitchboard.sol"; +import "../contracts/protocol/SocketBatcher.sol"; +import "../contracts/protocol/SocketFeeManager.sol"; +import "../contracts/protocol/base/MessagePlugBase.sol"; + +import "../contracts/evmx/watcher/Watcher.sol"; +import "../contracts/evmx/watcher/Configurations.sol"; +import "../contracts/evmx/watcher/RequestHandler.sol"; +import "../contracts/evmx/watcher/PromiseResolver.sol"; +import "../contracts/evmx/watcher/precompiles/WritePrecompile.sol"; +import "../contracts/evmx/watcher/precompiles/ReadPrecompile.sol"; +import "../contracts/evmx/watcher/precompiles/SchedulePrecompile.sol"; + +import "../contracts/evmx/helpers/AddressResolver.sol"; +import "../contracts/evmx/helpers/AsyncDeployer.sol"; +import "../contracts/evmx/helpers/DeployForwarder.sol"; +import "../contracts/evmx/plugs/ContractFactoryPlug.sol"; +import "../contracts/evmx/fees/FeesManager.sol"; +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"; + +contract SetupStore is Test { + uint256 c = 1; + uint64 version = 1; + + uint256 watcherPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + uint256 transmitterPrivateKey = + 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d; + address watcherEOA = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address transmitterEOA = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address socketOwner = address(uint160(c++)); + + uint32 arbChainSlug = 421614; + uint32 optChainSlug = 11155420; + uint32 evmxSlug = 1; + + uint256 expiryTime = 86400; + uint256 bidTimeout = 86400; + uint256 maxReAuctionCount = 10; + uint256 auctionEndDelaySeconds = 0; + uint256 maxScheduleDelayInSeconds = 86500; + uint256 maxMsgValueLimit = 1 ether; + + uint256 writeFees = 10000; + uint256 readFees = 10000; + uint256 scheduleCallbackFees = 10000; + uint256 scheduleFeesPerSecond = 10000; + uint256 triggerFees = 10000; + uint256 socketFees = 0; + uint256 msgSbFees = 1000000000000000; // 0.001 ETH + + uint256 public watcherNonce; + uint256 public payloadIdCounter; + uint256 public triggerCounter; + uint256 public asyncPromiseCounter; + uint32 public optCCTPDomain = 2; + uint32 public arbCCTPDomain = 3; + struct SocketContracts { + uint32 chainSlug; + uint256 triggerPrefix; + Socket socket; + SocketFeeManager socketFeeManager; + FastSwitchboard switchboard; + CCTPSwitchboard cctpSwitchboard; + MessageSwitchboard messageSwitchboard; + CCTPMessageTransmitter cctpMessageTransmitter; + SocketBatcher socketBatcher; + ContractFactoryPlug contractFactoryPlug; + FeesPlug feesPlug; + TestUSDC testUSDC; + } + SocketContracts public arbConfig; + SocketContracts public optConfig; + + FeesManager feesManagerImpl; + AddressResolver addressResolverImpl; + AsyncDeployer asyncDeployerImpl; + Watcher watcherImpl; + AuctionManager auctionManagerImpl; + DeployForwarder deployForwarderImpl; + Configurations configurationsImpl; + RequestHandler requestHandlerImpl; + PromiseResolver promiseResolverImpl; + WritePrecompile writePrecompileImpl; + + ERC1967Factory public proxyFactory; + FeesManager feesManager; + FeesPool feesPool; + AddressResolver public addressResolver; + AsyncDeployer public asyncDeployer; + DeployForwarder public deployForwarder; + AuctionManager auctionManager; + + Watcher public watcher; + Configurations public configurations; + RequestHandler public requestHandler; + PromiseResolver public promiseResolver; + WritePrecompile public writePrecompile; + ReadPrecompile public readPrecompile; + SchedulePrecompile public schedulePrecompile; +} + +contract DeploySetup is SetupStore { + event Initialized(uint64 version); + + //////////////////////////////////// Setup //////////////////////////////////// + function _deploy() internal { + _deployEVMxCore(); + + // chain core contracts + arbConfig = _deploySocket(arbChainSlug); + _configureChain(arbChainSlug); + optConfig = _deploySocket(optChainSlug); + _configureChain(optChainSlug); + + vm.startPrank(watcherEOA); + auctionManager.grantRole(TRANSMITTER_ROLE, transmitterEOA); + feesPool.grantRole(FEE_MANAGER_ROLE, address(feesManager)); + + // setup address resolver + addressResolver.setWatcher(address(watcher)); + addressResolver.setAsyncDeployer(address(asyncDeployer)); + addressResolver.setDefaultAuctionManager(address(auctionManager)); + addressResolver.setFeesManager(address(feesManager)); + addressResolver.setDeployForwarder(address(deployForwarder)); + + requestHandler.setPrecompile(WRITE, writePrecompile); + requestHandler.setPrecompile(READ, readPrecompile); + requestHandler.setPrecompile(SCHEDULE, schedulePrecompile); + + watcher.setCoreContracts( + address(requestHandler), + address(configurations), + address(promiseResolver) + ); + + vm.stopPrank(); + + vm.startPrank(socketOwner); + arbConfig.cctpSwitchboard.addRemoteEndpoint( + optChainSlug, + addressToBytes32(address(optConfig.cctpSwitchboard)), + optCCTPDomain + ); + optConfig.cctpSwitchboard.addRemoteEndpoint( + arbChainSlug, + addressToBytes32(address(arbConfig.cctpSwitchboard)), + arbCCTPDomain + ); + + arbConfig.messageSwitchboard.setSiblingConfig( + optChainSlug, + msgSbFees, + toBytes32Format(address(optConfig.socket)), + toBytes32Format(address(optConfig.messageSwitchboard)) + ); + optConfig.messageSwitchboard.setSiblingConfig( + arbChainSlug, + msgSbFees, + toBytes32Format(address(arbConfig.socket)), + toBytes32Format(address(arbConfig.messageSwitchboard)) + ); + vm.stopPrank(); + + // transfer eth to fees pool for native fee payouts + vm.deal(address(feesPool), 100000 ether); + + _connectCorePlugs(); + _setupTransmitter(); + } + + function _setupTransmitter() internal { + vm.startPrank(transmitterEOA); + arbConfig.testUSDC.mint(address(transmitterEOA), 100 ether); + arbConfig.testUSDC.approve(address(arbConfig.feesPlug), 100 ether); + + arbConfig.feesPlug.depositCreditAndNative( + address(arbConfig.testUSDC), + address(transmitterEOA), + 100 ether + ); + + feesManager.approve(address(auctionManager), type(uint256).max); + vm.stopPrank(); + } + + function _connectCorePlugs() internal { + AppGatewayConfig[] memory configs = new AppGatewayConfig[](4); + configs[0] = AppGatewayConfig({ + chainSlug: arbChainSlug, + plug: toBytes32Format(address(arbConfig.feesPlug)), + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(feesManager)), + switchboardId: arbConfig.switchboard.switchboardId() + }) + }); + configs[1] = AppGatewayConfig({ + chainSlug: optChainSlug, + plug: toBytes32Format(address(optConfig.feesPlug)), + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(feesManager)), + switchboardId: optConfig.switchboard.switchboardId() + }) + }); + configs[2] = AppGatewayConfig({ + chainSlug: arbChainSlug, + plug: toBytes32Format(address(arbConfig.contractFactoryPlug)), + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(writePrecompile)), + switchboardId: arbConfig.switchboard.switchboardId() + }) + }); + configs[3] = AppGatewayConfig({ + chainSlug: optChainSlug, + plug: toBytes32Format(address(optConfig.contractFactoryPlug)), + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(writePrecompile)), + switchboardId: optConfig.switchboard.switchboardId() + }) + }); + + watcherMultiCall( + address(configurations), + abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) + ); + } + + 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_, + triggerPrefix: (uint256(chainSlug_) << 224) | + (uint256(uint160(address(socket))) << 64), + socket: socket, + socketFeeManager: new SocketFeeManager(socketOwner, socketFees), + switchboard: new FastSwitchboard(chainSlug_, socket, socketOwner), + cctpSwitchboard: new CCTPSwitchboard( + chainSlug_, + socket, + socketOwner, + address(cctpMessageTransmitter) + ), + messageSwitchboard: new MessageSwitchboard(chainSlug_, socket, socketOwner), + cctpMessageTransmitter: cctpMessageTransmitter, + socketBatcher: new SocketBatcher(socketOwner, socket), + contractFactoryPlug: new ContractFactoryPlug(address(socket), socketOwner), + feesPlug: new FeesPlug(address(socket), socketOwner), + testUSDC: new TestUSDC("USDC", "USDC", 6, socketOwner, 1000000000000000000000000) + }); + } + + function _configureChain(uint32 chainSlug_) internal { + SocketContracts memory socketConfig = getSocketConfig(chainSlug_); + Socket socket = socketConfig.socket; + FastSwitchboard switchboard = socketConfig.switchboard; + CCTPSwitchboard cctpSwitchboard = socketConfig.cctpSwitchboard; + MessageSwitchboard messageSwitchboard = socketConfig.messageSwitchboard; + FeesPlug feesPlug = socketConfig.feesPlug; + ContractFactoryPlug contractFactoryPlug = socketConfig.contractFactoryPlug; + + vm.startPrank(socketOwner); + // socket + socket.grantRole(GOVERNANCE_ROLE, address(socketOwner)); + socket.grantRole(RESCUE_ROLE, address(socketOwner)); + socket.grantRole(SWITCHBOARD_DISABLER_ROLE, address(socketOwner)); + + // switchboard + switchboard.registerSwitchboard(); + switchboard.grantRole(WATCHER_ROLE, watcherEOA); + switchboard.grantRole(RESCUE_ROLE, address(socketOwner)); + + cctpSwitchboard.registerSwitchboard(); + cctpSwitchboard.grantRole(WATCHER_ROLE, watcherEOA); + + messageSwitchboard.registerSwitchboard(); + messageSwitchboard.grantRole(WATCHER_ROLE, watcherEOA); + + feesPlug.grantRole(RESCUE_ROLE, address(socketOwner)); + feesPlug.whitelistToken(address(socketConfig.testUSDC)); + feesPlug.connectSocket( + toBytes32Format(address(feesManager)), + address(socket), + switchboard.switchboardId() + ); + + contractFactoryPlug.grantRole(RESCUE_ROLE, address(socketOwner)); + contractFactoryPlug.connectSocket( + toBytes32Format(address(writePrecompile)), + address(socket), + switchboard.switchboardId() + ); + vm.stopPrank(); + + vm.startPrank(watcherEOA); + configurations.setSocket(chainSlug_, toBytes32Format(address(socket))); + configurations.setSwitchboard(chainSlug_, FAST, switchboard.switchboardId()); + configurations.setSwitchboard(chainSlug_, CCTP, cctpSwitchboard.switchboardId()); + + // plugs + feesManager.setFeesPlug(chainSlug_, toBytes32Format(address(feesPlug))); + + // precompiles + writePrecompile.updateChainMaxMsgValueLimits(chainSlug_, maxMsgValueLimit); + writePrecompile.setContractFactoryPlugs( + chainSlug_, + toBytes32Format(address(contractFactoryPlug)) + ); + vm.stopPrank(); + } + + function _deployEVMxCore() internal { + proxyFactory = new ERC1967Factory(); + feesPool = new FeesPool(watcherEOA); + + // Deploy implementations for upgradeable contracts + feesManagerImpl = new FeesManager(); + addressResolverImpl = new AddressResolver(); + asyncDeployerImpl = new AsyncDeployer(); + watcherImpl = new Watcher(); + auctionManagerImpl = new AuctionManager(); + deployForwarderImpl = new DeployForwarder(); + configurationsImpl = new Configurations(); + promiseResolverImpl = new PromiseResolver(); + requestHandlerImpl = new RequestHandler(); + writePrecompileImpl = new WritePrecompile(); + + // Deploy and initialize proxies + address addressResolverProxy = _deployAndVerifyProxy( + address(addressResolverImpl), + watcherEOA, + abi.encodeWithSelector(AddressResolver.initialize.selector, watcherEOA) + ); + addressResolver = AddressResolver(addressResolverProxy); + + address feesManagerProxy = _deployAndVerifyProxy( + address(feesManagerImpl), + watcherEOA, + abi.encodeWithSelector( + FeesManager.initialize.selector, + evmxSlug, + address(addressResolver), + address(feesPool), + watcherEOA, + writeFees, + FAST + ) + ); + feesManager = FeesManager(feesManagerProxy); + + address asyncDeployerProxy = _deployAndVerifyProxy( + address(asyncDeployerImpl), + watcherEOA, + abi.encodeWithSelector( + AsyncDeployer.initialize.selector, + watcherEOA, + address(addressResolver) + ) + ); + asyncDeployer = AsyncDeployer(asyncDeployerProxy); + + address auctionManagerProxy = _deployAndVerifyProxy( + address(auctionManagerImpl), + watcherEOA, + abi.encodeWithSelector( + AuctionManager.initialize.selector, + evmxSlug, + uint128(bidTimeout), + maxReAuctionCount, + auctionEndDelaySeconds, + address(addressResolver), + watcherEOA + ) + ); + auctionManager = AuctionManager(auctionManagerProxy); + + address deployForwarderProxy = _deployAndVerifyProxy( + address(deployForwarderImpl), + watcherEOA, + abi.encodeWithSelector( + DeployForwarder.initialize.selector, + watcherEOA, + address(addressResolver), + FAST + ) + ); + deployForwarder = DeployForwarder(deployForwarderProxy); + + address watcherProxy = _deployAndVerifyProxy( + address(watcherImpl), + watcherEOA, + abi.encodeWithSelector( + Watcher.initialize.selector, + evmxSlug, + watcherEOA, + address(addressResolver), + transmitterEOA, + bytes32(0), // transmitterSolana - using 0 for now + triggerFees + ) + ); + watcher = Watcher(watcherProxy); + + address requestHandlerProxy = _deployAndVerifyProxy( + address(requestHandlerImpl), + watcherEOA, + abi.encodeWithSelector( + RequestHandler.initialize.selector, + watcherEOA, + address(addressResolver) + ) + ); + requestHandler = RequestHandler(requestHandlerProxy); + + address configurationsProxy = _deployAndVerifyProxy( + address(configurationsImpl), + watcherEOA, + abi.encodeWithSelector(Configurations.initialize.selector, address(watcher), watcherEOA) + ); + configurations = Configurations(configurationsProxy); + + address writePrecompileProxy = _deployAndVerifyProxy( + address(writePrecompileImpl), + watcherEOA, + abi.encodeWithSelector( + WritePrecompile.initialize.selector, + watcherEOA, + address(watcher), + writeFees, + expiryTime + ) + ); + writePrecompile = WritePrecompile(writePrecompileProxy); + + address promiseResolverProxy = _deployAndVerifyProxy( + address(promiseResolverImpl), + watcherEOA, + abi.encodeWithSelector(PromiseResolver.initialize.selector, address(watcher)) + ); + promiseResolver = PromiseResolver(promiseResolverProxy); + + // non proxy contracts + readPrecompile = new ReadPrecompile(address(watcher), readFees, expiryTime); + schedulePrecompile = new SchedulePrecompile( + address(watcher), + maxScheduleDelayInSeconds, + scheduleFeesPerSecond, + scheduleCallbackFees, + expiryTime + ); + } + + function _deployAndVerifyProxy( + address implementation_, + address owner_, + bytes memory data_ + ) internal returns (address) { + vm.expectEmit(true, true, true, false); + emit Initialized(version); + return address(proxyFactory.deployAndCall(implementation_, owner_, data_)); + } + + function getSocketConfig(uint32 chainSlug_) internal view returns (SocketContracts memory) { + return chainSlug_ == arbChainSlug ? arbConfig : optConfig; + } + + function watcherMultiCall(address contractAddress_, bytes memory data_) internal { + WatcherMultiCallParams[] memory params = new WatcherMultiCallParams[](1); + params[0] = WatcherMultiCallParams({ + contractAddress: contractAddress_, + data: data_, + nonce: watcherNonce, + signature: _createWatcherSignature(contractAddress_, data_) + }); + watcherNonce++; + watcher.watcherMultiCall(params); + } + + function _createWatcherSignature( + address contractAddress_, + bytes memory data_ + ) internal view returns (bytes memory) { + bytes32 digest = keccak256( + abi.encode(address(watcher), evmxSlug, watcherNonce, contractAddress_, data_) + ); + return createSignature(digest, watcherPrivateKey); + } + + function createSignature( + bytes32 digest_, + uint256 privateKey_ + ) public pure returns (bytes memory sig) { + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest); + sig = new bytes(65); + bytes1 v32 = bytes1(sigV); + assembly { + mstore(add(sig, 96), v32) + mstore(add(sig, 32), sigR) + mstore(add(sig, 64), sigS) + } + } + + function predictAsyncPromiseAddress( + address invoker_, + address forwarder_ + ) internal returns (address) { + bytes memory asyncPromiseBytecode = type(AsyncPromise).creationCode; + bytes memory constructorArgs = abi.encode(invoker_, forwarder_, address(addressResolver)); + bytes memory combinedBytecode = abi.encodePacked(asyncPromiseBytecode, constructorArgs); + + bytes32 salt = keccak256(abi.encodePacked(constructorArgs, asyncPromiseCounter++)); + + bytes32 hash = keccak256( + abi.encodePacked( + bytes1(0xff), + address(addressResolver), + salt, + keccak256(combinedBytecode) + ) + ); + + return address(uint160(uint256(hash))); + } +} + +contract FeesSetup is DeploySetup { + event Deposited( + uint32 indexed chainSlug, + address indexed token, + address indexed appGateway, + uint256 creditAmount, + uint256 nativeAmount + ); + event CreditsWrapped(address indexed consumeFrom, uint256 amount); + event CreditsUnwrapped(address indexed consumeFrom, uint256 amount); + event CreditsTransferred(address indexed from, address indexed to, uint256 amount); + + function deploy() internal { + _deploy(); + + depositNativeAndCredits(arbChainSlug, 100 ether, 100 ether, address(transmitterEOA)); + approve(address(auctionManager), address(transmitterEOA)); + } + + function depositNativeAndCredits( + uint32 chainSlug_, + uint256 credits_, + uint256 native_, + address user_ + ) internal { + depositNativeAndCreditsWithData(chainSlug_, credits_, native_, user_, user_); + } + + // mints test token and deposits the given native and credits to given `user_` + function depositNativeAndCreditsWithData( + uint32 chainSlug_, + uint256 credits_, + uint256 native_, + address user_, + address receiver_ + ) internal { + SocketContracts memory socketConfig = getSocketConfig(chainSlug_); + TestUSDC token = socketConfig.testUSDC; + + uint256 userBalance = token.balanceOf(user_); + uint256 feesPlugBalance = token.balanceOf(address(socketConfig.feesPlug)); + + token.mint(address(user_), 100 ether); + assertEq( + token.balanceOf(user_), + userBalance + 100 ether, + "User should have 100 more test tokens" + ); + + vm.startPrank(user_); + token.approve(address(socketConfig.feesPlug), 100 ether); + socketConfig.feesPlug.depositCreditAndNative(address(token), user_, 100 ether); + vm.stopPrank(); + + assertEq( + token.balanceOf(address(socketConfig.feesPlug)), + feesPlugBalance + 100 ether, + "Fees plug should have 100 more test tokens" + ); + + // uint256 currentCredits = feesManager.balanceOf(user_); + // uint256 currentNative = address(user_).balance; + + vm.expectEmit(true, true, true, false); + emit Deposited(chainSlug_, address(token), receiver_, credits_, native_); + + watcherMultiCall( + address(feesManager), + abi.encodeWithSelector( + Credit.deposit.selector, + chainSlug_, + address(token), + receiver_, + native_, + credits_ + ) + ); + + // assertEq( + // feesManager.balanceOf(user_), + // currentCredits + credits_, + // "User should have more credits" + // ); + // assertEq(address(user_).balance, currentNative + native_, "User should have more native"); + } + + function approve(address appGateway_, address user_) internal { + uint256 approval = feesManager.allowance(user_, appGateway_); + if (approval > 0) return; + + hoax(user_); + feesManager.approve(appGateway_, type(uint256).max); + + assertEq( + feesManager.isApproved(user_, appGateway_), + true, + "App gateway should be approved" + ); + } + + function permit(address appGateway_, address user_, uint256 userPrivateKey_) internal { + bool approval = feesManager.isApproved(user_, appGateway_); + if (approval) return; + + uint256 value = type(uint256).max; + uint256 deadline = block.timestamp + 1 hours; + bytes32 permitTypehash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + bytes32 structHash = keccak256( + abi.encode( + permitTypehash, + user_, + appGateway_, + value, + feesManager.nonces(user_), + deadline + ) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", feesManager.DOMAIN_SEPARATOR(), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey_, digest); + + feesManager.permit(user_, appGateway_, value, deadline, v, r, s); + assertEq( + feesManager.isApproved(user_, appGateway_), + true, + "App gateway should be approved" + ); + } +} + +contract AuctionSetup is FeesSetup { + event BidPlaced(uint40 requestCount, Bid bid); + event AuctionStarted(uint40 requestCount); + event AuctionEnded(uint40 requestCount, Bid winningBid); + + function getBidAmount(uint40 requestCount) internal view returns (uint256) { + return watcher.getRequestParams(requestCount).requestFeesDetails.maxFees / 2; + } + + function placeBid(uint40 requestCount) internal { + uint256 bidAmount = getBidAmount(requestCount); + + bytes memory transmitterSignature = createSignature( + keccak256(abi.encode(address(auctionManager), evmxSlug, requestCount, bidAmount, "")), + transmitterPrivateKey + ); + + if (auctionEndDelaySeconds == 0) { + vm.expectEmit(true, true, true, false); + emit AuctionEnded( + requestCount, + Bid({fee: bidAmount, transmitter: transmitterEOA, extraData: bytes("")}) + ); + } else { + vm.expectEmit(true, true, true, false); + emit AuctionStarted(requestCount); + } + + vm.expectEmit(true, true, true, false); + emit BidPlaced( + requestCount, + Bid({transmitter: transmitterEOA, fee: bidAmount, extraData: bytes("")}) + ); + auctionManager.bid(requestCount, bidAmount, transmitterSignature, bytes("")); + } + + function endAuction(uint40 requestCount_) internal { + if (auctionEndDelaySeconds == 0) return; + + // todo: handle other cases + + uint256 bidAmount = getBidAmount(requestCount_); + // bytes memory watcherSignature = createSignature( + // keccak256(abi.encode(address(watcher), evmxSlug, requestCount_, bidAmount, "")), + // watcherPrivateKey + // ); + + vm.expectEmit(true, true, true, true); + emit AuctionEnded( + requestCount_, + Bid({fee: bidAmount, transmitter: transmitterEOA, extraData: bytes("")}) + ); + + // promiseResolver.resolvePromises(); + } + + function bidAndEndAuction(uint40 requestCount) internal { + placeBid(requestCount); + endAuction(requestCount); + } + + // tests: + // bid and end auction with delay + // bid and end auction with delay and expire bid +} + +contract WatcherSetup is AuctionSetup { + event ReadRequested(Transaction transaction, uint256 readAtBlockNumber, bytes32 payloadId); + event ScheduleRequested(bytes32 payloadId, uint256 deadline); + event ScheduleResolved(bytes32 payloadId); + event WriteProofRequested( + address transmitter, + bytes32 digest, + bytes32 prevBatchDigestHash, + uint256 deadline, + PayloadParams payloadParams + ); + event WriteProofUploaded(bytes32 indexed payloadId, bytes proof); + + function executeDeployMultiChain( + IAppGateway appGateway_, + uint32[] memory chainSlugs_, + bytes32[] memory contractIds_ + ) internal returns (uint40 requestCount) { + return _executeDeploy(appGateway_, chainSlugs_, contractIds_); + } + + function executeDeploy( + IAppGateway appGateway_, + uint32 chainSlug_, + bytes32[] memory contractIds_ + ) internal returns (uint40 requestCount) { + uint32[] memory chainSlugs = new uint32[](1); + chainSlugs[0] = chainSlug_; + return _executeDeploy(appGateway_, chainSlugs, contractIds_); + } + + function _executeDeploy( + IAppGateway appGateway_, + uint32[] memory chainSlugs_, + bytes32[] memory contractIds_ + ) internal returns (uint40 requestCount) { + requestCount = executeRequest(); + for (uint i = 0; i < chainSlugs_.length; i++) { + setupGatewayAndPlugs(chainSlugs_[i], appGateway_, contractIds_); + } + } + + function executeRequest() internal returns (uint40 requestCount) { + requestCount = watcher.getCurrentRequestCount(); + requestCount = requestCount == 0 ? 0 : requestCount - 1; + + executeRequest(requestCount); + } + + function executeRequest(uint40 requestCount) internal { + RequestParams memory requestParams = requestHandler.getRequest(requestCount); + uint40[] memory batches = requestHandler.getRequestBatchIds(requestCount); + + // bids and executes schedule request if created for endAuction + if (requestParams.writeCount != 0) bidAndEndAuction(requestCount); + + bool isRequestExecuted; + for (uint i = 0; i < batches.length; i++) { + isRequestExecuted = _processBatch(batches[i]); + if (!isRequestExecuted) break; + } + + requestParams = requestHandler.getRequest(requestCount); + assertEq(requestParams.requestTrackingParams.isRequestExecuted, isRequestExecuted); + } + + function _processBatch(uint40 batchCount_) internal returns (bool) { + bytes32[] memory payloadIds = requestHandler.getBatchPayloadIds(batchCount_); + + PromiseReturnData[] memory promiseReturnData = new PromiseReturnData[](1); + bool success; + for (uint i = 0; i < payloadIds.length; i++) { + PayloadParams memory payloadParams = watcher.getPayloadParams(payloadIds[i]); + + if (payloadParams.callType == READ) { + (success, promiseReturnData[0]) = _processRead(payloadParams); + } else if (payloadParams.callType == WRITE) { + (success, promiseReturnData[0]) = _processWrite(payloadParams); + } else if (payloadParams.callType == SCHEDULE) { + vm.warp(payloadParams.deadline - expiryTime); + promiseReturnData[0] = PromiseReturnData({ + exceededMaxCopy: false, + payloadId: payloadParams.payloadId, + returnData: bytes("") + }); + success = true; + } + + if (success) { + _resolvePromise(promiseReturnData); + } else { + vm.warp(payloadParams.deadline); + _markRevert(promiseReturnData[0], true); + return false; + } + } + + return true; + } + + function _processRead( + PayloadParams memory payloadParams + ) internal returns (bool success, PromiseReturnData memory promiseReturnData) { + (Transaction memory transaction, ) = abi.decode( + payloadParams.precompileData, + (Transaction, uint256) + ); + + bytes memory returnData; + address target = fromBytes32Format(transaction.target); + (success, returnData) = target.call(transaction.payload); + promiseReturnData = PromiseReturnData({ + exceededMaxCopy: false, + payloadId: payloadParams.payloadId, + returnData: returnData + }); + } + + function _processWrite( + PayloadParams memory payloadParams + ) internal returns (bool success, PromiseReturnData memory promiseReturnData) { + bytes32 payloadId = payloadParams.payloadId; + + ( + uint32 chainSlug, + uint64 switchboard, + bytes32 digest, + DigestParams memory digestParams + ) = _validateAndGetDigest(payloadParams); + + bytes memory watcherProof = _uploadProof(payloadId, digest, switchboard, chainSlug); + + return + _executeWrite( + chainSlug, + switchboard, + digest, + digestParams, + payloadParams, + watcherProof + ); + } + + function _uploadProof( + bytes32 payloadId, + bytes32 digest, + uint64 switchboard, + uint32 chainSlug + ) internal returns (bytes memory proof) { + address sbAddress = getSocketConfig(chainSlug).socket.switchboardAddresses(switchboard); + proof = createSignature( + // create sigDigest which get signed by watcher + keccak256(abi.encodePacked(toBytes32Format(sbAddress), chainSlug, digest)), + watcherPrivateKey + ); + + vm.expectEmit(true, true, true, false); + emit WriteProofUploaded(payloadId, proof); + watcherMultiCall( + address(writePrecompile), + abi.encodeWithSelector(WritePrecompile.uploadProof.selector, payloadId, proof) + ); + assertEq(writePrecompile.watcherProofs(payloadId), proof); + } + + function _validateAndGetDigest( + PayloadParams memory payloadParams + ) + internal + view + returns ( + uint32 chainSlug, + uint64 switchboard, + bytes32 digest, + DigestParams memory digestParams + ) + { + ( + address appGateway, + Transaction memory transaction, + , + uint256 gasLimit, + uint256 value, + uint64 switchboard_ + ) = abi.decode( + payloadParams.precompileData, + (address, Transaction, WriteFinality, uint256, uint256, uint64) + ); + + chainSlug = transaction.chainSlug; + switchboard = switchboard_; + + bytes32 prevBatchDigestHash = writePrecompile.getPrevBatchDigestHash( + uint40(payloadParams.payloadPointer >> 120), + uint40(payloadParams.payloadPointer >> 80) + ); + digestParams = DigestParams( + toBytes32Format(address(getSocketConfig(transaction.chainSlug).socket)), + toBytes32Format(transmitterEOA), + payloadParams.payloadId, + payloadParams.deadline, + payloadParams.callType, + gasLimit, + value, + transaction.payload, + transaction.target, + toBytes32Format(appGateway), + prevBatchDigestHash, + bytes("") + ); + + digest = writePrecompile.getDigest(digestParams); + assertEq(writePrecompile.digestHashes(payloadParams.payloadId), digest); + } + + function _executeWrite( + uint32 chainSlug, + uint64 switchboard, + bytes32 digest, + DigestParams memory digestParams, + PayloadParams memory payloadParams, + bytes memory watcherProof + ) internal returns (bool success, PromiseReturnData memory promiseReturnData) { + // this is a signature for the socket batcher (only used for EVM) + bytes memory transmitterSig = createSignature( + keccak256( + abi.encode(address(getSocketConfig(chainSlug).socket), payloadParams.payloadId) + ), + transmitterPrivateKey + ); + bytes memory returnData; + ExecuteParams memory executeParams = ExecuteParams({ + callType: digestParams.callType, + deadline: digestParams.deadline, + gasLimit: digestParams.gasLimit, + value: digestParams.value, + payload: digestParams.payload, + target: fromBytes32Format(digestParams.target), + payloadPointer: payloadParams.payloadPointer, + prevBatchDigestHash: digestParams.prevBatchDigestHash, + extraData: digestParams.extraData + }); + + if (switchboard == getSocketConfig(chainSlug).switchboard.switchboardId()) { + (success, returnData) = getSocketConfig(chainSlug).socketBatcher.attestAndExecute( + executeParams, + getSocketConfig(chainSlug).switchboard.switchboardId(), + digest, + watcherProof, + transmitterSig, + transmitterEOA + ); + } else if (switchboard == getSocketConfig(chainSlug).cctpSwitchboard.switchboardId()) { + (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, + getSocketConfig(chainSlug).cctpSwitchboard.switchboardId() + ); + } + + function _prepareCCTPBatchData( + uint32 chainSlug, + PayloadParams memory payloadParams + ) internal view returns (CCTPBatchParams memory cctpBatchParams) { + uint40[] memory requestBatchIds = requestHandler.getRequestBatchIds( + uint40(payloadParams.payloadPointer >> 120) + ); + uint40 currentBatchCount = uint40(payloadParams.payloadPointer >> 80); + + 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( + address(promiseResolver), + abi.encodeWithSelector(PromiseResolver.resolvePromises.selector, promiseReturnData) + ); + } + + function _markRevert( + PromiseReturnData memory promiseReturnData, + bool isRevertingOnchain_ + ) internal { + watcherMultiCall( + address(promiseResolver), + abi.encodeWithSelector( + PromiseResolver.markRevert.selector, + promiseReturnData, + isRevertingOnchain_ + ) + ); + } + + function setupGatewayAndPlugs( + uint32 chainSlug_, + IAppGateway appGateway_, + bytes32[] memory contractIds_ + ) internal { + // 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 = _countValidPlugs(appGateway_, contractIds_, chainSlug_); + + // Create array with exact size needed + AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount); + _populateConfigs(configs, appGateway_, contractIds_, chainSlug_); + + // Only call watcher if we have valid configs + if (validPlugCount > 0) { + watcherMultiCall( + address(configurations), + abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) + ); + } + } + + function _countValidPlugs( + IAppGateway appGateway_, + bytes32[] memory contractIds_, + uint32 chainSlug_ + ) internal view returns (uint256 validCount) { + for (uint i = 0; i < contractIds_.length; i++) { + bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); + if (plug != bytes32(0)) { + validCount++; + } + } + } + + function _populateConfigs( + AppGatewayConfig[] memory configs, + IAppGateway appGateway_, + bytes32[] memory contractIds_, + uint32 chainSlug_ + ) internal view { + uint256 configIndex = 0; + uint64 switchboardId = configurations.switchboards(chainSlug_, appGateway_.sbType()); + + for (uint i = 0; i < contractIds_.length; i++) { + bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); + if (plug != bytes32(0)) { + configs[configIndex] = AppGatewayConfig({ + plug: plug, + chainSlug: chainSlug_, + plugConfig: PlugConfigGeneric({ + appGatewayId: toBytes32Format(address(appGateway_)), + switchboardId: switchboardId + }) + }); + configIndex++; + } + } + } +} + +contract AppGatewayBaseSetup is WatcherSetup { + function getOnChainAndForwarderAddresses( + uint32 chainSlug_, + bytes32 contractId_, + IAppGateway appGateway_ + ) internal view returns (bytes32, address) { + bytes32 onChainContract = appGateway_.getOnChainAddress(contractId_, chainSlug_); + address forwarder = appGateway_.forwarderAddresses(contractId_, chainSlug_); + return (onChainContract, forwarder); + } + + // todo: add checks for request params and payload params created to match what is expected + + function checkRequestParams( + uint40 requestCount, + RequestParams memory expectedRequest + ) internal view { + RequestParams memory actualRequest = watcher.getRequestParams(requestCount); + // RequestParams checks + assertEq( + actualRequest.appGateway, + expectedRequest.appGateway, + "Request: appGateway mismatch" + ); + assertEq( + actualRequest.auctionManager, + expectedRequest.auctionManager, + "Request: auctionManager mismatch" + ); + assertEq( + actualRequest.writeCount, + expectedRequest.writeCount, + "Request: writeCount mismatch" + ); + assertEq( + keccak256(actualRequest.onCompleteData), + keccak256(expectedRequest.onCompleteData), + "Request: onCompleteData mismatch" + ); + // Nested struct checks (RequestTrackingParams) + assertEq( + actualRequest.requestTrackingParams.isRequestCancelled, + expectedRequest.requestTrackingParams.isRequestCancelled, + "RequestTrackingParams: isRequestCancelled mismatch" + ); + assertEq( + actualRequest.requestTrackingParams.isRequestExecuted, + expectedRequest.requestTrackingParams.isRequestExecuted, + "RequestTrackingParams: isRequestExecuted mismatch" + ); + assertEq( + actualRequest.requestTrackingParams.currentBatch, + expectedRequest.requestTrackingParams.currentBatch, + "RequestTrackingParams: currentBatch mismatch" + ); + assertEq( + actualRequest.requestTrackingParams.currentBatchPayloadsLeft, + expectedRequest.requestTrackingParams.currentBatchPayloadsLeft, + "RequestTrackingParams: currentBatchPayloadsLeft mismatch" + ); + assertEq( + actualRequest.requestTrackingParams.payloadsRemaining, + expectedRequest.requestTrackingParams.payloadsRemaining, + "RequestTrackingParams: payloadsRemaining mismatch" + ); + // Nested struct checks (RequestFeesDetails) + assertEq( + actualRequest.requestFeesDetails.maxFees, + expectedRequest.requestFeesDetails.maxFees, + "RequestFeesDetails: maxFees mismatch" + ); + assertEq( + actualRequest.requestFeesDetails.consumeFrom, + expectedRequest.requestFeesDetails.consumeFrom, + "RequestFeesDetails: consumeFrom mismatch" + ); + assertEq( + actualRequest.requestFeesDetails.winningBid.fee, + expectedRequest.requestFeesDetails.winningBid.fee, + "RequestFeesDetails: winningBid.fee mismatch" + ); + assertEq( + actualRequest.requestFeesDetails.winningBid.transmitter, + expectedRequest.requestFeesDetails.winningBid.transmitter, + "RequestFeesDetails: winningBid.transmitter mismatch" + ); + assertEq( + keccak256(actualRequest.requestFeesDetails.winningBid.extraData), + keccak256(expectedRequest.requestFeesDetails.winningBid.extraData), + "RequestFeesDetails: winningBid.extraData mismatch" + ); + } + + function checkPayloadParams(PayloadParams[] memory expectedPayloads) internal view { + for (uint i = 0; i < expectedPayloads.length; i++) { + PayloadParams memory expectedPayload = expectedPayloads[i]; + PayloadParams memory actualPayload = watcher.getPayloadParams( + expectedPayload.payloadId + ); + // PayloadParams checks + assertEq( + actualPayload.payloadPointer, + expectedPayload.payloadPointer, + "Payload: payloadPointer mismatch" + ); + assertEq( + actualPayload.callType, + expectedPayload.callType, + "Payload: callType mismatch" + ); + assertEq( + actualPayload.asyncPromise, + expectedPayload.asyncPromise, + "Payload: asyncPromise mismatch" + ); + assertEq( + actualPayload.appGateway, + expectedPayload.appGateway, + "Payload: appGateway mismatch" + ); + assertEq( + actualPayload.payloadId, + expectedPayload.payloadId, + "Payload: payloadId mismatch" + ); + assertEq( + actualPayload.resolvedAt, + expectedPayload.resolvedAt, + "Payload: resolvedAt mismatch" + ); + assertEq( + actualPayload.deadline, + expectedPayload.deadline, + "Payload: deadline mismatch" + ); + assertEq( + keccak256(actualPayload.precompileData), + keccak256(expectedPayload.precompileData), + "Payload: precompileData mismatch" + ); + } + } + + function _encodeTriggerId(address socket_, uint32 chainSlug_) internal returns (bytes32) { + return + bytes32( + (uint256(chainSlug_) << 224) | (uint256(uint160(socket_)) << 64) | triggerCounter++ + ); + } +} + +contract MessageSwitchboardSetup is DeploySetup { + uint256 msgSbGasLimit = 100000; + + event TriggerProcessed( + uint32 optChainSlug, + uint256 switchboardFees, + bytes32 digest, + DigestParams digestParams + ); + + function _getTriggerData( + MessagePlugBase srcPlug_, + MessagePlugBase dstPlug_, + SocketContracts memory srcSocketConfig_, + SocketContracts memory dstSocketConfig_, + bytes memory payload_ + ) internal view returns (uint160 payloadPointer, DigestParams memory digestParams) { + bytes32 triggerId = srcPlug_.getNextTriggerId(srcSocketConfig_.chainSlug); + uint40 payloadCounter = srcSocketConfig_.messageSwitchboard.payloadCounter(); + + payloadPointer = + (uint160(srcSocketConfig_.chainSlug) << 120) | + (uint160(uint64(uint256(triggerId))) << 80) | + payloadCounter; + + bytes32 payloadId = createPayloadId( + payloadPointer, + dstSocketConfig_.messageSwitchboard.switchboardId(), + dstSocketConfig_.chainSlug + ); + + digestParams = _createDigestParams( + srcSocketConfig_.chainSlug, + address(srcPlug_), + address(dstPlug_), + address(dstSocketConfig_.socket), + payloadId, + triggerId, + payload_ + ); + } + + function _executeOnDestination( + DigestParams memory digestParams_, + uint160 payloadPointer_ + ) internal { + _attestPayload(digestParams_); + _execute(digestParams_, payloadPointer_); + } + + // Helper function to attest a payload + function _attestPayload(DigestParams memory digestParams_) internal { + bytes32 attestDigest = keccak256( + abi.encodePacked( + toBytes32Format(address(optConfig.messageSwitchboard)), + optConfig.chainSlug, + _createDigest(digestParams_) + ) + ); + + bytes memory signature = createSignature(attestDigest, watcherPrivateKey); + optConfig.messageSwitchboard.attest(digestParams_, signature); + } + + function _createDigestParams( + uint32 srcChainSlug_, + address srcPlug_, + address dstPlug_, + address dstSocket_, + bytes32 payloadId_, + bytes32 triggerId_, + bytes memory payload_ + ) internal view returns (DigestParams memory digestParams) { + bytes memory extraData = abi.encode(srcChainSlug_, toBytes32Format(srcPlug_)); + digestParams = DigestParams({ + socket: toBytes32Format(dstSocket_), + transmitter: bytes32(0), + payloadId: payloadId_, + deadline: block.timestamp + 3600, + callType: WRITE, + gasLimit: msgSbGasLimit, + value: uint256(0), + payload: payload_, + target: toBytes32Format(dstPlug_), + appGatewayId: APP_GATEWAY_ID, + prevBatchDigestHash: triggerId_, + extraData: extraData + }); + } + + function _createDigest(DigestParams memory digest_) internal pure returns (bytes32) { + return + keccak256( + abi.encodePacked( + digest_.socket, + digest_.transmitter, + digest_.payloadId, + digest_.deadline, + digest_.callType, + digest_.gasLimit, + digest_.value, + digest_.payload, + digest_.target, + digest_.appGatewayId, + digest_.prevBatchDigestHash, + digest_.extraData + ) + ); + } + + // Helper function to execute on destination chain + function _execute(DigestParams memory digestParams_, uint160 payloadPointer_) internal { + // this is a signature for the socket batcher (only used for EVM) + ExecuteParams memory executeParams = ExecuteParams({ + callType: digestParams_.callType, + deadline: digestParams_.deadline, + gasLimit: digestParams_.gasLimit, + value: digestParams_.value, + payload: digestParams_.payload, + target: fromBytes32Format(digestParams_.target), + payloadPointer: payloadPointer_, + prevBatchDigestHash: digestParams_.prevBatchDigestHash, + extraData: digestParams_.extraData + }); + + TransmissionParams memory transmissionParams = TransmissionParams({ + socketFees: 0, + refundAddress: socketOwner, + extraData: bytes(""), + transmitterProof: bytes("") + }); + + optConfig.socket.execute(executeParams, transmissionParams); + } +} + +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/deprecated/test/Utils.t.sol b/deprecated/test/Utils.t.sol new file mode 100644 index 00000000..ff8cef98 --- /dev/null +++ b/deprecated/test/Utils.t.sol @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "../contracts/utils/common/IdUtils.sol"; +import "../contracts/utils/common/Converters.sol"; + +/** + * @title IdUtilsTest + * @dev Tests for IdUtils utility functions + */ +contract IdUtilsTest is Test { + function testCreatePayloadId() public pure { + uint160 payloadPointer = 12345; + uint64 switchboardId = 67890; + uint32 chainSlug = 1; + + bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug); + + // Verify the structure + uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); + uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); + uint160 payloadPointerFromId = uint160(uint256(payloadId)); + + assertEq(chainSlugFromId, chainSlug, "Chain slug should match"); + assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match"); + assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match"); + } + + function testCreatePayloadIdWithZeroValues() public pure { + bytes32 payloadId = createPayloadId(0, 0, 0); + + assertEq(payloadId, bytes32(0), "Payload ID should be zero for zero inputs"); + } + + function testCreatePayloadIdWithMaxValues() public pure { + uint160 maxPayloadPointer = type(uint160).max; + uint64 maxSwitchboardId = type(uint64).max; + uint32 maxChainSlug = type(uint32).max; + + bytes32 payloadId = createPayloadId(maxPayloadPointer, maxSwitchboardId, maxChainSlug); + + // Verify the structure + uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); + uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); + uint160 payloadPointerFromId = uint160(uint256(payloadId)); + + assertEq(chainSlugFromId, maxChainSlug, "Chain slug should match"); + assertEq(switchboardIdFromId, maxSwitchboardId, "Switchboard ID should match"); + assertEq(payloadPointerFromId, maxPayloadPointer, "Payload pointer should match"); + } + + function testCreatePayloadIdFuzz( + uint160 payloadPointer, + uint64 switchboardId, + uint32 chainSlug + ) public pure { + bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug); + + // Verify the structure + uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); + uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); + uint160 payloadPointerFromId = uint160(uint256(payloadId)); + + assertEq(chainSlugFromId, chainSlug, "Chain slug should match"); + assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match"); + assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match"); + } +} + +/** + * @title ConvertersTest + * @dev Tests for Converters utility functions + */ +contract ConvertersTest is Test { + function testToBytes32Format() public pure { + address testAddr = address(0x1234567890123456789012345678901234567890); + bytes32 result = toBytes32Format(testAddr); + + assertEq(result, bytes32(uint256(uint160(testAddr))), "Conversion should be correct"); + } + + function testToBytes32FormatWithZeroAddress() public pure { + bytes32 result = toBytes32Format(address(0)); + + assertEq(result, bytes32(0), "Zero address should convert to zero bytes32"); + } + + function testFromBytes32Format() public pure { + address originalAddr = address(0x1234567890123456789012345678901234567890); + bytes32 bytes32Format = toBytes32Format(originalAddr); + + address convertedAddr = fromBytes32Format(bytes32Format); + + assertEq(convertedAddr, originalAddr, "Conversion should be reversible"); + } + + function testFromBytes32FormatWithZeroAddress() public pure { + bytes32 zeroBytes32 = bytes32(0); + address convertedAddr = fromBytes32Format(zeroBytes32); + + assertEq(convertedAddr, address(0), "Zero bytes32 should convert to zero address"); + } + + function testFromBytes32FormatWithInvalidAddress() public { + // Create a bytes32 with non-zero upper bits + bytes32 invalidBytes32 = bytes32(uint256(1) << 160); + + try this.fromBytes32FormatWrapper(invalidBytes32) { + fail(); + } catch { + // Expected to revert + } + } + + function fromBytes32FormatWrapper( + bytes32 bytes32FormatAddress + ) external pure returns (address) { + return fromBytes32Format(bytes32FormatAddress); + } + + function testFromBytes32FormatWithMaxValidAddress() public pure { + address maxAddr = address(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); + bytes32 bytes32Format = toBytes32Format(maxAddr); + + address convertedAddr = fromBytes32Format(bytes32Format); + + assertEq(convertedAddr, maxAddr, "Max address should convert correctly"); + } + + function testConvertersRoundTrip() public pure { + address originalAddr = address(0xabCDEF1234567890ABcDEF1234567890aBCDeF12); + + bytes32 bytes32Format = toBytes32Format(originalAddr); + address convertedAddr = fromBytes32Format(bytes32Format); + + assertEq(convertedAddr, originalAddr, "Round trip conversion should work"); + } + + function testConvertersFuzz(address addr) public pure { + // Skip addresses that would cause overflow + vm.assume(uint256(uint160(addr)) <= type(uint160).max); + + bytes32 bytes32Format = toBytes32Format(addr); + address convertedAddr = fromBytes32Format(bytes32Format); + + assertEq(convertedAddr, addr, "Fuzz test should pass"); + } +} diff --git a/deprecated/test/apps/Counter.t.sol b/deprecated/test/apps/Counter.t.sol new file mode 100644 index 00000000..8e6783db --- /dev/null +++ b/deprecated/test/apps/Counter.t.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {CounterAppGateway} from "./app-gateways/counter/CounterAppGateway.sol"; +import {Counter} from "./app-gateways/counter/Counter.sol"; +import "../SetupTest.t.sol"; + +contract CounterTest is AppGatewayBaseSetup { + uint256 feesAmount = 0.01 ether; + + bytes32 counterId; + bytes32[] contractIds = new bytes32[](1); + CounterAppGateway counterGateway; + + event CounterScheduleResolved(uint256 creationTimestamp, uint256 executionTimestamp); + + function setUp() public { + deploy(); + + counterGateway = new CounterAppGateway(address(addressResolver), feesAmount); + depositNativeAndCredits(arbChainSlug, 1 ether, 0, address(counterGateway)); + counterId = counterGateway.counter(); + contractIds[0] = counterId; + } + + function deployCounterApp(uint32 chainSlug) internal returns (uint40 requestCount) { + counterGateway.deployContracts(chainSlug); + requestCount = executeDeploy(counterGateway, chainSlug, contractIds); + } + + function testCounterDeployment() external { + deployCounterApp(arbChainSlug); + + (bytes32 onChain, address forwarder) = getOnChainAndForwarderAddresses( + arbChainSlug, + counterId, + counterGateway + ); + + assertEq( + IForwarder(forwarder).getChainSlug(), + arbChainSlug, + "Forwarder chainSlug should be correct" + ); + assertEq( + IForwarder(forwarder).getOnChainAddress(), + onChain, + "Forwarder onChainAddress should be correct" + ); + } + + function testCounterDeploymentWithoutAsync() external { + vm.expectRevert(abi.encodeWithSelector(AsyncModifierNotSet.selector)); + counterGateway.deployContractsWithoutAsync(arbChainSlug); + } + + function testCounterIncrement() external { + deployCounterApp(arbChainSlug); + + (bytes32 arbCounterBytes32, address arbCounterForwarder) = getOnChainAndForwarderAddresses( + arbChainSlug, + counterId, + counterGateway + ); + address arbCounter = fromBytes32Format(arbCounterBytes32); + + uint256 arbCounterBefore = Counter(arbCounter).counter(); + + address[] memory instances = new address[](1); + instances[0] = arbCounterForwarder; + counterGateway.incrementCounters(instances); + executeRequest(); + + assertEq(Counter(arbCounter).counter(), arbCounterBefore + 1); + } + + function testCounterIncrementMultipleChains() public { + deployCounterApp(arbChainSlug); + deployCounterApp(optChainSlug); + + (bytes32 arbCounterBytes32, address arbCounterForwarder) = getOnChainAndForwarderAddresses( + arbChainSlug, + counterId, + counterGateway + ); + address arbCounter = fromBytes32Format(arbCounterBytes32); + (bytes32 optCounterBytes32, address optCounterForwarder) = getOnChainAndForwarderAddresses( + optChainSlug, + counterId, + counterGateway + ); + address optCounter = fromBytes32Format(optCounterBytes32); + + uint256 arbCounterBefore = Counter(arbCounter).counter(); + uint256 optCounterBefore = Counter(optCounter).counter(); + + address[] memory instances = new address[](2); + instances[0] = arbCounterForwarder; + instances[1] = optCounterForwarder; + counterGateway.incrementCounters(instances); + + bool incremented = counterGateway.incremented(); + assertEq(incremented, false); + executeRequest(); + + incremented = counterGateway.incremented(); + assertEq(incremented, true); + + assertEq(Counter(arbCounter).counter(), arbCounterBefore + 1); + assertEq(Counter(optCounter).counter(), optCounterBefore + 1); + } + + function testCounterReadMultipleChains() external { + testCounterIncrementMultipleChains(); + + (, address arbCounterForwarder) = getOnChainAndForwarderAddresses( + arbChainSlug, + counterId, + counterGateway + ); + (, address optCounterForwarder) = getOnChainAndForwarderAddresses( + optChainSlug, + counterId, + counterGateway + ); + + address[] memory instances = new address[](2); + instances[0] = arbCounterForwarder; + instances[1] = optCounterForwarder; + + counterGateway.readCounters(instances); + executeRequest(); + } + + function testCounterSchedule() external { + deployCounterApp(arbChainSlug); + + uint256 creationTimestamp = block.timestamp; + counterGateway.setSchedule(100); + + vm.expectEmit(true, true, true, false); + emit CounterScheduleResolved(creationTimestamp, block.timestamp); + executeRequest(); + + assertLe(block.timestamp, creationTimestamp + 100 + expiryTime); + } +} diff --git a/test/apps/ParallelCounter.t.sol b/deprecated/test/apps/ParallelCounter.t.sol similarity index 100% rename from test/apps/ParallelCounter.t.sol rename to deprecated/test/apps/ParallelCounter.t.sol diff --git a/test/apps/SuperToken.t.sol b/deprecated/test/apps/SuperToken.t.sol similarity index 100% rename from test/apps/SuperToken.t.sol rename to deprecated/test/apps/SuperToken.t.sol diff --git a/test/apps/app-gateways/counter/Counter.sol b/deprecated/test/apps/app-gateways/counter/Counter.sol similarity index 100% rename from test/apps/app-gateways/counter/Counter.sol rename to deprecated/test/apps/app-gateways/counter/Counter.sol diff --git a/test/apps/app-gateways/counter/CounterAppGateway.sol b/deprecated/test/apps/app-gateways/counter/CounterAppGateway.sol similarity index 100% rename from test/apps/app-gateways/counter/CounterAppGateway.sol rename to deprecated/test/apps/app-gateways/counter/CounterAppGateway.sol diff --git a/test/apps/app-gateways/counter/ICounter.sol b/deprecated/test/apps/app-gateways/counter/ICounter.sol similarity index 100% rename from test/apps/app-gateways/counter/ICounter.sol rename to deprecated/test/apps/app-gateways/counter/ICounter.sol diff --git a/test/apps/app-gateways/counter/MessageCounter.sol b/deprecated/test/apps/app-gateways/counter/MessageCounter.sol similarity index 100% rename from test/apps/app-gateways/counter/MessageCounter.sol rename to deprecated/test/apps/app-gateways/counter/MessageCounter.sol diff --git a/test/apps/app-gateways/super-token/ISuperToken.sol b/deprecated/test/apps/app-gateways/super-token/ISuperToken.sol similarity index 100% rename from test/apps/app-gateways/super-token/ISuperToken.sol rename to deprecated/test/apps/app-gateways/super-token/ISuperToken.sol diff --git a/test/apps/app-gateways/super-token/SuperToken.sol b/deprecated/test/apps/app-gateways/super-token/SuperToken.sol similarity index 100% rename from test/apps/app-gateways/super-token/SuperToken.sol rename to deprecated/test/apps/app-gateways/super-token/SuperToken.sol diff --git a/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol b/deprecated/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol similarity index 100% rename from test/apps/app-gateways/super-token/SuperTokenAppGateway.sol rename to deprecated/test/apps/app-gateways/super-token/SuperTokenAppGateway.sol diff --git a/test/evmx/AuctionManager.t.sol b/deprecated/test/evmx/AuctionManager.t.sol similarity index 100% rename from test/evmx/AuctionManager.t.sol rename to deprecated/test/evmx/AuctionManager.t.sol diff --git a/test/evmx/FeesTest.t.sol b/deprecated/test/evmx/FeesTest.t.sol similarity index 100% rename from test/evmx/FeesTest.t.sol rename to deprecated/test/evmx/FeesTest.t.sol diff --git a/test/evmx/ProxyMigration.t.sol b/deprecated/test/evmx/ProxyMigration.t.sol similarity index 99% rename from test/evmx/ProxyMigration.t.sol rename to deprecated/test/evmx/ProxyMigration.t.sol index cb1acfa3..89bd6883 100644 --- a/test/evmx/ProxyMigration.t.sol +++ b/deprecated/test/evmx/ProxyMigration.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.21; import "./ProxyStorage.t.sol"; -import "../mock/MockWatcherPrecompile.sol"; +// import "../mock/MockWatcherPrecompile.sol"; contract MigrationTest is ProxyStorageAssertions { // ERC1967Factory emits this event with both proxy and implementation addresses diff --git a/test/evmx/ProxyStorage.t.sol b/deprecated/test/evmx/ProxyStorage.t.sol similarity index 100% rename from test/evmx/ProxyStorage.t.sol rename to deprecated/test/evmx/ProxyStorage.t.sol diff --git a/test/evmx/Watcher.t.sol b/deprecated/test/evmx/Watcher.t.sol similarity index 98% rename from test/evmx/Watcher.t.sol rename to deprecated/test/evmx/Watcher.t.sol index dafcfbee..c31ad456 100644 --- a/test/evmx/Watcher.t.sol +++ b/deprecated/test/evmx/Watcher.t.sol @@ -3,9 +3,7 @@ pragma solidity ^0.8.21; import "../SetupTest.t.sol"; import "../../contracts/evmx/watcher/Watcher.sol"; -import "../../contracts/evmx/watcher/RequestHandler.sol"; import "../../contracts/evmx/watcher/Configurations.sol"; -import "../../contracts/evmx/watcher/PromiseResolver.sol"; import "../../contracts/evmx/watcher/precompiles/ReadPrecompile.sol"; import "../../contracts/evmx/watcher/precompiles/WritePrecompile.sol"; import "../../contracts/evmx/watcher/precompiles/SchedulePrecompile.sol"; @@ -261,7 +259,7 @@ contract WatcherTest is AppGatewayBaseSetup { } function testRequestHandlerSubmitRequest() public { - QueueParams[] memory queueParams = new QueueParams[](0); + RawPayload[] memory RawPayload = new RawPayload[](0); hoax(watcherAddress); requestHandler.submitRequest( @@ -269,7 +267,7 @@ contract WatcherTest is AppGatewayBaseSetup { address(0x1), address(0x2), address(0x3), - queueParams, + RawPayload, "" ); @@ -280,7 +278,7 @@ contract WatcherTest is AppGatewayBaseSetup { address(0x1), address(0x2), address(0x3), - queueParams, + RawPayload, "" ); } @@ -383,7 +381,7 @@ contract WatcherTest is AppGatewayBaseSetup { function testRequestHandlerAssignTransmitter() public { uint40 requestCount = 0; appGateway.deployContracts(arbConfig.chainSlug); - Bid memory bid = Bid({fee: 100, transmitter: transmitterEOA, extraData: ""}); + Bid memory bid = Bid({fee: 100, transmitter: transmitterEOA, extraData: bytes("")}); hoax(nonOwner); vm.expectRevert(abi.encodeWithSelector(InvalidCaller.selector)); @@ -399,7 +397,7 @@ contract WatcherTest is AppGatewayBaseSetup { requestHandler.assignTransmitter(requestCount, bid); } - function testRequestHandlerUpdateRequestAndProcessBatch() public { + function testRequestHandlerupdateRequest() public { appGateway.deployContracts(arbConfig.chainSlug); uint40 requestCount = 0; uint40[] memory batches = requestHandler.getRequestBatchIds(requestCount); @@ -408,13 +406,13 @@ contract WatcherTest is AppGatewayBaseSetup { hoax(nonOwner); vm.expectRevert(abi.encodeWithSelector(NotPromiseResolver.selector)); - requestHandler.updateRequestAndProcessBatch(requestCount, payloadId); + requestHandler.updateRequest(requestCount, payloadId); hoax(watcherAddress); requestHandler.cancelRequest(requestCount, address(appGateway)); hoax(address(promiseResolver)); vm.expectRevert(abi.encodeWithSelector(RequestAlreadyCancelled.selector)); - requestHandler.updateRequestAndProcessBatch(requestCount, payloadId); + requestHandler.updateRequest(requestCount, payloadId); } // ============ CONFIGURATIONS ACCESS CONTROL TESTS ============ @@ -493,7 +491,7 @@ contract WatcherTest is AppGatewayBaseSetup { uint40[] memory batches = requestHandler.getRequestBatchIds(requestCount); bytes32[] memory payloadIds = requestHandler.getBatchPayloadIds(batches[0]); bytes32 payloadId = payloadIds[0]; - Bid memory bid = Bid({fee: 100, transmitter: transmitterEOA, extraData: ""}); + Bid memory bid = Bid({fee: 100, transmitter: transmitterEOA, extraData: bytes("")}); hoax(address(auctionManager)); requestHandler.assignTransmitter(requestCount, bid); diff --git a/test/mock/CCTPMessageTransmitter.sol b/deprecated/test/mock/CCTPMessageTransmitter.sol similarity index 100% rename from test/mock/CCTPMessageTransmitter.sol rename to deprecated/test/mock/CCTPMessageTransmitter.sol diff --git a/test/mock/MockERC721.sol b/deprecated/test/mock/MockERC721.sol similarity index 100% rename from test/mock/MockERC721.sol rename to deprecated/test/mock/MockERC721.sol diff --git a/test/mock/MockFastSwitchboard.sol b/deprecated/test/mock/MockFastSwitchboard.sol similarity index 98% rename from test/mock/MockFastSwitchboard.sol rename to deprecated/test/mock/MockFastSwitchboard.sol index 6ed61324..72e3c669 100644 --- a/test/mock/MockFastSwitchboard.sol +++ b/deprecated/test/mock/MockFastSwitchboard.sol @@ -45,7 +45,7 @@ contract MockFastSwitchboard is ISwitchboard { return switchboardId; } - function processTrigger( + function processPayload( address plug_, bytes32 triggerId_, bytes calldata payload_, diff --git a/test/mock/MockFeesManager.sol b/deprecated/test/mock/MockFeesManager.sol similarity index 100% rename from test/mock/MockFeesManager.sol rename to deprecated/test/mock/MockFeesManager.sol diff --git a/test/mock/MockPlug.sol b/deprecated/test/mock/MockPlug.sol similarity index 100% rename from test/mock/MockPlug.sol rename to deprecated/test/mock/MockPlug.sol diff --git a/test/mock/MockSocket.sol b/deprecated/test/mock/MockSocket.sol similarity index 100% rename from test/mock/MockSocket.sol rename to deprecated/test/mock/MockSocket.sol diff --git a/deprecated/test/protocol/Socket.t.sol b/deprecated/test/protocol/Socket.t.sol new file mode 100644 index 00000000..e53bd001 --- /dev/null +++ b/deprecated/test/protocol/Socket.t.sol @@ -0,0 +1,949 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.21; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; + +import {MockFastSwitchboard} from "../mock/MockFastSwitchboard.sol"; +import {MockPlug, MockTarget} from "../mock/MockPlug.sol"; +import {MockFeeManager} from "../mock/MockFeesManager.sol"; +import {SuperToken} from "../apps/app-gateways/super-token/SuperToken.sol"; +import {MockERC721} from "../mock/MockERC721.sol"; +import {ISocket} from "../../contracts/protocol/interfaces/ISocket.sol"; +import "../../contracts/protocol/Socket.sol"; +import "../../contracts/protocol/SocketUtils.sol"; +import "../../contracts/utils/common/Errors.sol"; +import "../../contracts/utils/common/Constants.sol"; +import "../../contracts/utils/common/Structs.sol"; + +/** + * @title SocketTestWrapper + * @dev Wrapper contract to expose internal functions for testing + */ +contract SocketTestWrapper is Socket { + constructor( + uint32 chainSlug_, + address owner_, + string memory version_ + ) Socket(chainSlug_, owner_, version_) {} + + // Expose internal functions for testing + function createDigest( + address transmitter_, + bytes32 payloadId_, + bytes32 appGatewayId_, + ExecuteParams calldata executeParams_ + ) external view returns (bytes32) { + return _createDigest(transmitter_, payloadId_, appGatewayId_, executeParams_); + } + + function encodeTriggerId() external returns (bytes32) { + return _encodeTriggerId(); + } + + function executeInternal( + bytes32 payloadId_, + ExecuteParams calldata executeParams_, + TransmissionParams calldata transmissionParams_ + ) external payable returns (bool, bytes memory) { + return _execute(payloadId_, executeParams_, transmissionParams_); + } +} + +/** + * @title SocketTestBase + * @dev Base contract for Socket protocol unit tests + * Provides common setup, utilities, and mock contracts + */ +contract SocketTestBase is Test { + uint256 c = 1; + string constant VERSION = "1.0.0"; + address public socketOwner = address(uint160(c++)); + address public transmitter = address(uint160(c++)); + address public testUser = address(uint160(c++)); + + uint32 constant TEST_CHAIN_SLUG = 1; + bytes32 constant TEST_APP_GATEWAY_ID = keccak256("TEST_APP_GATEWAY"); + bytes constant TEST_PAYLOAD = hex"1234567890abcdef"; + bytes constant TEST_OVERRIDES = hex"abcdef"; + + // Contracts + Socket public socket; + MockFastSwitchboard public mockSwitchboard; + MockPlug public mockPlug; + MockFeeManager public mockFeeManager; + SuperToken public mockToken; + MockTarget public mockTarget; + SocketTestWrapper public socketWrapper; + + uint64 public switchboardId; + ExecuteParams public executeParams; + TransmissionParams public transmissionParams; + + function setUp() public virtual { + socket = new Socket(TEST_CHAIN_SLUG, socketOwner, VERSION); + mockSwitchboard = new MockFastSwitchboard(TEST_CHAIN_SLUG, address(socket), socketOwner); + mockPlug = new MockPlug(); + mockFeeManager = new MockFeeManager(); + mockToken = new SuperToken("Test Token", "TEST", 18, testUser, 1000000000000000000); + mockTarget = new MockTarget(); + socketWrapper = new SocketTestWrapper(TEST_CHAIN_SLUG, socketOwner, VERSION); + mockToken.setOwner(socketOwner); + + // Set up initial state + vm.startPrank(socketOwner); + socket.grantRole(GOVERNANCE_ROLE, socketOwner); + socket.grantRole(RESCUE_ROLE, socketOwner); + socket.grantRole(SWITCHBOARD_DISABLER_ROLE, socketOwner); + + socket.setSocketFeeManager(address(mockFeeManager)); + mockToken.setSocket(address(socket)); + vm.stopPrank(); + + switchboardId = mockSwitchboard.registerSwitchboard(); + mockPlug.initSocket(TEST_APP_GATEWAY_ID, address(socket), switchboardId); + + executeParams = _createExecuteParams(); + transmissionParams = _createTransmissionParams(); + + vm.deal(transmitter, 100 ether); + vm.deal(testUser, 100 ether); + } + + function _createExecuteParams() internal view returns (ExecuteParams memory) { + return + ExecuteParams({ + callType: WRITE, + payloadPointer: 1, + deadline: block.timestamp + 1 hours, + gasLimit: 100000, + value: 0, + prevBatchDigestHash: bytes32(0), + target: address(mockPlug), + payload: TEST_PAYLOAD, + extraData: bytes("") + }); + } + + function _createTransmissionParams() internal view returns (TransmissionParams memory) { + return + TransmissionParams({ + socketFees: 0, + refundAddress: testUser, + extraData: bytes(""), + transmitterProof: bytes("") + }); + } + + function createSignature( + bytes32 digest_, + uint256 privateKey_ + ) public pure returns (bytes memory sig) { + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest); + sig = new bytes(65); + bytes1 v32 = bytes1(sigV); + assembly { + mstore(add(sig, 96), v32) + mstore(add(sig, 32), sigR) + mstore(add(sig, 64), sigS) + } + } +} + +/** + * @title SocketExecuteTest + * @dev Tests for Socket execute function + */ +contract SocketExecuteTest is SocketTestBase { + function testConstructorWithValidParameters() public view { + assertEq(socket.chainSlug(), TEST_CHAIN_SLUG, "Chain slug should match"); + assertEq(socket.owner(), socketOwner, "Owner should match"); + assertEq(socket.version(), keccak256(bytes(VERSION)), "Version should match"); + assertEq(socket.gasLimitBuffer(), 105, "Gas limit buffer should be 105"); + } + + function testExecuteDeadlinePassed() public { + executeParams.deadline = block.timestamp - 1; // Past deadline + vm.expectRevert(DeadlinePassed.selector); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + } + + function testExecutePlugNotFound() public { + executeParams.target = address(0x999); // Non-existent plug + vm.expectRevert(PlugNotFound.selector); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + } + + function testExecuteInsufficientValue() public { + executeParams.value = 1 ether; + transmissionParams.socketFees = 0.5 ether; + + vm.expectRevert(Socket.InsufficientMsgValue.selector); + socket.execute{value: 0.5 ether}(executeParams, transmissionParams); + } + + function testExecuteInvalidSwitchboardDisabled() public { + hoax(socketOwner); + socket.disableSwitchboard(switchboardId); + + vm.expectRevert(InvalidSwitchboard.selector); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + } + + function testExecuteWithInvalidCallType() public { + executeParams.callType = bytes4(0x12345678); // Invalid call type + + vm.expectRevert(InvalidCallType.selector); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + } + + function testExecuteRefundIfExecutionFails() public { + executeParams.target = address(mockPlug); + executeParams.value = 0.5 ether; + executeParams.payload = abi.encodeWithSelector( + mockPlug.callMockTarget.selector, + address(mockTarget), + abi.encodeWithSelector(mockTarget.increment.selector) + ); + + // Set up mock target to revert + mockTarget.setShouldRevert(true); + + uint256 userBalance = testUser.balance; + transmissionParams.refundAddress = testUser; + + hoax(transmitter); + (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); + assertFalse(success, "Execution should fail"); + + // Check that refund was sent + assertEq(testUser.balance, userBalance + 1 ether, "Refund should be sent to user"); + + // Set up mock target to revert + mockTarget.setShouldRevert(false); + userBalance = testUser.balance; + + hoax(transmitter); + (success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); + assertTrue(success, "Execution should succeed"); + + // Check that refund was sent + assertEq(testUser.balance, userBalance, "Refund should not be sent to user"); + } + + function testExecuteWithValidParameters() public { + // Use mockPlug as target since it's connected to socket + executeParams.target = address(mockPlug); + executeParams.payload = abi.encodeWithSelector( + mockPlug.processPayload.selector, + TEST_PAYLOAD + ); + + bytes32 payloadId = createPayloadId( + executeParams.payloadPointer, + switchboardId, + TEST_CHAIN_SLUG + ); + + vm.expectEmit(true, true, true, true, address(socket)); + emit ISocket.ExecutionSuccess(payloadId, false, bytes(abi.encode(true))); + (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); + assertTrue(success, "Execution should succeed"); + } + + function testExecuteWithPayloadAlreadyExecuted() public { + executeParams.payload = abi.encodeWithSelector( + mockPlug.processPayload.selector, + TEST_PAYLOAD + ); + + bytes32 payloadId = createPayloadId( + executeParams.payloadPointer, + switchboardId, + TEST_CHAIN_SLUG + ); + + vm.expectEmit(true, true, true, true, address(socket)); + emit ISocket.ExecutionSuccess(payloadId, false, bytes(abi.encode(true))); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + + vm.expectRevert( + abi.encodeWithSelector(Socket.PayloadAlreadyExecuted.selector, ExecutionStatus.Executed) + ); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + } + + function testExecuteWithVerificationFailed() public { + // Override the allowPayload function to return false + mockSwitchboard.setIsPayloadAllowed(false); + + vm.expectRevert(Socket.VerificationFailed.selector); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + } + + function testExecuteWithFailedExecution() public { + executeParams.target = address(mockPlug); + executeParams.payload = abi.encodeWithSelector( + mockPlug.callMockTarget.selector, + address(mockTarget), + abi.encodeWithSelector(mockTarget.increment.selector) + ); + + // Set up mock target to revert + mockTarget.setShouldRevert(true); + + bytes32 payloadId = createPayloadId( + executeParams.payloadPointer, + switchboardId, + TEST_CHAIN_SLUG + ); + + vm.expectEmit(true, true, true, true, address(socket)); + emit ISocket.ExecutionFailed( + payloadId, + false, + abi.encodeWithSelector(MockPlug.CallFailed.selector) + ); + (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); + assertFalse(success, "Execution should fail"); + assertEq(mockTarget.counter(), 0, "Target should not be called"); + } + + function testExecuteWithExceededMaxCopyBytes() public { + // Set up mock target to return large data + executeParams.target = address(mockPlug); + executeParams.payload = abi.encodeWithSelector(mockPlug.returnLargeData.selector); + + (bool success, bytes memory returnData) = socket.execute{value: 1 ether}( + executeParams, + transmissionParams + ); + + // The return data should be truncated to maxCopyBytes (2048 bytes) + assertEq(returnData.length, 2048, "Return data should be exactly maxCopyBytes"); + assertLt(returnData.length, 3072, "Return data should be truncated"); + assertTrue(success, "Execution should succeed even with large return data"); + } + + function testExecutionRetryIfFailing() public { + executeParams.target = address(mockPlug); + executeParams.payload = abi.encodeWithSelector( + mockPlug.callMockTarget.selector, + address(mockTarget), + abi.encodeWithSelector(mockTarget.increment.selector) + ); + + bytes32 payloadId = createPayloadId( + executeParams.payloadPointer, + switchboardId, + TEST_CHAIN_SLUG + ); + + mockTarget.setShouldRevert(true); + vm.expectEmit(true, true, true, true, address(socket)); + emit ISocket.ExecutionFailed( + payloadId, + false, + abi.encodeWithSelector(MockPlug.CallFailed.selector) + ); + (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); + assertFalse(success, "First execution should fail"); + + mockTarget.setShouldRevert(false); + vm.expectEmit(true, true, true, true, address(socket)); + emit ISocket.ExecutionSuccess(payloadId, false, bytes(abi.encode(true))); + (success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); + assertTrue(success, "Second execution should succeed"); + } + + function testGasUsageForExecute() public { + executeParams.target = address(mockPlug); + executeParams.payload = abi.encodeWithSelector( + mockPlug.callMockTarget.selector, + address(mockTarget), + abi.encodeWithSelector(mockTarget.increment.selector) + ); + + uint256 gasBefore = gasleft(); + socket.execute{value: 1 ether}(executeParams, transmissionParams); + uint256 gasUsed = gasBefore - gasleft(); + console.log("gasUsed", gasUsed); + } + + function testExecuteLowGasLimit() public { + // set high gas limit + executeParams.gasLimit = 10000000; + + vm.expectRevert(Socket.LowGasLimit.selector); + socket.execute{value: 1 ether, gas: 100000}(executeParams, transmissionParams); + } +} + +/** + * @title SocketTriggerTest + * @dev Tests for Socket triggerAppGateway function + */ +contract SocketTriggerTest is SocketTestBase { + function testTriggerAppGatewayPlugNotFound() public { + bytes memory triggerData = abi.encodeWithSelector( + mockPlug.processPayload.selector, + TEST_PAYLOAD + ); + + hoax(testUser); + vm.expectRevert(PlugNotFound.selector); + socket.triggerAppGateway{value: 1 ether}(triggerData); + } + + function testTriggerAppGatewayWithInvalidSwitchboard() public { + // Give mockPlug some funds + vm.deal(address(mockPlug), 10 ether); + + hoax(socketOwner); + socket.disableSwitchboard(switchboardId); + + bytes memory triggerData = abi.encodeWithSelector( + mockPlug.processPayload.selector, + TEST_PAYLOAD + ); + + vm.expectRevert(InvalidSwitchboard.selector); + hoax(address(mockPlug)); + socket.triggerAppGateway{value: 1 ether}(triggerData); + } + + function testGasUsageForTriggerAppGateway() public { + // Give mockPlug some funds + vm.deal(address(mockPlug), 10 ether); + + bytes memory triggerData = abi.encodeWithSelector( + mockPlug.processPayload.selector, + TEST_PAYLOAD + ); + + uint256 gasBefore = gasleft(); + hoax(address(mockPlug)); + socket.triggerAppGateway{value: 1 ether}(triggerData); + uint256 gasUsed = gasBefore - gasleft(); + + // Gas usage should be reasonable + console.log("gasUsed", gasUsed); + } +} + +/** + * @title SocketConnectDisconnectTest + * @dev Tests for Socket connect and disconnect functions + */ +contract SocketConnectDisconnectTest is SocketTestBase { + function testConnectWithInvalidSwitchboard() public { + vm.expectRevert(InvalidSwitchboard.selector); + mockPlug.initSocket(TEST_APP_GATEWAY_ID, address(socket), switchboardId + 1); + } + + function testConnectWithNewSwitchboard() public { + // Create a new switchboard + MockFastSwitchboard newSwitchboard = new MockFastSwitchboard( + TEST_CHAIN_SLUG, + address(socket), + socketOwner + ); + uint64 newSwitchboardId = newSwitchboard.registerSwitchboard(); + mockPlug.initSocket(TEST_APP_GATEWAY_ID, address(socket), newSwitchboardId); + + (bytes32 appGatewayId, uint64 switchboardId) = socket.getPlugConfig(address(mockPlug)); + assertEq(appGatewayId, TEST_APP_GATEWAY_ID, "App gateway ID should match"); + assertEq(switchboardId, newSwitchboardId, "Switchboard ID should match"); + } + + // Try to disconnect a plug that was never connected + function testDisconnectWithPlugNotConnected() public { + MockPlug newPlug = new MockPlug(); + newPlug.setSocket(address(socket)); + + vm.expectRevert(SocketConfig.PlugNotConnected.selector); + newPlug.disconnect(); + } +} + +/** + * @title SocketSwitchboardManagementTest + * @dev Tests for Socket switchboard management functions + */ +contract SocketSwitchboardManagementTest is SocketTestBase { + function testRegisterSwitchboardWithExistingSwitchboard() public { + assertEq(mockSwitchboard.switchboardId(), socket.switchboardIds(address(mockSwitchboard))); + + // Try to register the same switchboard again + vm.expectRevert(SocketConfig.SwitchboardExists.selector); + mockSwitchboard.registerSwitchboard(); + } + + function testDisableSwitchboardWithGovernanceRole() public { + hoax(socketOwner); + socket.disableSwitchboard(switchboardId); + + // Try to register the same switchboard again + vm.expectRevert(SocketConfig.SwitchboardExists.selector); + mockSwitchboard.registerSwitchboard(); + + assertEq( + uint256(socket.isValidSwitchboard(switchboardId)), + uint256(SwitchboardStatus.DISABLED), + "Switchboard should be disabled" + ); + } + + function testEnableSwitchboardWithGovernanceRole() public { + // First disable the switchboard + hoax(socketOwner); + socket.disableSwitchboard(switchboardId); + assertEq( + uint256(socket.isValidSwitchboard(switchboardId)), + uint256(SwitchboardStatus.DISABLED), + "Switchboard should be disabled" + ); + + // Then enable it + hoax(socketOwner); + socket.enableSwitchboard(switchboardId); + assertEq( + uint256(socket.isValidSwitchboard(switchboardId)), + uint256(SwitchboardStatus.REGISTERED), + "Switchboard should be enabled" + ); + } + + function testSwitchboardStatusValidation() public { + // Test initial status + assertEq( + uint256(socket.isValidSwitchboard(switchboardId)), + uint256(SwitchboardStatus.REGISTERED), + "Switchboard should be registered initially" + ); + + // Test disabled status + hoax(socketOwner); + socket.disableSwitchboard(switchboardId); + assertEq( + uint256(socket.isValidSwitchboard(switchboardId)), + uint256(SwitchboardStatus.DISABLED), + "Switchboard should be disabled" + ); + + // Test enabled status + hoax(socketOwner); + socket.enableSwitchboard(switchboardId); + + assertEq( + uint256(socket.isValidSwitchboard(switchboardId)), + uint256(SwitchboardStatus.REGISTERED), + "Switchboard should be enabled" + ); + } +} + +/** + * @title SocketSetterTest + * @dev Tests for Socket setter functions + */ +contract SocketSetterTest is SocketTestBase { + function testSetGasLimitBuffer() public { + uint256 newBuffer = 110; + + hoax(testUser); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); + socket.setGasLimitBuffer(newBuffer); + + hoax(socketOwner); + socket.setGasLimitBuffer(newBuffer); + + assertEq(socket.gasLimitBuffer(), newBuffer, "Gas limit buffer should be updated"); + } + + function testSetMaxCopyBytes() public { + uint16 newMaxCopyBytes = 4096; + + hoax(testUser); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); + socket.setMaxCopyBytes(newMaxCopyBytes); + + hoax(socketOwner); + socket.setMaxCopyBytes(newMaxCopyBytes); + assertEq(socket.maxCopyBytes(), newMaxCopyBytes, "Max copy bytes should be updated"); + } + + function testSetSocketFeeManager() public { + address newFeeManager = address(0x123); + + hoax(testUser); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); + socket.setSocketFeeManager(newFeeManager); + + hoax(socketOwner); + socket.setSocketFeeManager(newFeeManager); + assertEq( + address(socket.socketFeeManager()), + newFeeManager, + "Socket fee manager should be updated" + ); + } + + function testConfigurationValidation() public { + // Test initial configuration + assertEq(socket.gasLimitBuffer(), 105, "Initial gas limit buffer should be 105"); + assertEq(socket.maxCopyBytes(), 2048, "Initial max copy bytes should be 2048"); + assertEq( + address(socket.socketFeeManager()), + address(mockFeeManager), + "Initial fee manager should be mockFeeManager" + ); + + MockFeeManager newFeeManager = new MockFeeManager(); + // Test configuration updates + vm.startPrank(socketOwner); + socket.setGasLimitBuffer(120); + socket.setMaxCopyBytes(4096); + socket.setSocketFeeManager(address(newFeeManager)); + vm.stopPrank(); + + assertEq(socket.gasLimitBuffer(), 120, "Gas limit buffer should be updated"); + assertEq(socket.maxCopyBytes(), 4096, "Max copy bytes should be updated"); + assertEq( + address(socket.socketFeeManager()), + address(newFeeManager), + "Fee manager should be updated" + ); + } +} + +/** + * @title SocketDigestTest + * @dev Tests for digest creation functionality + */ +contract SocketDigestTest is SocketTestBase { + function testCreateDigestWithBasicPayload() public view { + ExecuteParams memory params = ExecuteParams({ + target: address(mockTarget), + value: 1 ether, + gasLimit: 100000, + deadline: block.timestamp + 3600, + callType: WRITE, + payload: TEST_PAYLOAD, + payloadPointer: uint160(1), + prevBatchDigestHash: bytes32(uint256(0)), + extraData: bytes("") + }); + + bytes32 digest = socketWrapper.createDigest( + transmitter, + bytes32(uint256(0x123)), + TEST_APP_GATEWAY_ID, + params + ); + + assertTrue(digest != bytes32(0), "Digest should not be zero"); + } + + function testCreateDigestWithLargePayload() public view { + bytes memory largePayload = new bytes(1000); + for (uint256 i = 0; i < 1000; i++) { + largePayload[i] = bytes1(uint8(i % 256)); + } + + ExecuteParams memory params = ExecuteParams({ + target: address(mockTarget), + value: 2 ether, + gasLimit: 200000, + deadline: block.timestamp + 7200, + callType: WRITE, + payload: largePayload, + payloadPointer: uint160(1), + prevBatchDigestHash: bytes32(uint256(0)), + extraData: bytes("") + }); + + bytes32 digest = socketWrapper.createDigest( + transmitter, + bytes32(uint256(0xdef)), + TEST_APP_GATEWAY_ID, + params + ); + + assertTrue(digest != bytes32(0), "Digest should not be zero"); + } + + function testCreateDigestWithDifferentTransmitters() public view { + ExecuteParams memory params = ExecuteParams({ + target: address(mockTarget), + value: 1 ether, + gasLimit: 100000, + deadline: block.timestamp + 3600, + callType: WRITE, + payload: TEST_PAYLOAD, + payloadPointer: uint160(1), + prevBatchDigestHash: bytes32(0), + extraData: bytes("") + }); + + bytes32 digest1 = socketWrapper.createDigest( + transmitter, + bytes32(uint256(0x123)), + TEST_APP_GATEWAY_ID, + params + ); + + bytes32 digest2 = socketWrapper.createDigest( + address(uint160(0x456)), + bytes32(uint256(0x123)), + TEST_APP_GATEWAY_ID, + params + ); + + assertTrue(digest1 != digest2, "Digests should be different for different transmitters"); + } + + function testCreateDigestWithDifferentPayloads() public view { + ExecuteParams memory params1 = ExecuteParams({ + target: address(mockTarget), + value: 1 ether, + gasLimit: 100000, + deadline: block.timestamp + 3600, + callType: WRITE, + payload: TEST_PAYLOAD, + payloadPointer: uint160(1), + prevBatchDigestHash: bytes32(0), + extraData: bytes("") + }); + + ExecuteParams memory params2 = ExecuteParams({ + target: address(mockTarget), + value: 1 ether, + gasLimit: 100000, + deadline: block.timestamp + 3600, + callType: WRITE, + payload: hex"abcdef", + payloadPointer: uint160(1), + prevBatchDigestHash: bytes32(0), + extraData: bytes("") + }); + + bytes32 digest1 = socketWrapper.createDigest( + transmitter, + bytes32(uint256(0x123)), + TEST_APP_GATEWAY_ID, + params1 + ); + + bytes32 digest2 = socketWrapper.createDigest( + transmitter, + bytes32(uint256(0x123)), + TEST_APP_GATEWAY_ID, + params2 + ); + + assertTrue(digest1 != digest2, "Digests should be different for different payloads"); + } +} + +/** + * @title SocketTriggerIdTest + * @dev Tests for trigger ID encoding and uniqueness + */ +contract SocketTriggerIdTest is SocketTestBase { + function testEncodeTriggerIdIncrementsCounter() public { + bytes32 triggerId1 = socketWrapper.encodeTriggerId(); + bytes32 triggerId2 = socketWrapper.encodeTriggerId(); + bytes32 triggerId3 = socketWrapper.encodeTriggerId(); + + assertTrue(triggerId1 != triggerId2, "Trigger IDs should be different"); + assertTrue(triggerId2 != triggerId3, "Trigger IDs should be different"); + assertTrue(triggerId1 != triggerId3, "Trigger IDs should be different"); + } + + function testTriggerIdUniqueness() public { + bytes32[] memory triggerIds = new bytes32[](100); + + for (uint256 i = 0; i < 100; i++) { + triggerIds[i] = socketWrapper.encodeTriggerId(); + } + + // Check for uniqueness + for (uint256 i = 0; i < 100; i++) { + for (uint256 j = i + 1; j < 100; j++) { + assertTrue(triggerIds[i] != triggerIds[j], "Trigger IDs should be unique"); + assertEq( + uint32(uint256(triggerIds[i]) >> 224), + TEST_CHAIN_SLUG, + "Chain slug should match" + ); + assertEq( + address(uint160(uint256(triggerIds[i]) >> 64)), + address(socketWrapper), + "Socket address should match" + ); + assertEq(uint64(uint256(triggerIds[i])), i, "Counter should increment"); + } + } + } + + function testTriggerIdFormatFuzz(uint64 counter) public { + vm.assume(counter < type(uint64).max - 1000); + + // Set the counter to a specific value + uint256 counterSlot = uint256(57); + vm.store(address(socketWrapper), bytes32(counterSlot), bytes32(uint256(counter))); + + bytes32 triggerId = socketWrapper.encodeTriggerId(); + uint32 chainSlugFromId = uint32(uint256(triggerId) >> 224); + address socketAddressFromId = address(uint160(uint256(triggerId) >> 64)); + uint64 counterFromId = uint64(uint256(triggerId)); + + assertEq(chainSlugFromId, TEST_CHAIN_SLUG, "Chain slug should match"); + assertEq(socketAddressFromId, address(socketWrapper), "Socket address should match"); + assertEq(counterFromId, counter, "Counter should match"); + } +} + +/** + * @title SocketSimulationTest + * @dev Tests for simulation functionality + */ +contract SocketSimulationTest is SocketTestBase { + function testSimulationModifier() public { + SocketUtils.SimulateParams[] memory params = new SocketUtils.SimulateParams[](1); + params[0] = SocketUtils.SimulateParams({ + target: address(mockTarget), + value: 1 ether, + gasLimit: 100000, + payload: TEST_PAYLOAD + }); + + // Should revert when called by non-off-chain caller + vm.expectRevert(SocketUtils.OnlyOffChain.selector); + socket.simulate(params); + } +} + +/** + * @title SocketRescueTest + * @dev Tests for rescue functionality + */ +contract SocketRescueTest is SocketTestBase { + function testRescueFunds() public { + // Send some ETH to the socket + vm.deal(address(socket), 10 ether); + uint256 balanceBefore = testUser.balance; + + hoax(socketOwner); + socket.rescueFunds(ETH_ADDRESS, testUser, 5 ether); + + uint256 balanceAfter = testUser.balance; + assertEq(balanceAfter - balanceBefore, 5 ether, "User should receive rescued ETH"); + } + + function testRescueFundsWithNonRescueRole() public { + vm.deal(address(socket), 10 ether); + + hoax(testUser); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, RESCUE_ROLE)); + socket.rescueFunds(address(0), testUser, 5 ether); + } + + function testRescueTokenFunds() public { + // Transfer some tokens to the socket + hoax(address(socket)); + mockToken.mint(address(socket), 1000); + uint256 balanceBefore = mockToken.balanceOf(testUser); + + hoax(socketOwner); + socket.rescueFunds(address(mockToken), testUser, 500); + + uint256 balanceAfter = mockToken.balanceOf(testUser); + assertEq(balanceAfter - balanceBefore, 500, "User should receive rescued tokens"); + } + + function testSendingEthToSocket() public { + vm.expectRevert("Socket does not accept ETH"); + (bool success, ) = address(socket).call{value: 1 ether}(""); + } + + function testRescueNFT() public { + // Deploy a mock ERC721 NFT and mint one to this test contract + MockERC721 mockNFT = new MockERC721(); + uint256 tokenId = 1; + mockNFT.mint(address(socket), tokenId); + assertEq(mockNFT.ownerOf(tokenId), address(socket), "Socket should own the NFT"); + + hoax(socketOwner); + socket.rescueFunds(address(mockNFT), testUser, tokenId); + + // Check that testUser received the NFT + assertEq(mockNFT.ownerOf(tokenId), testUser, "User should receive rescued NFT"); + } +} + +/** + * @title SocketFeeManagerTest + * @dev Tests for fee manager functionality + */ +contract SocketFeeManagerTest is SocketTestBase { + function testFeeCollectedIfExecutionSuccess() public { + // Set up execution parameters with fees + executeParams.gasLimit = 100000; + transmissionParams.socketFees = 0.1 ether; + executeParams.target = address(mockPlug); + executeParams.payload = abi.encodeWithSelector( + mockPlug.processPayload.selector, + TEST_PAYLOAD + ); + + // Execute with fees + socket.execute{value: 1.1 ether}(executeParams, transmissionParams); + + // Check that fees were collected + assertEq(address(mockFeeManager).balance, 0.1 ether, "Fee manager should receive fees"); + } + + function testGasUsage() public { + executeParams.gasLimit = 100000; + transmissionParams.socketFees = 0.1 ether; + + // mockSwitchboard.setTransmitter(transmitter); + + uint256 gasBefore = gasleft(); + + vm.deal(testUser, 2 ether); + hoax(testUser); + socket.execute{value: 1.1 ether}(executeParams, transmissionParams); + + uint256 gasUsed = gasBefore - gasleft(); + console.log("Gas used for execution with fees:", gasUsed); + + assertTrue(gasUsed > 0, "Gas should be used"); + } + + function testFeeManagerNotSet() public { + // Remove fee manager + hoax(socketOwner); + socket.setSocketFeeManager(address(0)); + + executeParams.gasLimit = 100000; + transmissionParams.socketFees = 0.1 ether; + + // mockSwitchboard.setTransmitter(transmitter); + + // Should still execute successfully without fee manager + vm.deal(testUser, 2 ether); + hoax(testUser); + socket.execute{value: 1.1 ether}(executeParams, transmissionParams); + + // No fees should be collected + assertEq( + address(mockFeeManager).balance, + 0, + "No fees should be collected when fee manager is not set" + ); + } +} diff --git a/test/protocol/SocketFeeManager.t.sol b/deprecated/test/protocol/SocketFeeManager.t.sol similarity index 100% rename from test/protocol/SocketFeeManager.t.sol rename to deprecated/test/protocol/SocketFeeManager.t.sol diff --git a/test/protocol/TriggerTest.t.sol b/deprecated/test/protocol/TriggerTest.t.sol similarity index 100% rename from test/protocol/TriggerTest.t.sol rename to deprecated/test/protocol/TriggerTest.t.sol diff --git a/test/protocol/switchboards/FastSwitchboardTest.t.sol b/deprecated/test/protocol/switchboards/FastSwitchboardTest.t.sol similarity index 98% rename from test/protocol/switchboards/FastSwitchboardTest.t.sol rename to deprecated/test/protocol/switchboards/FastSwitchboardTest.t.sol index f27ab1fb..5f624033 100644 --- a/test/protocol/switchboards/FastSwitchboardTest.t.sol +++ b/deprecated/test/protocol/switchboards/FastSwitchboardTest.t.sol @@ -21,7 +21,7 @@ contract FastSwitchboardExtended is FastSwitchboard { address owner_ ) FastSwitchboard(chainSlug_, socket_, owner_) {} - function processTrigger( + function processPayload( address plug_, bytes32 triggerId_, bytes calldata payload_, @@ -143,7 +143,7 @@ contract FastSwitchboardTest is AppGatewayBaseSetup { bytes("test payload"), bytes("test overrides") ); - fastSwitchboardExtended.processTrigger( + fastSwitchboardExtended.processPayload( address(0x123), bytes32(uint256(0x456)), bytes("test payload"), diff --git a/test/protocol/switchboards/MessageSwitchboardTest.t copy.sol b/deprecated/test/protocol/switchboards/MessageSwitchboardTest.t copy.sol similarity index 100% rename from test/protocol/switchboards/MessageSwitchboardTest.t copy.sol rename to deprecated/test/protocol/switchboards/MessageSwitchboardTest.t copy.sol diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 00000000..52883c2a --- /dev/null +++ b/foundry.lock @@ -0,0 +1,8 @@ +{ + "lib/forge-std": { + "rev": "f90623596aecbf678c41d4d45ca81ce0e43c8219" + }, + "lib/solady": { + "rev": "836c169fe357b3c23ad5d5755a9b4fbbfad7a99b" + } +} \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index c21573c5..ecb58a89 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,37 +1,35 @@ [profile.default] -solc_version = "0.8.22" +solc_version = "0.8.28" src = "contracts" out = "out" libs = ["lib"] ffi = true optimizer = true -optimizer_runs = 200 +optimizer_runs = 1 evm_version = 'paris' via_ir = false +[profile.default.optimizer_details] +yul = true + +[profile.default.optimizer_details.yulDetails] +stackAllocation = true +optimizerSteps = "u" + [labels] -0x3d6EB76db49BF4b9aAf01DBB79fCEC2Ee71e44e2 = "AddressResolver" -0xe37aFa3Aa95E153B8dD0FE8456CBF345cB4C51F7 = "AddressResolverImpl" -0xFA19dDA03A79f8Aef83C0505BF70ECa0Ac42608E = "AsyncDeployer" -0xb3A5132Df72F1597ab474d73d387ecF8647af669 = "AsyncDeployerImpl" -0xe2B1A11E8473095581DEF8d9D11eC63BBdd62ceE = "AsyncPromiseImpl" -0xcd5e9029a73890A5A3146bAddd272D65ac11521c = "AuctionManager" -0xB604FBcA01897315D2d62A346DBf29796A4825D9 = "AuctionManagerImpl" -0x71B89bA78B9431d4E984893cD6885d39AD6c3c7A = "Configurations" -0x117c63A8c9a980ddC60B2bF2b4701C9267f66394 = "ConfigurationsImpl" -0xb6E6e6FCd2636B83C443628f3f5e42cB5Fcd44fD = "DeployForwarder" -0xf05f680E0611b81eD0255A1Cd829540504765711 = "DeployForwarderImpl" -0x4023941D9AB563b1c4d447B3f2A9dd2F1eF19fCA = "ERC1967Factory" -0xB63ab15c208A16a0480036C06e8828A4682E0B34 = "FeesManager" -0x9f3CDba2262DF94e415E092A4228ee7E6846ea1b = "FeesManagerImpl" -0x3B1f4ABA1667EeB992B623E7c6d119728cEd3b15 = "FeesPool" -0xB1a504eC5C2d8206Fc73a46EeF5E5018585Eb240 = "ForwarderImpl" -0xFB349dcc5A1cB87Ff3A2b91C343814647AE820FC = "PromiseResolver" -0x74D52027137a450b68315478AAE4528Ba839ea13 = "ReadPrecompile" -0x3C183Ad26A11A6691d43D031Fae3D51DaDC643Df = "RequestHandler" -0x0303B6f54afA36B0808FDE6aaE9c3eD271b01119 = "RequestHandlerImpl" -0xEE7b72D53FeC4Bed9F56CcEaD49217d152A22aC5 = "SchedulePrecompile" -0x2566Bef2e914c7482d6FCB4955403fb0865951A5 = "Watcher" -0x03029500B038980745c5a671f271340CF9AF5830 = "WatcherImpl" -0xc6506b1C3f34297B4de32f08d8d50CB0E9e64842 = "WritePrecompile" -0xcd460687fe2a74ddEE8f2E3d791e1df306713353 = "WritePrecompileImpl" +0xAaee0de4a720e8733a397a3B57fcE3B306Cc7dAe = "AddressResolver" +0x8f1BE258CF821f11fdCC392DAe314BF0781b2CE4 = "AddressResolverImpl" +0x0cd70607156B1Bb13b0b8eA84d4eb7EbcF7D910A = "AsyncDeployer" +0x146e90184C8BF39CA625B87455671f5F847e562D = "AsyncDeployerImpl" +0xd1c545ed0e7556e0ff67053D2Bf238e47525770C = "AsyncPromiseImpl" +0x40CFF44CaBF79fA755cBAE57feDD8a0e7df66827 = "ERC1967Factory" +0xC5d76C44DbAd3d6aAd28D0983E5ACbc48F40561c = "FeesManager" +0x9B2aAA7aA9500CF3177d0607109dB8Cca13c39c1 = "FeesManagerImpl" +0x5454EaAcF0088F90831a5b6BE48E33b1519436A6 = "FeesPool" +0xD39b436c11B24450549a156Fe85EC3D29da7FB75 = "ForwarderImpl" +0xb870CAf474e88d231afB2ABFc56d76E454d3645f = "ReadPrecompile" +0x1526F70B3C5dD3FF9758f3aAD41E8A8edB40A057 = "SchedulePrecompile" +0x2A6DA10F3B3175Cddf0879527e5AAb94f31980F5 = "Watcher" +0xEEB8A591090d3A73E07d597e418A0Bc5BdF2810b = "WatcherImpl" +0x618F6c88Db4EB1D72427C9d92CD20ed9b862F0fe = "WritePrecompile" +0xBB77f2a581f26041f3cc343d33Cf0b9F6d250F21 = "WritePrecompileImpl" diff --git a/hardhat-scripts/admin/disable-sb.ts b/hardhat-scripts/admin/disable-sb.ts index edd24d1c..acc404c1 100644 --- a/hardhat-scripts/admin/disable-sb.ts +++ b/hardhat-scripts/admin/disable-sb.ts @@ -45,7 +45,7 @@ async function disableSBOnChain( ).connect(socketSigner); // Check if SB is already disabled - const sbStatus = await fastSwitchboard.isValidSwitchboard(sbAddr); + const sbStatus = await fastSwitchboard.switchboardStatus(sbAddr); if (Number(sbStatus) === 1) { console.log(`Fast Switchboard ${sbAddr} on ${chain} is already disabled`); return; diff --git a/hardhat-scripts/admin/disconnect.ts b/hardhat-scripts/admin/disconnect.ts index d7f195a9..c37d7a27 100644 --- a/hardhat-scripts/admin/disconnect.ts +++ b/hardhat-scripts/admin/disconnect.ts @@ -1,6 +1,6 @@ import { constants, Wallet } from "ethers"; import { ChainAddressesObj, ChainSlug, Contracts } from "../../src"; -import { chains, EVMX_CHAIN_ID, getFeesPlugChains, mode } from "../config"; +import { chains, EVMX_CHAIN_ID, getGasStationChains, mode } from "../config"; import { AppGatewayConfig, DeploymentAddresses, @@ -18,7 +18,7 @@ import { getWatcherSigner, sendWatcherMultiCallWithNonce } from "../utils/sign"; import { isConfigSetOnEVMx, isConfigSetOnSocket } from "../utils"; // update this map to disconnect plugs from chains not in this list -const feesPlugChains = getFeesPlugChains(); +const gasStationChains = getGasStationChains(); export const main = async () => { try { @@ -74,13 +74,13 @@ export const disconnectPlugsOnSocket = async () => { // Disconnect plugs on each chain await Promise.all( chains.map(async (chain) => { - // skip if chain is in feesPlugChains or not in addresses - if (feesPlugChains.includes(chain) || !addresses[chain]) return; + // skip if chain is in gasStationChains or not in addresses + if (gasStationChains.includes(chain) || !addresses[chain]) return; const socketSigner = getSocketSigner(chain as ChainSlug); const addr = addresses[chain]!; - if (addr[Contracts.FeesPlug]) { - await disconnectPlug(chain, Contracts.FeesPlug, socketSigner, addr); + if (addr[Contracts.GasStation]) { + await disconnectPlug(chain, Contracts.GasStation, socketSigner, addr); } }) ); @@ -96,10 +96,10 @@ export const updateConfigEVMx = async () => { // Set up Watcher contract const signer = getWatcherSigner(); const EVMxAddresses = addresses[EVMX_CHAIN_ID]!; - const feesManagerContract = ( + const gasAccountManagerContract = ( await getInstance( - Contracts.FeesManager, - EVMxAddresses[Contracts.FeesManager] + Contracts.GasAccountManager, + EVMxAddresses[Contracts.GasAccountManager] ) ).connect(signer); @@ -113,13 +113,13 @@ export const updateConfigEVMx = async () => { // Collect configs for each chain and plug await Promise.all( chains.map(async (chain) => { - // skip if chain is in feesPlugChains or not in addresses - if (feesPlugChains.includes(chain) || !addresses[chain]) return; + // skip if chain is in gasStationChains or not in addresses + if (gasStationChains.includes(chain) || !addresses[chain]) return; const addr = addresses[chain]!; const appGatewayId = BYTES32_ZERO; const switchboardId = "0"; - const plugContract = Contracts.FeesPlug; + const plugContract = Contracts.GasStation; if (!addr[plugContract]) return; @@ -146,13 +146,15 @@ export const updateConfigEVMx = async () => { // update fees manager - const currentFeesPlug = await feesManagerContract.feesPlugs(chain); - if (currentFeesPlug.toString() === BYTES32_ZERO.toString()) { + const currentGasStation = await gasAccountManagerContract.gasStations( + chain + ); + if (currentGasStation.toString() === BYTES32_ZERO.toString()) { console.log(`Fees plug already set on ${chain}`); return; } - const tx = await feesManagerContract.functions["setFeesPlug"]( + const tx = await gasAccountManagerContract.functions["setGasStation"]( Number(chain), BYTES32_ZERO, { diff --git a/hardhat-scripts/admin/rescue.ts b/hardhat-scripts/admin/rescue.ts index a113ecb3..98c6d277 100644 --- a/hardhat-scripts/admin/rescue.ts +++ b/hardhat-scripts/admin/rescue.ts @@ -70,9 +70,9 @@ const createContractAddrArray = (chainSlug: number): string[] => { } let addressArray: string[] = []; - if (chainAddresses.SocketFeesManager) - addressArray.push(chainAddresses.SocketFeesManager); - if (chainAddresses.FeesPlug) addressArray.push(chainAddresses.FeesPlug); + if (chainAddresses.SocketGasAccountManager) + addressArray.push(chainAddresses.SocketGasAccountManager); + if (chainAddresses.GasStation) addressArray.push(chainAddresses.GasStation); addressArray.push(chainAddresses.Socket); addressArray.push(chainAddresses.SocketBatcher); addressArray.push(chainAddresses.FastSwitchboard); diff --git a/hardhat-scripts/config/config.ts b/hardhat-scripts/config/config.ts index f6eb1e45..e7ee998f 100644 --- a/hardhat-scripts/config/config.ts +++ b/hardhat-scripts/config/config.ts @@ -14,7 +14,7 @@ export const mode = process.env.DEPLOYMENT_MODE as // Mode-specific configuration interface interface ModeConfig { chains: ChainSlug[]; - feesPlugChains: ChainSlug[]; + gasStationChains: ChainSlug[]; evmChainId: number; addresses: { watcher: string; @@ -26,17 +26,13 @@ interface ModeConfig { // Configuration for each deployment mode const MODE_CONFIGS: Record = { [DeploymentMode.LOCAL]: { - chains: [ - ChainSlug.ARBITRUM_SEPOLIA, - ChainSlug.OPTIMISM_SEPOLIA, - // ChainSlug.BASE_SEPOLIA, - ], - feesPlugChains: [], // Will use chains by default - evmChainId: 7625382, + chains: [ChainSlug.ARBITRUM_SEPOLIA, ChainSlug.OPTIMISM_SEPOLIA], + gasStationChains: [], // Will use chains by default + evmChainId: 14323, addresses: { - watcher: "0xb62505feacC486e809392c65614Ce4d7b051923b", - transmitter: "0x138e9840861C983DC0BB9b3e941FB7C0e9Ade320", - socketOwner: "0x3339Cf48f1F9cf31b6F8c2664d144c7444eBBB18", + watcher: "0x4512EB56716a2bcBE25bee93dCbb05B95FF603b0", + transmitter: "0x97a1C3e929Ff1246b7347d4e8Ed51748Bbe1d39a", + socketOwner: "0x4512EB56716a2bcBE25bee93dCbb05B95FF603b0", }, }, [DeploymentMode.DEV]: { @@ -64,12 +60,12 @@ const MODE_CONFIGS: Record = { ChainSlug.MAINNET, // ChainSlug.PLUME, ], - feesPlugChains: [], // Will use chains by default + gasStationChains: [], // Will use chains by default evmChainId: 14323, addresses: { - watcher: "0xb62505feacC486e809392c65614Ce4d7b051923b", - transmitter: "0x138e9840861C983DC0BB9b3e941FB7C0e9Ade320", - socketOwner: "0x3339Cf48f1F9cf31b6F8c2664d144c7444eBBB18", + watcher: "0x4512EB56716a2bcBE25bee93dCbb05B95FF603b0", + transmitter: "0x97a1C3e929Ff1246b7347d4e8Ed51748Bbe1d39a", + socketOwner: "0x4512EB56716a2bcBE25bee93dCbb05B95FF603b0", }, }, [DeploymentMode.STAGE]: { @@ -98,7 +94,7 @@ const MODE_CONFIGS: Record = { // ChainSlug.FLOW, // ChainSlug.RISE_TESTNET, ], - feesPlugChains: [], // Will use chains by default + gasStationChains: [], // Will use chains by default evmChainId: 14323, // dummy stage // evmChainId: 12921, addresses: { @@ -130,7 +126,7 @@ const MODE_CONFIGS: Record = { ChainSlug.SONIC, ChainSlug.UNICHAIN, ], - feesPlugChains: [ + gasStationChains: [ ChainSlug.ARBITRUM, ChainSlug.AVALANCHE, ChainSlug.BASE, @@ -169,10 +165,10 @@ export const getChains = (): ChainSlug[] => { return getCurrentModeConfig().chains; }; -export const getFeesPlugChains = (): ChainSlug[] => { +export const getGasStationChains = (): ChainSlug[] => { const config = getCurrentModeConfig(); - return config.feesPlugChains.length > 0 - ? config.feesPlugChains + return config.gasStationChains.length > 0 + ? config.gasStationChains : config.chains; }; @@ -224,6 +220,8 @@ export const cronOnlyChains: Array = [ // Derived chain lists (depend on current mode) export const IndexerHighChains: Array = [ + ChainSlug.ARBITRUM_SEPOLIA, + ChainSlug.OPTIMISM_SEPOLIA, ChainSlug.MAINNET, ChainSlug.OPTIMISM, ChainSlug.ARBITRUM, @@ -294,7 +292,7 @@ export const MAX_SCHEDULE_DELAY_SECONDS = 60 * 60 * 24; // 24 hours export const UPGRADE_VERSION = 1; // Transmitter thresholds -export const TRANSMITTER_CREDIT_THRESHOLD = ethers.utils.parseEther("100"); // 100 ETH threshold +export const TRANSMITTER_GAS_THRESHOLD = ethers.utils.parseEther("100"); // 100 ETH threshold export const TRANSMITTER_NATIVE_THRESHOLD = ethers.utils.parseEther("100"); // 100 ETH threshold // Performance settings diff --git a/hardhat-scripts/constants/constants.ts b/hardhat-scripts/constants/constants.ts index da2495ec..7768ec29 100644 --- a/hardhat-scripts/constants/constants.ts +++ b/hardhat-scripts/constants/constants.ts @@ -13,3 +13,5 @@ export const BYTES32_ZERO = ethers.constants.HashZero; export const MSG_SB_FEES = "100000000"; export const FEE_MANAGER_WRITE_MAX_FEES = ethers.utils.parseEther("10"); + +export const DEFAULT_DEADLINE = 86400; diff --git a/hardhat-scripts/constants/feeConstants.ts b/hardhat-scripts/constants/feeConstants.ts index b570ddc5..5446c48a 100644 --- a/hardhat-scripts/constants/feeConstants.ts +++ b/hardhat-scripts/constants/feeConstants.ts @@ -342,7 +342,7 @@ export const tokens: TokenMap = { }; export const feePools: { [key: string]: string } = { - [DeploymentMode.LOCAL]: "0x9De353dD1131aB4e502590D3a1832652FA316268", + [DeploymentMode.LOCAL]: "", [DeploymentMode.DEV]: "0x13A3018920c7b56B20dd34E29C298121025E6de4", [DeploymentMode.STAGE]: "0xC8d803B7c1719cdF21392405879D1B56398045C4", [DeploymentMode.PROD]: "0x3B1f4ABA1667EeB992B623E7c6d119728cEd3b15", diff --git a/hardhat-scripts/constants/types.ts b/hardhat-scripts/constants/types.ts index 1a229d3c..53eadcc8 100644 --- a/hardhat-scripts/constants/types.ts +++ b/hardhat-scripts/constants/types.ts @@ -7,7 +7,7 @@ export type DeploymentAddresses = { export interface WatcherMultiCallParams { contractAddress: string; data: string; - nonce: number; + nonce: number | string; signature: string; } diff --git a/hardhat-scripts/deploy/1.deploy.ts b/hardhat-scripts/deploy/1.deploy.ts index 734c8c23..48f2bdd9 100644 --- a/hardhat-scripts/deploy/1.deploy.ts +++ b/hardhat-scripts/deploy/1.deploy.ts @@ -2,31 +2,23 @@ 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, chains, EVMX_CHAIN_ID, WRITE_EXPIRY_TIME, READ_EXPIRY_TIME, SCHEDULE_EXPIRY_TIME, - getFeesPlugChains, logConfig, - MAX_RE_AUCTION_COUNT, MAX_SCHEDULE_DELAY_SECONDS, mode, READ_FEES, SCHEDULE_CALLBACK_FEES, SCHEDULE_FEES_PER_SECOND, skipEVMXDeployment, - TRIGGER_FEES, + transmitter, WRITE_FEES, + TRIGGER_FEES, } from "../config/config"; import { DeploymentAddresses, @@ -34,6 +26,7 @@ import { getFeePool, IMPLEMENTATION_SLOT, FEE_MANAGER_WRITE_MAX_FEES, + DEFAULT_DEADLINE, } from "../constants"; import { DeployParams, @@ -121,16 +114,16 @@ const deployEVMxContracts = async () => { const feePool = getFeePool(); if (feePool?.length == 0) { - const feesPool = await getOrDeploy( - Contracts.FeesPool, - Contracts.FeesPool, - "contracts/evmx/fees/FeesPool.sol", + const gasVault = await getOrDeploy( + Contracts.GasVault, + Contracts.GasVault, + "contracts/evmx/fees/GasVault.sol", [EVMxOwner], deployUtils ); - deployUtils.addresses[Contracts.FeesPool] = feesPool.address; + deployUtils.addresses[Contracts.GasVault] = gasVault.address; } else { - deployUtils.addresses[Contracts.FeesPool] = feePool; + deployUtils.addresses[Contracts.GasVault] = feePool; } deployUtils = await deployContractWithProxy( @@ -147,12 +140,12 @@ const deployEVMxContracts = async () => { ); deployUtils = await deployContractWithProxy( - Contracts.FeesManager, - `contracts/evmx/fees/FeesManager.sol`, + Contracts.GasAccountManager, + `contracts/evmx/fees/GasAccountManager.sol`, [ EVMX_CHAIN_ID, addressResolver.address, - deployUtils.addresses[Contracts.FeesPool], + deployUtils.addresses[Contracts.GasVault], EVMxOwner, FEE_MANAGER_WRITE_MAX_FEES, FAST_SWITCHBOARD_TYPE, @@ -164,7 +157,7 @@ const deployEVMxContracts = async () => { deployUtils = await deployContractWithProxy( Contracts.AsyncDeployer, `contracts/evmx/helpers/AsyncDeployer.sol`, - [EVMxOwner, addressResolver.address], + [EVMxOwner, addressResolver.address, DEFAULT_DEADLINE], proxyFactory, deployUtils ); @@ -184,58 +177,18 @@ const deployEVMxContracts = async () => { deployUtils = await deployContractWithProxy( Contracts.Watcher, `contracts/evmx/watcher/Watcher.sol`, - [EVMX_CHAIN_ID, TRIGGER_FEES, EVMxOwner, addressResolver.address], - proxyFactory, - deployUtils - ); - - deployUtils = await deployContractWithProxy( - Contracts.AuctionManager, - `contracts/evmx/AuctionManager.sol`, [ EVMX_CHAIN_ID, - BID_TIMEOUT, - MAX_RE_AUCTION_COUNT, - AUCTION_END_DELAY_SECONDS, - addressResolver.address, EVMxOwner, + addressResolver.address, + transmitter, + "0x0000000000000000000000000000000000000000000000000000000000000000", // transmitterSolana - using 0 for now + TRIGGER_FEES, ], proxyFactory, deployUtils ); - deployUtils = await deployContractWithProxy( - Contracts.DeployForwarder, - `contracts/evmx/helpers/DeployForwarder.sol`, - [EVMxOwner, addressResolver.address, FAST_SWITCHBOARD_TYPE], - proxyFactory, - deployUtils - ); - - deployUtils = await deployContractWithProxy( - Contracts.Configurations, - `contracts/evmx/watcher/Configurations.sol`, - [deployUtils.addresses[Contracts.Watcher], EVMxOwner], - proxyFactory, - deployUtils - ); - - deployUtils = await deployContractWithProxy( - Contracts.RequestHandler, - `contracts/evmx/watcher/RequestHandler.sol`, - [EVMxOwner, addressResolver.address], - proxyFactory, - deployUtils - ); - - deployUtils = await deployContractWithProxy( - Contracts.PromiseResolver, - `contracts/evmx/watcher/PromiseResolver.sol`, - [deployUtils.addresses[Contracts.Watcher]], - proxyFactory, - deployUtils - ); - deployUtils = await deployContractWithProxy( Contracts.WritePrecompile, `contracts/evmx/watcher/precompiles/WritePrecompile.sol`, @@ -347,50 +300,25 @@ 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; - - // contractName = Contracts.MessageSwitchboard; - // const messageSwitchboard: Contract = await getOrDeploy( - // contractName, - // contractName, - // `contracts/protocol/switchboard/${contractName}.sol`, - // [chain as ChainSlug, socket.address, socketOwner], - // deployUtils - // ); - // deployUtils.addresses[contractName] = messageSwitchboard.address; - - contractName = Contracts.FeesPlug; - const feesPlug: Contract = await getOrDeploy( + contractName = Contracts.GasStation; + const gasStation: Contract = await getOrDeploy( contractName, contractName, `contracts/evmx/plugs/${contractName}.sol`, [socket.address, socketOwner], deployUtils ); - deployUtils.addresses[contractName] = feesPlug.address; + deployUtils.addresses[contractName] = gasStation.address; - contractName = Contracts.ContractFactoryPlug; - const contractFactoryPlug: Contract = await getOrDeploy( - contractName, - contractName, - `contracts/evmx/plugs/${contractName}.sol`, - [socket.address, socketOwner], - deployUtils - ); - deployUtils.addresses[contractName] = contractFactoryPlug.address; + // contractName = Contracts.ContractFactoryPlug; + // const contractFactoryPlug: Contract = await getOrDeploy( + // contractName, + // contractName, + // `contracts/evmx/plugs/${contractName}.sol`, + // [socket.address, socketOwner], + // deployUtils + // ); + // deployUtils.addresses[contractName] = contractFactoryPlug.address; deployUtils.addresses.startBlock = (deployUtils.addresses.startBlock diff --git a/hardhat-scripts/deploy/2.roles.ts b/hardhat-scripts/deploy/2.roles.ts index 0f43c4d3..d3d11cec 100644 --- a/hardhat-scripts/deploy/2.roles.ts +++ b/hardhat-scripts/deploy/2.roles.ts @@ -25,26 +25,25 @@ import { getWatcherSigner, getSocketSigner } from "../utils/sign"; export const REQUIRED_ROLES = { EVMx: { - AuctionManager: [ROLES.TRANSMITTER_ROLE], - FeesPool: [ROLES.FEE_MANAGER_ROLE], + GasVault: [ROLES.FEE_MANAGER_ROLE], }, Chain: { FastSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], - CCTPSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], - MessageSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], + // CCTPSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], + // MessageSwitchboard: [ROLES.WATCHER_ROLE, ROLES.RESCUE_ROLE], Socket: [ ROLES.GOVERNANCE_ROLE, ROLES.RESCUE_ROLE, ROLES.SWITCHBOARD_DISABLER_ROLE, ], - FeesPlug: [ROLES.RESCUE_ROLE], - ContractFactoryPlug: [ROLES.RESCUE_ROLE], + GasStation: [ROLES.RESCUE_ROLE], + // ContractFactoryPlug: [ROLES.RESCUE_ROLE], }, }; async function setRoleForContract( contractName: Contracts, - contractAddress: string | number, + contractAddress: string, targetAddress: string, roleName: string, signer: Wallet, @@ -120,18 +119,9 @@ async function setRolesForEVMx(addresses: DeploymentAddresses) { const signer = await getSigner(EVMX_CHAIN_ID, true); await setRoleForContract( - Contracts.AuctionManager, - chainAddresses[Contracts.AuctionManager], - transmitter, - ROLES.TRANSMITTER_ROLE, - signer, - EVMX_CHAIN_ID - ); - - await setRoleForContract( - Contracts.FeesPool, - chainAddresses[Contracts.FeesPool], - chainAddresses[Contracts.FeesManager], + Contracts.GasVault, + chainAddresses[Contracts.GasVault], + chainAddresses[Contracts.GasAccountManager], ROLES.FEE_MANAGER_ROLE, signer, EVMX_CHAIN_ID @@ -148,11 +138,6 @@ export const main = async () => { for (const chain of chains) { await setRolesOnChain(chain, addresses); } - // const limit = pLimit(CONCURRENCY_LIMIT); - // const chainTasks = chains.map((chain) => - // limit(() => setRolesOnChain(chain, addresses)) - // ); - // await Promise.all(chainTasks); await setRolesForEVMx(addresses); } catch (error) { diff --git a/hardhat-scripts/deploy/3.configureChains.ts b/hardhat-scripts/deploy/3.configureChains.ts index 8effd77d..3488c30c 100644 --- a/hardhat-scripts/deploy/3.configureChains.ts +++ b/hardhat-scripts/deploy/3.configureChains.ts @@ -6,7 +6,7 @@ import { ChainAddressesObj, ChainSlug, Contracts } from "../../src"; import { chains, EVMX_CHAIN_ID, - getFeesPlugChains, + getGasStationChains, MAX_MSG_VALUE_LIMIT, mode, } from "../config"; @@ -94,8 +94,8 @@ export const configureChains = async (addresses: DeploymentAddresses) => { // messageSwitchboardId.toString() // ] = chainAddresses[Contracts.MessageSwitchboard]; - if (chainAddresses[Contracts.FeesPlug]) { - await whitelistToken(chain, chainAddresses[Contracts.FeesPlug], signer); + if (chainAddresses[Contracts.GasStation]) { + await whitelistToken(chain, chainAddresses[Contracts.GasStation], signer); } await setMaxMsgValueLimit(chain); @@ -146,13 +146,10 @@ async function setOnchainContracts( const chainAddresses = addresses[chain] as ChainAddressesObj; const socket = toBytes32FormatHexString(chainAddresses[Contracts.Socket]); - const contractFactory = toBytes32FormatHexString( - chainAddresses[Contracts.ContractFactoryPlug] - ); await updateContractSettings( EVMX_CHAIN_ID, - Contracts.Configurations, + Contracts.Watcher, "switchboards", [chain, FAST_SWITCHBOARD_TYPE], fastSwitchboardId, @@ -163,7 +160,7 @@ async function setOnchainContracts( // await updateContractSettings( // EVMX_CHAIN_ID, - // Contracts.Configurations, + // Contracts.Watcher, // "switchboards", // [chain, CCTP_SWITCHBOARD_TYPE], // cctpSwitchboardId, @@ -174,7 +171,7 @@ async function setOnchainContracts( await updateContractSettings( EVMX_CHAIN_ID, - Contracts.Configurations, + Contracts.Watcher, "sockets", [chain], toBytes32FormatHexString(socket), @@ -183,33 +180,22 @@ async function setOnchainContracts( signer ); - if (chainAddresses[Contracts.FeesPlug]) { - const feesPlug = toBytes32FormatHexString( - chainAddresses[Contracts.FeesPlug]! + if (chainAddresses[Contracts.GasStation]) { + const gasStation = toBytes32FormatHexString( + chainAddresses[Contracts.GasStation]! ); await updateContractSettings( EVMX_CHAIN_ID, - Contracts.FeesManager, - "feesPlugs", + Contracts.GasAccountManager, + "gasStations", [chain], - toBytes32FormatHexString(feesPlug).toString(), - "setFeesPlug", - [chain, toBytes32FormatHexString(feesPlug)], + toBytes32FormatHexString(gasStation).toString(), + "setGasStation", + [chain, toBytes32FormatHexString(gasStation)], signer ); } - - await updateContractSettings( - EVMX_CHAIN_ID, - Contracts.WritePrecompile, - "contractFactoryPlugs", - [chain], - toBytes32FormatHexString(contractFactory).toString(), - "setContractFactoryPlugs", - [chain, toBytes32FormatHexString(contractFactory)], - signer - ); } // const setSiblingConfig = async ( @@ -364,7 +350,7 @@ const registerSb = async ( // send overrides while reading capacitor to avoid errors on mantle chain // some chains give balance error if gas price is used with from address as zero // therefore override from address as well - switchboardId = await socket.switchboardIds(sbAddress, { + switchboardId = await socket.switchboardAddressToId(sbAddress, { from: signer.address, ...(await overrides(chain)), }); @@ -390,37 +376,37 @@ const registerSb = async ( export const whitelistToken = async ( chain: number, - feesPlugAddress: string, + gasStationAddress: string, signer: Signer ) => { console.log("Whitelisting token"); - if (!getFeesPlugChains().includes(chain as ChainSlug)) { + if (!getGasStationChains().includes(chain as ChainSlug)) { console.log( "Skipping whitelisting token for fees plug, not part of fees plug chains" ); return; } - console.log("feesPlugAddress: ", feesPlugAddress); - const feesPlugContract = ( - await getInstance(Contracts.FeesPlug, feesPlugAddress) + console.log("gasStationAddress: ", gasStationAddress); + const gasStationContract = ( + await getInstance(Contracts.GasStation, gasStationAddress) ).connect(signer); - // console.log("feesPlugContract: ", feesPlugContract); + // console.log("gasStationContract: ", gasStationContract); const tokens = getFeeTokens(chain); console.log("tokens: ", tokens); if (tokens.length == 0) return; for (const token of tokens) { console.log("token: ", token); - const isWhitelisted = await feesPlugContract.whitelistedTokens(token, { + const isWhitelisted = await gasStationContract.whitelistedTokens(token, { ...(await getReadOverrides(chain as ChainSlug)), }); if (!isWhitelisted) { - const tx = await feesPlugContract.whitelistToken(token, { + const tx = await gasStationContract.whitelistToken(token, { ...(await overrides(chain)), }); console.log( - `Whitelisting token ${token} for ${feesPlugContract.address}, txHash: ${tx.hash}` + `Whitelisting token ${token} for ${gasStationContract.address}, txHash: ${tx.hash}` ); await tx.wait(); } else { diff --git a/hardhat-scripts/deploy/4.configureEVMx.ts b/hardhat-scripts/deploy/4.configureEVMx.ts index 77b2ea27..e9d370cf 100644 --- a/hardhat-scripts/deploy/4.configureEVMx.ts +++ b/hardhat-scripts/deploy/4.configureEVMx.ts @@ -47,22 +47,11 @@ export const configureEVMx = async (evmxAddresses: EVMxAddressesObj) => { await updateContractSettings( EVMX_CHAIN_ID, Contracts.AddressResolver, - "feesManager__", + "gasAccountManager__", [], - evmxAddresses[Contracts.FeesManager], - "setFeesManager", - [evmxAddresses[Contracts.FeesManager]], - signer - ); - - await updateContractSettings( - EVMX_CHAIN_ID, - Contracts.AddressResolver, - "defaultAuctionManager", - [], - evmxAddresses[Contracts.AuctionManager], - "setDefaultAuctionManager", - [evmxAddresses[Contracts.AuctionManager]], + evmxAddresses[Contracts.GasAccountManager], + "setGasAccountManager", + [evmxAddresses[Contracts.GasAccountManager]], signer ); @@ -77,20 +66,20 @@ export const configureEVMx = async (evmxAddresses: EVMxAddressesObj) => { signer ); - await updateContractSettings( - EVMX_CHAIN_ID, - Contracts.AddressResolver, - "deployForwarder__", - [], - evmxAddresses[Contracts.DeployForwarder], - "setDeployForwarder", - [evmxAddresses[Contracts.DeployForwarder]], - signer - ); + // await updateContractSettings( + // EVMX_CHAIN_ID, + // Contracts.AddressResolver, + // "deployForwarder__", + // [], + // evmxAddresses[Contracts.DeployForwarder], + // "setDeployForwarder", + // [evmxAddresses[Contracts.DeployForwarder]], + // signer + // ); await updateContractSettings( EVMX_CHAIN_ID, - Contracts.RequestHandler, + Contracts.Watcher, "precompiles", [READ], evmxAddresses[Contracts.ReadPrecompile], @@ -101,7 +90,7 @@ export const configureEVMx = async (evmxAddresses: EVMxAddressesObj) => { await updateContractSettings( EVMX_CHAIN_ID, - Contracts.RequestHandler, + Contracts.Watcher, "precompiles", [WRITE], evmxAddresses[Contracts.WritePrecompile], @@ -112,7 +101,7 @@ export const configureEVMx = async (evmxAddresses: EVMxAddressesObj) => { await updateContractSettings( EVMX_CHAIN_ID, - Contracts.RequestHandler, + Contracts.Watcher, "precompiles", [SCHEDULE], evmxAddresses[Contracts.SchedulePrecompile], @@ -120,20 +109,18 @@ export const configureEVMx = async (evmxAddresses: EVMxAddressesObj) => { [SCHEDULE, evmxAddresses[Contracts.SchedulePrecompile]], signer ); - - await setWatcherCoreContracts(evmxAddresses); }; const checkAndSetMaxFees = async (evmxAddresses: EVMxAddressesObj) => { - const feesManagerContract = ( + const gasAccountManagerContract = ( await getInstance( - Contracts.FeesManager, - evmxAddresses[Contracts.FeesManager] + Contracts.GasAccountManager, + evmxAddresses[Contracts.GasAccountManager] ) ).connect(getWatcherSigner()); const allChains = [...chains, EVMX_CHAIN_ID]; - const currentMaxFeesArray = await feesManagerContract.getChainMaxFees( + const currentMaxFeesArray = await gasAccountManagerContract.getChainMaxFees( allChains ); const maxFeesUpdateArray: { chainSlug: number; maxFees: string }[] = []; @@ -155,7 +142,7 @@ const checkAndSetMaxFees = async (evmxAddresses: EVMxAddressesObj) => { if (maxFeesUpdateArray.length > 0) { const chains = maxFeesUpdateArray.map((item) => item.chainSlug); const maxFees = maxFeesUpdateArray.map((item) => item.maxFees); - let tx = await feesManagerContract.setChainMaxFees(chains, maxFees); + let tx = await gasAccountManagerContract.setChainMaxFees(chains, maxFees); console.log( `Setting Chain Max Fees for chains: ${chains.join(", ")} tx hash: ${ tx.hash @@ -165,39 +152,6 @@ const checkAndSetMaxFees = async (evmxAddresses: EVMxAddressesObj) => { } }; -export const setWatcherCoreContracts = async ( - evmxAddresses: EVMxAddressesObj -) => { - const watcherContract = ( - await getInstance(Contracts.Watcher, evmxAddresses[Contracts.Watcher]) - ).connect(getWatcherSigner()); - - const requestHandlerSet = await watcherContract.requestHandler__(); - const PromiseResolverSet = await watcherContract.promiseResolver__(); - const ConfigurationsSet = await watcherContract.configurations__(); - - if ( - requestHandlerSet.toLowerCase() !== - evmxAddresses[Contracts.RequestHandler].toLowerCase() || - PromiseResolverSet.toLowerCase() !== - evmxAddresses[Contracts.PromiseResolver].toLowerCase() || - ConfigurationsSet.toLowerCase() !== - evmxAddresses[Contracts.Configurations].toLowerCase() - ) { - console.log("Setting watcher core contracts"); - const tx = await watcherContract.setCoreContracts( - evmxAddresses[Contracts.RequestHandler], - evmxAddresses[Contracts.Configurations], - evmxAddresses[Contracts.PromiseResolver], - { ...(await overrides(EVMX_CHAIN_ID)) } - ); - console.log("Watcher core contracts set tx: ", tx.hash); - await tx.wait(); - } else { - console.log("Watcher core contracts are already set"); - } -}; - main() .then(() => process.exit(0)) .catch((error: Error) => { diff --git a/hardhat-scripts/deploy/5.fundTransfers.ts b/hardhat-scripts/deploy/5.fundTransfers.ts index 584e2ef2..daf8565a 100644 --- a/hardhat-scripts/deploy/5.fundTransfers.ts +++ b/hardhat-scripts/deploy/5.fundTransfers.ts @@ -9,30 +9,30 @@ import { import { getAddresses, getWatcherSigner } from "../utils"; config(); -export const fundFeesPool = async (watcherSigner: Signer) => { +export const fundGasVault = async (watcherSigner: Signer) => { const addresses = getAddresses(mode); - const feesPoolAddress = addresses[EVMX_CHAIN_ID][Contracts.FeesPool]; - const feesPoolBalance = await watcherSigner.provider!.getBalance( - feesPoolAddress + const gasVaultAddress = addresses[EVMX_CHAIN_ID][Contracts.GasVault]; + const gasVaultBalance = await watcherSigner.provider!.getBalance( + gasVaultAddress ); console.log({ - feesPoolAddress, - feesPoolBalance, + gasVaultAddress, + gasVaultBalance, FEES_POOL_FUNDING_AMOUNT_THRESHOLD, }); - if (feesPoolBalance.gte(FEES_POOL_FUNDING_AMOUNT_THRESHOLD)) { + if (gasVaultBalance.gte(FEES_POOL_FUNDING_AMOUNT_THRESHOLD)) { console.log( - `Fees pool ${feesPoolAddress} already has sufficient balance, skipping funding` + `Gas vault ${gasVaultAddress} already has sufficient balance, skipping funding` ); return; } const tx = await watcherSigner.sendTransaction({ - to: feesPoolAddress, + to: gasVaultAddress, value: FEES_POOL_FUNDING_AMOUNT_THRESHOLD, }); console.log( - `Funding fees pool ${feesPoolAddress} with ${FEES_POOL_FUNDING_AMOUNT_THRESHOLD} ETH, txHash: `, + `Funding gas vault ${gasVaultAddress} with ${FEES_POOL_FUNDING_AMOUNT_THRESHOLD} ETH, txHash: `, tx.hash ); await tx.wait(); @@ -41,7 +41,7 @@ export const fundFeesPool = async (watcherSigner: Signer) => { const main = async () => { console.log("Fund transfers"); const watcherSigner = getWatcherSigner(); - await fundFeesPool(watcherSigner); + await fundGasVault(watcherSigner); }; main(); diff --git a/hardhat-scripts/deploy/6.connect.ts b/hardhat-scripts/deploy/6.connect.ts index a0f09b7c..d4adc321 100644 --- a/hardhat-scripts/deploy/6.connect.ts +++ b/hardhat-scripts/deploy/6.connect.ts @@ -1,7 +1,11 @@ -import { Wallet } from "ethers"; +import { ethers, Wallet } from "ethers"; import { ChainAddressesObj, ChainSlug, Contracts } from "../../src"; import { chains, CONCURRENCY_LIMIT, EVMX_CHAIN_ID, mode } from "../config"; -import { AppGatewayConfig, DeploymentAddresses } from "../constants"; +import { + AppGatewayConfig, + DeploymentAddresses, + WatcherMultiCallParams, +} from "../constants"; import { checkIfAppGatewayIdExists, getAddresses, @@ -11,11 +15,12 @@ import { overrides, toBytes32FormatHexString, } from "../utils"; -import { getWatcherSigner, sendWatcherMultiCallWithNonce } from "../utils/sign"; +import { getWatcherSigner, signWatcherMessage } from "../utils/sign"; import { isConfigSetOnEVMx, isConfigSetOnSocket } from "../utils"; import pLimit from "p-limit"; -const plugs = [Contracts.ContractFactoryPlug, Contracts.FeesPlug]; +// const plugs = [Contracts.ContractFactoryPlug, Contracts.GasStation]; +const plugs = [Contracts.GasStation]; // Main function to connect plugs on all chains export const main = async () => { @@ -121,10 +126,7 @@ export const updateConfigEVMx = async () => { const signer = getWatcherSigner(); const EVMxAddresses = addresses[EVMX_CHAIN_ID]!; const configurationsContract = ( - await getInstance( - Contracts.Configurations, - EVMxAddresses[Contracts.Configurations] - ) + await getInstance(Contracts.Watcher, EVMxAddresses[Contracts.Watcher]) ).connect(signer); // Collect configs for each chain and plug @@ -171,14 +173,27 @@ export const updateConfigEVMx = async () => { // Update configs if any changes needed if (appConfigs.length > 0) { console.log({ appConfigs }); - const calldata = configurationsContract.interface.encodeFunctionData( - "setAppGatewayConfigs", + const calldata = ethers.utils.defaultAbiCoder.encode( + [ + "tuple(tuple(bytes32 appGatewayId,uint64 switchboardId) plugConfig,bytes32 plug,uint32 chainSlug)[]", + ], [appConfigs] ); - const tx = await sendWatcherMultiCallWithNonce( + + const { nonce, signature } = await signWatcherMessage( configurationsContract.address, calldata ); + + const params: WatcherMultiCallParams = { + contractAddress: configurationsContract.address, + data: calldata, + nonce: nonce, + signature, + }; + const tx = await configurationsContract.setAppGatewayConfigs(params, { + ...(await overrides(EVMX_CHAIN_ID as ChainSlug)), + }); console.log(`Updating EVMx Config tx hash: ${tx.hash}`); await tx.wait(); } diff --git a/hardhat-scripts/deploy/7.upload.ts b/hardhat-scripts/deploy/7.upload.ts index 02e5e462..02d9204d 100644 --- a/hardhat-scripts/deploy/7.upload.ts +++ b/hardhat-scripts/deploy/7.upload.ts @@ -24,7 +24,7 @@ const getBucketName = () => { const getFileName = () => { switch (mode) { case DeploymentMode.LOCAL: - return process.env.CONFIG_FILE_NAME || "pocConfig.json"; + return process.env.CONFIG_FILE_NAME || "localConfig.json"; case DeploymentMode.DEV: return "devConfig.json"; case DeploymentMode.STAGE: diff --git a/hardhat-scripts/deploy/8.setupEnv.ts b/hardhat-scripts/deploy/8.setupEnv.ts index a348422e..2f3e8fe0 100644 --- a/hardhat-scripts/deploy/8.setupEnv.ts +++ b/hardhat-scripts/deploy/8.setupEnv.ts @@ -31,10 +31,8 @@ const updatedLines = lines.map((line) => { return `ADDRESS_RESOLVER=${latestEVMxAddresses[Contracts.AddressResolver]}`; } else if (line.startsWith("WATCHER=")) { return `WATCHER=${latestEVMxAddresses[Contracts.Watcher]}`; - } else if (line.startsWith("AUCTION_MANAGER=")) { - return `AUCTION_MANAGER=${latestEVMxAddresses[Contracts.AuctionManager]}`; } else if (line.startsWith("FEES_MANAGER=")) { - return `FEES_MANAGER=${latestEVMxAddresses[Contracts.FeesManager]}`; + return `FEES_MANAGER=${latestEVMxAddresses[Contracts.GasAccountManager]}`; } else if (line.startsWith("ARBITRUM_SOCKET=")) { return `ARBITRUM_SOCKET=${arbSepoliaAddresses[Contracts.Socket]}`; } else if (line.startsWith("ARBITRUM_SWITCHBOARD=")) { @@ -42,9 +40,9 @@ const updatedLines = lines.map((line) => { arbSepoliaAddresses[Contracts.FastSwitchboard] }`; } else if (line.startsWith("ARBITRUM_FEES_PLUG=")) { - const feesPlug = arbSepoliaAddresses[Contracts.FeesPlug]; - if (feesPlug) { - return `ARBITRUM_FEES_PLUG=${feesPlug}`; + const gasStation = arbSepoliaAddresses[Contracts.GasStation]; + if (gasStation) { + return `ARBITRUM_FEES_PLUG=${gasStation}`; } else { return line; } diff --git a/hardhat-scripts/deploy/9.setupTransmitter.ts b/hardhat-scripts/deploy/9.setupTransmitter.ts index 4bbe3431..28da9cd1 100644 --- a/hardhat-scripts/deploy/9.setupTransmitter.ts +++ b/hardhat-scripts/deploy/9.setupTransmitter.ts @@ -3,7 +3,7 @@ import { ChainSlug, Contracts, EVMxAddressesObj } from "../../src"; import { EVMX_CHAIN_ID, mode, - TRANSMITTER_CREDIT_THRESHOLD, + TRANSMITTER_GAS_THRESHOLD, TRANSMITTER_NATIVE_THRESHOLD, } from "../config/config"; import { getAddresses } from "../utils/address"; @@ -13,16 +13,15 @@ import { getTransmitterSigner, getWatcherSigner } from "../utils/sign"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; let evmxAddresses: EVMxAddressesObj; -let feesManagerContract: Contract; +let gasAccountManagerContract: Contract; let transmitterSigner: SignerWithAddress | Wallet; let transmitterAddress: string; export const main = async () => { console.log("Setting up transmitter..."); await init(); - await checkAndDepositCredits(transmitterAddress); + await checkAndDepositForGass(transmitterAddress); await checkAndDepositNative(transmitterAddress); - await approveAuctionManager(); console.log("Transmitter setup complete!"); }; @@ -30,54 +29,31 @@ export const main = async () => { export const init = async () => { const addresses = getAddresses(mode); evmxAddresses = addresses[EVMX_CHAIN_ID] as EVMxAddressesObj; - feesManagerContract = await getInstance( - Contracts.FeesManager, - evmxAddresses[Contracts.FeesManager] + gasAccountManagerContract = await getInstance( + Contracts.GasAccountManager, + evmxAddresses[Contracts.GasAccountManager] ); transmitterSigner = getTransmitterSigner(EVMX_CHAIN_ID as ChainSlug); transmitterAddress = await transmitterSigner.getAddress(); }; -export const approveAuctionManager = async () => { - const auctionManagerAddress = evmxAddresses[Contracts.AuctionManager]; - const isAlreadyApproved = await feesManagerContract - .connect(transmitterSigner) - .isApproved(transmitterAddress, auctionManagerAddress); - - if (!isAlreadyApproved) { - console.log("Approving auction manager"); - const tx = await feesManagerContract - .connect(transmitterSigner) - ["approve(address,bool)"]( - auctionManagerAddress, - true, - await overrides(EVMX_CHAIN_ID as ChainSlug) - ); - console.log("Auction manager approval tx hash:", tx.hash); - await tx.wait(); - console.log("Auction manager approved"); - } else { - console.log("Auction manager already approved"); - } -}; - -export const checkAndDepositCredits = async (transmitter: string) => { - console.log("Checking and depositing credits"); - const credits = await feesManagerContract +export const checkAndDepositForGass = async (transmitter: string) => { + console.log("Checking and depositing gas"); + const gas = await gasAccountManagerContract .connect(transmitterSigner) .balanceOf(transmitter); - if (credits.lt(TRANSMITTER_CREDIT_THRESHOLD)) { - console.log("Depositing credits for transmitter..."); - const tx = await feesManagerContract + if (gas.lt(TRANSMITTER_GAS_THRESHOLD)) { + console.log("Depositing gas for transmitter..."); + const tx = await gasAccountManagerContract .connect(getWatcherSigner()) - .wrap(transmitter, { + .wrapToGas(transmitter, { ...(await overrides(EVMX_CHAIN_ID as ChainSlug)), - value: TRANSMITTER_CREDIT_THRESHOLD, + value: TRANSMITTER_GAS_THRESHOLD, }); - console.log("Credits wrap tx hash:", tx.hash); + console.log("Gas wrap tx hash:", tx.hash); await tx.wait(); - console.log("Credits wrapped"); + console.log("Gas wrapped"); } }; diff --git a/hardhat-scripts/deploy/UpgradeForwarder.ts b/hardhat-scripts/deploy/UpgradeForwarder.ts index 349a665c..7c985793 100644 --- a/hardhat-scripts/deploy/UpgradeForwarder.ts +++ b/hardhat-scripts/deploy/UpgradeForwarder.ts @@ -1,7 +1,7 @@ import { config as dotenvConfig } from "dotenv"; dotenvConfig(); -import { ChainSlug } from "@socket.tech/socket-protocol"; +import { ChainSlug } from "../../src"; import { Wallet } from "ethers"; import { ChainAddressesObj, Contracts } from "../../src"; import { EVMX_CHAIN_ID, mode } from "../config"; diff --git a/hardhat-scripts/deploy/UpgradePromise.ts b/hardhat-scripts/deploy/UpgradePromise.ts index 7e0b8435..b6ea1fa7 100644 --- a/hardhat-scripts/deploy/UpgradePromise.ts +++ b/hardhat-scripts/deploy/UpgradePromise.ts @@ -1,7 +1,7 @@ import { config as dotenvConfig } from "dotenv"; dotenvConfig(); -import { ChainSlug } from "@socket.tech/socket-protocol"; +import { ChainSlug } from "../../src"; import { Wallet } from "ethers"; import { ChainAddressesObj, Contracts } from "../../src"; import { EVMX_CHAIN_ID, mode } from "../config"; diff --git a/hardhat-scripts/deploy/WhitelistFeesReceiver.ts b/hardhat-scripts/deploy/WhitelistFeesReceiver.ts deleted file mode 100644 index 1d7e3aed..00000000 --- a/hardhat-scripts/deploy/WhitelistFeesReceiver.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { config as dotenvConfig } from "dotenv"; -dotenvConfig(); - -import { Contracts, EVMxAddressesObj } from "../../src"; -import { Wallet } from "ethers"; -import { EVMX_CHAIN_ID, mode } from "../config"; -import { DeploymentAddresses } from "../constants"; -import { getAddresses, getInstance, getWatcherSigner } from "../utils"; - -const ADDRESS_TO_WHITELIST = "0xbC4D50311708FFAFC1A26882fdab17cBfE55CBB9"; - -export const main = async () => { - let addresses: DeploymentAddresses; - try { - console.log("Configuring EVMx contracts"); - addresses = getAddresses(mode) as unknown as DeploymentAddresses; - const evmxAddresses = addresses[EVMX_CHAIN_ID] as EVMxAddressesObj; - const signer: Wallet = getWatcherSigner(); - - const feesManager = await getInstance( - Contracts.FeesManager, - evmxAddresses[Contracts.FeesManager] - ); - const tx = await feesManager - .connect(signer) - .setWhitelistedReceiver(ADDRESS_TO_WHITELIST, true); - console.log("Fees manager whitelisted receiver set tx: ", tx.hash); - await tx.wait(); - console.log("Fees manager whitelisted receiver set"); - } catch (error) { - console.log("Error:", error); - } -}; - -main() - .then(() => process.exit(0)) - .catch((error: Error) => { - console.error(error); - process.exit(1); - }); diff --git a/hardhat-scripts/s3Config/buildConfig.ts b/hardhat-scripts/s3Config/buildConfig.ts index d8541a6b..e25c1c27 100644 --- a/hardhat-scripts/s3Config/buildConfig.ts +++ b/hardhat-scripts/s3Config/buildConfig.ts @@ -16,6 +16,7 @@ import { mainnetChains, mode, testnetChains, + transmitter, } from "../config/config"; import { getAddresses } from "../utils/address"; import { getChainName, rpcKeys, wssRpcKeys } from "../utils/networks"; @@ -33,6 +34,7 @@ export const getS3Config = () => { const config: S3Config = { supportedChainSlugs, version: version[mode], + transmitterEOA: transmitter, chains: {}, tokens, testnetChainSlugs: testnetChains, diff --git a/hardhat-scripts/test/chainTest.ts b/hardhat-scripts/test/chainTest.ts index a243c257..312dfd15 100644 --- a/hardhat-scripts/test/chainTest.ts +++ b/hardhat-scripts/test/chainTest.ts @@ -41,7 +41,7 @@ interface StatusResponse { status: string; response: Array<{ status: string; - requestCount: number; + payloadCount: number; writePayloads: Array<{ payloadId: string; chainSlug: number; @@ -57,7 +57,7 @@ class ChainTester { private provider: ethers.providers.JsonRpcProvider; private wallet: ethers.Wallet; private counterAppGateway: ethers.Contract; - private feesManager: ethers.Contract; + private gasAccountManager: ethers.Contract; private results: ChainTestResult[] = []; constructor() { @@ -73,10 +73,10 @@ class ChainTester { "function counter() view returns (bytes32)", ]; - // FeesManager ABI (minimal required functions) - const feesManagerABI = [ - "function totalBalanceOf(address) view returns (uint256)", - "function getBlockedCredits(address) view returns (uint256)", + // GasAccountManager ABI (minimal required functions) + const gasAccountManagerABI = [ + "function totalGas(address) view returns (uint256)", + "function getPayloadEscrow(address) view returns (uint256)", "function balanceOf(address) view returns (uint256)", ]; @@ -86,9 +86,9 @@ class ChainTester { this.wallet ); - this.feesManager = new ethers.Contract( + this.gasAccountManager = new ethers.Contract( process.env.FEES_MANAGER!, - feesManagerABI, + gasAccountManagerABI, this.provider ); } @@ -100,23 +100,21 @@ class ChainTester { try { const appGatewayAddress = process.env.COUNTER_APP_GATEWAY!; - const feesManagerAddress = process.env.FEES_MANAGER!; + const gasAccountManagerAddress = process.env.FEES_MANAGER!; - const totalCredits = await this.feesManager.totalBalanceOf( + const totalGas = await this.gasAccountManager.totalGas(appGatewayAddress); + const payloadEscrow = await this.gasAccountManager.getPayloadEscrow( appGatewayAddress ); - const blockedCredits = await this.feesManager.getBlockedCredits( + const availableFees = await this.gasAccountManager.balanceOf( appGatewayAddress ); - const availableFees = await this.feesManager.balanceOf(appGatewayAddress); console.log(`Counter App Gateway: ${appGatewayAddress}`); - console.log(`Fees Manager: ${feesManagerAddress}`); + console.log(`Fees Manager: ${gasAccountManagerAddress}`); + console.log(`Total Gas: ${ethers.utils.formatEther(totalGas)} ETH`); console.log( - `Total Credits: ${ethers.utils.formatEther(totalCredits)} ETH` - ); - console.log( - `Blocked Credits: ${ethers.utils.formatEther(blockedCredits)} ETH` + `Payload Escrow: ${ethers.utils.formatEther(payloadEscrow)} ETH` ); console.log( `Available Fees: ${ethers.utils.formatEther(availableFees)} ETH` diff --git a/hardhat-scripts/utils/gatewayId.ts b/hardhat-scripts/utils/gatewayId.ts index a24fc02d..716e1a0e 100644 --- a/hardhat-scripts/utils/gatewayId.ts +++ b/hardhat-scripts/utils/gatewayId.ts @@ -13,9 +13,9 @@ export const getAppGatewayId = ( address = addresses?.[EVMX_CHAIN_ID]?.[Contracts.WritePrecompile]; if (!address) throw new Error(`WritePrecompile not found on EVMX`); return ethers.utils.hexZeroPad(address, 32); - case Contracts.FeesPlug: - address = addresses?.[EVMX_CHAIN_ID]?.[Contracts.FeesManager]; - if (!address) throw new Error(`FeesManager not found on EVMX`); + case Contracts.GasStation: + address = addresses?.[EVMX_CHAIN_ID]?.[Contracts.GasAccountManager]; + if (!address) throw new Error(`GasAccountManager not found on EVMX`); return ethers.utils.hexZeroPad(address, 32); default: throw new Error(`Unknown plug: ${plug}`); diff --git a/hardhat-scripts/utils/sign.ts b/hardhat-scripts/utils/sign.ts index b296d565..2fbccea0 100644 --- a/hardhat-scripts/utils/sign.ts +++ b/hardhat-scripts/utils/sign.ts @@ -15,7 +15,7 @@ export const getWatcherSigner = () => { export const getSocketSigner = (chainSlug: ChainSlug) => { const provider = getProviderFromChainSlug(chainSlug); - return new ethers.Wallet(process.env.SOCKET_SIGNER_KEY as string, provider); + return new ethers.Wallet(process.env.SOCKET_PRIVATE_KEY as string, provider); }; export const getTransmitterSigner = (chainSlug: ChainSlug) => { @@ -59,7 +59,7 @@ export const sendWatcherMultiCallWithNonce = async ( const params: WatcherMultiCallParams = { contractAddress: targetContractAddress, data: calldata, - nonce, + nonce: Number(nonce), signature, }; diff --git a/hardhat.config.ts b/hardhat.config.ts index 40ec837b..b2976d4f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -15,7 +15,7 @@ import type { } from "hardhat/types"; import { resolve } from "path"; import fs from "fs"; - +import { constants } from "ethers"; import { getJsonRpcUrl } from "./hardhat-scripts/utils/networks"; import { ChainId, @@ -31,13 +31,13 @@ dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) }); // Ensure that we have all the environment variables we need. // TODO: fix it for setup scripts -// if (!process.env.SOCKET_SIGNER_KEY) throw new Error("No private key found"); +// if (!process.env.SOCKET_PRIVATE_KEY) throw new Error("No private key found"); const privateKey: HardhatNetworkAccountUserConfig = process.env - .SOCKET_SIGNER_KEY as unknown as HardhatNetworkAccountUserConfig; + .SOCKET_PRIVATE_KEY as unknown as HardhatNetworkAccountUserConfig; function getChainConfig(chainSlug: ChainSlug): NetworkUserConfig { return { - accounts: [`0x${privateKey}`], + accounts: [`${constants.HashZero}`], chainId: ChainSlugToId[chainSlug], url: getJsonRpcUrl(chainSlug), }; @@ -277,7 +277,7 @@ const config: HardhatUserConfig = { }), }, solidity: { - version: "0.8.22", + version: "0.8.28", settings: { evmVersion: "paris", optimizer: { diff --git a/lib/forge-std b/lib/forge-std index 1eea5bae..f9062359 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 +Subproject commit f90623596aecbf678c41d4d45ca81ce0e43c8219 diff --git a/lib/solady b/lib/solady index 6c2d0da6..836c169f 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit 6c2d0da6397e3c016aabc3f298de1b92c6ce7405 +Subproject commit 836c169fe357b3c23ad5d5755a9b4fbbfad7a99b diff --git a/package.json b/package.json index 6732c0a5..f9834491 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "publishConfig": { "access": "public" }, - "version": "1.1.48", + "version": "1.1.49-test.9", "description": "socket protocol", "scripts": { "build": "yarn abi && tsc --project lib.tsconfig.json", @@ -23,7 +23,9 @@ "test:gas": "npx hardhat run hardhat-scripts/test/gas-fees.ts --no-compile", "publish-core": "yarn build && yarn publish --patch --no-git-tag-version", "trace": "source .env && bash trace.sh", - "add:chain": "npx hardhat run hardhat-scripts/addChain/index.ts --no-compile" + "add:chain": "npx hardhat run hardhat-scripts/addChain/index.ts --no-compile", + "coverage": "forge coverage", + "coverage-report": "forge coverage --report lcov && genhtml lcov.info -o coverage-report --branch-coverage --ignore-errors inconsistent" }, "pre-commit": [], "author": "", diff --git a/script/counter/DeployCounterPlug.s.sol b/script/counter/DeployCounterPlug.s.sol new file mode 100644 index 00000000..dee6277a --- /dev/null +++ b/script/counter/DeployCounterPlug.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Counter} from "../../test/apps/counter/Counter.sol"; +import {toBytes32Format} from "../../contracts/utils/common/Converters.sol"; + +// source .env && forge script script/counter/DeployCounterPlug.s.sol --broadcast --skip-simulation +contract DeployCounterPlug is Script { + function run() external { + string memory rpc = vm.envString("ARBITRUM_SEPOLIA_RPC"); + vm.createSelectFork(rpc); + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + address appGateway = vm.envAddress("APP_GATEWAY"); + address socket = vm.envAddress("ARBITRUM_SEPOLIA_SOCKET"); + + Counter counter = new Counter(); + counter.initSocket(toBytes32Format(appGateway), socket, 1); + console.log("CounterPlug deployed:", address(counter)); + } +} diff --git a/script/counter/DeployEVMxCounterApp.s.sol b/script/counter/DeployEVMxCounterApp.s.sol index 5bf183ca..0f7b4b27 100644 --- a/script/counter/DeployEVMxCounterApp.s.sol +++ b/script/counter/DeployEVMxCounterApp.s.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; +import {CounterAppGateway} from "../../test/apps/counter/CounterAppGateway.sol"; // source .env && forge script script/counter/deployEVMxCounterApp.s.sol --broadcast --skip-simulation contract CounterDeploy is Script { diff --git a/script/counter/IncrementCountersFromApp.s.sol b/script/counter/IncrementCountersFromApp.s.sol index 92c688c7..f9ac599b 100644 --- a/script/counter/IncrementCountersFromApp.s.sol +++ b/script/counter/IncrementCountersFromApp.s.sol @@ -1,59 +1,38 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; +import {CounterAppGateway} from "../../test/apps/counter/CounterAppGateway.sol"; +import {toBytes32Format} from "../../contracts/utils/common/Converters.sol"; -// source .env && forge script script/counter/IncrementCountersFromApp.s.sol --broadcast --skip-simulation --legacy --gas-price 0 -// source .env && cast send $APP_GATEWAY "incrementCounters(address[])" '[0xdA908E7491499d64944Ea5Dc967135a0F22d2057]' --private-key $PRIVATE_KEY --legacy --gas-price 0 +// source .env && forge script script/counter/IncrementCountersFromApp.s.sol --broadcast --skip-simulation +// source .env && cast send 0x1Bb3770C1e25Ff498Cb25E4f91481E610428f0fd "incrementCounters(address)" '0x4382D89Db86dBFBDa96366E4029Ca962E01c232F' --private-key $PRIVATE_KEY contract IncrementCounters is Script { function run() external { string memory socketRPC = vm.envString("EVMX_RPC"); uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.createSelectFork(socketRPC); + vm.startBroadcast(deployerPrivateKey); CounterAppGateway gateway = CounterAppGateway(vm.envAddress("APP_GATEWAY")); + address counter = vm.envAddress("PLUG"); + + // gateway.uploadPlug(421614, gateway.counter(), toBytes32Format(counter)); address counterForwarderArbitrumSepolia = gateway.forwarderAddresses( gateway.counter(), 421614 ); - address counterForwarderOptimismSepolia = gateway.forwarderAddresses( - gateway.counter(), - 11155420 - ); - address counterForwarderBaseSepolia = gateway.forwarderAddresses(gateway.counter(), 84532); - // Count non-zero addresses - uint256 nonZeroCount = 0; - if (counterForwarderArbitrumSepolia != address(0)) nonZeroCount++; - if (counterForwarderOptimismSepolia != address(0)) nonZeroCount++; - if (counterForwarderBaseSepolia != address(0)) nonZeroCount++; + console.log("counterForwarderArbitrumSepolia:", counterForwarderArbitrumSepolia); - address[] memory instances = new address[](nonZeroCount); - uint256 index = 0; + // Count non-zero addresses if (counterForwarderArbitrumSepolia != address(0)) { - instances[index] = counterForwarderArbitrumSepolia; - index++; + gateway.incrementCounters(counterForwarderArbitrumSepolia); } else { console.log("Arbitrum Sepolia forwarder not yet deployed"); } - if (counterForwarderOptimismSepolia != address(0)) { - instances[index] = counterForwarderOptimismSepolia; - index++; - } else { - console.log("Optimism Sepolia forwarder not yet deployed"); - } - if (counterForwarderBaseSepolia != address(0)) { - instances[index] = counterForwarderBaseSepolia; - index++; - } else { - console.log("Base Sepolia forwarder not yet deployed"); - } - - vm.startBroadcast(deployerPrivateKey); - gateway.incrementCounters(instances); } } diff --git a/script/counter/ReadOnchainCounters.s.sol b/script/counter/ReadOnchainCounters.s.sol index 4a041e95..a6467b40 100644 --- a/script/counter/ReadOnchainCounters.s.sol +++ b/script/counter/ReadOnchainCounters.s.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {Counter} from "../../test/apps/app-gateways/counter/Counter.sol"; -import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; +import {Counter} from "../../test/apps/counter/Counter.sol"; +import {CounterAppGateway} from "../../test/apps/counter/CounterAppGateway.sol"; import {fromBytes32Format} from "../../contracts/utils/common/Converters.sol"; contract CheckCounters is Script { diff --git a/script/counter/SetFees.s.sol b/script/counter/SetFees.s.sol index 758ff54a..7ed721db 100644 --- a/script/counter/SetFees.s.sol +++ b/script/counter/SetFees.s.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; +import {CounterAppGateway} from "../../test/apps/counter/CounterAppGateway.sol"; // source .env && forge script script/counter/DeployCounterOnchain.s.sol --broadcast --skip-simulation --legacy --gas-price 0 contract CounterSetFees is Script { diff --git a/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol b/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol index 3783000b..de58775f 100644 --- a/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol +++ b/script/counter/WithdrawFeesArbitrumFeesPlug.s.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; -import {FeesManager} from "../../contracts/evmx/fees/FeesManager.sol"; -import {CounterAppGateway} from "../../test/apps/app-gateways/counter/CounterAppGateway.sol"; +import {GasAccountManager} from "../../contracts/evmx/fees/GasAccountManager.sol"; +import {GasAccountToken} from "../../contracts/evmx/fees/GasAccountToken.sol"; +import {AddressResolver} from "../../contracts/evmx/helpers/AddressResolver.sol"; + +import {CounterAppGateway} from "../../test/apps/counter/CounterAppGateway.sol"; // @notice This script is used to withdraw fees from EVMX to Arbitrum Sepolia // @dev Make sure your app has withdrawFeeTokens() function implemented. You can check its implementation in CounterAppGateway.sol @@ -12,12 +15,15 @@ contract WithdrawFees is Script { function run() external { // EVMX Check available fees vm.createSelectFork(vm.envString("EVMX_RPC")); - FeesManager feesManager = FeesManager(payable(vm.envAddress("FEES_MANAGER"))); + AddressResolver addressResolver = AddressResolver(vm.envAddress("ADDRESS_RESOLVER")); + GasAccountToken gasAccountToken = GasAccountToken( + address(addressResolver.gasAccountToken__()) + ); address appGatewayAddress = vm.envAddress("APP_GATEWAY"); address token = vm.envAddress("USDC"); CounterAppGateway appGateway = CounterAppGateway(appGatewayAddress); - uint256 availableFees = feesManager.balanceOf(appGatewayAddress); + uint256 availableFees = gasAccountToken.balanceOf(appGatewayAddress); console.log("Available fees:", availableFees); if (availableFees > 0) { @@ -45,7 +51,7 @@ contract WithdrawFees is Script { vm.createSelectFork(vm.envString("EVMX_RPC")); vm.startBroadcast(privateKey); console.log("Withdrawing amount:", amountToWithdraw); - appGateway.withdrawCredits(421614, token, amountToWithdraw, sender); + appGateway.withdrawToChain(421614, token, amountToWithdraw, sender); vm.stopBroadcast(); // Switch back to Arbitrum Sepolia to check final balance diff --git a/script/helpers/CheckDepositedGas.s.sol b/script/helpers/CheckDepositedGas.s.sol new file mode 100644 index 00000000..da0e4c60 --- /dev/null +++ b/script/helpers/CheckDepositedGas.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {GasAccountToken} from "../../contracts/evmx/fees/GasAccountToken.sol"; +import {AddressResolver} from "../../contracts/evmx/helpers/AddressResolver.sol"; + +contract CheckDepositedGas is Script { + function run() external { + vm.createSelectFork(vm.envString("EVMX_RPC")); + AddressResolver addressResolver = AddressResolver(vm.envAddress("ADDRESS_RESOLVER")); + GasAccountToken gasAccountToken = GasAccountToken( + address(addressResolver.gasAccountToken__()) + ); + address appGateway = vm.envAddress("APP_GATEWAY"); + + uint256 totalGas = gasAccountToken.totalBalanceOf(appGateway); + uint256 payloadEscrow = totalGas - gasAccountToken.balanceOf(appGateway); + console.log("App Gateway:", appGateway); + console.log("Fees Manager:", address(gasAccountToken)); + console.log("totalGas fees:", totalGas); + console.log("payloadEscrow fees:", payloadEscrow); + + uint256 availableFees = gasAccountToken.balanceOf(appGateway); + console.log("Available fees:", availableFees); + } +} diff --git a/script/helpers/DepositGas.s.sol b/script/helpers/DepositGas.s.sol new file mode 100644 index 00000000..b4003182 --- /dev/null +++ b/script/helpers/DepositGas.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {GasStation} from "../../contracts/evmx/plugs/GasStation.sol"; +import {TestUSDC} from "../../contracts/evmx/mocks/TestUSDC.sol"; + +// source .env && forge script script/helpers/DepositGasToken.s.sol --broadcast --skip-simulation +contract depositGasToken is Script { + function run() external { + uint256 feesAmount = 2000000; // 2 USDC + vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC")); + + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privateKey); + GasStation gasStation = GasStation(payable(vm.envAddress("ARBITRUM_FEES_PLUG"))); + address appGateway = vm.envAddress("APP_GATEWAY"); + TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC")); + + // mint test USDC to sender + testUSDCContract.mint(vm.addr(privateKey), feesAmount); + // approve fees plug to spend test USDC + testUSDCContract.approve(address(gasStation), feesAmount); + + address sender = vm.addr(privateKey); + console.log("Sender address:", sender); + uint256 balance = testUSDCContract.balanceOf(sender); + console.log("Sender balance in wei:", balance); + console.log("App Gateway:", appGateway); + console.log("Fees Plug:", address(gasStation)); + console.log("Fees Amount:", feesAmount); + gasStation.depositGas(address(testUSDCContract), appGateway, feesAmount); + } +} diff --git a/script/helpers/DepositGasAndNative.s.sol b/script/helpers/DepositGasAndNative.s.sol new file mode 100644 index 00000000..d8b184b8 --- /dev/null +++ b/script/helpers/DepositGasAndNative.s.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {GasStation} from "../../contracts/evmx/plugs/GasStation.sol"; +import {TestUSDC} from "../../contracts/evmx/mocks/TestUSDC.sol"; + +// source .env && forge script script/helpers/depositGasTokenAndNative.s.sol --broadcast --skip-simulation +contract depositGasTokenAndNative is Script { + function run() external { + uint256 feesAmount = 100000000; // 100 USDC + vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC")); + + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privateKey); + GasStation gasStation = GasStation(payable(vm.envAddress("ARBITRUM_FEES_PLUG"))); + address appGateway = vm.envAddress("APP_GATEWAY"); + TestUSDC testUSDCContract = TestUSDC(vm.envAddress("ARBITRUM_TEST_USDC")); + + // mint test USDC to sender + testUSDCContract.mint(vm.addr(privateKey), feesAmount); + // approve fees plug to spend test USDC + testUSDCContract.approve(address(gasStation), feesAmount); + + address sender = vm.addr(privateKey); + console.log("Sender address:", sender); + uint256 balance = testUSDCContract.balanceOf(sender); + console.log("Sender balance in wei:", balance); + console.log("App Gateway:", appGateway); + console.log("Fees Plug:", address(gasStation)); + console.log("Fees Amount:", feesAmount); + gasStation.depositGasTokenAndNative(address(testUSDCContract), appGateway, feesAmount); + } +} diff --git a/script/helpers/DepositGasMainnet.s.sol b/script/helpers/DepositGasMainnet.s.sol new file mode 100644 index 00000000..ef784fa9 --- /dev/null +++ b/script/helpers/DepositGasMainnet.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {GasStation} from "../../contracts/evmx/plugs/GasStation.sol"; +import {TestUSDC} from "../../contracts/evmx/mocks/TestUSDC.sol"; +import "solady/tokens/ERC20.sol"; + +// source .env && forge script script/helpers/DepositGasMainnet.s.sol --broadcast --skip-simulation +contract DepositGasMainnet is Script { + function run() external { + uint256 feesAmount = 1000000; // 1 USDC + vm.createSelectFork(vm.envString("ARBITRUM_RPC")); + + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(privateKey); + GasStation gasStation = GasStation(payable(vm.envAddress("ARBITRUM_FEES_PLUG"))); + address appGateway = vm.envAddress("APP_GATEWAY"); + ERC20 USDCContract = ERC20(vm.envAddress("ARBITRUM_USDC")); + + // approve fees plug to spend test USDC + USDCContract.approve(address(gasStation), feesAmount); + + address sender = vm.addr(privateKey); + console.log("Sender address:", sender); + uint256 balance = USDCContract.balanceOf(sender); + console.log("Sender USDC balance:", balance); + if (balance < feesAmount) { + revert("Sender does not have enough USDC"); + } + console.log("App Gateway:", appGateway); + console.log("Fees Plug:", address(gasStation)); + console.log("Fees Amount:", feesAmount); + gasStation.depositGas(address(USDCContract), appGateway, feesAmount); + } +} diff --git a/script/helpers/TransferRemainingGas.s.sol b/script/helpers/TransferRemainingGas.s.sol new file mode 100644 index 00000000..f9ecc91c --- /dev/null +++ b/script/helpers/TransferRemainingGas.s.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {GasAccountManager} from "../../contracts/evmx/fees/GasAccountManager.sol"; +import {IAppGateway} from "../../contracts/evmx/interfaces/IAppGateway.sol"; +import {AddressResolver} from "../../contracts/evmx/helpers/AddressResolver.sol"; +import {GasAccountToken} from "../../contracts/evmx/fees/GasAccountToken.sol"; + +contract TransferRemainingGas is Script { + function run() external { + string memory rpc = vm.envString("EVMX_RPC"); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + AddressResolver addressResolver = AddressResolver(vm.envAddress("ADDRESS_RESOLVER")); + GasAccountToken gasAccountToken = GasAccountToken( + address(addressResolver.gasAccountToken__()) + ); + GasAccountManager gasAccountManager = GasAccountManager( + address(addressResolver.gasAccountManager__()) + ); + address appGateway = vm.envAddress("APP_GATEWAY"); + address newAppGateway = vm.envAddress("NEW_APP_GATEWAY"); + + uint256 totalGas = gasAccountToken.totalBalanceOf(appGateway); + uint256 payloadEscrow = totalGas - gasAccountToken.balanceOf(appGateway); + console.log("App Gateway:", appGateway); + console.log("New App Gateway:", newAppGateway); + console.log("Fees Manager:", address(gasAccountManager)); + console.log("totalGas fees:", totalGas); + console.log("payloadEscrow fees:", payloadEscrow); + + uint256 availableFees = gasAccountToken.balanceOf(appGateway); + console.log("Available fees:", availableFees); + bytes memory data = abi.encodeWithSignature( + "transferFrom(address,address,uint256)", + appGateway, + newAppGateway, + availableFees + ); + (bool success, ) = appGateway.call(data); + require(success, "Transfer failed"); + vm.stopBroadcast(); + } +} diff --git a/script/helpers/WithdrawRemainingGas.s.sol b/script/helpers/WithdrawRemainingGas.s.sol new file mode 100644 index 00000000..f462445b --- /dev/null +++ b/script/helpers/WithdrawRemainingGas.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {GasAccountManager} from "../../contracts/evmx/fees/GasAccountManager.sol"; +import {AddressResolver} from "../../contracts/evmx/helpers/AddressResolver.sol"; +import {GasAccountToken} from "../../contracts/evmx/fees/GasAccountToken.sol"; + +contract WithdrawRemainingGas is Script { + function run() external { + string memory rpc = vm.envString("EVMX_RPC"); + vm.createSelectFork(rpc); + + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + AddressResolver addressResolver = AddressResolver(vm.envAddress("ADDRESS_RESOLVER")); + GasAccountToken gasAccountToken = GasAccountToken( + address(addressResolver.gasAccountToken__()) + ); + address appGateway = vm.envAddress("APP_GATEWAY"); + + uint256 totalGasFees = gasAccountToken.totalBalanceOf(appGateway); + uint256 payloadEscrow = totalGasFees - gasAccountToken.balanceOf(appGateway); + console.log("App Gateway:", appGateway); + console.log("Fees Manager:", address(gasAccountToken)); + console.log("total gas fees:", totalGasFees); + console.log("payloadEscrow gas fees:", payloadEscrow); + + uint256 availableGas = gasAccountToken.balanceOf(appGateway); + console.log("Available gas:", availableGas); + gasAccountToken.transferFrom(appGateway, vm.addr(deployerPrivateKey), availableGas); + + vm.stopBroadcast(); + } +} diff --git a/src/enums.ts b/src/enums.ts index 89d2aef3..c6c8d564 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -12,8 +12,8 @@ export enum Events { PlugConnected = "PlugConnected", AppGatewayCallRequested = "AppGatewayCallRequested", - // FeesPlug - FeesDeposited = "FeesDeposited", + // GasStation + GasDeposited = "GasDeposited", // Watcher TriggerFailed = "TriggerFailed", @@ -27,12 +27,11 @@ export enum Events { // Configurations PlugAdded = "PlugAdded", - // RequestHandler - RequestSubmitted = "RequestSubmitted", - RequestCancelled = "RequestCancelled", FeesIncreased = "FeesIncreased", - RequestSettled = "RequestSettled", - RequestCompletedWithErrors = "RequestCompletedWithErrors", + PayloadSubmitted = "PayloadSubmitted", + PayloadResolved = "PayloadResolved", + PayloadCancelled = "PayloadCancelled", + PayloadSettled = "PayloadSettled", // WritePrecompile WriteProofRequested = "WriteProofRequested", @@ -46,13 +45,13 @@ export enum Events { ScheduleResolved = "ScheduleResolved", // AuctionManager - AuctionEnded = "AuctionEnded", - AuctionRestarted = "AuctionRestarted", + // AuctionEnded = "AuctionEnded", + // AuctionRestarted = "AuctionRestarted", } export enum Contracts { Socket = "Socket", - FeesPlug = "FeesPlug", + GasStation = "GasStation", ContractFactoryPlug = "ContractFactoryPlug", FastSwitchboard = "FastSwitchboard", FastSwitchboardId = "FastSwitchboardId", @@ -61,18 +60,16 @@ export enum Contracts { MessageSwitchboard = "MessageSwitchboard", MessageSwitchboardId = "MessageSwitchboardId", SocketBatcher = "SocketBatcher", - SocketFeeManager = "SocketFeeManager", + NetworkFeeCollector = "NetworkFeeCollector", AddressResolver = "AddressResolver", Watcher = "Watcher", - RequestHandler = "RequestHandler", Configurations = "Configurations", PromiseResolver = "PromiseResolver", - AuctionManager = "AuctionManager", - FeesManager = "FeesManager", + GasAccountManager = "GasAccountManager", WritePrecompile = "WritePrecompile", ReadPrecompile = "ReadPrecompile", SchedulePrecompile = "SchedulePrecompile", - FeesPool = "FeesPool", + GasVault = "GasVault", AsyncDeployer = "AsyncDeployer", DeployForwarder = "DeployForwarder", Forwarder = "Forwarder", diff --git a/src/events.ts b/src/events.ts index c70e5ffb..a85768bd 100644 --- a/src/events.ts +++ b/src/events.ts @@ -7,26 +7,22 @@ export const socketEvents = [ Events.AppGatewayCallRequested, ]; -export const feesPlugEvents = [Events.FeesDeposited]; +export const gasStationEvents = [Events.GasDeposited]; -export const watcherEvents = [Events.TriggerFailed, Events.TriggerSucceeded]; - -export const promiseResolverEvents = [ +export const watcherEvents = [ + Events.TriggerFailed, + Events.TriggerSucceeded, Events.PromiseResolved, Events.PromiseNotResolved, Events.MarkedRevert, -]; - -export const requestHandlerEvents = [ - Events.RequestSubmitted, Events.FeesIncreased, - Events.RequestCancelled, - Events.RequestSettled, - Events.RequestCompletedWithErrors, + Events.PayloadSubmitted, + Events.PayloadResolved, + Events.PayloadCancelled, + Events.PayloadSettled, + Events.PlugAdded, ]; -export const configurationsEvents = [Events.PlugAdded]; - export const writePrecompileEvents = [ Events.WriteProofRequested, Events.WriteProofUploaded, @@ -39,7 +35,7 @@ export const schedulePrecompileEvents = [ Events.ScheduleResolved, ]; -export const auctionManagerEvents = [ - Events.AuctionEnded, - Events.AuctionRestarted, -]; +// export const auctionManagerEvents = [ +// Events.AuctionEnded, +// Events.AuctionRestarted, +// ]; diff --git a/src/signer.ts b/src/signer.ts index 18658fbb..51ff7ba6 100644 --- a/src/signer.ts +++ b/src/signer.ts @@ -1,5 +1,6 @@ import { ethers } from "ethers"; import { v4 as uuidv4 } from "uuid"; + export const signWatcherMultiCallMessage = async ( watcherContractAddress: string, evmxChainId: number, diff --git a/src/types.ts b/src/types.ts index 87efd831..99d31023 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,32 +25,28 @@ export type ChainAddressesObj = { CCTPSwitchboardId?: string; MessageSwitchboardId?: string; ContractFactoryPlug: string; - SocketFeesManager?: string; - FeesPlug?: string; + SocketGasAccountManager?: string; + GasStation?: string; startBlock: number; SwitchboardIdToAddressMap: { [switchboardId: string]: string }; }; export type EVMxAddressesObj = { + ERC1967Factory: string; + GasVault: string; AddressResolver: string; + GasAccountManager: string; AsyncDeployer: string; - AuctionManager: string; - Configurations: string; - DeployForwarder: string; - FeesManager: string; - FeesPool: string; - PromiseResolver: string; - ReadPrecompile: string; - RequestHandler: string; - SchedulePrecompile: string; Watcher: string; WritePrecompile: string; - ERC1967Factory: string; + ReadPrecompile: string; + SchedulePrecompile: string; startBlock: number; }; export type S3Config = { version: string; + transmitterEOA: string; chains: { [chainSlug: number]: ChainConfig }; tokens: TokenMap; supportedChainSlugs: number[]; diff --git a/test/PausableTest.t.sol b/test/PausableTest.t.sol new file mode 100644 index 00000000..3bf67bc0 --- /dev/null +++ b/test/PausableTest.t.sol @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "forge-std/Test.sol"; +import "../contracts/protocol/Socket.sol"; +import "../contracts/evmx/watcher/Watcher.sol"; +import "../contracts/evmx/helpers/AddressResolver.sol"; +import "../contracts/utils/common/AccessRoles.sol"; +import "../contracts/utils/Pausable.sol"; +import "../contracts/utils/AccessControl.sol"; +import "../contracts/utils/common/Constants.sol"; +import "solady/utils/ERC1967Factory.sol"; + +/** + * @title PausableTest + * @notice Unit tests for pause/unpause functionality with PAUSER_ROLE and UNPAUSER_ROLE + */ +contract PausableTest is Test { + // Test addresses + address owner = address(0x1000); + address pauser = address(0x2000); + address unpauser = address(0x3000); + address unauthorized = address(0x4000); + + // Test constants + uint32 constant CHAIN_SLUG = 1; + + // Contracts + Socket socket; + Watcher watcher; + + AddressResolver addressResolver; + + function setUp() public { + // Deploy Socket + socket = new Socket(CHAIN_SLUG, owner, GAS_LIMIT_BUFFER, MAX_COPY_BYTES); + + ERC1967Factory proxyFactory = new ERC1967Factory(); + // Deploy and initialize Watcher + Watcher watcherImpl = new Watcher(); + bytes memory data = abi.encodeWithSelector( + Watcher.initialize.selector, + 1, + owner, + address(0), + address(0), + bytes32(0), + 0 + ); + watcher = Watcher(proxyFactory.deployAndCall(address(watcherImpl), owner, data)); + } + + // ==================== Socket Tests ==================== + + function test_Socket_Pause_ByOwner_ShouldRevert() public { + vm.prank(owner); + vm.expectRevert(); + socket.pause(); + } + + function test_Socket_Pause_ByPauser_ShouldSucceed() public { + vm.prank(owner); + socket.grantRole(PAUSER_ROLE, pauser); + + vm.prank(pauser); + vm.expectEmit(true, false, false, false); + emit Pausable.Paused(); + socket.pause(); + + assertTrue(socket.paused()); + } + + function test_Socket_Pause_ByUnauthorized_ShouldRevert() public { + vm.prank(unauthorized); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, PAUSER_ROLE)); + socket.pause(); + } + + function test_Socket_Unpause_ByOwner_ShouldRevert() public { + // First pause it + vm.prank(owner); + socket.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + socket.pause(); + + // Try to unpause as owner (should fail) + vm.prank(owner); + vm.expectRevert(); + socket.unpause(); + } + + function test_Socket_Unpause_ByUnpauser_ShouldSucceed() public { + // First pause it + vm.prank(owner); + socket.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + socket.pause(); + + // Grant unpauser role and unpause + vm.prank(owner); + socket.grantRole(GOVERNANCE_ROLE, unpauser); + + vm.prank(unpauser); + vm.expectEmit(true, false, false, false); + emit Pausable.Unpaused(); + socket.unpause(); + + assertFalse(socket.paused()); + } + + function test_Socket_Unpause_ByUnauthorized_ShouldRevert() public { + // First pause it + vm.prank(owner); + socket.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + socket.pause(); + + // Try to unpause as unauthorized + vm.prank(unauthorized); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); + socket.unpause(); + } + + function test_Socket_Execute_WhenPaused_ShouldRevert() public { + // Pause the contract + vm.prank(owner); + socket.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + socket.pause(); + + ExecutionParams memory executionParams = ExecutionParams({ + callType: WRITE, + target: address(socket), + deadline: block.timestamp + 1000, + value: 0, + payloadId: bytes32(0), + prevBatchDigestHash: bytes32(0), + source: bytes(""), + payload: bytes(""), + extraData: bytes(""), + gasLimit: 1000000 + }); + TransmissionParams memory transmissionParams = TransmissionParams({ + socketFees: 0, + transmitterProof: bytes(""), + extraData: bytes(""), + refundAddress: address(0) + }); + + vm.expectRevert(abi.encodeWithSelector(Pausable.ContractPaused.selector)); + socket.execute(executionParams, transmissionParams); + } + + // ==================== Watcher Tests ==================== + + function test_Watcher_Initialize_ThenPause() public { + // Note: Watcher needs initialization, but for testing pause functionality + // we can test the pause mechanism directly + // In a real scenario, Watcher would be initialized first + + // For this test, we'll assume Watcher is already initialized + // and focus on the pause/unpause functionality + + // Grant pauser role (owner would do this) + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + + vm.prank(pauser); + vm.expectEmit(true, false, false, false); + emit Pausable.Paused(); + watcher.pause(); + + assertTrue(watcher.paused()); + } + + function test_Watcher_Pause_ByPauser_ShouldSucceed() public { + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + + vm.prank(pauser); + watcher.pause(); + + assertTrue(watcher.paused()); + } + + function test_Watcher_Pause_ByUnauthorized_ShouldRevert() public { + vm.prank(unauthorized); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, PAUSER_ROLE)); + watcher.pause(); + } + + function test_Watcher_Unpause_ByUnpauser_ShouldSucceed() public { + // First pause it + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + watcher.pause(); + + // Grant unpauser role and unpause + vm.prank(owner); + watcher.grantRole(GOVERNANCE_ROLE, unpauser); + + vm.prank(unpauser); + vm.expectEmit(true, false, false, false); + emit Pausable.Unpaused(); + watcher.unpause(); + + assertFalse(watcher.paused()); + } + + function test_Watcher_Unpause_ByUnauthorized_ShouldRevert() public { + // First pause it + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + watcher.pause(); + + // Try to unpause as unauthorized + vm.prank(unauthorized); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); + watcher.unpause(); + } + + function test_Watcher_ExecutePayload_WhenPaused_ShouldRevert() public { + // Pause the contract + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + watcher.pause(); + + // The executePayload function should revert due to whenNotPaused modifier + assertTrue(watcher.paused()); + } + + function test_Watcher_ResolvePayload_WhenPaused_ShouldRevert() public { + // Pause the contract + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + watcher.pause(); + + // The resolvePayload function should revert due to whenNotPaused modifier + assertTrue(watcher.paused()); + } + + function test_Watcher_executePayload_WhenPaused_ShouldRevert() public { + // Pause the contract + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + watcher.pause(); + + vm.expectRevert(abi.encodeWithSelector(Pausable.ContractPaused.selector)); + watcher.executePayload(); + } + + function test_Watcher_resolvePayload_WhenPaused_ShouldRevert() public { + // Pause the contract + vm.prank(owner); + watcher.grantRole(PAUSER_ROLE, pauser); + vm.prank(pauser); + watcher.pause(); + + vm.expectRevert(abi.encodeWithSelector(Pausable.ContractPaused.selector)); + watcher.resolvePayload( + WatcherMultiCallParams({ + contractAddress: address(watcher), + data: "0x", + nonce: 0, + signature: bytes("0x") + }) + ); + } +} diff --git a/test/SetupTest.t.sol b/test/SetupTest.t.sol index 7c808510..2fc83fbb 100644 --- a/test/SetupTest.t.sol +++ b/test/SetupTest.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "forge-std/Test.sol"; import "../contracts/utils/common/Structs.sol"; @@ -7,40 +7,35 @@ import "../contracts/utils/common/Errors.sol"; import "../contracts/utils/common/Constants.sol"; import "../contracts/utils/common/AccessRoles.sol"; import "../contracts/utils/common/IdUtils.sol"; +import "./Utils.t.sol"; 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/switchboard/EVMxSwitchboard.sol"; import "../contracts/protocol/switchboard/MessageSwitchboard.sol"; import "../contracts/protocol/SocketBatcher.sol"; -import "../contracts/protocol/SocketFeeManager.sol"; +import "../contracts/protocol/NetworkFeeCollector.sol"; import "../contracts/protocol/base/MessagePlugBase.sol"; import "../contracts/evmx/watcher/Watcher.sol"; import "../contracts/evmx/watcher/Configurations.sol"; -import "../contracts/evmx/watcher/RequestHandler.sol"; -import "../contracts/evmx/watcher/PromiseResolver.sol"; import "../contracts/evmx/watcher/precompiles/WritePrecompile.sol"; import "../contracts/evmx/watcher/precompiles/ReadPrecompile.sol"; import "../contracts/evmx/watcher/precompiles/SchedulePrecompile.sol"; +import "../contracts/evmx/helpers/ForwarderSolana.sol"; import "../contracts/evmx/helpers/AddressResolver.sol"; import "../contracts/evmx/helpers/AsyncDeployer.sol"; -import "../contracts/evmx/helpers/DeployForwarder.sol"; -import "../contracts/evmx/plugs/ContractFactoryPlug.sol"; -import "../contracts/evmx/fees/FeesManager.sol"; -import "../contracts/evmx/fees/FeesPool.sol"; -import "../contracts/evmx/plugs/FeesPlug.sol"; -import "../contracts/evmx/AuctionManager.sol"; +import "../contracts/evmx/fees/GasAccountManager.sol"; +import "../contracts/evmx/fees/GasAccountToken.sol"; +import "../contracts/evmx/fees/GasEscrow.sol"; +import "../contracts/evmx/fees/GasVault.sol"; +import {GasStation} from "../contracts/evmx/plugs/GasStation.sol"; import "../contracts/evmx/mocks/TestUSDC.sol"; - -import "./mock/CCTPMessageTransmitter.sol"; - import "solady/utils/ERC1967Factory.sol"; -contract SetupStore is Test { +contract SetupStore is Test, Utils { uint256 c = 1; uint64 version = 1; @@ -57,6 +52,7 @@ contract SetupStore is Test { uint256 expiryTime = 86400; uint256 bidTimeout = 86400; + uint256 defaultDeadline = 86400; uint256 maxReAuctionCount = 10; uint256 auctionEndDelaySeconds = 0; uint256 maxScheduleDelayInSeconds = 86500; @@ -69,53 +65,44 @@ contract SetupStore is Test { uint256 triggerFees = 10000; uint256 socketFees = 0; uint256 msgSbFees = 1000000000000000; // 0.001 ETH + uint256 feesAmount = 0.01 ether; uint256 public watcherNonce; uint256 public payloadIdCounter; uint256 public triggerCounter; uint256 public asyncPromiseCounter; - uint32 public optCCTPDomain = 2; - uint32 public arbCCTPDomain = 3; + struct SocketContracts { uint32 chainSlug; uint256 triggerPrefix; Socket socket; - SocketFeeManager socketFeeManager; - FastSwitchboard switchboard; - CCTPSwitchboard cctpSwitchboard; + NetworkFeeCollector networkFeeCollector; + EVMxSwitchboard switchboard; MessageSwitchboard messageSwitchboard; - CCTPMessageTransmitter cctpMessageTransmitter; SocketBatcher socketBatcher; - ContractFactoryPlug contractFactoryPlug; - FeesPlug feesPlug; + GasStation gasStation; TestUSDC testUSDC; } SocketContracts public arbConfig; SocketContracts public optConfig; - FeesManager feesManagerImpl; + GasAccountManager gasAccountManagerImpl; + GasAccountToken gasAccountTokenImpl; + GasEscrow gasEscrowImpl; AddressResolver addressResolverImpl; AsyncDeployer asyncDeployerImpl; Watcher watcherImpl; - AuctionManager auctionManagerImpl; - DeployForwarder deployForwarderImpl; - Configurations configurationsImpl; - RequestHandler requestHandlerImpl; - PromiseResolver promiseResolverImpl; WritePrecompile writePrecompileImpl; ERC1967Factory public proxyFactory; - FeesManager feesManager; - FeesPool feesPool; + GasAccountManager gasAccountManager; + GasAccountToken gasAccountToken; + GasEscrow gasEscrow; + GasVault gasVault; AddressResolver public addressResolver; AsyncDeployer public asyncDeployer; - DeployForwarder public deployForwarder; - AuctionManager auctionManager; Watcher public watcher; - Configurations public configurations; - RequestHandler public requestHandler; - PromiseResolver public promiseResolver; WritePrecompile public writePrecompile; ReadPrecompile public readPrecompile; SchedulePrecompile public schedulePrecompile; @@ -125,153 +112,107 @@ contract DeploySetup is SetupStore { event Initialized(uint64 version); //////////////////////////////////// Setup //////////////////////////////////// - function _deploy() internal { + function _deployContracts() internal { _deployEVMxCore(); - - // chain core contracts - arbConfig = _deploySocket(arbChainSlug); - _configureChain(arbChainSlug); - optConfig = _deploySocket(optChainSlug); - _configureChain(optChainSlug); + vm.deal(address(gasVault), 100000 ether); vm.startPrank(watcherEOA); - auctionManager.grantRole(TRANSMITTER_ROLE, transmitterEOA); - feesPool.grantRole(FEE_MANAGER_ROLE, address(feesManager)); + gasVault.grantRole(GAS_MANAGER_ROLE, address(gasAccountManager)); // setup address resolver addressResolver.setWatcher(address(watcher)); addressResolver.setAsyncDeployer(address(asyncDeployer)); - addressResolver.setDefaultAuctionManager(address(auctionManager)); - addressResolver.setFeesManager(address(feesManager)); - addressResolver.setDeployForwarder(address(deployForwarder)); - - requestHandler.setPrecompile(WRITE, writePrecompile); - requestHandler.setPrecompile(READ, readPrecompile); - requestHandler.setPrecompile(SCHEDULE, schedulePrecompile); - - watcher.setCoreContracts( - address(requestHandler), - address(configurations), - address(promiseResolver) - ); - + addressResolver.setGasAccountManager(address(gasAccountManager)); + addressResolver.setGasAccountToken(address(gasAccountToken)); + addressResolver.setGasEscrow(address(gasEscrow)); + addressResolver.setGasVault(address(gasVault)); + watcher.setPrecompile(WRITE, writePrecompile); + watcher.setPrecompile(READ, readPrecompile); + watcher.setPrecompile(SCHEDULE, schedulePrecompile); vm.stopPrank(); - vm.startPrank(socketOwner); - arbConfig.cctpSwitchboard.addRemoteEndpoint( - optChainSlug, - addressToBytes32(address(optConfig.cctpSwitchboard)), - optCCTPDomain - ); - optConfig.cctpSwitchboard.addRemoteEndpoint( - arbChainSlug, - addressToBytes32(address(arbConfig.cctpSwitchboard)), - arbCCTPDomain - ); + // chain core contracts + arbConfig = _deploySocket(arbChainSlug); + _configureChain(arbChainSlug); + optConfig = _deploySocket(optChainSlug); + _configureChain(optChainSlug); + + vm.startPrank(socketOwner); arbConfig.messageSwitchboard.setSiblingConfig( optChainSlug, - msgSbFees, toBytes32Format(address(optConfig.socket)), - toBytes32Format(address(optConfig.messageSwitchboard)) + toBytes32Format(address(optConfig.messageSwitchboard)), + uint32(optConfig.messageSwitchboard.switchboardId()) ); optConfig.messageSwitchboard.setSiblingConfig( arbChainSlug, - msgSbFees, toBytes32Format(address(arbConfig.socket)), - toBytes32Format(address(arbConfig.messageSwitchboard)) + toBytes32Format(address(arbConfig.messageSwitchboard)), + uint32(arbConfig.messageSwitchboard.switchboardId()) ); vm.stopPrank(); - - // transfer eth to fees pool for native fee payouts - vm.deal(address(feesPool), 100000 ether); - _connectCorePlugs(); - _setupTransmitter(); } - function _setupTransmitter() internal { - vm.startPrank(transmitterEOA); - arbConfig.testUSDC.mint(address(transmitterEOA), 100 ether); - arbConfig.testUSDC.approve(address(arbConfig.feesPlug), 100 ether); - - arbConfig.feesPlug.depositCreditAndNative( - address(arbConfig.testUSDC), - address(transmitterEOA), - 100 ether + function _connectCorePlugs() internal { + setupGatewayAndPlugs( + arbChainSlug, + address(gasAccountManager), + toBytes32Format(address(arbConfig.gasStation)) + ); + setupGatewayAndPlugs( + optChainSlug, + address(gasAccountManager), + toBytes32Format(address(optConfig.gasStation)) ); - - feesManager.approve(address(auctionManager), type(uint256).max); - vm.stopPrank(); } - function _connectCorePlugs() internal { - AppGatewayConfig[] memory configs = new AppGatewayConfig[](4); + function setupGatewayAndPlugs( + uint32 chainSlug_, + address appGateway_, + bytes32 contractId_ + ) internal { + // Create array with exact size needed + AppGatewayConfig[] memory configs = new AppGatewayConfig[](1); configs[0] = AppGatewayConfig({ - chainSlug: arbChainSlug, - plug: toBytes32Format(address(arbConfig.feesPlug)), + plug: contractId_, + chainSlug: chainSlug_, plugConfig: PlugConfigGeneric({ - appGatewayId: toBytes32Format(address(feesManager)), - switchboardId: arbConfig.switchboard.switchboardId() - }) - }); - configs[1] = AppGatewayConfig({ - chainSlug: optChainSlug, - plug: toBytes32Format(address(optConfig.feesPlug)), - plugConfig: PlugConfigGeneric({ - appGatewayId: toBytes32Format(address(feesManager)), - switchboardId: optConfig.switchboard.switchboardId() - }) - }); - configs[2] = AppGatewayConfig({ - chainSlug: arbChainSlug, - plug: toBytes32Format(address(arbConfig.contractFactoryPlug)), - plugConfig: PlugConfigGeneric({ - appGatewayId: toBytes32Format(address(writePrecompile)), - switchboardId: arbConfig.switchboard.switchboardId() - }) - }); - configs[3] = AppGatewayConfig({ - chainSlug: optChainSlug, - plug: toBytes32Format(address(optConfig.contractFactoryPlug)), - plugConfig: PlugConfigGeneric({ - appGatewayId: toBytes32Format(address(writePrecompile)), - switchboardId: optConfig.switchboard.switchboardId() + appGatewayId: toBytes32Format(appGateway_), + switchboardId: getSocketConfig(chainSlug_).switchboard.switchboardId() }) }); + hoax(watcherEOA); - watcherMultiCall( - address(configurations), - abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) - ); + WatcherMultiCallParams memory params = WatcherMultiCallParams({ + contractAddress: address(watcher), + data: abi.encode(configs), + nonce: watcherNonce, + signature: _createWatcherSignature(address(watcher), abi.encode(configs)) + }); + watcherNonce++; + watcher.setAppGatewayConfigs(params); } function _deploySocket(uint32 chainSlug_) internal returns (SocketContracts memory) { // socket - Socket socket = new Socket(chainSlug_, socketOwner, "test"); - CCTPMessageTransmitter cctpMessageTransmitter = new CCTPMessageTransmitter( - chainSlug_, - address(0) - ); + Socket socket = new Socket(chainSlug_, socketOwner, GAS_LIMIT_BUFFER, MAX_COPY_BYTES); return SocketContracts({ chainSlug: chainSlug_, triggerPrefix: (uint256(chainSlug_) << 224) | (uint256(uint160(address(socket))) << 64), socket: socket, - socketFeeManager: new SocketFeeManager(socketOwner, socketFees), - switchboard: new FastSwitchboard(chainSlug_, socket, socketOwner), - cctpSwitchboard: new CCTPSwitchboard( - chainSlug_, - socket, + networkFeeCollector: new NetworkFeeCollector( socketOwner, - address(cctpMessageTransmitter) + address(socket), + socketFees ), + switchboard: new EVMxSwitchboard(chainSlug_, socket, socketOwner, address(0), evmxSlug, 1), messageSwitchboard: new MessageSwitchboard(chainSlug_, socket, socketOwner), - cctpMessageTransmitter: cctpMessageTransmitter, socketBatcher: new SocketBatcher(socketOwner, socket), - contractFactoryPlug: new ContractFactoryPlug(address(socket), socketOwner), - feesPlug: new FeesPlug(address(socket), socketOwner), + gasStation: new GasStation(address(socket), socketOwner), testUSDC: new TestUSDC("USDC", "USDC", 6, socketOwner, 1000000000000000000000000) }); } @@ -279,11 +220,9 @@ contract DeploySetup is SetupStore { function _configureChain(uint32 chainSlug_) internal { SocketContracts memory socketConfig = getSocketConfig(chainSlug_); Socket socket = socketConfig.socket; - FastSwitchboard switchboard = socketConfig.switchboard; - CCTPSwitchboard cctpSwitchboard = socketConfig.cctpSwitchboard; + EVMxSwitchboard switchboard = socketConfig.switchboard; MessageSwitchboard messageSwitchboard = socketConfig.messageSwitchboard; - FeesPlug feesPlug = socketConfig.feesPlug; - ContractFactoryPlug contractFactoryPlug = socketConfig.contractFactoryPlug; + GasStation gasStation = socketConfig.gasStation; vm.startPrank(socketOwner); // socket @@ -292,63 +231,49 @@ contract DeploySetup is SetupStore { socket.grantRole(SWITCHBOARD_DISABLER_ROLE, address(socketOwner)); // switchboard - switchboard.registerSwitchboard(); - switchboard.grantRole(WATCHER_ROLE, watcherEOA); switchboard.grantRole(RESCUE_ROLE, address(socketOwner)); - - cctpSwitchboard.registerSwitchboard(); - cctpSwitchboard.grantRole(WATCHER_ROLE, watcherEOA); - - messageSwitchboard.registerSwitchboard(); - messageSwitchboard.grantRole(WATCHER_ROLE, watcherEOA); - - feesPlug.grantRole(RESCUE_ROLE, address(socketOwner)); - feesPlug.whitelistToken(address(socketConfig.testUSDC)); - feesPlug.connectSocket( - toBytes32Format(address(feesManager)), - address(socket), - switchboard.switchboardId() - ); - - contractFactoryPlug.grantRole(RESCUE_ROLE, address(socketOwner)); - contractFactoryPlug.connectSocket( - toBytes32Format(address(writePrecompile)), + switchboard.grantRole(GOVERNANCE_ROLE, address(socketOwner)); + switchboard.grantWatcherRole(watcherEOA); + + messageSwitchboard.grantRole(GOVERNANCE_ROLE, address(socketOwner)); + messageSwitchboard.grantRole(RESCUE_ROLE, address(socketOwner)); + messageSwitchboard.grantWatcherRole(arbChainSlug, watcherEOA); + messageSwitchboard.grantWatcherRole(optChainSlug, watcherEOA); + + gasStation.grantRole(RESCUE_ROLE, address(socketOwner)); + gasStation.whitelistToken(address(socketConfig.testUSDC)); + gasStation.connectSocket( + toBytes32Format(address(gasAccountManager)), address(socket), switchboard.switchboardId() ); vm.stopPrank(); vm.startPrank(watcherEOA); - configurations.setSocket(chainSlug_, toBytes32Format(address(socket))); - configurations.setSwitchboard(chainSlug_, FAST, switchboard.switchboardId()); - configurations.setSwitchboard(chainSlug_, CCTP, cctpSwitchboard.switchboardId()); + watcher.setSocket(chainSlug_, toBytes32Format(address(socket))); + watcher.setSwitchboard(chainSlug_, FAST, switchboard.switchboardId()); // plugs - feesManager.setFeesPlug(chainSlug_, toBytes32Format(address(feesPlug))); + gasAccountManager.setGasStation(chainSlug_, toBytes32Format(address(gasStation))); // precompiles writePrecompile.updateChainMaxMsgValueLimits(chainSlug_, maxMsgValueLimit); - writePrecompile.setContractFactoryPlugs( - chainSlug_, - toBytes32Format(address(contractFactoryPlug)) - ); vm.stopPrank(); } function _deployEVMxCore() internal { proxyFactory = new ERC1967Factory(); - feesPool = new FeesPool(watcherEOA); + gasVault = new GasVault(watcherEOA); + + ForwarderSolana forwarderSolana = new ForwarderSolana(); // Deploy implementations for upgradeable contracts - feesManagerImpl = new FeesManager(); + gasAccountManagerImpl = new GasAccountManager(); + gasAccountTokenImpl = new GasAccountToken(); + gasEscrowImpl = new GasEscrow(); addressResolverImpl = new AddressResolver(); asyncDeployerImpl = new AsyncDeployer(); watcherImpl = new Watcher(); - auctionManagerImpl = new AuctionManager(); - deployForwarderImpl = new DeployForwarder(); - configurationsImpl = new Configurations(); - promiseResolverImpl = new PromiseResolver(); - requestHandlerImpl = new RequestHandler(); writePrecompileImpl = new WritePrecompile(); // Deploy and initialize proxies @@ -359,58 +284,53 @@ contract DeploySetup is SetupStore { ); addressResolver = AddressResolver(addressResolverProxy); - address feesManagerProxy = _deployAndVerifyProxy( - address(feesManagerImpl), + address gasAccountManagerProxy = _deployAndVerifyProxy( + address(gasAccountManagerImpl), watcherEOA, abi.encodeWithSelector( - FeesManager.initialize.selector, - evmxSlug, + GasAccountManager.initialize.selector, address(addressResolver), - address(feesPool), watcherEOA, - writeFees, - FAST + feesAmount, + FAST, + address(forwarderSolana) ) ); - feesManager = FeesManager(feesManagerProxy); + gasAccountManager = GasAccountManager(gasAccountManagerProxy); - address asyncDeployerProxy = _deployAndVerifyProxy( - address(asyncDeployerImpl), + address gasAccountTokenProxy = _deployAndVerifyProxy( + address(gasAccountTokenImpl), watcherEOA, abi.encodeWithSelector( - AsyncDeployer.initialize.selector, + GasAccountToken.initialize.selector, watcherEOA, address(addressResolver) ) ); - asyncDeployer = AsyncDeployer(asyncDeployerProxy); + gasAccountToken = GasAccountToken(gasAccountTokenProxy); - address auctionManagerProxy = _deployAndVerifyProxy( - address(auctionManagerImpl), + address gasEscrowProxy = _deployAndVerifyProxy( + address(gasEscrowImpl), watcherEOA, abi.encodeWithSelector( - AuctionManager.initialize.selector, - evmxSlug, - uint128(bidTimeout), - maxReAuctionCount, - auctionEndDelaySeconds, - address(addressResolver), - watcherEOA + GasEscrow.initialize.selector, + watcherEOA, + address(gasAccountManager) ) ); - auctionManager = AuctionManager(auctionManagerProxy); + gasEscrow = GasEscrow(gasEscrowProxy); - address deployForwarderProxy = _deployAndVerifyProxy( - address(deployForwarderImpl), + address asyncDeployerProxy = _deployAndVerifyProxy( + address(asyncDeployerImpl), watcherEOA, abi.encodeWithSelector( - DeployForwarder.initialize.selector, + AsyncDeployer.initialize.selector, watcherEOA, address(addressResolver), - FAST + defaultDeadline ) ); - deployForwarder = DeployForwarder(deployForwarderProxy); + asyncDeployer = AsyncDeployer(asyncDeployerProxy); address watcherProxy = _deployAndVerifyProxy( address(watcherImpl), @@ -418,31 +338,15 @@ contract DeploySetup is SetupStore { abi.encodeWithSelector( Watcher.initialize.selector, evmxSlug, - triggerFees, watcherEOA, - address(addressResolver) + address(addressResolver), + address(transmitterEOA), + bytes32(0), // transmitterSolana - using 0 for now + triggerFees ) ); watcher = Watcher(watcherProxy); - address requestHandlerProxy = _deployAndVerifyProxy( - address(requestHandlerImpl), - watcherEOA, - abi.encodeWithSelector( - RequestHandler.initialize.selector, - watcherEOA, - address(addressResolver) - ) - ); - requestHandler = RequestHandler(requestHandlerProxy); - - address configurationsProxy = _deployAndVerifyProxy( - address(configurationsImpl), - watcherEOA, - abi.encodeWithSelector(Configurations.initialize.selector, address(watcher), watcherEOA) - ); - configurations = Configurations(configurationsProxy); - address writePrecompileProxy = _deployAndVerifyProxy( address(writePrecompileImpl), watcherEOA, @@ -456,13 +360,6 @@ contract DeploySetup is SetupStore { ); writePrecompile = WritePrecompile(writePrecompileProxy); - address promiseResolverProxy = _deployAndVerifyProxy( - address(promiseResolverImpl), - watcherEOA, - abi.encodeWithSelector(PromiseResolver.initialize.selector, address(watcher)) - ); - promiseResolver = PromiseResolver(promiseResolverProxy); - // non proxy contracts readPrecompile = new ReadPrecompile(address(watcher), readFees, expiryTime); schedulePrecompile = new SchedulePrecompile( @@ -488,18 +385,6 @@ contract DeploySetup is SetupStore { return chainSlug_ == arbChainSlug ? arbConfig : optConfig; } - function watcherMultiCall(address contractAddress_, bytes memory data_) internal { - WatcherMultiCallParams[] memory params = new WatcherMultiCallParams[](1); - params[0] = WatcherMultiCallParams({ - contractAddress: contractAddress_, - data: data_, - nonce: watcherNonce, - signature: _createWatcherSignature(contractAddress_, data_) - }); - watcherNonce++; - watcher.watcherMultiCall(params); - } - function _createWatcherSignature( address contractAddress_, bytes memory data_ @@ -510,21 +395,6 @@ contract DeploySetup is SetupStore { return createSignature(digest, watcherPrivateKey); } - function createSignature( - bytes32 digest_, - uint256 privateKey_ - ) public pure returns (bytes memory sig) { - bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); - (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest); - sig = new bytes(65); - bytes1 v32 = bytes1(sigV); - assembly { - mstore(add(sig, 96), v32) - mstore(add(sig, 32), sigR) - mstore(add(sig, 64), sigS) - } - } - function predictAsyncPromiseAddress( address invoker_, address forwarder_ @@ -546,6 +416,36 @@ contract DeploySetup is SetupStore { return address(uint160(uint256(hash))); } + + function _callTrigger( + uint32 chainSlug_, + bytes32 payloadId_, + bytes32 appGatewayId_, + bytes32 plug_, + bytes memory payload_, + bytes memory overrides_ + ) internal { + TriggerParams[] memory params = new TriggerParams[](1); + params[0] = TriggerParams({ + triggerId: payloadId_, + plug: plug_, + appGatewayId: appGatewayId_, + chainSlug: chainSlug_, + overrides: overrides_, + payload: payload_ + }); + bytes memory data = abi.encode(params); + + WatcherMultiCallParams memory watcherParams; + watcherParams = WatcherMultiCallParams({ + contractAddress: address(watcher), + data: data, + nonce: watcherNonce, + signature: _createWatcherSignature(address(watcher), data) + }); + watcherNonce++; + watcher.callAppGateways(watcherParams); + } } contract FeesSetup is DeploySetup { @@ -553,104 +453,108 @@ contract FeesSetup is DeploySetup { uint32 indexed chainSlug, address indexed token, address indexed appGateway, - uint256 creditAmount, + uint256 gasAmount, uint256 nativeAmount ); - event CreditsWrapped(address indexed consumeFrom, uint256 amount); - event CreditsUnwrapped(address indexed consumeFrom, uint256 amount); - event CreditsTransferred(address indexed from, address indexed to, uint256 amount); + event GasWrapped(address indexed consumeFrom, uint256 amount); + event GasUnwrapped(address indexed consumeFrom, uint256 amount); function deploy() internal { - _deploy(); + _deployContracts(); + SocketContracts memory socketConfig = getSocketConfig(arbChainSlug); + socketConfig.testUSDC.mint(address(socketConfig.gasStation), 100 ether); - depositNativeAndCredits(arbChainSlug, 100 ether, 100 ether, address(transmitterEOA)); - approve(address(auctionManager), address(transmitterEOA)); - } - - function depositNativeAndCredits( - uint32 chainSlug_, - uint256 credits_, - uint256 native_, - address user_ - ) internal { - depositNativeAndCreditsWithData(chainSlug_, credits_, native_, user_, user_); + hoax(address(gasAccountManager)); + gasAccountToken.mint(address(gasAccountManager), 100 ether); + depositNativeAndGas(arbChainSlug, 100 ether, address(transmitterEOA)); } - // mints test token and deposits the given native and credits to given `user_` - function depositNativeAndCreditsWithData( + function depositNativeAndGas( uint32 chainSlug_, - uint256 credits_, - uint256 native_, - address user_, + uint256 gasAmount_, address receiver_ ) internal { SocketContracts memory socketConfig = getSocketConfig(chainSlug_); TestUSDC token = socketConfig.testUSDC; - uint256 userBalance = token.balanceOf(user_); - uint256 feesPlugBalance = token.balanceOf(address(socketConfig.feesPlug)); + uint256 userBalance = token.balanceOf(receiver_); + uint256 gasStationBalance = token.balanceOf(address(socketConfig.gasStation)); - token.mint(address(user_), 100 ether); + token.mint(address(receiver_), 100 ether); assertEq( - token.balanceOf(user_), + token.balanceOf(receiver_), userBalance + 100 ether, "User should have 100 more test tokens" ); - vm.startPrank(user_); - token.approve(address(socketConfig.feesPlug), 100 ether); - socketConfig.feesPlug.depositCreditAndNative(address(token), user_, 100 ether); + vm.startPrank(receiver_); + token.approve(address(socketConfig.gasStation), 100 ether); + bytes32 payloadId = socketConfig.gasStation.depositGasTokenAndNative( + address(token), + receiver_, + gasAmount_ + ); vm.stopPrank(); assertEq( - token.balanceOf(address(socketConfig.feesPlug)), - feesPlugBalance + 100 ether, + token.balanceOf(address(socketConfig.gasStation)), + gasStationBalance + gasAmount_, "Fees plug should have 100 more test tokens" ); - // uint256 currentCredits = feesManager.balanceOf(user_); - // uint256 currentNative = address(user_).balance; + uint256 currentGas = gasAccountToken.balanceOf(receiver_); + uint256 currentNative = address(receiver_).balance; + uint256 native_ = gasAmount_ / 10; - vm.expectEmit(true, true, true, false); - emit Deposited(chainSlug_, address(token), receiver_, credits_, native_); + bytes memory payloadData = abi.encodeWithSelector( + GasAccountManager.depositFromChain.selector, + address(token), + receiver_, + gasAmount_ - native_, + native_ + ); - watcherMultiCall( - address(feesManager), - abi.encodeWithSelector( - Credit.deposit.selector, - chainSlug_, - address(token), - receiver_, - native_, - credits_ - ) + vm.expectEmit(true, true, true, false); + emit Deposited(chainSlug_, address(token), receiver_, gasAmount_, native_); + _callTrigger( + chainSlug_, + payloadId, + toBytes32Format(address(gasAccountManager)), + toBytes32Format(address(socketConfig.gasStation)), + payloadData, + abi.encode(block.timestamp + defaultDeadline) ); - // assertEq( - // feesManager.balanceOf(user_), - // currentCredits + credits_, - // "User should have more credits" - // ); - // assertEq(address(user_).balance, currentNative + native_, "User should have more native"); + // native amount might be minted to receiver_ if its a gateway with no fallback/receive + assertGe( + gasAccountToken.balanceOf(receiver_), + currentGas + gasAmount_ - native_, + "User should have more gas tokens" + ); + assertLe( + address(receiver_).balance, + currentNative + native_, + "User should have greater than or equal to as much native as deposited" + ); } function approve(address appGateway_, address user_) internal { - uint256 approval = feesManager.allowance(user_, appGateway_); + uint256 approval = gasAccountToken.allowance(user_, appGateway_); if (approval > 0) return; hoax(user_); - feesManager.approve(appGateway_, type(uint256).max); + gasAccountToken.approve(appGateway_, type(uint256).max); assertEq( - feesManager.isApproved(user_, appGateway_), - true, + gasAccountToken.allowance(user_, appGateway_), + type(uint256).max, "App gateway should be approved" ); } function permit(address appGateway_, address user_, uint256 userPrivateKey_) internal { - bool approval = feesManager.isApproved(user_, appGateway_); - if (approval) return; + uint256 allowance = gasAccountToken.allowance(user_, appGateway_); + if (allowance > 0) return; uint256 value = type(uint256).max; uint256 deadline = block.timestamp + 1 hours; @@ -664,91 +568,25 @@ contract FeesSetup is DeploySetup { user_, appGateway_, value, - feesManager.nonces(user_), + 1, // gasAccountManager.nonces(user_), // todo deadline ) ); bytes32 digest = keccak256( - abi.encodePacked("\x19\x01", feesManager.DOMAIN_SEPARATOR(), structHash) + abi.encodePacked("\x19\x01", gasAccountToken.DOMAIN_SEPARATOR(), structHash) ); (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPrivateKey_, digest); - feesManager.permit(user_, appGateway_, value, deadline, v, r, s); + gasAccountToken.permit(user_, appGateway_, value, deadline, v, r, s); assertEq( - feesManager.isApproved(user_, appGateway_), - true, + gasAccountToken.allowance(user_, appGateway_), + type(uint256).max, "App gateway should be approved" ); } } -contract AuctionSetup is FeesSetup { - event BidPlaced(uint40 requestCount, Bid bid); - event AuctionStarted(uint40 requestCount); - event AuctionEnded(uint40 requestCount, Bid winningBid); - - function getBidAmount(uint40 requestCount) internal view returns (uint256) { - return watcher.getRequestParams(requestCount).requestFeesDetails.maxFees / 2; - } - - function placeBid(uint40 requestCount) internal { - uint256 bidAmount = getBidAmount(requestCount); - - bytes memory transmitterSignature = createSignature( - keccak256(abi.encode(address(auctionManager), evmxSlug, requestCount, bidAmount, "")), - transmitterPrivateKey - ); - - if (auctionEndDelaySeconds == 0) { - vm.expectEmit(true, true, true, false); - emit AuctionEnded( - requestCount, - Bid({fee: bidAmount, transmitter: transmitterEOA, extraData: bytes("")}) - ); - } else { - vm.expectEmit(true, true, true, false); - emit AuctionStarted(requestCount); - } - - vm.expectEmit(true, true, true, false); - emit BidPlaced( - requestCount, - Bid({transmitter: transmitterEOA, fee: bidAmount, extraData: bytes("")}) - ); - auctionManager.bid(requestCount, bidAmount, transmitterSignature, bytes("")); - } - - function endAuction(uint40 requestCount_) internal { - if (auctionEndDelaySeconds == 0) return; - - // todo: handle other cases - - uint256 bidAmount = getBidAmount(requestCount_); - // bytes memory watcherSignature = createSignature( - // keccak256(abi.encode(address(watcher), evmxSlug, requestCount_, bidAmount, "")), - // watcherPrivateKey - // ); - - vm.expectEmit(true, true, true, true); - emit AuctionEnded( - requestCount_, - Bid({fee: bidAmount, transmitter: transmitterEOA, extraData: ""}) - ); - - // promiseResolver.resolvePromises(); - } - - function bidAndEndAuction(uint40 requestCount) internal { - placeBid(requestCount); - endAuction(requestCount); - } - - // tests: - // bid and end auction with delay - // bid and end auction with delay and expire bid -} - -contract WatcherSetup is AuctionSetup { +contract WatcherSetup is FeesSetup { event ReadRequested(Transaction transaction, uint256 readAtBlockNumber, bytes32 payloadId); event ScheduleRequested(bytes32 payloadId, uint256 deadline); event ScheduleResolved(bytes32 payloadId); @@ -757,99 +595,50 @@ contract WatcherSetup is AuctionSetup { bytes32 digest, bytes32 prevBatchDigestHash, uint256 deadline, - PayloadParams payloadParams + Payload payloadParams ); event WriteProofUploaded(bytes32 indexed payloadId, bytes proof); - function executeDeployMultiChain( - IAppGateway appGateway_, - uint32[] memory chainSlugs_, - bytes32[] memory contractIds_ - ) internal returns (uint40 requestCount) { - return _executeDeploy(appGateway_, chainSlugs_, contractIds_); - } - - function executeDeploy( - IAppGateway appGateway_, - uint32 chainSlug_, - bytes32[] memory contractIds_ - ) internal returns (uint40 requestCount) { - uint32[] memory chainSlugs = new uint32[](1); - chainSlugs[0] = chainSlug_; - return _executeDeploy(appGateway_, chainSlugs, contractIds_); - } - - function _executeDeploy( - IAppGateway appGateway_, - uint32[] memory chainSlugs_, - bytes32[] memory contractIds_ - ) internal returns (uint40 requestCount) { - requestCount = executeRequest(); - for (uint i = 0; i < chainSlugs_.length; i++) { - setupGatewayAndPlugs(chainSlugs_[i], appGateway_, contractIds_); - } - } + function executePayload() internal returns (bytes32 payloadId) { + payloadId = watcher.currentPayloadId(); + if (payloadId == bytes32(0)) return bytes32(0); - function executeRequest() internal returns (uint40 requestCount) { - requestCount = watcher.getCurrentRequestCount(); - requestCount = requestCount == 0 ? 0 : requestCount - 1; - - executeRequest(requestCount); + bool isPayloadExecuted = _processPayload(payloadId); + assertEq(watcher.getPayload(payloadId).isPayloadExecuted, isPayloadExecuted); } - function executeRequest(uint40 requestCount) internal { - RequestParams memory requestParams = requestHandler.getRequest(requestCount); - uint40[] memory batches = requestHandler.getRequestBatchIds(requestCount); - - // bids and executes schedule request if created for endAuction - if (requestParams.writeCount != 0) bidAndEndAuction(requestCount); + function _processPayload(bytes32 payloadId) internal returns (bool) { + PromiseReturnData memory promiseReturnData; + Payload memory payloadParams = watcher.getPayload(payloadId); - bool isRequestExecuted; - for (uint i = 0; i < batches.length; i++) { - isRequestExecuted = _processBatch(batches[i]); - if (!isRequestExecuted) break; + bool success; + if (payloadParams.callType == READ) { + (success, promiseReturnData) = _processRead(payloadParams); + } else if (payloadParams.callType == WRITE) { + (success, promiseReturnData) = _processWrite(payloadParams); + } else if (payloadParams.callType == SCHEDULE) { + vm.warp(payloadParams.deadline - expiryTime); + promiseReturnData = PromiseReturnData({ + exceededMaxCopy: false, + payloadId: payloadParams.payloadId, + returnData: bytes("") + }); + success = true; } - requestParams = requestHandler.getRequest(requestCount); - assertEq(requestParams.requestTrackingParams.isRequestExecuted, isRequestExecuted); - } - - function _processBatch(uint40 batchCount_) internal returns (bool) { - bytes32[] memory payloadIds = requestHandler.getBatchPayloadIds(batchCount_); - - PromiseReturnData[] memory promiseReturnData = new PromiseReturnData[](1); - bool success; - for (uint i = 0; i < payloadIds.length; i++) { - PayloadParams memory payloadParams = watcher.getPayloadParams(payloadIds[i]); - - if (payloadParams.callType == READ) { - (success, promiseReturnData[0]) = _processRead(payloadParams); - } else if (payloadParams.callType == WRITE) { - (success, promiseReturnData[0]) = _processWrite(payloadParams); - } else if (payloadParams.callType == SCHEDULE) { - vm.warp(payloadParams.deadline - expiryTime); - promiseReturnData[0] = PromiseReturnData({ - exceededMaxCopy: false, - payloadId: payloadParams.payloadId, - returnData: bytes("") - }); - success = true; - } - - if (success) { - _resolvePromise(promiseReturnData); - } else { - vm.warp(payloadParams.deadline); - _markRevert(promiseReturnData[0], true); - return false; - } + if (success) { + _resolvePayload(promiseReturnData); + } else { + vm.warp(payloadParams.deadline); + _markRevert(promiseReturnData, true); + return false; } return true; } function _processRead( - PayloadParams memory payloadParams + Payload memory payloadParams ) internal returns (bool success, PromiseReturnData memory promiseReturnData) { (Transaction memory transaction, ) = abi.decode( payloadParams.precompileData, @@ -867,34 +656,24 @@ contract WatcherSetup is AuctionSetup { } function _processWrite( - PayloadParams memory payloadParams + Payload memory payloadParams ) internal returns (bool success, PromiseReturnData memory promiseReturnData) { bytes32 payloadId = payloadParams.payloadId; ( uint32 chainSlug, - uint64 switchboard, + uint32 switchboard, bytes32 digest, DigestParams memory digestParams ) = _validateAndGetDigest(payloadParams); - bytes memory watcherProof = _uploadProof(payloadId, digest, switchboard, chainSlug); - - return - _executeWrite( - chainSlug, - switchboard, - digest, - digestParams, - payloadParams, - watcherProof - ); + return _executeWrite(chainSlug, digest, digestParams, payloadParams, watcherProof); } function _uploadProof( bytes32 payloadId, bytes32 digest, - uint64 switchboard, + uint32 switchboard, uint32 chainSlug ) internal returns (bytes memory proof) { address sbAddress = getSocketConfig(chainSlug).socket.switchboardAddresses(switchboard); @@ -906,21 +685,19 @@ contract WatcherSetup is AuctionSetup { vm.expectEmit(true, true, true, false); emit WriteProofUploaded(payloadId, proof); - watcherMultiCall( - address(writePrecompile), - abi.encodeWithSelector(WritePrecompile.uploadProof.selector, payloadId, proof) - ); + hoax(watcherEOA); + writePrecompile.uploadProof(payloadId, proof); assertEq(writePrecompile.watcherProofs(payloadId), proof); } function _validateAndGetDigest( - PayloadParams memory payloadParams + Payload memory payloadParams ) internal view returns ( uint32 chainSlug, - uint64 switchboard, + uint32 switchboard, bytes32 digest, DigestParams memory digestParams ) @@ -931,31 +708,27 @@ contract WatcherSetup is AuctionSetup { , uint256 gasLimit, uint256 value, - uint64 switchboard_ + uint32 switchboard_ ) = abi.decode( payloadParams.precompileData, - (address, Transaction, WriteFinality, uint256, uint256, uint64) + (address, Transaction, WriteFinality, uint256, uint256, uint32) ); chainSlug = transaction.chainSlug; switchboard = switchboard_; - bytes32 prevBatchDigestHash = writePrecompile.getPrevBatchDigestHash( - uint40(payloadParams.payloadPointer >> 120), - uint40(payloadParams.payloadPointer >> 80) - ); digestParams = DigestParams( - toBytes32Format(address(getSocketConfig(transaction.chainSlug).socket)), - toBytes32Format(transmitterEOA), - payloadParams.payloadId, payloadParams.deadline, - payloadParams.callType, gasLimit, + payloadParams.callType, + toBytes32Format(address(getSocketConfig(transaction.chainSlug).socket)), value, - transaction.payload, + toBytes32Format(transmitterEOA), + payloadParams.payloadId, transaction.target, - toBytes32Format(appGateway), - prevBatchDigestHash, + bytes32(0), + transaction.payload, + abi.encode(toBytes32Format(appGateway)), bytes("") ); @@ -965,135 +738,59 @@ contract WatcherSetup is AuctionSetup { function _executeWrite( uint32 chainSlug, - uint64 switchboard, bytes32 digest, DigestParams memory digestParams, - PayloadParams memory payloadParams, + Payload memory payloadParams, bytes memory watcherProof ) internal returns (bool success, PromiseReturnData memory promiseReturnData) { // this is a signature for the socket batcher (only used for EVM) - bytes memory transmitterSig = createSignature( - keccak256( - abi.encode(address(getSocketConfig(chainSlug).socket), payloadParams.payloadId) - ), - transmitterPrivateKey - ); - bytes memory returnData; - ExecuteParams memory executeParams = ExecuteParams({ + TransmissionParams memory transmissionParams; + + { + bytes memory transmitterSig = createSignature( + keccak256( + abi.encodePacked( + address(getSocketConfig(chainSlug).socket), + payloadParams.payloadId + ) + ), + transmitterPrivateKey + ); + transmissionParams = TransmissionParams({ + transmitterProof: transmitterSig, + socketFees: 0, + extraData: bytes(""), + refundAddress: transmitterEOA + }); + } + ExecutionParams memory executionParams = ExecutionParams({ callType: digestParams.callType, deadline: digestParams.deadline, gasLimit: digestParams.gasLimit, value: digestParams.value, - payload: digestParams.payload, target: fromBytes32Format(digestParams.target), - payloadPointer: payloadParams.payloadPointer, + payloadId: payloadParams.payloadId, prevBatchDigestHash: digestParams.prevBatchDigestHash, + source: digestParams.source, + payload: digestParams.payload, extraData: digestParams.extraData }); - if (switchboard == getSocketConfig(chainSlug).switchboard.switchboardId()) { + { + bytes memory returnData; (success, returnData) = getSocketConfig(chainSlug).socketBatcher.attestAndExecute( - executeParams, + executionParams, + transmissionParams, getSocketConfig(chainSlug).switchboard.switchboardId(), digest, - watcherProof, - transmitterSig, - transmitterEOA - ); - } else if (switchboard == getSocketConfig(chainSlug).cctpSwitchboard.switchboardId()) { - (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, - getSocketConfig(chainSlug).cctpSwitchboard.switchboardId() + watcherProof ); - } - - function _prepareCCTPBatchData( - uint32 chainSlug, - PayloadParams memory payloadParams - ) internal view returns (CCTPBatchParams memory cctpBatchParams) { - uint40[] memory requestBatchIds = requestHandler.getRequestBatchIds( - uint40(payloadParams.payloadPointer >> 120) - ); - uint40 currentBatchCount = uint40(payloadParams.payloadPointer >> 80); - - 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); + promiseReturnData = PromiseReturnData({ + exceededMaxCopy: false, + payloadId: payloadParams.payloadId, + returnData: returnData + }); } - return requestHandler.getBatchPayloadIds(currentBatchCount + 1); } function _getRemoteChainSlugs( @@ -1101,7 +798,7 @@ contract WatcherSetup is AuctionSetup { ) 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]); + Payload memory params = watcher.getPayload(payloadIds[i]); (, Transaction memory transaction, , , , ) = abi.decode( params.precompileData, (address, Transaction, WriteFinality, uint256, uint256, address) @@ -1111,103 +808,35 @@ contract WatcherSetup is AuctionSetup { 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( - address(promiseResolver), - abi.encodeWithSelector(PromiseResolver.resolvePromises.selector, promiseReturnData) - ); + function _resolvePayload(PromiseReturnData memory promiseReturnData) internal { + WatcherMultiCallParams memory params = WatcherMultiCallParams({ + contractAddress: address(watcher), + data: abi.encode(promiseReturnData, feesAmount / 2), + nonce: watcherNonce, + signature: _createWatcherSignature( + address(watcher), + abi.encode(promiseReturnData, feesAmount / 2) + ) + }); + watcherNonce++; + watcher.resolvePayload(params); } function _markRevert( PromiseReturnData memory promiseReturnData, bool isRevertingOnchain_ ) internal { - watcherMultiCall( - address(promiseResolver), - abi.encodeWithSelector( - PromiseResolver.markRevert.selector, - promiseReturnData, - isRevertingOnchain_ + WatcherMultiCallParams memory params = WatcherMultiCallParams({ + contractAddress: address(watcher), + data: abi.encode(promiseReturnData, isRevertingOnchain_), + nonce: watcherNonce, + signature: _createWatcherSignature( + address(watcher), + abi.encode(promiseReturnData, isRevertingOnchain_) ) - ); - } - - function setupGatewayAndPlugs( - uint32 chainSlug_, - IAppGateway appGateway_, - bytes32[] memory contractIds_ - ) internal { - // 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 = _countValidPlugs(appGateway_, contractIds_, chainSlug_); - - // Create array with exact size needed - AppGatewayConfig[] memory configs = new AppGatewayConfig[](validPlugCount); - _populateConfigs(configs, appGateway_, contractIds_, chainSlug_); - - // Only call watcher if we have valid configs - if (validPlugCount > 0) { - watcherMultiCall( - address(configurations), - abi.encodeWithSelector(Configurations.setAppGatewayConfigs.selector, configs) - ); - } - } - - function _countValidPlugs( - IAppGateway appGateway_, - bytes32[] memory contractIds_, - uint32 chainSlug_ - ) internal view returns (uint256 validCount) { - for (uint i = 0; i < contractIds_.length; i++) { - bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); - if (plug != bytes32(0)) { - validCount++; - } - } - } - - function _populateConfigs( - AppGatewayConfig[] memory configs, - IAppGateway appGateway_, - bytes32[] memory contractIds_, - uint32 chainSlug_ - ) internal view { - uint256 configIndex = 0; - uint64 switchboardId = configurations.switchboards(chainSlug_, appGateway_.sbType()); - - for (uint i = 0; i < contractIds_.length; i++) { - bytes32 plug = appGateway_.getOnChainAddress(contractIds_[i], chainSlug_); - if (plug != bytes32(0)) { - configs[configIndex] = AppGatewayConfig({ - plug: plug, - chainSlug: chainSlug_, - plugConfig: PlugConfigGeneric({ - appGatewayId: toBytes32Format(address(appGateway_)), - switchboardId: switchboardId - }) - }); - configIndex++; - } - } + }); + watcherNonce++; + watcher.markRevert(params); } } @@ -1222,105 +851,31 @@ contract AppGatewayBaseSetup is WatcherSetup { return (onChainContract, forwarder); } - // todo: add checks for request params and payload params created to match what is expected - - function checkRequestParams( - uint40 requestCount, - RequestParams memory expectedRequest - ) internal view { - RequestParams memory actualRequest = watcher.getRequestParams(requestCount); - // RequestParams checks - assertEq( - actualRequest.appGateway, - expectedRequest.appGateway, - "Request: appGateway mismatch" - ); - assertEq( - actualRequest.auctionManager, - expectedRequest.auctionManager, - "Request: auctionManager mismatch" - ); - assertEq( - actualRequest.writeCount, - expectedRequest.writeCount, - "Request: writeCount mismatch" - ); - assertEq( - keccak256(actualRequest.onCompleteData), - keccak256(expectedRequest.onCompleteData), - "Request: onCompleteData mismatch" - ); - // Nested struct checks (RequestTrackingParams) - assertEq( - actualRequest.requestTrackingParams.isRequestCancelled, - expectedRequest.requestTrackingParams.isRequestCancelled, - "RequestTrackingParams: isRequestCancelled mismatch" - ); - assertEq( - actualRequest.requestTrackingParams.isRequestExecuted, - expectedRequest.requestTrackingParams.isRequestExecuted, - "RequestTrackingParams: isRequestExecuted mismatch" - ); - assertEq( - actualRequest.requestTrackingParams.currentBatch, - expectedRequest.requestTrackingParams.currentBatch, - "RequestTrackingParams: currentBatch mismatch" - ); - assertEq( - actualRequest.requestTrackingParams.currentBatchPayloadsLeft, - expectedRequest.requestTrackingParams.currentBatchPayloadsLeft, - "RequestTrackingParams: currentBatchPayloadsLeft mismatch" - ); - assertEq( - actualRequest.requestTrackingParams.payloadsRemaining, - expectedRequest.requestTrackingParams.payloadsRemaining, - "RequestTrackingParams: payloadsRemaining mismatch" - ); - // Nested struct checks (RequestFeesDetails) - assertEq( - actualRequest.requestFeesDetails.maxFees, - expectedRequest.requestFeesDetails.maxFees, - "RequestFeesDetails: maxFees mismatch" - ); - assertEq( - actualRequest.requestFeesDetails.consumeFrom, - expectedRequest.requestFeesDetails.consumeFrom, - "RequestFeesDetails: consumeFrom mismatch" - ); - assertEq( - actualRequest.requestFeesDetails.winningBid.fee, - expectedRequest.requestFeesDetails.winningBid.fee, - "RequestFeesDetails: winningBid.fee mismatch" - ); - assertEq( - actualRequest.requestFeesDetails.winningBid.transmitter, - expectedRequest.requestFeesDetails.winningBid.transmitter, - "RequestFeesDetails: winningBid.transmitter mismatch" - ); - assertEq( - keccak256(actualRequest.requestFeesDetails.winningBid.extraData), - keccak256(expectedRequest.requestFeesDetails.winningBid.extraData), - "RequestFeesDetails: winningBid.extraData mismatch" - ); - } - - function checkPayloadParams(PayloadParams[] memory expectedPayloads) internal view { + function checkPayload(Payload[] memory expectedPayloads) internal view { for (uint i = 0; i < expectedPayloads.length; i++) { - PayloadParams memory expectedPayload = expectedPayloads[i]; - PayloadParams memory actualPayload = watcher.getPayloadParams( - expectedPayload.payloadId + Payload memory expectedPayload = expectedPayloads[i]; + Payload memory actualPayload = watcher.getPayload(expectedPayload.payloadId); + // Payload checks + assertEq( + actualPayload.callType, + expectedPayload.callType, + "Payload: callType mismatch" + ); + assertEq( + actualPayload.isPayloadCancelled, + expectedPayload.isPayloadCancelled, + "Payload: isPayloadCancelled mismatch" + ); + assertEq( + actualPayload.isPayloadExecuted, + expectedPayload.isPayloadExecuted, + "Payload: isPayloadExecuted mismatch" ); - // PayloadParams checks assertEq( actualPayload.payloadPointer, expectedPayload.payloadPointer, "Payload: payloadPointer mismatch" ); - assertEq( - actualPayload.callType, - expectedPayload.callType, - "Payload: callType mismatch" - ); assertEq( actualPayload.asyncPromise, expectedPayload.asyncPromise, @@ -1331,11 +886,17 @@ contract AppGatewayBaseSetup is WatcherSetup { expectedPayload.appGateway, "Payload: appGateway mismatch" ); + assertEq( + actualPayload.consumeFrom, + expectedPayload.consumeFrom, + "Payload: consumeFrom mismatch" + ); assertEq( actualPayload.payloadId, expectedPayload.payloadId, "Payload: payloadId mismatch" ); + assertEq(actualPayload.maxFees, expectedPayload.maxFees, "Payload: maxFees mismatch"); assertEq( actualPayload.resolvedAt, expectedPayload.resolvedAt, @@ -1365,51 +926,9 @@ contract AppGatewayBaseSetup is WatcherSetup { contract MessageSwitchboardSetup is DeploySetup { uint256 msgSbGasLimit = 100000; - event TriggerProcessed( - uint32 optChainSlug, - uint256 switchboardFees, - bytes32 digest, - DigestParams digestParams - ); - - function _getTriggerData( - MessagePlugBase srcPlug_, - MessagePlugBase dstPlug_, - SocketContracts memory srcSocketConfig_, - SocketContracts memory dstSocketConfig_, - bytes memory payload_ - ) internal view returns (uint160 payloadPointer, DigestParams memory digestParams) { - bytes32 triggerId = srcPlug_.getNextTriggerId(srcSocketConfig_.chainSlug); - uint40 payloadCounter = srcSocketConfig_.messageSwitchboard.payloadCounter(); - - payloadPointer = - (uint160(srcSocketConfig_.chainSlug) << 120) | - (uint160(uint64(uint256(triggerId))) << 80) | - payloadCounter; - - bytes32 payloadId = createPayloadId( - payloadPointer, - dstSocketConfig_.messageSwitchboard.switchboardId(), - dstSocketConfig_.chainSlug - ); - - digestParams = _createDigestParams( - srcSocketConfig_.chainSlug, - address(srcPlug_), - address(dstPlug_), - address(dstSocketConfig_.socket), - payloadId, - triggerId, - payload_ - ); - } - - function _executeOnDestination( - DigestParams memory digestParams_, - uint160 payloadPointer_ - ) internal { + function _executeOnDestination(DigestParams memory digestParams_) internal { _attestPayload(digestParams_); - _execute(digestParams_, payloadPointer_); + _execute(digestParams_); } // Helper function to attest a payload @@ -1423,7 +942,7 @@ contract MessageSwitchboardSetup is DeploySetup { ); bytes memory signature = createSignature(attestDigest, watcherPrivateKey); - optConfig.messageSwitchboard.attest(digestParams_, signature); + optConfig.messageSwitchboard.attest(attestDigest, signature); } function _createDigestParams( @@ -1432,10 +951,8 @@ contract MessageSwitchboardSetup is DeploySetup { address dstPlug_, address dstSocket_, bytes32 payloadId_, - bytes32 triggerId_, bytes memory payload_ ) internal view returns (DigestParams memory digestParams) { - bytes memory extraData = abi.encode(srcChainSlug_, toBytes32Format(srcPlug_)); digestParams = DigestParams({ socket: toBytes32Format(dstSocket_), transmitter: bytes32(0), @@ -1446,44 +963,54 @@ contract MessageSwitchboardSetup is DeploySetup { value: uint256(0), payload: payload_, target: toBytes32Format(dstPlug_), - appGatewayId: APP_GATEWAY_ID, - prevBatchDigestHash: triggerId_, - extraData: extraData + source: abi.encode(srcChainSlug_, toBytes32Format(srcPlug_)), + prevBatchDigestHash: bytes32(0), // No longer using triggerId + extraData: bytes("") // Contract now sets extraData to empty }); } function _createDigest(DigestParams memory digest_) internal pure returns (bytes32) { + bytes memory fixedPart = abi.encodePacked( + // Fixed-size fields + digest_.socket, + digest_.transmitter, + digest_.payloadId, + digest_.deadline, + digest_.callType, + digest_.gasLimit, + digest_.value, + digest_.target, + digest_.prevBatchDigestHash + ); + return keccak256( abi.encodePacked( - digest_.socket, - digest_.transmitter, - digest_.payloadId, - digest_.deadline, - digest_.callType, - digest_.gasLimit, - digest_.value, + fixedPart, + // Variable-length fields with length prefixes + uint32(digest_.payload.length), digest_.payload, - digest_.target, - digest_.appGatewayId, - digest_.prevBatchDigestHash, + uint32(digest_.source.length), + digest_.source, + uint32(digest_.extraData.length), digest_.extraData ) ); } // Helper function to execute on destination chain - function _execute(DigestParams memory digestParams_, uint160 payloadPointer_) internal { + function _execute(DigestParams memory digestParams_) internal { // this is a signature for the socket batcher (only used for EVM) - ExecuteParams memory executeParams = ExecuteParams({ + ExecutionParams memory executionParams = ExecutionParams({ callType: digestParams_.callType, deadline: digestParams_.deadline, gasLimit: digestParams_.gasLimit, value: digestParams_.value, payload: digestParams_.payload, target: fromBytes32Format(digestParams_.target), - payloadPointer: payloadPointer_, + payloadId: digestParams_.payloadId, prevBatchDigestHash: digestParams_.prevBatchDigestHash, + source: digestParams_.source, extraData: digestParams_.extraData }); @@ -1494,14 +1021,6 @@ contract MessageSwitchboardSetup is DeploySetup { transmitterProof: bytes("") }); - optConfig.socket.execute(executeParams, transmissionParams); + optConfig.socket.execute(executionParams, transmissionParams); } } - -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/Utils.t.sol b/test/Utils.t.sol index ff8cef98..354f4866 100644 --- a/test/Utils.t.sol +++ b/test/Utils.t.sol @@ -1,149 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; import "forge-std/Test.sol"; -import "../contracts/utils/common/IdUtils.sol"; -import "../contracts/utils/common/Converters.sol"; -/** - * @title IdUtilsTest - * @dev Tests for IdUtils utility functions - */ -contract IdUtilsTest is Test { - function testCreatePayloadId() public pure { - uint160 payloadPointer = 12345; - uint64 switchboardId = 67890; - uint32 chainSlug = 1; - - bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug); - - // Verify the structure - uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); - uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); - uint160 payloadPointerFromId = uint160(uint256(payloadId)); - - assertEq(chainSlugFromId, chainSlug, "Chain slug should match"); - assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match"); - assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match"); - } - - function testCreatePayloadIdWithZeroValues() public pure { - bytes32 payloadId = createPayloadId(0, 0, 0); - - assertEq(payloadId, bytes32(0), "Payload ID should be zero for zero inputs"); - } - - function testCreatePayloadIdWithMaxValues() public pure { - uint160 maxPayloadPointer = type(uint160).max; - uint64 maxSwitchboardId = type(uint64).max; - uint32 maxChainSlug = type(uint32).max; - - bytes32 payloadId = createPayloadId(maxPayloadPointer, maxSwitchboardId, maxChainSlug); - - // Verify the structure - uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); - uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); - uint160 payloadPointerFromId = uint160(uint256(payloadId)); - - assertEq(chainSlugFromId, maxChainSlug, "Chain slug should match"); - assertEq(switchboardIdFromId, maxSwitchboardId, "Switchboard ID should match"); - assertEq(payloadPointerFromId, maxPayloadPointer, "Payload pointer should match"); - } - - function testCreatePayloadIdFuzz( - uint160 payloadPointer, - uint64 switchboardId, - uint32 chainSlug - ) public pure { - bytes32 payloadId = createPayloadId(payloadPointer, switchboardId, chainSlug); - - // Verify the structure - uint32 chainSlugFromId = uint32(uint256(payloadId) >> 224); - uint64 switchboardIdFromId = uint64(uint256(payloadId) >> 160); - uint160 payloadPointerFromId = uint160(uint256(payloadId)); - - assertEq(chainSlugFromId, chainSlug, "Chain slug should match"); - assertEq(switchboardIdFromId, switchboardId, "Switchboard ID should match"); - assertEq(payloadPointerFromId, payloadPointer, "Payload pointer should match"); - } -} - -/** - * @title ConvertersTest - * @dev Tests for Converters utility functions - */ -contract ConvertersTest is Test { - function testToBytes32Format() public pure { - address testAddr = address(0x1234567890123456789012345678901234567890); - bytes32 result = toBytes32Format(testAddr); - - assertEq(result, bytes32(uint256(uint160(testAddr))), "Conversion should be correct"); - } - - function testToBytes32FormatWithZeroAddress() public pure { - bytes32 result = toBytes32Format(address(0)); - - assertEq(result, bytes32(0), "Zero address should convert to zero bytes32"); - } - - function testFromBytes32Format() public pure { - address originalAddr = address(0x1234567890123456789012345678901234567890); - bytes32 bytes32Format = toBytes32Format(originalAddr); - - address convertedAddr = fromBytes32Format(bytes32Format); - - assertEq(convertedAddr, originalAddr, "Conversion should be reversible"); - } - - function testFromBytes32FormatWithZeroAddress() public pure { - bytes32 zeroBytes32 = bytes32(0); - address convertedAddr = fromBytes32Format(zeroBytes32); - - assertEq(convertedAddr, address(0), "Zero bytes32 should convert to zero address"); - } - - function testFromBytes32FormatWithInvalidAddress() public { - // Create a bytes32 with non-zero upper bits - bytes32 invalidBytes32 = bytes32(uint256(1) << 160); - - try this.fromBytes32FormatWrapper(invalidBytes32) { - fail(); - } catch { - // Expected to revert +abstract contract Utils is Test { + function createSignature( + bytes32 digest_, + uint256 privateKey_ + ) public pure returns (bytes memory sig) { + bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); + (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest); + sig = new bytes(65); + bytes1 v32 = bytes1(sigV); + assembly { + mstore(add(sig, 96), v32) + mstore(add(sig, 32), sigR) + mstore(add(sig, 64), sigS) } } - function fromBytes32FormatWrapper( - bytes32 bytes32FormatAddress - ) external pure returns (address) { - return fromBytes32Format(bytes32FormatAddress); + function addressToBytes32(address addr_) public pure returns (bytes32) { + return bytes32(uint256(uint160(addr_))); } - function testFromBytes32FormatWithMaxValidAddress() public pure { - address maxAddr = address(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF); - bytes32 bytes32Format = toBytes32Format(maxAddr); - - address convertedAddr = fromBytes32Format(bytes32Format); - - assertEq(convertedAddr, maxAddr, "Max address should convert correctly"); - } - - function testConvertersRoundTrip() public pure { - address originalAddr = address(0xabCDEF1234567890ABcDEF1234567890aBCDeF12); - - bytes32 bytes32Format = toBytes32Format(originalAddr); - address convertedAddr = fromBytes32Format(bytes32Format); - - assertEq(convertedAddr, originalAddr, "Round trip conversion should work"); - } - - function testConvertersFuzz(address addr) public pure { - // Skip addresses that would cause overflow - vm.assume(uint256(uint160(addr)) <= type(uint160).max); - - bytes32 bytes32Format = toBytes32Format(addr); - address convertedAddr = fromBytes32Format(bytes32Format); - - assertEq(convertedAddr, addr, "Fuzz test should pass"); + function bytes32ToAddress(bytes32 addrBytes32_) public pure returns (address) { + return address(uint160(uint256(addrBytes32_))); } } diff --git a/test/apps/Counter.t.sol b/test/apps/Counter.t.sol index 8e6783db..80ee646d 100644 --- a/test/apps/Counter.t.sol +++ b/test/apps/Counter.t.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; +pragma solidity 0.8.28; -import {CounterAppGateway} from "./app-gateways/counter/CounterAppGateway.sol"; -import {Counter} from "./app-gateways/counter/Counter.sol"; +import {CounterAppGateway} from "./counter/CounterAppGateway.sol"; +import {Counter} from "./counter/Counter.sol"; import "../SetupTest.t.sol"; contract CounterTest is AppGatewayBaseSetup { - uint256 feesAmount = 0.01 ether; - bytes32 counterId; - bytes32[] contractIds = new bytes32[](1); + Counter counter; CounterAppGateway counterGateway; event CounterScheduleResolved(uint256 creationTimestamp, uint256 executionTimestamp); @@ -18,65 +16,51 @@ contract CounterTest is AppGatewayBaseSetup { deploy(); counterGateway = new CounterAppGateway(address(addressResolver), feesAmount); - depositNativeAndCredits(arbChainSlug, 1 ether, 0, address(counterGateway)); + depositNativeAndGas(arbChainSlug, 1 ether, address(counterGateway)); counterId = counterGateway.counter(); - contractIds[0] = counterId; } - function deployCounterApp(uint32 chainSlug) internal returns (uint40 requestCount) { - counterGateway.deployContracts(chainSlug); - requestCount = executeDeploy(counterGateway, chainSlug, contractIds); + function deployCounter(uint32 chainSlug) internal { + counter = new Counter(); + counter.initSocket( + toBytes32Format(address(counterGateway)), + address(getSocketConfig(chainSlug).socket), + getSocketConfig(chainSlug).switchboard.switchboardId() + ); + setupGatewayAndPlugs(chainSlug, address(counterGateway), toBytes32Format(address(counter))); + counterGateway.uploadPlug(chainSlug, counterId, toBytes32Format(address(counter))); } - function testCounterDeployment() external { - deployCounterApp(arbChainSlug); + function testCounterUpload() external { + deployCounter(arbChainSlug); - (bytes32 onChain, address forwarder) = getOnChainAndForwarderAddresses( + (bytes32 arbCounterBytes32, ) = getOnChainAndForwarderAddresses( arbChainSlug, counterId, counterGateway ); - - assertEq( - IForwarder(forwarder).getChainSlug(), - arbChainSlug, - "Forwarder chainSlug should be correct" - ); - assertEq( - IForwarder(forwarder).getOnChainAddress(), - onChain, - "Forwarder onChainAddress should be correct" - ); - } - - function testCounterDeploymentWithoutAsync() external { - vm.expectRevert(abi.encodeWithSelector(AsyncModifierNotSet.selector)); - counterGateway.deployContractsWithoutAsync(arbChainSlug); + assertEq(arbCounterBytes32, toBytes32Format(address(counter))); } function testCounterIncrement() external { - deployCounterApp(arbChainSlug); - + deployCounter(arbChainSlug); (bytes32 arbCounterBytes32, address arbCounterForwarder) = getOnChainAndForwarderAddresses( arbChainSlug, counterId, counterGateway ); address arbCounter = fromBytes32Format(arbCounterBytes32); - uint256 arbCounterBefore = Counter(arbCounter).counter(); - address[] memory instances = new address[](1); - instances[0] = arbCounterForwarder; - counterGateway.incrementCounters(instances); - executeRequest(); + counterGateway.incrementCounters(arbCounterForwarder); + executePayload(); assertEq(Counter(arbCounter).counter(), arbCounterBefore + 1); } function testCounterIncrementMultipleChains() public { - deployCounterApp(arbChainSlug); - deployCounterApp(optChainSlug); + deployCounter(arbChainSlug); + deployCounter(optChainSlug); (bytes32 arbCounterBytes32, address arbCounterForwarder) = getOnChainAndForwarderAddresses( arbChainSlug, @@ -94,16 +78,13 @@ contract CounterTest is AppGatewayBaseSetup { uint256 arbCounterBefore = Counter(arbCounter).counter(); uint256 optCounterBefore = Counter(optCounter).counter(); - address[] memory instances = new address[](2); - instances[0] = arbCounterForwarder; - instances[1] = optCounterForwarder; - counterGateway.incrementCounters(instances); + counterGateway.incrementCounters(arbCounterForwarder); + executePayload(); - bool incremented = counterGateway.incremented(); - assertEq(incremented, false); - executeRequest(); + counterGateway.incrementCounters(optCounterForwarder); + executePayload(); - incremented = counterGateway.incremented(); + bool incremented = counterGateway.incremented(); assertEq(incremented, true); assertEq(Counter(arbCounter).counter(), arbCounterBefore + 1); @@ -111,36 +92,28 @@ contract CounterTest is AppGatewayBaseSetup { } function testCounterReadMultipleChains() external { - testCounterIncrementMultipleChains(); + deployCounter(arbChainSlug); + deployCounter(optChainSlug); (, address arbCounterForwarder) = getOnChainAndForwarderAddresses( arbChainSlug, counterId, counterGateway ); - (, address optCounterForwarder) = getOnChainAndForwarderAddresses( - optChainSlug, - counterId, - counterGateway - ); - - address[] memory instances = new address[](2); - instances[0] = arbCounterForwarder; - instances[1] = optCounterForwarder; - counterGateway.readCounters(instances); - executeRequest(); + counterGateway.readCounters(arbCounterForwarder); + executePayload(); } function testCounterSchedule() external { - deployCounterApp(arbChainSlug); + deployCounter(arbChainSlug); uint256 creationTimestamp = block.timestamp; counterGateway.setSchedule(100); vm.expectEmit(true, true, true, false); emit CounterScheduleResolved(creationTimestamp, block.timestamp); - executeRequest(); + executePayload(); assertLe(block.timestamp, creationTimestamp + 100 + expiryTime); } diff --git a/test/apps/counter/Counter.sol b/test/apps/counter/Counter.sol new file mode 100644 index 00000000..53647270 --- /dev/null +++ b/test/apps/counter/Counter.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "solady/auth/Ownable.sol"; +import "../../../contracts/protocol/base/PlugBase.sol"; + +interface ICounterAppGateway { + function increase(uint256 value_) external returns (bytes32); +} + +contract Counter is Ownable, PlugBase { + uint256 public counter; + event CounterIncreased(uint256 value); + + function increase() external onlySocket { + counter++; + emit CounterIncreased(counter); + } + + function getCounter() external view returns (uint256) { + return counter; + } + + function increaseOnGateway(uint256 value_) external returns (bytes32) { + // can set overrides here: _setOverrides(params_); + return ICounterAppGateway(address(socket__)).increase(value_); + } +} diff --git a/test/apps/counter/CounterAppGateway.sol b/test/apps/counter/CounterAppGateway.sol new file mode 100644 index 00000000..057bb397 --- /dev/null +++ b/test/apps/counter/CounterAppGateway.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "../../../contracts/evmx/base/AppGatewayBase.sol"; +import "./Counter.sol"; +import "./ICounter.sol"; + +contract CounterAppGateway is AppGatewayBase, Ownable { + using OverrideParamsLib for OverrideParams; + + bytes32 public counter = _createContractId("counter"); + bytes32 public counter1 = _createContractId("counter1"); + + uint256 public counterVal; + uint256 public arbCounter; + uint256 public optCounter; + + bool public incremented; + bool public gasAccountManagerSwitch; + + event CounterScheduleResolved(uint256 creationTimestamp, uint256 executionTimestamp); + + constructor(address addressResolver_, uint256 fees_) { + overrideParams = overrideParams.setMaxFees(fees_); + _initializeOwner(msg.sender); + _initializeAppGateway(addressResolver_); + } + + function incrementCounters(address instances_) public async { + incremented = false; + ICounter(instances_).increase(); + then(this.onIncrementComplete.selector, bytes("")); + } + + function onIncrementComplete() public { + incremented = true; + } + + // for testing purposes + function incrementCountersWithoutAsync(address instance_) public { + Counter(instance_).increase(); + } + + function readCounters(address instance_) public async { + // the increase function is called on given list of instances + overrideParams = overrideParams.setRead(true).setParallel(true); + uint32 chainSlug = IForwarder(instance_).getChainSlug(); + ICounter(instance_).getCounter(); + then(this.setCounterValues.selector, abi.encode(chainSlug)); + } + + function readCounterAtBlock(address instance_, uint256 blockNumber_) public async { + uint32 chainSlug = IForwarder(instance_).getChainSlug(); + overrideParams = overrideParams.setRead(true).setParallel(true).setReadAtBlock( + uint64(blockNumber_) + ); + ICounter(instance_).getCounter(); + then(this.setCounterValues.selector, abi.encode(chainSlug)); + } + + function setCounterValues(bytes memory data, bytes memory returnData) external onlyPromises { + uint256 counterValue = abi.decode(returnData, (uint256)); + uint32 chainSlug = abi.decode(data, (uint32)); + if (chainSlug == 421614) { + arbCounter = counterValue; + } else if (chainSlug == 11155420) { + optCounter = counterValue; + } + } + + // trigger from a chain + function uploadPlug(uint32 chainSlug_, bytes32 contractId_, bytes32 plugAddress_) public { + forwarderAddresses[contractId_][chainSlug_] = asyncDeployer__() + .getOrDeployForwarderContract(plugAddress_, chainSlug_); + _setValidPlug(true, chainSlug_, plugAddress_); + } + + function increase(uint256 value_) external onlyWatcher { + counterVal += value_; + } + + // Schedule + function setSchedule(uint256 delayInSeconds_) public async { + _setSchedule(delayInSeconds_); + then(this.resolveSchedule.selector, abi.encode(block.timestamp)); + } + + function resolveSchedule(bytes memory data, bytes memory) external onlyPromises { + uint256 creationTimestamp = abi.decode(data, (uint256)); + emit CounterScheduleResolved(creationTimestamp, block.timestamp); + } + + // UTILS + function setMaxFees(uint256 fees_) public { + overrideParams = overrideParams.setMaxFees(fees_); + } + + function withdrawToChain( + uint32 chainSlug_, + address token_, + uint256 amount_, + address receiver_ + ) external { + _withdrawToChain(chainSlug_, token_, amount_, receiver_); + } + + function testOnChainRevert(uint32 chainSlug) public async { + address instance = forwarderAddresses[counter][chainSlug]; + ICounter(instance).wrongFunction(); + } + + function testCallBackRevert(uint32 chainSlug) public async { + // the increase function is called on given list of instances + overrideParams = overrideParams.setRead(true).setParallel(true); + address instance = forwarderAddresses[counter][chainSlug]; + ICounter(instance).getCounter(); + // wrong function call in callback so it reverts + then(this.withdrawToChain.selector, abi.encode(chainSlug)); + } + + function increaseFees(bytes32 payloadId_, uint256 newMaxFees_) public { + _increaseFees(payloadId_, newMaxFees_); + } +} diff --git a/test/apps/counter/ICounter.sol b/test/apps/counter/ICounter.sol new file mode 100644 index 00000000..f4456fda --- /dev/null +++ b/test/apps/counter/ICounter.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +interface ICounter { + function increase() external; + + function getCounter() external; + + // A function that is not part of the interface, used for testing on-chian revert. + function wrongFunction() external; +} diff --git a/test/encode.t.sol b/test/encode.t.sol new file mode 100644 index 00000000..18fb31a5 --- /dev/null +++ b/test/encode.t.sol @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "forge-std/Test.sol"; +import "../contracts/protocol/Socket.sol"; +import "../contracts/evmx/watcher/Watcher.sol"; +import "../contracts/evmx/helpers/AddressResolver.sol"; +import "../contracts/utils/common/AccessRoles.sol"; +import "../contracts/utils/Pausable.sol"; +import "../contracts/utils/AccessControl.sol"; +import "solady/utils/ERC1967Factory.sol"; + +/** + * @title PausableTest + * @notice Unit tests for pause/unpause functionality with PAUSER_ROLE and UNPAUSER_ROLE + */ +contract PausableTest is Test { + function setUp() public {} + + function test_encode_source() public { + uint32 sourceChainSlug = 1; + bytes32 sourceId = bytes32(0); + bytes memory source = abi.encode(sourceChainSlug, sourceId); + bytes memory source1 = abi.encodePacked(sourceChainSlug, sourceId); + console.logBytes(source); + console.logBytes(source1); + + console.log("encoding appGatewayId"); + console.logBytes(abi.encode(sourceId)); + } + + /// @notice Decodes packed bytes back to sourceChainSlug and sourceId + /// @param packed The packed bytes from abi.encodePacked(sourceChainSlug, sourceId) + /// @return sourceChainSlug The decoded chain slug (uint32) + /// @return sourceId The decoded source ID (bytes32) + function decodePackedSource( + bytes memory packed + ) public pure returns (uint32 sourceChainSlug, bytes32 sourceId) { + require(packed.length >= 36, "Invalid packed length"); + + // Method 1: Using assembly (most efficient) + // In bytes memory, first 32 bytes contain the length + // Data starts at offset 32 + assembly { + // Read first 32 bytes of data (contains uint32 in rightmost 4 bytes) + let firstWord := mload(add(packed, 32)) + // Extract uint32 from rightmost 4 bytes (shift right by 224 bits = 28 bytes) + sourceChainSlug := shr(224, firstWord) + + // Read next 32 bytes starting at offset 36 (skip 4 bytes for uint32) + sourceId := mload(add(packed, 36)) + } + } + + /// @notice Decode method using pure Solidity (no assembly) - Recommended + /// @param packed The packed bytes from abi.encodePacked(sourceChainSlug, sourceId) + /// @return sourceChainSlug The decoded chain slug (uint32) + /// @return sourceId The decoded source ID (bytes32) + function decodePackedSourceNoAssembly( + bytes memory packed + ) public pure returns (uint32 sourceChainSlug, bytes32 sourceId) { + require(packed.length >= 36, "Invalid packed length"); + + // Extract first 4 bytes directly - Solidity allows bytes4(bytes memory) + bytes4 slugBytes = bytes4(packed); + // Convert bytes4 to uint32 (direct cast works) + sourceChainSlug = uint32(slugBytes); + + // Extract next 32 bytes (bytes 4-35) and convert to bytes32 + // Build bytes32 by combining bytes in big-endian order + uint256 idValue; + for (uint256 i = 0; i < 32; i++) { + idValue |= uint256(uint8(packed[4 + i])) << (248 - (i * 8)); + } + sourceId = bytes32(idValue); + } + + /// @notice Test the decode function + function test_decode_source() public { + uint32 sourceChainSlug = 1; + bytes32 sourceId = bytes32(uint256(2)); + bytes memory packed = abi.encodePacked(sourceChainSlug, sourceId); + + // Test assembly version + (uint32 decodedSlug, bytes32 decodedId) = decodePackedSource(packed); + assertEq(decodedSlug, sourceChainSlug, "Chain slug mismatch (assembly)"); + assertEq(decodedId, sourceId, "Source ID mismatch (assembly)"); + + // Test pure Solidity version + (uint32 decodedSlug2, bytes32 decodedId2) = decodePackedSourceNoAssembly(packed); + assertEq(decodedSlug2, sourceChainSlug, "Chain slug mismatch (no assembly)"); + assertEq(decodedId2, sourceId, "Source ID mismatch (no assembly)"); + + console.log("Decoded chain slug (assembly):", decodedSlug); + console.log("Decoded chain slug (no assembly):", decodedSlug2); + console.logBytes32(decodedId); + console.logBytes32(decodedId2); + } + + /// @notice Test fallback function double encoding pattern + function test_fallback_double_encoding() public { + // Deploy the mock fallback contract + MockFallbackContract mock = new MockFallbackContract(); + + bytes32 expectedPayloadId = bytes32(uint256(0x123456789abcdef)); + + // Call the fallback function with some dummy data + (bool success, bytes memory returnData) = address(mock).call{value: 0}( + abi.encodeWithSignature("someRandomFunction(uint256)", 42) + ); + + require(success, "Call failed"); + + console.log("\n=== Fallback Double Encoding Test ==="); + console.log("Expected payloadId:"); + console.logBytes32(expectedPayloadId); + + console.log("\nRaw return data from fallback:"); + console.logBytes(returnData); + console.log("Return data length:", returnData.length); + + // Decode using double decode: first decode gets the inner encoded bytes, second gets the bytes32 + // This is the reverse of: abi.encode(abi.encode(payloadId)) + bytes memory innerEncoded = abi.decode(returnData, (bytes)); + bytes32 decodedPayloadId = abi.decode(innerEncoded, (bytes32)); + + // Alternative: one-liner version + // bytes32 decodedPayloadId = abi.decode(abi.decode(returnData, (bytes)), (bytes32)); + + console.log("\nDecoded payloadId:"); + console.logBytes32(decodedPayloadId); + + assertEq(decodedPayloadId, expectedPayloadId, "PayloadId mismatch after decoding"); + + console.log("\nSuccessfully decoded payloadId from double-encoded fallback return"); + } + + /// @notice Test what happens with single encoding (this would fail in real usage) + function test_fallback_single_vs_double_encoding() public { + MockFallbackSingleEncode singleEncode = new MockFallbackSingleEncode(); + MockFallbackDoubleEncode doubleEncode = new MockFallbackDoubleEncode(); + + bytes32 expectedPayloadId = bytes32(uint256(0x123456789abcdef)); + + console.log("\n=== Comparing Single vs Double Encoding ==="); + + // Test single encoding + (bool success1, bytes memory returnData1) = address(singleEncode).call( + abi.encodeWithSignature("dummy()") + ); + require(success1, "Single encode call failed"); + + console.log("\nSingle encode return data:"); + console.logBytes(returnData1); + console.log("Length:", returnData1.length); + + // Test double encoding + (bool success2, bytes memory returnData2) = address(doubleEncode).call( + abi.encodeWithSignature("dummy()") + ); + require(success2, "Double encode call failed"); + + console.log("\nDouble encode return data:"); + console.logBytes(returnData2); + console.log("Length:", returnData2.length); + + // Try to decode both + console.log("\n--- Decoding Results ---"); + + // Single encoding: abi.encode(bytes32) just gives you the bytes32 padded to 32 bytes + // Can decode directly as bytes32 + bytes32 decoded1 = abi.decode(returnData1, (bytes32)); + console.log("Decoded from single encoding:"); + console.logBytes32(decoded1); + assertEq(decoded1, expectedPayloadId, "Single encoding decoded correctly (raw bytes32)"); + + // Double encoding: abi.encode(abi.encode(bytes32)) wraps it in ABI structure (offset + length + data) + // Need to decode twice: first to get inner bytes, then to get bytes32 + bytes32 decoded2 = abi.decode(abi.decode(returnData2, (bytes)), (bytes32)); + console.log("Decoded from double encoding:"); + console.logBytes32(decoded2); + assertEq(decoded2, expectedPayloadId, "Double encoding decoded correctly (ABI-encoded)"); + + console.log("\nBoth methods decoded successfully"); + console.log( + "Note: Single encoding returns 32 bytes, double encoding returns 96 bytes (offset + length + data)" + ); + } + + /// @notice Test using interface call (like GasStation.sol) vs raw .call() + /// @dev This test demonstrates the critical difference: + /// - Interface calls: Solidity auto-decodes one layer → need ONE more decode + /// - Raw .call(): No auto-decode → need TWO decodes + function test_interface_call_vs_raw_call() public { + MockSocketWithFallback mockSocket = new MockSocketWithFallback(); + bytes32 expectedPayloadId = bytes32(uint256(0x123456789abcdef)); + + console.log("\n=== Interface Call vs Raw Call Test ==="); + console.log("Expected payloadId:"); + console.logBytes32(expectedPayloadId); + + // Method 1: Using interface (like GasStation.sol does) + // This is how GasStation.sol calls Socket's fallback + IMockDepositInterface mockInterface = IMockDepositInterface(address(mockSocket)); + uint64 returnedPayloadId = mockInterface.depositFromChain( + address(0x123), + address(0x456), + 1000, + 100 + ); + + console.log("Returned payloadId:", returnedPayloadId); + // console.log("\nMethod 1 - Interface call (GasStation.sol pattern):"); + // console.logBytes(returnedPayloadId); + // console.log("Note: Solidity auto-decoded the outer ABI layer!"); + + // Method 2: Using raw .call() + // (bool success, bytes memory rawCallReturn) = address(mockSocket).call( + // abi.encodeWithSignature( + // "depositFromChain(address,address,uint256,uint256)", + // address(0x123), + // address(0x456), + // 1000, + // 100 + // ) + // ); + // require(success, "Raw call failed"); + + // console.log("\nMethod 2 - Raw .call():"); + // console.logBytes(rawCallReturn); + // console.log("Return data length:", rawCallReturn.length); + // console.log("Note: Raw bytes from fallback, no auto-decode"); + + // // Now decode both + // console.log("\n--- Decoding Results ---"); + + // // CRITICAL: For interface call, Solidity ALREADY decoded the outer ABI layer! + // // So returnedBytes contains abi.encode(payloadId) which is just the bytes32 + // // We only need ONE decode + // bytes32 decodedFromInterface = bytes32(returnedPayloadId); + // console.log("Decoded from interface call (ONE decode):"); + // console.logBytes32(decodedFromInterface); + + // // For raw call: No auto-decode, the full double-encoded data is returned + // // We need TWO decodes to unwrap: abi.encode(abi.encode(payloadId)) + // bytes32 decodedFromRawCall = bytes32(uint256(abi.decode(rawCallReturn, (uint256)))); + // console.log("Decoded from raw call (TWO decodes):"); + // console.logBytes32(decodedFromRawCall); + + // // Both should give us the expected payloadId + // assertEq(decodedFromInterface, expectedPayloadId, "Interface call decode mismatch"); + // assertEq(decodedFromRawCall, expectedPayloadId, "Raw call decode mismatch"); + + // console.log("\nConclusion: Interface calls need 1 decode, raw calls need 2 decodes!"); + } + + function test_gas_encode() public { + bytes32 payloadId = bytes32(uint256(0x123456789abcdef)); + uint256 start_gas = gasleft(); + // To mimic abi.encode(abi.encode(payloadId)), we need to create the ABI structure: + // - Offset: 0x20 (32 bytes, points to where length field starts) + // - Length: 0x20 (32 bytes, the length of the bytes32) + // - Data: the actual payloadId (32 bytes) + // Note: Order is offset FIRST, then length, then data + uint256 offset = 0x20; // Points to length field (32 bytes from start) + uint256 length = 0x20; // Length of bytes32 is 32 bytes + bytes memory encoded = abi.encodePacked(offset, length, payloadId); + uint256 end_gas = gasleft(); + + uint256 start_gas2 = gasleft(); + bytes memory encoded2 = abi.encode(abi.encode(payloadId)); + uint256 end_gas2 = gasleft(); + console.logBytes(encoded2); + console.log("Gas used:", start_gas2 - end_gas2); + console.log("Encoded length:", encoded2.length); + console.logBytes(encoded); + console.log("Gas used:", start_gas - end_gas); + console.log("Encoded length:", encoded.length); + + // Verify they match + assertEq(encoded.length, encoded2.length, "Length mismatch"); + assertEq(keccak256(encoded), keccak256(encoded2), "Encoded bytes don't match"); + } +} + +/// @notice Interface for testing (mimics IGasAccountManager in GasStation.sol) +interface IMockDepositInterface { + function depositFromChain( + address token_, + address receiver_, + uint256 gasAmount_, + uint256 nativeAmount_ + ) external returns (uint64); +} + +/// @notice Mock Socket contract with fallback (mimics real Socket behavior) +contract MockSocketWithFallback { + fallback(bytes calldata) external payable returns (bytes memory) { + uint64 payloadId = uint64(uint256(0x123456789abcdef)); + // Double encode like Socket.sol does + return abi.encode(payloadId); + } +} + +/// @notice Mock contract with fallback that returns a fixed payloadId (double encoded) +contract MockFallbackContract { + fallback(bytes calldata) external payable returns (bytes memory) { + bytes32 payloadId = bytes32(uint256(0x123456789abcdef)); + // Double encode: first encode converts bytes32 to bytes, second encode adds ABI structure + return abi.encode(abi.encode(payloadId)); + } +} + +/// @notice Mock contract with single encoding (raw bytes32) +contract MockFallbackSingleEncode { + fallback(bytes calldata) external payable returns (bytes memory) { + bytes32 payloadId = bytes32(uint256(0x123456789abcdef)); + // Single encode: just converts bytes32 to bytes (32 bytes) + return abi.encode(payloadId); + } +} + +/// @notice Mock contract with double encoding (ABI-encoded bytes) +contract MockFallbackDoubleEncode { + fallback(bytes calldata) external payable returns (bytes memory) { + bytes32 payloadId = bytes32(uint256(0x123456789abcdef)); + // Double encode: adds offset and length for proper ABI decoding + return abi.encode(abi.encode(payloadId)); + } +} diff --git a/test/mock/MockWatcherPrecompile.sol b/test/mock/MockWatcherPrecompile.sol deleted file mode 100644 index 6d66417c..00000000 --- a/test/mock/MockWatcherPrecompile.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import "../../contracts/evmx/watcher/Trigger.sol"; - -/// @title WatcherPrecompile -/// @notice Contract that handles payload verification, execution and app configurations -contract MockWatcherPrecompile is Trigger { - uint256 public newValue; - - function initialize(uint256 newValue_) external reinitializer(2) { - newValue = newValue_; - } - - function getRequestParams( - uint40 requestCount_ - ) external view override returns (RequestParams memory) {} - - function getPayloadParams( - bytes32 payloadId_ - ) external view override returns (PayloadParams memory) {} - - function getCurrentRequestCount() external view override returns (uint40) {} - - function queue( - QueueParams calldata queueParams_, - address appGateway_ - ) external override returns (address, uint40) {} - - function clearQueue() external override {} - - function submitRequest( - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes calldata onCompleteData - ) external override returns (uint40 requestCount, address[] memory promises) {} - - function queueAndSubmit( - QueueParams memory queue_, - uint256 maxFees, - address auctionManager, - address consumeFrom, - bytes calldata onCompleteData - ) external override returns (uint40 requestCount, address[] memory promises) {} - - function getPrecompileFees( - bytes4 precompile_, - bytes memory precompileData_ - ) external view override returns (uint256) {} - - function cancelRequest(uint40 requestCount_) external override {} - - function increaseFees(uint40 requestCount_, uint256 newFees_) external override {} - - function setIsValidPlug( - bool isValid_, - uint32 chainSlug_, - bytes32 onchainAddress_ - ) external override {} - - function isWatcher(address account_) external view override returns (bool) {} - - function watcherMultiCall(WatcherMultiCallParams[] memory params_) external payable { - if (isNonceUsed[params_[0].nonce]) revert NonceUsed(); - isNonceUsed[params_[0].nonce] = true; - } -} diff --git a/test/mocks/MockPlug.sol b/test/mocks/MockPlug.sol new file mode 100644 index 00000000..f2534d10 --- /dev/null +++ b/test/mocks/MockPlug.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "../../contracts/protocol/base/MessagePlugBase.sol"; + +contract MockPlug is MessagePlugBase { + uint32 public chainSlug; + bytes32 public triggerId; + + constructor(address socket_, uint32 switchboardId_) MessagePlugBase(socket_, switchboardId_) {} + + function setSocket(address socket_) external { + _setSocket(socket_); + } + + function setChainSlug(uint32 chainSlug_) external { + chainSlug = chainSlug_; + } + + function setOverrides(bytes memory overrides_) external { + _setOverrides(overrides_); + } + + function getOverrides() external view returns (bytes memory) { + return overrides; + } + + function trigger(bytes memory data) external { + // Mock trigger function + triggerId = keccak256(data); + } + + function getTriggerId() external view returns (bytes32) { + return triggerId; + } + + // New method to trigger Socket's triggerAppGateway + function triggerSocket(bytes memory data) external payable returns (bytes32) { + return socket__.sendPayload{value: msg.value}(data); + } + + // Method to connect to socket + function connectToSocket(address socket_, uint32 switchboardId_) external { + _setSocket(socket_); + switchboardId = switchboardId_; + socket__.connect(switchboardId_, ""); + switchboard = socket__.switchboardAddresses(switchboardId_); + } + + // Method to increase fees for payload + function increaseFeesForPayload(bytes32 payloadId_, bytes memory feesData_) external payable { + socket__.increaseFeesForPayload{value: msg.value}(payloadId_, feesData_); + } + + function registerSibling(uint32 chainSlug_, address siblingPlug_) external { + _registerSibling(chainSlug_, siblingPlug_); + } +} diff --git a/test/protocol/Socket.t.sol b/test/protocol/Socket.t.sol index 1a71ec20..2ac75fb9 100644 --- a/test/protocol/Socket.t.sol +++ b/test/protocol/Socket.t.sol @@ -1,20 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.21; - -import {Test} from "forge-std/Test.sol"; -import {console} from "forge-std/console.sol"; - -import {MockFastSwitchboard} from "../mock/MockFastSwitchboard.sol"; -import {MockPlug, MockTarget} from "../mock/MockPlug.sol"; -import {MockFeeManager} from "../mock/MockFeesManager.sol"; -import {SuperToken} from "../apps/app-gateways/super-token/SuperToken.sol"; -import {MockERC721} from "../mock/MockERC721.sol"; +pragma solidity 0.8.28; +import "forge-std/Test.sol"; import "../../contracts/protocol/Socket.sol"; import "../../contracts/protocol/SocketUtils.sol"; +import "../../contracts/protocol/SocketConfig.sol"; +import "../../contracts/utils/common/Structs.sol"; import "../../contracts/utils/common/Errors.sol"; import "../../contracts/utils/common/Constants.sol"; -import "../../contracts/utils/common/Structs.sol"; +import "../../contracts/utils/common/AccessRoles.sol"; +import "../../contracts/utils/common/IdUtils.sol"; +import "../../contracts/utils/common/Converters.sol"; +import "../../contracts/protocol/interfaces/ISocket.sol"; +import "../../contracts/protocol/interfaces/ISwitchboard.sol"; +import "../../contracts/protocol/interfaces/IPlug.sol"; +import {SafeTransferLib} from "../../lib/solady/src/utils/SafeTransferLib.sol"; +import {ERC1967Factory} from "solady/utils/ERC1967Factory.sol"; +import "../Utils.t.sol"; +import "../../contracts/evmx/watcher/precompiles/WritePrecompile.sol"; /** * @title SocketTestWrapper @@ -24,106 +27,331 @@ contract SocketTestWrapper is Socket { constructor( uint32 chainSlug_, address owner_, - string memory version_ - ) Socket(chainSlug_, owner_, version_) {} + uint256 gasLimitBuffer_, + uint16 maxCopyBytes_ + ) Socket(chainSlug_, owner_, gasLimitBuffer_, maxCopyBytes_) {} // Expose internal functions for testing function createDigest( address transmitter_, - bytes32 payloadId_, - bytes32 appGatewayId_, - ExecuteParams calldata executeParams_ + ExecutionParams calldata executionParams_ ) external view returns (bytes32) { - return _createDigest(transmitter_, payloadId_, appGatewayId_, executeParams_); - } - - function encodeTriggerId() external returns (bytes32) { - return _encodeTriggerId(); + return _createDigest(transmitter_, executionParams_); } function executeInternal( - bytes32 payloadId_, - ExecuteParams calldata executeParams_, + ExecutionParams calldata executionParams_, TransmissionParams calldata transmissionParams_ ) external payable returns (bool, bytes memory) { - return _execute(payloadId_, executeParams_, transmissionParams_); + return _execute(executionParams_, transmissionParams_); + } +} + +/** + * @title SimpleMockPlug + * @dev Simple mock plug for Socket.t.sol tests that doesn't auto-connect + */ +contract SimpleMockPlug is IPlug { + ISocket public socket__; + bytes public overridesData = hex"1234"; + bool public shouldRevert = false; + + function overrides() external view override returns (bytes memory) { + return overridesData; + } + + function setOverrides(bytes memory newOverrides) external { + overridesData = newOverrides; + } + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } + + function connectToSocket(address socket_, uint32 switchboardId_) external { + socket__ = ISocket(socket_); + socket__.connect(switchboardId_, ""); + } + + function disconnect() external { + socket__.disconnect(); + } + + function sendPayload(bytes calldata callData_) external payable returns (bytes32) { + return socket__.sendPayload{value: msg.value}(callData_); + } + + function increaseFeesForPayload(bytes32 payloadId_, bytes calldata feesData_) external payable { + socket__.increaseFeesForPayload{value: msg.value}(payloadId_, feesData_); + } + + // Required by IPlug but not used in these tests + function initSocket(bytes32, address, uint32) external override {} + + // Fallback to handle calls - will revert if shouldRevert is true + fallback() external payable { + if (shouldRevert) revert("SimpleMockPlug: revert"); + } + + receive() external payable { + if (shouldRevert) revert("SimpleMockPlug: revert"); + } +} + +/** + * @title MockSwitchboard + * @dev Mock switchboard for testing + */ +contract MockSwitchboard is ISwitchboard { + address public owner; + address public immutable socket; + uint32 public immutable chainSlug; + uint32 public switchboardId; + bool public isPayloadAllowed = true; + address public transmitter = address(0x1234); + uint64 public payloadCounter; + + constructor(uint32 chainSlug_, address socket_, address owner_) { + chainSlug = chainSlug_; + socket = socket_; + owner = owner_; + } + + function registerSwitchboard() external returns (uint32) { + // Must be called by the switchboard itself + switchboardId = ISocket(socket).registerSwitchboard(); + return switchboardId; + } + + function allowPayload(bytes32, bytes32, address, bytes memory) external view returns (bool) { + return isPayloadAllowed; + } + + function setIsPayloadAllowed(bool allowed) external { + isPayloadAllowed = allowed; + } + + function getTransmitter( + address sender_, + bytes32, + bytes calldata + ) external view returns (address) { + return transmitter != address(0) ? transmitter : sender_; + } + + function setTransmitter(address transmitter_) external { + transmitter = transmitter_; + } + + function processPayload( + address /* plug_ */, + bytes calldata /* payload_ */, + bytes calldata /* overrides_ */ + ) external payable returns (bytes32 payloadId_) { + // Create payload ID with verification info matching the socket's chain and this switchboard + payloadId_ = createPayloadId( + chainSlug, // source chain slug + switchboardId, // source switchboard id + chainSlug, // verification chain slug (same for local testing) + switchboardId, // verification switchboard id + payloadCounter++ // pointer + ); + } + + function updatePlugConfig(address, bytes memory) external {} + + function getPlugConfig(address, bytes memory) external pure returns (bytes memory) { + return ""; + } + + function increaseFeesForPayload(bytes32, address, bytes calldata) external payable {} +} + +/** + * @title MockFeeManager + * @dev Mock fee manager for testing + */ +contract MockFeeManager { + bool public collectNetworkFeeCalled = false; + ExecutionParams public lastExecutionParams; + TransmissionParams public lastTransmissionParams; + + function collectNetworkFee( + ExecutionParams memory executionParams_, + TransmissionParams memory transmissionParams_ + ) external payable { + collectNetworkFeeCalled = true; + lastExecutionParams = executionParams_; + lastTransmissionParams = transmissionParams_; + } + + function getMinSocketFees() external pure returns (uint256) { + return 0.001 ether; + } + + function setSocketFees(uint256) external {} + + function socketFees() external pure returns (uint256) { + return 0.001 ether; + } + + function reset() external { + collectNetworkFeeCalled = false; + } +} + +/** + * @title MockTarget + * @dev Mock target contract for execution testing + * Also implements IPlug so it can be used as a plug for testing + */ +contract MockTarget is IPlug { + uint256 public counter = 0; + bool public shouldRevert = false; + bytes public overridesData = hex"1234"; + ISocket public socket__; + + function overrides() external view override returns (bytes memory) { + return overridesData; + } + + function initSocket(bytes32, address, uint32) external override {} + + function connectToSocket(address socket_, uint32 switchboardId_) external { + socket__ = ISocket(socket_); + socket__.connect(switchboardId_, ""); + } + + function increment() external payable { + if (shouldRevert) revert("MockTarget: revert"); + counter++; + } + + function setShouldRevert(bool _shouldRevert) external { + shouldRevert = _shouldRevert; + } + + function returnLargeData() external pure returns (bytes memory) { + return new bytes(3072); // 3KB to test maxCopyBytes truncation + } +} + +/** + * @title RevertingRefundReceiver + * @dev Contract that reverts when receiving ETH - used to test refund failure scenarios + */ +contract RevertingRefundReceiver { + receive() external payable { + revert("RevertingRefundReceiver: Cannot receive ETH"); + } + + fallback() external payable { + revert("RevertingRefundReceiver: Cannot receive ETH"); } } /** * @title SocketTestBase * @dev Base contract for Socket protocol unit tests - * Provides common setup, utilities, and mock contracts */ -contract SocketTestBase is Test { +contract SocketTestBase is Test, Utils { uint256 c = 1; - string constant VERSION = "1.0.0"; address public socketOwner = address(uint160(c++)); address public transmitter = address(uint160(c++)); address public testUser = address(uint160(c++)); uint32 constant TEST_CHAIN_SLUG = 1; - bytes32 constant TEST_APP_GATEWAY_ID = keccak256("TEST_APP_GATEWAY"); bytes constant TEST_PAYLOAD = hex"1234567890abcdef"; bytes constant TEST_OVERRIDES = hex"abcdef"; // Contracts Socket public socket; - MockFastSwitchboard public mockSwitchboard; - MockPlug public mockPlug; + MockSwitchboard public mockSwitchboard; + SimpleMockPlug public mockPlug; MockFeeManager public mockFeeManager; - SuperToken public mockToken; MockTarget public mockTarget; SocketTestWrapper public socketWrapper; + WritePrecompile public writePrecompile; - uint64 public switchboardId; - ExecuteParams public executeParams; + uint32 public switchboardId; + ExecutionParams public executionParams; TransmissionParams public transmissionParams; - event ExecutionSuccess(bytes32 payloadId, bool exceededMaxCopy, bytes returnData); - event ExecutionFailed(bytes32 payloadId, bool exceededMaxCopy, bytes returnData); - function setUp() public virtual { - socket = new Socket(TEST_CHAIN_SLUG, socketOwner, VERSION); - mockSwitchboard = new MockFastSwitchboard(TEST_CHAIN_SLUG, address(socket), socketOwner); - mockPlug = new MockPlug(); + socket = new Socket(TEST_CHAIN_SLUG, socketOwner, GAS_LIMIT_BUFFER, MAX_COPY_BYTES); + mockSwitchboard = new MockSwitchboard(TEST_CHAIN_SLUG, address(socket), socketOwner); + mockPlug = new SimpleMockPlug(); mockFeeManager = new MockFeeManager(); - mockToken = new SuperToken("Test Token", "TEST", 18, testUser, 1000000000000000000); mockTarget = new MockTarget(); - socketWrapper = new SocketTestWrapper(TEST_CHAIN_SLUG, socketOwner, VERSION); - mockToken.setOwner(socketOwner); + socketWrapper = new SocketTestWrapper( + TEST_CHAIN_SLUG, + socketOwner, + GAS_LIMIT_BUFFER, + MAX_COPY_BYTES + ); + writePrecompile = _deployWritePrecompile(); // Set up initial state vm.startPrank(socketOwner); socket.grantRole(GOVERNANCE_ROLE, socketOwner); socket.grantRole(RESCUE_ROLE, socketOwner); socket.grantRole(SWITCHBOARD_DISABLER_ROLE, socketOwner); - - socket.setSocketFeeManager(address(mockFeeManager)); - mockToken.setSocket(address(socket)); + socket.grantRole(PAUSER_ROLE, socketOwner); + socket.grantRole(GOVERNANCE_ROLE, socketOwner); + socket.setNetworkFeeCollector(address(mockFeeManager)); vm.stopPrank(); + // Register switchboard - must be called by the switchboard itself + vm.prank(address(mockSwitchboard)); switchboardId = mockSwitchboard.registerSwitchboard(); - mockPlug.initSocket(TEST_APP_GATEWAY_ID, address(socket), switchboardId); - executeParams = _createExecuteParams(); + // Now connect the plug (after switchboard is registered) + mockPlug.connectToSocket(address(socket), switchboardId); + mockSwitchboard.setTransmitter(transmitter); + + executionParams = _createExecutionParams(); transmissionParams = _createTransmissionParams(); vm.deal(transmitter, 100 ether); vm.deal(testUser, 100 ether); } - function _createExecuteParams() internal view returns (ExecuteParams memory) { + function _deployWritePrecompile() internal returns (WritePrecompile) { + WritePrecompile writePrecompileImpl = new WritePrecompile(); + ERC1967Factory proxyFactory = new ERC1967Factory(); + address proxy = proxyFactory.deployAndCall( + address(writePrecompileImpl), + address(this), + abi.encodeWithSelector( + WritePrecompile.initialize.selector, + address(0), address(0), 1, 1 + ) + ); + writePrecompile = WritePrecompile(proxy); + return writePrecompile; + } + + + function _createExecutionParams() internal view returns (ExecutionParams memory) { + bytes32 payloadId = createPayloadId( + TEST_CHAIN_SLUG, // source chain slug + switchboardId, // source switchboard id + TEST_CHAIN_SLUG, // verification chain slug (matches socket) + switchboardId, // verification switchboard id (matches plug's switchboard) + 1 // pointer + ); + return - ExecuteParams({ + ExecutionParams({ callType: WRITE, - payloadPointer: 1, + payloadId: payloadId, deadline: block.timestamp + 1 hours, gasLimit: 100000, value: 0, prevBatchDigestHash: bytes32(0), target: address(mockPlug), payload: TEST_PAYLOAD, + source: abi.encode(TEST_CHAIN_SLUG, toBytes32Format(address(mockPlug))), extraData: bytes("") }); } @@ -137,705 +365,817 @@ contract SocketTestBase is Test { transmitterProof: bytes("") }); } +} - function createSignature( - bytes32 digest_, - uint256 privateKey_ - ) public pure returns (bytes memory sig) { - bytes32 digest = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest_)); - (uint8 sigV, bytes32 sigR, bytes32 sigS) = vm.sign(privateKey_, digest); - sig = new bytes(65); - bytes1 v32 = bytes1(sigV); - assembly { - mstore(add(sig, 96), v32) - mstore(add(sig, 32), sigR) - mstore(add(sig, 64), sigS) - } +/** + * @title SocketConstructorTest + * @dev Tests for Socket constructor + */ +contract SocketConstructorTest is SocketTestBase { + function test_Constructor() public view { + assertEq(socket.chainSlug(), TEST_CHAIN_SLUG, "Chain slug should match"); + assertEq(socket.owner(), socketOwner, "Owner should match"); + assertEq(socket.gasLimitBuffer(), 105, "Gas limit buffer should be 105"); } } /** * @title SocketExecuteTest - * @dev Tests for Socket execute function + * @dev Tests for Socket execute function - Part 1: Basic execution */ contract SocketExecuteTest is SocketTestBase { - function testConstructorWithValidParameters() public view { - assertEq(socket.chainSlug(), TEST_CHAIN_SLUG, "Chain slug should match"); - assertEq(socket.owner(), socketOwner, "Owner should match"); - assertEq(socket.version(), keccak256(bytes(VERSION)), "Version should match"); - assertEq(socket.gasLimitBuffer(), 105, "Gas limit buffer should be 105"); + function test_Execute_WithValidParameters() public { + bytes32 payloadId = executionParams.payloadId; + + vm.expectEmit(true, false, false, true); + emit ISocket.ExecutionSuccess(payloadId, false, bytes("")); + + hoax(transmitter); + (bool success, ) = socket.execute{value: 1 ether}(executionParams, transmissionParams); + assertTrue(success, "Execution should succeed"); + + // Verify the execution status + assertEq( + uint256(uint8(socket.executionStatus(payloadId))), + uint256(uint8(ExecutionStatus.Executed)), + "Status should be Executed" + ); } - function testExecuteDeadlinePassed() public { - executeParams.deadline = block.timestamp - 1; // Past deadline + function test_Execute_RevertsWhenDeadlinePassed() public { + executionParams.deadline = block.timestamp - 1; + vm.expectRevert(DeadlinePassed.selector); - socket.execute{value: 1 ether}(executeParams, transmissionParams); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); } - function testExecutePlugNotFound() public { - executeParams.target = address(0x999); // Non-existent plug - vm.expectRevert(PlugNotFound.selector); - socket.execute{value: 1 ether}(executeParams, transmissionParams); - } + function test_Execute_RevertsWhenInvalidCallType() public { + executionParams.callType = bytes4(0x12345678); - function testExecuteInsufficientValue() public { - executeParams.value = 1 ether; - transmissionParams.socketFees = 0.5 ether; + vm.expectRevert(InvalidCallType.selector); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); + } - vm.expectRevert(Socket.InsufficientMsgValue.selector); - socket.execute{value: 0.5 ether}(executeParams, transmissionParams); + function test_Execute_RevertsWhenPlugNotConnected() public { + executionParams.target = address(0x999); + vm.expectRevert(PlugNotConnected.selector); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); } - function testExecuteInvalidSwitchboardDisabled() public { + function test_Execute_RevertsWhenSwitchboardDisabled() public { hoax(socketOwner); socket.disableSwitchboard(switchboardId); vm.expectRevert(InvalidSwitchboard.selector); - socket.execute{value: 1 ether}(executeParams, transmissionParams); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); } - function testExecuteWithInvalidCallType() public { - executeParams.callType = bytes4(0x12345678); // Invalid call type + function test_Execute_RevertsWhenInsufficientValue() public { + executionParams.value = 1 ether; + transmissionParams.socketFees = 0.5 ether; - vm.expectRevert(InvalidCallType.selector); - socket.execute{value: 1 ether}(executeParams, transmissionParams); + vm.expectRevert(InsufficientMsgValue.selector); + hoax(transmitter); + socket.execute{value: 0.5 ether}(executionParams, transmissionParams); } - function testExecuteRefundIfExecutionFails() public { - executeParams.target = address(mockPlug); - executeParams.value = 0.5 ether; - executeParams.payload = abi.encodeWithSelector( - mockPlug.callMockTarget.selector, - address(mockTarget), - abi.encodeWithSelector(mockTarget.increment.selector) + function test_Execute_RevertsWhenInvalidVerificationChainSlug() public { + // Create payload ID with wrong verification chain slug + bytes32 invalidPayloadId = createPayloadId( + TEST_CHAIN_SLUG, // source chain slug + switchboardId, // source switchboard id + 999, // verification chain slug (wrong) + switchboardId, // verification switchboard id + 1 // pointer ); + executionParams.payloadId = invalidPayloadId; - // Set up mock target to revert - mockTarget.setShouldRevert(true); + vm.expectRevert(InvalidVerificationChainSlug.selector); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); + } - uint256 userBalance = testUser.balance; - transmissionParams.refundAddress = testUser; + function test_Execute_RevertsWhenInvalidVerificationSwitchboardId() public { + // Create payload ID with wrong verification switchboard id + bytes32 invalidPayloadId = createPayloadId( + TEST_CHAIN_SLUG, // source chain slug + switchboardId, // source switchboard id + TEST_CHAIN_SLUG, // verification chain slug (correct) + 999, // verification switchboard id (wrong) + 1 // pointer + ); + executionParams.payloadId = invalidPayloadId; + vm.expectRevert(InvalidVerificationSwitchboardId.selector); hoax(transmitter); - (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); - assertFalse(success, "Execution should fail"); + socket.execute{value: 1 ether}(executionParams, transmissionParams); + } - // Check that refund was sent - assertEq(testUser.balance, userBalance + 1 ether, "Refund should be sent to user"); + function test_Execute_RevertsWhenPayloadAlreadyExecuted() public { + bytes32 payloadId = executionParams.payloadId; - // Set up mock target to revert - mockTarget.setShouldRevert(false); - userBalance = testUser.balance; + // First execution succeeds + hoax(transmitter); + (bool success, ) = socket.execute{value: 1 ether}(executionParams, transmissionParams); + assertTrue(success, "First execution should succeed"); + // Second execution should revert + vm.expectRevert(abi.encodeWithSelector(PayloadAlreadyExecuted.selector)); hoax(transmitter); - (success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); - assertTrue(success, "Execution should succeed"); + socket.execute{value: 1 ether}(executionParams, transmissionParams); + } - // Check that refund was sent - assertEq(testUser.balance, userBalance, "Refund should not be sent to user"); + function test_Execute_RevertsWhenVerificationFailed() public { + mockSwitchboard.setIsPayloadAllowed(false); + + vm.expectRevert(VerificationFailed.selector); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); } - function testExecuteWithValidParameters() public { - // Use mockPlug as target since it's connected to socket - executeParams.target = address(mockPlug); - executeParams.payload = abi.encodeWithSelector( - mockPlug.processPayload.selector, - TEST_PAYLOAD - ); + function test_Execute_RevertsWhenLowGasLimit() public { + executionParams.gasLimit = 10000000; + vm.expectRevert(LowGasLimit.selector); + hoax(transmitter); + socket.execute{value: 1 ether, gas: 100000}(executionParams, transmissionParams); + } +} + +/** + * @title SocketExecuteTestPart2 + * @dev Tests for Socket execute function - Part 2: Execution results and refunds + */ +contract SocketExecuteTestPart2 is SocketTestBase { + function test_ExecutionFailedEvent() public { + // Create a plug that will revert when called + SimpleMockPlug revertingPlug = new SimpleMockPlug(); + revertingPlug.connectToSocket(address(socket), switchboardId); + revertingPlug.setShouldRevert(true); + // Update execute params to use the reverting plug + executionParams.target = address(revertingPlug); + // Use any payload - the plug will revert in fallback + executionParams.payload = abi.encode("revert"); + + // Update payload ID to match (use a different pointer to avoid conflicts) bytes32 payloadId = createPayloadId( - executeParams.payloadPointer, + TEST_CHAIN_SLUG, + switchboardId, + TEST_CHAIN_SLUG, switchboardId, - TEST_CHAIN_SLUG + 999 // Different pointer + ); + executionParams.payloadId = payloadId; + + // When a contract reverts with a message, Solidity ABI-encodes it + // The returnData contains: Error(string) selector (0x08c379a0) + offset + length + string data + // We check that returnData is not empty and contains the error selector + bytes memory expectedRevertData = abi.encodeWithSignature( + "Error(string)", + "SimpleMockPlug: revert" ); - vm.expectEmit(true, true, true, true, address(socket)); - emit ExecutionSuccess(payloadId, false, bytes(abi.encode(true))); - (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); - assertTrue(success, "Execution should succeed"); + vm.expectEmit(true, false, false, true); + emit ISocket.ExecutionFailed(executionParams.payloadId, false, expectedRevertData); + hoax(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); } - function testExecuteWithPayloadAlreadyExecuted() public { - executeParams.payload = abi.encodeWithSelector( - mockPlug.processPayload.selector, - TEST_PAYLOAD - ); + function test_Execute_RefundsWhenExecutionFails() public { + // Create a plug that will revert when called + SimpleMockPlug revertingPlug = new SimpleMockPlug(); + revertingPlug.connectToSocket(address(socket), switchboardId); + revertingPlug.setShouldRevert(true); + + // Update execute params to use the reverting plug + executionParams.target = address(revertingPlug); + // Use any payload - the plug will revert in fallback + executionParams.payload = abi.encode("revert"); + // Update payload ID to match (use a different pointer to avoid conflicts) bytes32 payloadId = createPayloadId( - executeParams.payloadPointer, + TEST_CHAIN_SLUG, switchboardId, - TEST_CHAIN_SLUG + TEST_CHAIN_SLUG, + switchboardId, + 999 // Different pointer ); + executionParams.payloadId = payloadId; + + uint256 userBalance = testUser.balance; + transmissionParams.refundAddress = testUser; - vm.expectEmit(true, true, true, true, address(socket)); - emit ExecutionSuccess(payloadId, false, bytes(abi.encode(true))); - socket.execute{value: 1 ether}(executeParams, transmissionParams); + hoax(transmitter); + (bool success, ) = socket.execute{value: 1 ether}(executionParams, transmissionParams); + assertFalse(success, "Execution should fail"); - vm.expectRevert( - abi.encodeWithSelector(Socket.PayloadAlreadyExecuted.selector, ExecutionStatus.Executed) + // Check that refund was sent + assertEq(testUser.balance, userBalance + 1 ether, "Refund should be sent to user"); + assertEq( + uint256(uint8(socket.executionStatus(payloadId))), + uint256(uint8(ExecutionStatus.Reverted)), + "Status should be Reverted" ); - socket.execute{value: 1 ether}(executeParams, transmissionParams); } - function testExecuteWithVerificationFailed() public { - // Override the allowPayload function to return false - mockSwitchboard.setIsPayloadAllowed(false); + function test_Execute_RefundsToMsgSenderWhenRefundAddressIsZero() public { + // Create a plug that will revert when called + SimpleMockPlug revertingPlug = new SimpleMockPlug(); + revertingPlug.connectToSocket(address(socket), switchboardId); + revertingPlug.setShouldRevert(true); + + // Update execute params to use the reverting plug + executionParams.target = address(revertingPlug); + executionParams.payload = abi.encode("revert"); + + // Update payload ID to match + bytes32 payloadId = createPayloadId( + TEST_CHAIN_SLUG, + switchboardId, + TEST_CHAIN_SLUG, + switchboardId, + 998 // Different pointer + ); + executionParams.payloadId = payloadId; - vm.expectRevert(Socket.VerificationFailed.selector); - socket.execute{value: 1 ether}(executeParams, transmissionParams); + transmissionParams.refundAddress = address(0); + + vm.deal(transmitter, 10 ether); + uint256 transmitterBalance = transmitter.balance; + vm.prank(transmitter); + socket.execute{value: 1 ether}(executionParams, transmissionParams); + // Balance same as before, as refund sent to caller (transmitter) + assertEq(transmitter.balance, transmitterBalance, "Refund should be sent to transmitter"); } - function testExecuteWithFailedExecution() public { - executeParams.target = address(mockPlug); - executeParams.payload = abi.encodeWithSelector( - mockPlug.callMockTarget.selector, - address(mockTarget), - abi.encodeWithSelector(mockTarget.increment.selector) + function test_Execute_CollectsFeesWhenExecutionSucceeds() public { + transmissionParams.socketFees = 0.1 ether; + bytes32 payloadId = executionParams.payloadId; + + // Reset fee manager state + mockFeeManager.reset(); + uint256 networkFeeCollectorBalance = address(mockFeeManager).balance; + + hoax(transmitter); + (bool success, ) = socket.execute{value: 1.1 ether}(executionParams, transmissionParams); + assertTrue(success, "Execution should succeed"); + + assertTrue(mockFeeManager.collectNetworkFeeCalled(), "Fee manager should be called"); + assertEq( + address(mockFeeManager).balance, + networkFeeCollectorBalance + 0.1 ether, + "Fees should be collected" ); + } + + function test_Execute_WorksWithoutFeeManager() public { + hoax(socketOwner); + socket.setNetworkFeeCollector(address(0)); + hoax(transmitter); + + bytes32 payloadId = executionParams.payloadId; + vm.expectEmit(true, false, false, true); + emit ISocket.ExecutionSuccess(payloadId, false, bytes("")); + (bool success, ) = socket.execute{value: 1 ether}(executionParams, transmissionParams); + assertTrue(success, "Execution should succeed without fee manager"); + } + + function test_Execute_HandlesExceededMaxCopyBytes() public { + // Connect mockTarget as a plug + mockTarget.connectToSocket(address(socket), switchboardId); - // Set up mock target to revert - mockTarget.setShouldRevert(true); + // Update execute params to call mockTarget.returnLargeData() + executionParams.target = address(mockTarget); + executionParams.payload = abi.encodeWithSelector(mockTarget.returnLargeData.selector); + // Update payload ID to match (use a different pointer to avoid conflicts) bytes32 payloadId = createPayloadId( - executeParams.payloadPointer, + TEST_CHAIN_SLUG, switchboardId, - TEST_CHAIN_SLUG - ); - - vm.expectEmit(true, true, true, true, address(socket)); - emit ExecutionFailed( - payloadId, - false, - abi.encodeWithSelector(MockPlug.CallFailed.selector) + TEST_CHAIN_SLUG, + switchboardId, + 997 // Different pointer ); - (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); - assertFalse(success, "Execution should fail"); - assertEq(mockTarget.counter(), 0, "Target should not be called"); - } + executionParams.payloadId = payloadId; - function testExecuteWithExceededMaxCopyBytes() public { - // Set up mock target to return large data - executeParams.target = address(mockPlug); - executeParams.payload = abi.encodeWithSelector(mockPlug.returnLargeData.selector); + // Update source to match new target + executionParams.source = abi.encode(TEST_CHAIN_SLUG, toBytes32Format(address(mockPlug))); + hoax(transmitter); (bool success, bytes memory returnData) = socket.execute{value: 1 ether}( - executeParams, + executionParams, transmissionParams ); - // The return data should be truncated to maxCopyBytes (2048 bytes) - assertEq(returnData.length, 2048, "Return data should be exactly maxCopyBytes"); - assertLt(returnData.length, 3072, "Return data should be truncated"); - assertTrue(success, "Execution should succeed even with large return data"); + assertTrue(success, "Execution should succeed"); + // MockTarget.returnLargeData() returns 3072 bytes, but maxCopyBytes is 2048 + // So returnData should be truncated to 2048 bytes + assertEq(returnData.length, 2048, "Return data should be truncated to maxCopyBytes (2048)"); } - function testExecutionRetryIfFailing() public { - executeParams.target = address(mockPlug); - executeParams.payload = abi.encodeWithSelector( - mockPlug.callMockTarget.selector, - address(mockTarget), - abi.encodeWithSelector(mockTarget.increment.selector) - ); + function test_Execute_TryCallRevert_ValueStaysInSocketAndGetsRefunded() public { + // Create a plug that will revert when called + SimpleMockPlug revertingPlug = new SimpleMockPlug(); + revertingPlug.connectToSocket(address(socket), switchboardId); + revertingPlug.setShouldRevert(true); + // Update execute params to use the reverting plug with a value + executionParams.target = address(revertingPlug); + executionParams.value = 0.5 ether; // Set a value to send with tryCall + executionParams.payload = abi.encode("revert"); + + // Update payload ID to match (use a different pointer to avoid conflicts) bytes32 payloadId = createPayloadId( - executeParams.payloadPointer, + TEST_CHAIN_SLUG, + switchboardId, + TEST_CHAIN_SLUG, switchboardId, - TEST_CHAIN_SLUG + 996 // Different pointer + ); + executionParams.payloadId = payloadId; + + uint256 socketBalanceBefore = address(socket).balance; + uint256 userBalance = testUser.balance; + transmissionParams.refundAddress = testUser; + transmissionParams.socketFees = 0.1 ether; + + // Send 1 ether total: 0.5 ether for executionParams.value + 0.1 ether for fees + 0.4 ether extra + uint256 msgValue = 1 ether; + hoax(transmitter); + (bool success, ) = socket.execute{value: msgValue}(executionParams, transmissionParams); + assertFalse(success, "Execution should fail"); + + // Verify that when tryCall reverts, the executionParams_.value never left Socket + // The entire msg.value should be refunded (including the value that was attempted to be sent) + assertEq( + testUser.balance, + userBalance + msgValue, + "Full msg.value should be refunded (executionParams.value never left Socket)" ); - mockTarget.setShouldRevert(true); - vm.expectEmit(true, true, true, true, address(socket)); - emit ExecutionFailed( - payloadId, - false, - abi.encodeWithSelector(MockPlug.CallFailed.selector) + // Verify Socket balance: should have received msgValue, then refunded it all + assertEq( + address(socket).balance, + socketBalanceBefore, + "Socket balance should be unchanged after refund" ); - (bool success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); - assertFalse(success, "First execution should fail"); - mockTarget.setShouldRevert(false); - vm.expectEmit(true, true, true, true, address(socket)); - emit ExecutionSuccess(payloadId, false, bytes(abi.encode(true))); - (success, ) = socket.execute{value: 1 ether}(executeParams, transmissionParams); - assertTrue(success, "Second execution should succeed"); + // Verify status is Reverted + assertEq( + uint256(uint8(socket.executionStatus(payloadId))), + uint256(uint8(ExecutionStatus.Reverted)), + "Status should be Reverted" + ); } - function testGasUsageForExecute() public { - executeParams.target = address(mockPlug); - executeParams.payload = abi.encodeWithSelector( - mockPlug.callMockTarget.selector, - address(mockTarget), - abi.encodeWithSelector(mockTarget.increment.selector) + function test_Execute_RefundRevert_ValueReturnsToCaller() public { + // Create a plug that will revert when called + SimpleMockPlug revertingPlug = new SimpleMockPlug(); + revertingPlug.connectToSocket(address(socket), switchboardId); + revertingPlug.setShouldRevert(true); + + // Create a contract that reverts when receiving ETH + RevertingRefundReceiver revertingReceiver = new RevertingRefundReceiver(); + + // Update execute params to use the reverting plug + executionParams.target = address(revertingPlug); + executionParams.payload = abi.encode("revert"); + + // Update payload ID to match (use a different pointer to avoid conflicts) + bytes32 payloadId = createPayloadId( + TEST_CHAIN_SLUG, + switchboardId, + TEST_CHAIN_SLUG, + switchboardId, + 995 // Different pointer ); + executionParams.payloadId = payloadId; - uint256 gasBefore = gasleft(); - socket.execute{value: 1 ether}(executeParams, transmissionParams); - uint256 gasUsed = gasBefore - gasleft(); - console.log("gasUsed", gasUsed); - } + // Set the reverting receiver as the refund address + transmissionParams.refundAddress = address(revertingReceiver); + transmissionParams.socketFees = 0.1 ether; - function testExecuteLowGasLimit() public { - // set high gas limit - executeParams.gasLimit = 10000000; + uint256 msgValue = 1 ether; - vm.expectRevert(Socket.LowGasLimit.selector); - socket.execute{value: 1 ether, gas: 100000}(executeParams, transmissionParams); + // The transaction should revert because safeTransferETH will revert + vm.expectRevert(SafeTransferLib.ETHTransferFailed.selector); + vm.startPrank(transmitter); + uint256 transmitterBalance = transmitter.balance; + socket.execute{value: msgValue}(executionParams, transmissionParams); + + assertEq( + transmitter.balance, + transmitterBalance, + "Full msg.value should be refunded (executionParams.value never left Socket)" + ); + vm.stopPrank(); } } /** - * @title SocketTriggerTest - * @dev Tests for Socket triggerAppGateway function + * @title SocketSendPayloadTest + * @dev Tests for Socket sendPayload function (outbound payloads) */ -contract SocketTriggerTest is SocketTestBase { - function testTriggerAppGatewayPlugNotFound() public { - bytes memory triggerData = abi.encodeWithSelector( - mockPlug.processPayload.selector, - TEST_PAYLOAD - ); +contract SocketSendPayloadTest is SocketTestBase { + event PayloadRequested( + bytes32 indexed payloadId, + address indexed plug, + uint32 indexed switchboardId, + bytes overrides, + bytes payload + ); + + function test_SendPayload_WithValidParameters() public { + bytes memory payload = abi.encode("test data"); + vm.deal(address(mockPlug), 10 ether); - hoax(testUser); - vm.expectRevert(PlugNotFound.selector); - socket.triggerAppGateway{value: 1 ether}(triggerData); + hoax(address(mockPlug)); + bytes32 payloadId = socket.sendPayload{value: 1 ether}(payload); + + assertTrue(payloadId != bytes32(0), "Payload ID should be created"); } - function testTriggerAppGatewayWithInvalidSwitchboard() public { - // Give mockPlug some funds - vm.deal(address(mockPlug), 10 ether); + function test_SendPayload_RevertsWhenPlugNotConnected() public { + SimpleMockPlug newPlug = new SimpleMockPlug(); + bytes memory payload = abi.encode("test data"); + vm.deal(address(newPlug), 10 ether); + + vm.expectRevert(PlugNotConnected.selector); + hoax(address(newPlug)); + socket.sendPayload{value: 1 ether}(payload); + } + function test_SendPayload_RevertsWhenSwitchboardDisabled() public { hoax(socketOwner); socket.disableSwitchboard(switchboardId); - bytes memory triggerData = abi.encodeWithSelector( - mockPlug.processPayload.selector, - TEST_PAYLOAD - ); + bytes memory payload = abi.encode("test data"); + vm.deal(address(mockPlug), 10 ether); vm.expectRevert(InvalidSwitchboard.selector); hoax(address(mockPlug)); - socket.triggerAppGateway{value: 1 ether}(triggerData); + socket.sendPayload{value: 1 ether}(payload); } - function testGasUsageForTriggerAppGateway() public { - // Give mockPlug some funds + function test_Fallback_ForwardsToSendPayload() public { + bytes memory payload = abi.encode("test data"); vm.deal(address(mockPlug), 10 ether); - bytes memory triggerData = abi.encodeWithSelector( - mockPlug.processPayload.selector, - TEST_PAYLOAD - ); - - uint256 gasBefore = gasleft(); hoax(address(mockPlug)); - socket.triggerAppGateway{value: 1 ether}(triggerData); - uint256 gasUsed = gasBefore - gasleft(); + (bool success, bytes memory result) = address(socket).call{value: 1 ether}(payload); - // Gas usage should be reasonable - console.log("gasUsed", gasUsed); + assertTrue(success, "Fallback should succeed"); + assertEq(result.length, 96, "Should return payload ID"); } -} -/** - * @title SocketConnectDisconnectTest - * @dev Tests for Socket connect and disconnect functions - */ -contract SocketConnectDisconnectTest is SocketTestBase { - function testConnectWithInvalidSwitchboard() public { - vm.expectRevert(InvalidSwitchboard.selector); - mockPlug.initSocket(TEST_APP_GATEWAY_ID, address(socket), switchboardId + 1); + function test_Receive_Reverts() public { + vm.expectRevert("Socket does not accept ETH"); + (bool success, ) = payable(address(socket)).call{value: 1 ether}(""); } - function testConnectWithNewSwitchboard() public { - // Create a new switchboard - MockFastSwitchboard newSwitchboard = new MockFastSwitchboard( + function test_IncreaseFeesForPayload_WithValidParameters() public { + bytes32 payloadId = createPayloadId( TEST_CHAIN_SLUG, - address(socket), - socketOwner + switchboardId, + TEST_CHAIN_SLUG, + switchboardId, + 1 ); - uint64 newSwitchboardId = newSwitchboard.registerSwitchboard(); - mockPlug.initSocket(TEST_APP_GATEWAY_ID, address(socket), newSwitchboardId); + bytes memory feesData = abi.encode(0.1 ether); + vm.deal(address(mockPlug), 10 ether); - (bytes32 appGatewayId, uint64 switchboardId) = socket.getPlugConfig(address(mockPlug)); - assertEq(appGatewayId, TEST_APP_GATEWAY_ID, "App gateway ID should match"); - assertEq(switchboardId, newSwitchboardId, "Switchboard ID should match"); + hoax(address(mockPlug)); + socket.increaseFeesForPayload{value: 0.1 ether}(payloadId, feesData); + // Should not revert } - // Try to disconnect a plug that was never connected - function testDisconnectWithPlugNotConnected() public { - MockPlug newPlug = new MockPlug(); - newPlug.setSocket(address(socket)); + function test_IncreaseFeesForPayload_RevertsWhenPlugNotConnected() public { + SimpleMockPlug newPlug = new SimpleMockPlug(); + bytes32 payloadId = createPayloadId( + TEST_CHAIN_SLUG, + switchboardId, + TEST_CHAIN_SLUG, + switchboardId, + 1 + ); + bytes memory feesData = abi.encode(0.1 ether); + vm.deal(address(newPlug), 10 ether); - vm.expectRevert(SocketConfig.PlugNotConnected.selector); - newPlug.disconnect(); + vm.expectRevert(PlugNotConnected.selector); + hoax(address(newPlug)); + socket.increaseFeesForPayload{value: 0.1 ether}(payloadId, feesData); } } /** - * @title SocketSwitchboardManagementTest - * @dev Tests for Socket switchboard management functions + * @title SocketConfigTest + * @dev Tests for SocketConfig functionality (connect, disconnect, switchboard management, setters) */ -contract SocketSwitchboardManagementTest is SocketTestBase { - function testRegisterSwitchboardWithExistingSwitchboard() public { - assertEq(mockSwitchboard.switchboardId(), socket.switchboardIds(address(mockSwitchboard))); +contract SocketConfigTest is SocketTestBase { + function test_Connect_WithValidSwitchboard() public { + SimpleMockPlug newPlug = new SimpleMockPlug(); + bytes memory plugConfig = abi.encode("test config"); - // Try to register the same switchboard again - vm.expectRevert(SocketConfig.SwitchboardExists.selector); - mockSwitchboard.registerSwitchboard(); + // PlugConnected event has no indexed parameters, so check only data + vm.expectEmit(true, true, false, true); + emit ISocket.PlugConnected(address(newPlug), switchboardId, plugConfig); + + hoax(address(newPlug)); + socket.connect(switchboardId, plugConfig); + + assertEq( + socket.plugSwitchboardIds(address(newPlug)), + switchboardId, + "Plug should be connected" + ); + } + + function test_Connect_WithInvalidSwitchboard_Reverts() public { + SimpleMockPlug newPlug = new SimpleMockPlug(); + + vm.expectRevert(InvalidSwitchboard.selector); + hoax(address(newPlug)); + socket.connect(0, bytes("")); } - function testDisableSwitchboardWithGovernanceRole() public { + function test_Connect_WithDisabledSwitchboard_Reverts() public { hoax(socketOwner); socket.disableSwitchboard(switchboardId); - // Try to register the same switchboard again - vm.expectRevert(SocketConfig.SwitchboardExists.selector); - mockSwitchboard.registerSwitchboard(); + SimpleMockPlug newPlug = new SimpleMockPlug(); + vm.expectRevert(InvalidSwitchboard.selector); + hoax(address(newPlug)); + socket.connect(switchboardId, bytes("")); + } + + function test_Disconnect_WithConnectedPlug() public { + vm.expectEmit(true, false, false, true); + emit ISocket.PlugDisconnected(address(mockPlug)); + + hoax(address(mockPlug)); + socket.disconnect(); + + assertEq(socket.plugSwitchboardIds(address(mockPlug)), 0, "Plug should be disconnected"); + } + + function test_Disconnect_WithUnconnectedPlug_Reverts() public { + SimpleMockPlug newPlug = new SimpleMockPlug(); + + vm.expectRevert(PlugNotConnected.selector); + hoax(address(newPlug)); + socket.disconnect(); + } + + function test_UpdatePlugConfig_WithConnectedPlug() public { + bytes memory newplugConfig = abi.encode("new config"); + + hoax(address(mockPlug)); + socket.connect(switchboardId, newplugConfig); + // Should not revert. not checking config, as mockSwitchboard returns empty + } + + function test_RegisterSwitchboard_Success() public { + MockSwitchboard newSwitchboard = new MockSwitchboard( + TEST_CHAIN_SLUG, + address(socket), + socketOwner + ); + uint32 newSwitchboardId = newSwitchboard.registerSwitchboard(); + assertTrue(newSwitchboardId > 0, "Switchboard ID should be assigned"); assertEq( - uint256(socket.isValidSwitchboard(switchboardId)), - uint256(SwitchboardStatus.DISABLED), - "Switchboard should be disabled" + uint256(socket.switchboardStatus(newSwitchboardId)), + uint256(SwitchboardStatus.REGISTERED), + "Switchboard should be registered" ); + assertEq( + socket.switchboardAddresses(newSwitchboardId), + address(newSwitchboard), + "Address should match" + ); + } + + function test_RegisterSwitchboard_AlreadyExists_Reverts() public { + vm.expectRevert(SwitchboardExists.selector); + mockSwitchboard.registerSwitchboard(); } - function testEnableSwitchboardWithGovernanceRole() public { - // First disable the switchboard + function test_DisableSwitchboard_WithValidRole() public { hoax(socketOwner); + vm.expectEmit(true, false, false, true); + emit ISocket.SwitchboardDisabled(switchboardId); socket.disableSwitchboard(switchboardId); + assertEq( - uint256(socket.isValidSwitchboard(switchboardId)), + uint256(socket.switchboardStatus(switchboardId)), uint256(SwitchboardStatus.DISABLED), "Switchboard should be disabled" ); - - // Then enable it - hoax(socketOwner); - socket.enableSwitchboard(switchboardId); - assertEq( - uint256(socket.isValidSwitchboard(switchboardId)), - uint256(SwitchboardStatus.REGISTERED), - "Switchboard should be enabled" - ); } - function testSwitchboardStatusValidation() public { - // Test initial status - assertEq( - uint256(socket.isValidSwitchboard(switchboardId)), - uint256(SwitchboardStatus.REGISTERED), - "Switchboard should be registered initially" + function test_DisableSwitchboard_WithoutRole_Reverts() public { + vm.expectRevert( + abi.encodeWithSelector(AccessControl.NoPermit.selector, SWITCHBOARD_DISABLER_ROLE) ); + hoax(testUser); + socket.disableSwitchboard(switchboardId); + } - // Test disabled status + function test_EnableSwitchboard_WithValidRole() public { + // First disable hoax(socketOwner); socket.disableSwitchboard(switchboardId); - assertEq( - uint256(socket.isValidSwitchboard(switchboardId)), - uint256(SwitchboardStatus.DISABLED), - "Switchboard should be disabled" - ); - // Test enabled status + // Then enable hoax(socketOwner); + vm.expectEmit(true, false, false, true); + emit ISocket.SwitchboardEnabled(switchboardId); socket.enableSwitchboard(switchboardId); assertEq( - uint256(socket.isValidSwitchboard(switchboardId)), + uint256(socket.switchboardStatus(switchboardId)), uint256(SwitchboardStatus.REGISTERED), "Switchboard should be enabled" ); } -} - -/** - * @title SocketSetterTest - * @dev Tests for Socket setter functions - */ -contract SocketSetterTest is SocketTestBase { - function testSetGasLimitBuffer() public { - uint256 newBuffer = 110; - - hoax(testUser); - vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); - socket.setGasLimitBuffer(newBuffer); + function test_EnableSwitchboard_WithoutRole_Reverts() public { hoax(socketOwner); - socket.setGasLimitBuffer(newBuffer); + socket.disableSwitchboard(switchboardId); - assertEq(socket.gasLimitBuffer(), newBuffer, "Gas limit buffer should be updated"); + vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); + hoax(testUser); + socket.enableSwitchboard(switchboardId); } - function testSetMaxCopyBytes() public { - uint16 newMaxCopyBytes = 4096; - - hoax(testUser); - vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); - socket.setMaxCopyBytes(newMaxCopyBytes); + function test_SetNetworkFeeCollector_WithValidRole() public { + MockFeeManager newFeeManager = new MockFeeManager(); hoax(socketOwner); - socket.setMaxCopyBytes(newMaxCopyBytes); - assertEq(socket.maxCopyBytes(), newMaxCopyBytes, "Max copy bytes should be updated"); + socket.setNetworkFeeCollector(address(newFeeManager)); + + assertEq( + address(socket.networkFeeCollector()), + address(newFeeManager), + "Fee manager should be updated" + ); } - function testSetSocketFeeManager() public { - address newFeeManager = address(0x123); + function test_SetNetworkFeeCollector_WithoutRole_Reverts() public { + MockFeeManager newFeeManager = new MockFeeManager(); - hoax(testUser); vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, GOVERNANCE_ROLE)); - socket.setSocketFeeManager(newFeeManager); - - hoax(socketOwner); - socket.setSocketFeeManager(newFeeManager); - assertEq( - address(socket.socketFeeManager()), - newFeeManager, - "Socket fee manager should be updated" - ); + hoax(testUser); + socket.setNetworkFeeCollector(address(newFeeManager)); } - function testConfigurationValidation() public { - // Test initial configuration - assertEq(socket.gasLimitBuffer(), 105, "Initial gas limit buffer should be 105"); - assertEq(socket.maxCopyBytes(), 2048, "Initial max copy bytes should be 2048"); - assertEq( - address(socket.socketFeeManager()), - address(mockFeeManager), - "Initial fee manager should be mockFeeManager" + function test_GetPlugConfig_WithConnectedPlug() public { + (uint32 returnedSwitchboardId, bytes memory plugConfig) = socket.getPlugConfig( + address(mockPlug), + bytes("") ); - MockFeeManager newFeeManager = new MockFeeManager(); - // Test configuration updates - vm.startPrank(socketOwner); - socket.setGasLimitBuffer(120); - socket.setMaxCopyBytes(4096); - socket.setSocketFeeManager(address(newFeeManager)); - vm.stopPrank(); + assertEq(returnedSwitchboardId, switchboardId, "Switchboard ID should match"); + // Config data comes from switchboard, which returns empty in mock + } - assertEq(socket.gasLimitBuffer(), 120, "Gas limit buffer should be updated"); - assertEq(socket.maxCopyBytes(), 4096, "Max copy bytes should be updated"); - assertEq( - address(socket.socketFeeManager()), - address(newFeeManager), - "Fee manager should be updated" + function test_GetPlugSwitchboard_WithConnectedPlug() public { + (uint32 returnedSwitchboardId, address switchboardAddress) = socket.getPlugSwitchboard( + address(mockPlug) ); + + assertEq(returnedSwitchboardId, switchboardId, "Switchboard ID should match"); + assertEq(switchboardAddress, address(mockSwitchboard), "Switchboard address should match"); } } /** - * @title SocketDigestTest - * @dev Tests for digest creation functionality + * @title SocketUtilsTest + * @dev Tests for SocketUtils functionality (simulate, digest creation, trigger ID) */ -contract SocketDigestTest is SocketTestBase { - function testCreateDigestWithBasicPayload() public view { - ExecuteParams memory params = ExecuteParams({ +contract SocketUtilsTest is SocketTestBase { + function test_Simulate_OnlyOffChainCaller() public { + SimulateParams[] memory params = new SimulateParams[](1); + params[0] = SimulateParams({ target: address(mockTarget), value: 1 ether, gasLimit: 100000, - deadline: block.timestamp + 3600, - callType: WRITE, - payload: TEST_PAYLOAD, - payloadPointer: uint160(1), - prevBatchDigestHash: bytes32(uint256(0)), - extraData: bytes("") + payload: TEST_PAYLOAD }); - bytes32 digest = socketWrapper.createDigest( - transmitter, - bytes32(uint256(0x123)), - TEST_APP_GATEWAY_ID, - params - ); - - assertTrue(digest != bytes32(0), "Digest should not be zero"); + // Should revert when called by non-off-chain caller + vm.expectRevert(OnlyOffChain.selector); + socket.simulate(params); } - function testCreateDigestWithLargePayload() public view { - bytes memory largePayload = new bytes(1000); - for (uint256 i = 0; i < 1000; i++) { - largePayload[i] = bytes1(uint8(i % 256)); - } - - ExecuteParams memory params = ExecuteParams({ + function test_Simulate_WithOffChainCaller() public { + SimulateParams[] memory params = new SimulateParams[](1); + params[0] = SimulateParams({ target: address(mockTarget), - value: 2 ether, - gasLimit: 200000, - deadline: block.timestamp + 7200, - callType: WRITE, - payload: largePayload, - payloadPointer: uint160(1), - prevBatchDigestHash: bytes32(uint256(0)), - extraData: bytes("") + value: 0, + gasLimit: 100000, + payload: abi.encodeWithSelector(mockTarget.increment.selector) }); - bytes32 digest = socketWrapper.createDigest( - transmitter, - bytes32(uint256(0xdef)), - TEST_APP_GATEWAY_ID, - params - ); + // Call as OFF_CHAIN_CALLER (address(0xDEAD)) + vm.prank(address(0xDEAD)); + SimulationResult[] memory results = socket.simulate(params); - assertTrue(digest != bytes32(0), "Digest should not be zero"); + assertEq(results.length, 1, "Should return one result"); + assertTrue(results[0].success, "Simulation should succeed"); } - function testCreateDigestWithDifferentTransmitters() public view { - ExecuteParams memory params = ExecuteParams({ + function test_Simulate_WithMultipleParams() public { + SimulateParams[] memory params = new SimulateParams[](3); + params[0] = SimulateParams({ target: address(mockTarget), - value: 1 ether, + value: 0, gasLimit: 100000, - deadline: block.timestamp + 3600, - callType: WRITE, - payload: TEST_PAYLOAD, - payloadPointer: uint160(1), - prevBatchDigestHash: bytes32(0), - extraData: bytes("") + payload: abi.encodeWithSelector(mockTarget.increment.selector) }); - - bytes32 digest1 = socketWrapper.createDigest( - transmitter, - bytes32(uint256(0x123)), - TEST_APP_GATEWAY_ID, - params - ); - - bytes32 digest2 = socketWrapper.createDigest( - address(uint160(0x456)), - bytes32(uint256(0x123)), - TEST_APP_GATEWAY_ID, - params - ); - - assertTrue(digest1 != digest2, "Digests should be different for different transmitters"); - } - - function testCreateDigestWithDifferentPayloads() public view { - ExecuteParams memory params1 = ExecuteParams({ + params[1] = SimulateParams({ target: address(mockTarget), - value: 1 ether, + value: 0, gasLimit: 100000, - deadline: block.timestamp + 3600, - callType: WRITE, - payload: TEST_PAYLOAD, - payloadPointer: uint160(1), - prevBatchDigestHash: bytes32(0), - extraData: bytes("") + payload: abi.encodeWithSelector(mockTarget.increment.selector) }); - - ExecuteParams memory params2 = ExecuteParams({ + params[2] = SimulateParams({ target: address(mockTarget), - value: 1 ether, + value: 0, gasLimit: 100000, - deadline: block.timestamp + 3600, - callType: WRITE, - payload: hex"abcdef", - payloadPointer: uint160(1), - prevBatchDigestHash: bytes32(0), - extraData: bytes("") + payload: abi.encodeWithSelector(mockTarget.increment.selector) }); - bytes32 digest1 = socketWrapper.createDigest( - transmitter, - bytes32(uint256(0x123)), - TEST_APP_GATEWAY_ID, - params1 - ); - - bytes32 digest2 = socketWrapper.createDigest( - transmitter, - bytes32(uint256(0x123)), - TEST_APP_GATEWAY_ID, - params2 - ); - - assertTrue(digest1 != digest2, "Digests should be different for different payloads"); - } -} - -/** - * @title SocketTriggerIdTest - * @dev Tests for trigger ID encoding and uniqueness - */ -contract SocketTriggerIdTest is SocketTestBase { - function testEncodeTriggerIdIncrementsCounter() public { - bytes32 triggerId1 = socketWrapper.encodeTriggerId(); - bytes32 triggerId2 = socketWrapper.encodeTriggerId(); - bytes32 triggerId3 = socketWrapper.encodeTriggerId(); - - assertTrue(triggerId1 != triggerId2, "Trigger IDs should be different"); - assertTrue(triggerId2 != triggerId3, "Trigger IDs should be different"); - assertTrue(triggerId1 != triggerId3, "Trigger IDs should be different"); - } - - function testTriggerIdUniqueness() public { - bytes32[] memory triggerIds = new bytes32[](100); - - for (uint256 i = 0; i < 100; i++) { - triggerIds[i] = socketWrapper.encodeTriggerId(); - } + vm.prank(address(0xDEAD)); + SimulationResult[] memory results = socket.simulate(params); - // Check for uniqueness - for (uint256 i = 0; i < 100; i++) { - for (uint256 j = i + 1; j < 100; j++) { - assertTrue(triggerIds[i] != triggerIds[j], "Trigger IDs should be unique"); - assertEq( - uint32(uint256(triggerIds[i]) >> 224), - TEST_CHAIN_SLUG, - "Chain slug should match" - ); - assertEq( - address(uint160(uint256(triggerIds[i]) >> 64)), - address(socketWrapper), - "Socket address should match" - ); - assertEq(uint64(uint256(triggerIds[i])), i, "Counter should increment"); - } + assertEq(results.length, 3, "Should return three results"); + for (uint256 i = 0; i < 3; i++) { + assertTrue(results[i].success, "All simulations should succeed"); } } - function testTriggerIdFormatFuzz(uint64 counter) public { - vm.assume(counter < type(uint64).max - 1000); + function test_CreateDigest_WithValidParameters_Solana() public view { + bytes32 payloadId = createPayloadId( + 14323, // origin chain slug - evmx + 1, // origin watcher id + CHAIN_SLUG_SOLANA_MAINNET, // verification chain slug (matches socket) + 1, // verification switchboard id (matches plug's switchboard) + 600 // pointer / counter + ); - // Set the counter to a specific value - uint256 counterSlot = uint256(57); - vm.store(address(socketWrapper), bytes32(counterSlot), bytes32(uint256(counter))); + // console.log("Payload ID:"); + // console.logBytes32(payloadId); + + // tScktAw75rtsBQtpVd98iRoCsiNLSwxr42tVTPvw98E + bytes32 socketSolana = 0x0d2d95ff192334e1f14b578f79635d3cd2ff42ef71eab74a89f92b5c383938c3; + // pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ + bytes32 transmitterSolana = 0x0c1a5886fe1093df9fc438c296f9f7275b7718b6bc0e156d8d336c58f083996d; + // tSTmyNniybBaDXPWbwbNaFS2Z9Qg65hWkcc3jBnGypK + bytes32 targetSolana = 0x0d2d692c8c7f3ff61408499c3eb4865321405fb1c9bfc5014a63672453973780; + address appGateway = address(0xCDB5fE8572725B20A2C0Db85DDb0D025bCC16f86); + + // real value taken from 07-calculate-digest.ts in Solana Socket repo test + bytes memory payloadPacked = hex"0d2d692c8c7f3ff61408499c3eb4865321405fb1c9bfc5014a63672453973780cb33f7992e094d6c65e0e95cf54e70c3470840e69d739dfcfcf2e1805fc913d1e8b3cf7c50f0b707dba43ef8042a34802d4f1768c72798bafa24d741c89f9ccf9ded6d20f1f5b9c56cb90ef89fc52d355aaaa868c42738eff11f50d1f81f522a04feb6778939c89983aac734e237dc22f49d7b4418d378a516df15a255d084cb000000000000000000000000000000000000000000000000000000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a93339e12fb69289a640420f0000000000"; + + DigestParams memory digestParams = DigestParams({ + socket: socketSolana, + transmitter: transmitterSolana, + payloadId: payloadId, + deadline: 1766666666, + callType: WRITE, + gasLimit: 0.5 * 10 ** 9, // 0.5 SOL + value: 0, + payload: payloadPacked, + target: targetSolana, + source: abi.encodePacked(toBytes32Format(appGateway)), + prevBatchDigestHash: bytes32(0x614bf23210ee3f9fd714634e1ea66f52cea9e6177140d533a981d511ebd785d3), + extraData: bytes("") + }); - bytes32 triggerId = socketWrapper.encodeTriggerId(); - uint32 chainSlugFromId = uint32(uint256(triggerId) >> 224); - address socketAddressFromId = address(uint160(uint256(triggerId) >> 64)); - uint64 counterFromId = uint64(uint256(triggerId)); + // console.log("Source:"); + // console.logBytes(abi.encodePacked(toBytes32Format(appGateway))); - assertEq(chainSlugFromId, TEST_CHAIN_SLUG, "Chain slug should match"); - assertEq(socketAddressFromId, address(socketWrapper), "Socket address should match"); - assertEq(counterFromId, counter, "Counter should match"); - } -} -/** - * @title SocketSimulationTest - * @dev Tests for simulation functionality - */ -contract SocketSimulationTest is SocketTestBase { - function testSimulationModifier() public { - SocketUtils.SimulateParams[] memory params = new SocketUtils.SimulateParams[](1); - params[0] = SocketUtils.SimulateParams({ - target: address(mockTarget), - value: 1 ether, - gasLimit: 100000, - payload: TEST_PAYLOAD - }); + bytes32 digest = writePrecompile.getDigest(digestParams); - // Should revert when called by non-off-chain caller - vm.expectRevert(SocketUtils.OnlyOffChain.selector); - socket.simulate(params); + // console.log("Digest solana:"); + // console.logBytes32(digest); + + assertTrue(digest == bytes32(0x6842eaeba430c89bd05048c9f63394a6c2b0ca5621ae636fef29d4b5293dbc6f)); } } /** * @title SocketRescueTest - * @dev Tests for rescue functionality + * @dev Tests for rescue funds functionality */ contract SocketRescueTest is SocketTestBase { - function testRescueFunds() public { - // Send some ETH to the socket + function test_RescueFunds_ETH_WithValidRole() public { vm.deal(address(socket), 10 ether); uint256 balanceBefore = testUser.balance; @@ -846,107 +1186,51 @@ contract SocketRescueTest is SocketTestBase { assertEq(balanceAfter - balanceBefore, 5 ether, "User should receive rescued ETH"); } - function testRescueFundsWithNonRescueRole() public { + function test_RescueFunds_ETH_WithoutRole_Reverts() public { vm.deal(address(socket), 10 ether); - hoax(testUser); vm.expectRevert(abi.encodeWithSelector(AccessControl.NoPermit.selector, RESCUE_ROLE)); - socket.rescueFunds(address(0), testUser, 5 ether); + hoax(testUser); + socket.rescueFunds(ETH_ADDRESS, testUser, 5 ether); } - function testRescueTokenFunds() public { - // Transfer some tokens to the socket - hoax(address(socket)); - mockToken.mint(address(socket), 1000); - uint256 balanceBefore = mockToken.balanceOf(testUser); + function test_RescueFunds_ERC20_WithValidRole() public { + // Deploy a simple ERC20 mock + MockERC20 token = new MockERC20(); + token.mint(address(socket), 1000); + + uint256 balanceBefore = token.balanceOf(testUser); hoax(socketOwner); - socket.rescueFunds(address(mockToken), testUser, 500); + socket.rescueFunds(address(token), testUser, 500); - uint256 balanceAfter = mockToken.balanceOf(testUser); + uint256 balanceAfter = token.balanceOf(testUser); assertEq(balanceAfter - balanceBefore, 500, "User should receive rescued tokens"); } - function testSendingEthToSocket() public { - vm.expectRevert("Socket does not accept ETH"); - (bool success, ) = address(socket).call{value: 1 ether}(""); - } - - function testRescueNFT() public { - // Deploy a mock ERC721 NFT and mint one to this test contract - MockERC721 mockNFT = new MockERC721(); - uint256 tokenId = 1; - mockNFT.mint(address(socket), tokenId); - assertEq(mockNFT.ownerOf(tokenId), address(socket), "Socket should own the NFT"); + function test_RescueFunds_ZeroAddress_Reverts() public { + vm.deal(address(socket), 10 ether); hoax(socketOwner); - socket.rescueFunds(address(mockNFT), testUser, tokenId); - - // Check that testUser received the NFT - assertEq(mockNFT.ownerOf(tokenId), testUser, "User should receive rescued NFT"); + vm.expectRevert(ZeroAddress.selector); + socket.rescueFunds(ETH_ADDRESS, address(0), 5 ether); } } /** - * @title SocketFeeManagerTest - * @dev Tests for fee manager functionality + * @title MockERC20 + * @dev Simple ERC20 mock for rescue tests */ -contract SocketFeeManagerTest is SocketTestBase { - function testFeeCollectedIfExecutionSuccess() public { - // Set up execution parameters with fees - executeParams.gasLimit = 100000; - transmissionParams.socketFees = 0.1 ether; - executeParams.target = address(mockPlug); - executeParams.payload = abi.encodeWithSelector( - mockPlug.processPayload.selector, - TEST_PAYLOAD - ); - - // Execute with fees - socket.execute{value: 1.1 ether}(executeParams, transmissionParams); - - // Check that fees were collected - assertEq(address(mockFeeManager).balance, 0.1 ether, "Fee manager should receive fees"); - } - - function testGasUsage() public { - executeParams.gasLimit = 100000; - transmissionParams.socketFees = 0.1 ether; - - // mockSwitchboard.setTransmitter(transmitter); +contract MockERC20 { + mapping(address => uint256) public balanceOf; - uint256 gasBefore = gasleft(); - - vm.deal(testUser, 2 ether); - hoax(testUser); - socket.execute{value: 1.1 ether}(executeParams, transmissionParams); - - uint256 gasUsed = gasBefore - gasleft(); - console.log("Gas used for execution with fees:", gasUsed); - - assertTrue(gasUsed > 0, "Gas should be used"); + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; } - function testFeeManagerNotSet() public { - // Remove fee manager - hoax(socketOwner); - socket.setSocketFeeManager(address(0)); - - executeParams.gasLimit = 100000; - transmissionParams.socketFees = 0.1 ether; - - // mockSwitchboard.setTransmitter(transmitter); - - // Should still execute successfully without fee manager - vm.deal(testUser, 2 ether); - hoax(testUser); - socket.execute{value: 1.1 ether}(executeParams, transmissionParams); - - // No fees should be collected - assertEq( - address(mockFeeManager).balance, - 0, - "No fees should be collected when fee manager is not set" - ); + function transfer(address to, uint256 amount) external returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + return true; } } diff --git a/test/protocol/switchboard/EVMxSwitchboard.t.sol b/test/protocol/switchboard/EVMxSwitchboard.t.sol new file mode 100644 index 00000000..f9ad4e00 --- /dev/null +++ b/test/protocol/switchboard/EVMxSwitchboard.t.sol @@ -0,0 +1,951 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "forge-std/Test.sol"; +import "../../../contracts/protocol/Socket.sol"; +import "../../../contracts/protocol/switchboard/EVMxSwitchboard.sol"; +import "../../../contracts/protocol/switchboard/MessageSwitchboard.sol"; +import "../../../contracts/utils/common/IdUtils.sol"; +import "../../../contracts/utils/common/Structs.sol"; +import "../../../contracts/utils/common/Constants.sol"; +import "../../../contracts/utils/common/Converters.sol"; +import {WATCHER_ROLE} from "../../../contracts/utils/common/AccessRoles.sol"; +import "../../mocks/MockPlug.sol"; +import "../../Utils.t.sol"; + +/** + * @title EVMxSwitchboardTestBase + * @dev Base contract for EVMxSwitchboard tests with common setup and helper methods + */ +contract EVMxSwitchboardTestBase is Test, Utils { + // Test constants + uint32 constant CHAIN_SLUG = 1; + uint32 constant OTHER_CHAIN_SLUG = 2; + uint32 constant EVMX_CHAIN_SLUG = 100; + uint32 constant WATCHER_ID = 1; + + address owner = address(0x1000); + address plugOwner = address(0x2000); + address watcher = address(0x3000); + + // Private key for watcher signing + uint256 watcherPrivateKey = 0x1111111111111111111111111111111111111111111111111111111111111111; + + Socket socket; + EVMxSwitchboard evmxSwitchboard; + MessageSwitchboard messageSwitchboard; + MockPlug mockPlug; + + uint32 public switchboardId; + ExecutionParams public executionParams; + TransmissionParams public transmissionParams; + + function setUp() public virtual { + // Deploy Socket + socket = new Socket(CHAIN_SLUG, owner, GAS_LIMIT_BUFFER, MAX_COPY_BYTES); + + // Deploy switchboards + evmxSwitchboard = new EVMxSwitchboard( + CHAIN_SLUG, + socket, + owner, + address(0), + EVMX_CHAIN_SLUG, + WATCHER_ID + ); + messageSwitchboard = new MessageSwitchboard(CHAIN_SLUG, socket, owner); + + hoax(owner); + evmxSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + hoax(owner); + evmxSwitchboard.grantWatcherRole(getWatcherAddress()); + + // Get switchboard ID + switchboardId = evmxSwitchboard.switchboardId(); + + // Create a mock plug + mockPlug = new MockPlug(address(socket), switchboardId); + + // Connect plug to socket + hoax(plugOwner); + mockPlug.connectToSocket(address(socket), switchboardId); + } + + /** + * @dev Helper to create ExecutionParams with default values + */ + function _createExecutionParams( + bytes32 payloadId_, + address target_, + bytes memory payload_, + bytes memory source_ + ) internal view returns (ExecutionParams memory) { + return + ExecutionParams({ + callType: WRITE, + payloadId: payloadId_, + deadline: block.timestamp + 3600, + gasLimit: 100000, + value: 0, + payload: payload_, + target: target_, + prevBatchDigestHash: bytes32(0), + source: source_, + extraData: bytes("") + }); + } + + /** + * @dev Helper to create ExecutionParams with custom values + */ + function _createExecutionParams( + bytes32 payloadId_, + address target_, + bytes memory payload_, + bytes memory source_, + uint256 deadline_, + uint256 gasLimit_, + uint256 value_ + ) internal view returns (ExecutionParams memory) { + return + ExecutionParams({ + callType: WRITE, + payloadId: payloadId_, + deadline: deadline_, + gasLimit: gasLimit_, + value: value_, + payload: payload_, + target: target_, + prevBatchDigestHash: bytes32(0), + source: source_, + extraData: bytes("") + }); + } + + /** + * @dev Helper to create TransmissionParams with default values + */ + function _createTransmissionParams() internal pure returns (TransmissionParams memory) { + return + TransmissionParams({ + socketFees: 0, + refundAddress: address(0), + extraData: bytes(""), + transmitterProof: bytes("") + }); + } + + /** + * @dev Helper to create TransmissionParams with custom values + */ + function _createTransmissionParams( + uint256 socketFees_, + address refundAddress_ + ) internal pure returns (TransmissionParams memory) { + return + TransmissionParams({ + socketFees: socketFees_, + refundAddress: refundAddress_, + extraData: bytes(""), + transmitterProof: bytes("") + }); + } + + /** + * @dev Helper to create and connect a trigger plug + */ + function _createTriggerPlug() internal returns (MockPlug) { + MockPlug triggerPlug = new MockPlug(address(socket), switchboardId); + vm.prank(plugOwner); + triggerPlug.connectToSocket(address(socket), switchboardId); + return triggerPlug; + } + + /** + * @dev Helper to create default payload and overrides for processPayload tests + */ + function _createPayloadAndOverrides() + internal + pure + returns (bytes memory payload, bytes memory overrides) + { + payload = abi.encode("test"); + overrides = abi.encode(uint256(0)); // Pass 0 to use default deadline + } + + /** + * @dev Helper to get watcher address from private key + */ + function getWatcherAddress() public view returns (address) { + return vm.addr(watcherPrivateKey); + } + + /** + * @dev Helper to create signature for attest function + */ + function _createAttestSignature(bytes32 digest_) internal view returns (bytes memory) { + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(evmxSwitchboard)), CHAIN_SLUG, digest_) + ); + return createSignature(signatureDigest, watcherPrivateKey); + } +} + +/** + * @title SocketPayloadIdVerificationTest + * @dev Tests for payload ID verification in Socket.execute() and EVMxSwitchboard payload creation + */ +contract SocketPayloadIdVerificationTest is EVMxSwitchboardTestBase { + // ============================================ + // TESTS - Socket.execute() Payload ID Verification + // ============================================ + + function test_Execute_VerifiesPayloadId_CorrectDestination() public { + // Create a valid payload ID for this chain and switchboard + bytes32 payloadId = createPayloadId( + OTHER_CHAIN_SLUG, // source chain slug + 100, // source switchboard id + CHAIN_SLUG, // verification chain slug (matches socket) + switchboardId, // verification switchboard id (matches plug's switchboard) + 12345 // pointer + ); + + ExecutionParams memory execParams = _createExecutionParams( + payloadId, + address(mockPlug), + abi.encode("test"), + abi.encode(OTHER_CHAIN_SLUG, toBytes32Format(address(0x1234))) + ); + + TransmissionParams memory transParams = _createTransmissionParams(); + + // Verify that payload ID check passes (doesn't revert with InvalidVerificationChainSlug or InvalidVerificationSwitchboardId) + // The execution should proceed past payload ID verification to the switchboard's allowPayload check. + // It will fail with InvalidSource because the source doesn't match the plug's appGatewayId. + // This confirms payload ID verification passed - we reached allowPayload which comes after payload ID check. + vm.expectRevert(InvalidSource.selector); + socket.execute{value: 0}(execParams, transParams); + + // If we get InvalidSource, it means: + // 1. ✅ Payload ID verification passed (didn't revert with InvalidVerificationChainSlug/InvalidVerificationSwitchboardId) + // 2. ✅ We reached the switchboard's allowPayload check (comes after payload ID verification) + // 3. ✅ allowPayload failed with InvalidSource (expected, since source doesn't match plug config) + } + + function test_Execute_WrongChainSlug_Reverts() public { + // Create payload ID with wrong verification chain slug + bytes32 payloadId = createPayloadId( + OTHER_CHAIN_SLUG, + 100, + OTHER_CHAIN_SLUG, // Wrong chain slug (doesn't match socket's chainSlug) + switchboardId, + 12345 + ); + + ExecutionParams memory execParams = _createExecutionParams( + payloadId, + address(mockPlug), + abi.encode("test"), + abi.encode(OTHER_CHAIN_SLUG, toBytes32Format(address(0x1234))) + ); + + TransmissionParams memory transParams = _createTransmissionParams(); + + vm.expectRevert(InvalidVerificationChainSlug.selector); + socket.execute{value: 0}(execParams, transParams); + } + + function test_Execute_WrongSwitchboardId_Reverts() public { + // Create payload ID with wrong verification switchboard ID + bytes32 payloadId = createPayloadId( + OTHER_CHAIN_SLUG, + 100, + CHAIN_SLUG, // Correct chain slug + 999, // Wrong switchboard ID (doesn't match plug's switchboard) + 12345 + ); + + ExecutionParams memory execParams = _createExecutionParams( + payloadId, + address(mockPlug), + abi.encode("test"), + abi.encode(OTHER_CHAIN_SLUG, toBytes32Format(address(0x1234))) + ); + + TransmissionParams memory transParams = _createTransmissionParams(); + + vm.expectRevert(InvalidVerificationSwitchboardId.selector); + socket.execute{value: 0}(execParams, transParams); + } + + // ============================================ + // TESTS - EVMxSwitchboard Payload Creation + // ============================================ + + function test_EVMxSwitchboard_ProcessPayload_CreatesTriggerPayloadId() public { + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + payload = abi.encode("test trigger"); // Override for this specific test + + // Get counter before + uint64 counterBefore = evmxSwitchboard.payloadCounter(); + + // Call processPayload (must be called by socket) + vm.prank(address(socket)); + bytes32 payloadId = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug), + payload, + overrides + ); + + // Verify counter incremented + assertEq(evmxSwitchboard.payloadCounter(), counterBefore + 1); + + // Verify payload ID structure + ( + uint32 sourceChainSlug, + uint32 sourceId, + uint32 verificationChainSlug, + uint32 verificationId, + uint64 pointer + ) = decodePayloadId(payloadId); + + assertEq(sourceChainSlug, CHAIN_SLUG, "Source chain slug should match source"); + assertEq(sourceId, switchboardId, "Source ID should match switchboard ID"); + assertEq(verificationChainSlug, EVMX_CHAIN_SLUG, "Verification chain slug should be EVMX"); + assertEq(verificationId, WATCHER_ID, "Verification ID should be watcher ID"); + assertEq(pointer, counterBefore, "Pointer should match counter before increment"); + } + + function test_EVMxSwitchboard_ProcessPayload_EmitsPayloadRequested() public { + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + payload = abi.encode("test trigger"); // Override for this specific test + + // Get counter before to calculate expected payload ID + uint64 counterBefore = evmxSwitchboard.payloadCounter(); + bytes32 expectedPayloadId = createPayloadId( + CHAIN_SLUG, + switchboardId, + EVMX_CHAIN_SLUG, + WATCHER_ID, + counterBefore + ); + + // Expect PayloadRequested event - overrides will be replaced with default deadline + bytes memory expectedOverrides = abi.encode( + block.timestamp + evmxSwitchboard.defaultDeadline() + ); + vm.expectEmit(true, true, true, true); + emit EVMxSwitchboard.PayloadRequested( + expectedPayloadId, + address(triggerPlug), + switchboardId, + expectedOverrides, + payload + ); + + // Call processPayload + vm.prank(address(socket)); + evmxSwitchboard.processPayload{value: 0}(address(triggerPlug), payload, overrides); + } + + function test_EVMxSwitchboard_ProcessPayload_CounterIncrements() public { + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + + uint64 counter1 = evmxSwitchboard.payloadCounter(); + + vm.prank(address(socket)); + evmxSwitchboard.processPayload{value: 0}(address(triggerPlug), payload, overrides); + + uint64 counter2 = evmxSwitchboard.payloadCounter(); + + vm.prank(address(socket)); + evmxSwitchboard.processPayload{value: 0}(address(triggerPlug), payload, overrides); + + uint64 counter3 = evmxSwitchboard.payloadCounter(); + + assertEq(counter2, counter1 + 1, "Counter should increment"); + assertEq(counter3, counter2 + 1, "Counter should increment again"); + } + + function test_EVMxSwitchboard_ProcessPayload_MultiplePayloads_UniqueIds() public { + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + + vm.prank(address(socket)); + bytes32 payloadId1 = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug), + payload, + overrides + ); + + vm.prank(address(socket)); + bytes32 payloadId2 = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug), + payload, + overrides + ); + + // All should be unique + assertNotEq(payloadId1, payloadId2, "Payload IDs should be unique"); + + // Verify they only differ in pointer + ( + uint32 source1, + uint32 sourceId1, + uint32 verif1, + uint32 verifId1, + uint64 pointer1 + ) = decodePayloadId(payloadId1); + ( + uint32 source2, + uint32 sourceId2, + uint32 verif2, + uint32 verifId2, + uint64 pointer2 + ) = decodePayloadId(payloadId2); + + assertEq(source1, source2); + assertEq(sourceId1, sourceId2); + assertEq(verif1, verif2); + assertEq(verifId1, verifId2); + + // Only pointers should differ + assertEq(pointer2, pointer1 + 1); + } + + // ============================================ + // TESTS - EVMxSwitchboard Attest Function + // ============================================ + + function test_Attest_Success() public { + bytes32 digest = keccak256(abi.encode("test payload")); + bytes memory signature = _createAttestSignature(digest); + + vm.expectEmit(true, true, false, false); + emit SwitchboardBase.Attested(digest, getWatcherAddress()); + + vm.prank(getWatcherAddress()); + evmxSwitchboard.attest(digest, signature); + + assertTrue(evmxSwitchboard.isValid(digest), "Digest should be valid"); + } + + function test_Attest_AlreadyAttested_Reverts() public { + bytes32 digest = keccak256(abi.encode("test payload")); + bytes memory signature = _createAttestSignature(digest); + + // First attest - should succeed + vm.prank(getWatcherAddress()); + evmxSwitchboard.attest(digest, signature); + + // Second attest - should revert + vm.prank(getWatcherAddress()); + vm.expectRevert(AlreadyAttested.selector); + evmxSwitchboard.attest(digest, signature); + } + + function test_Attest_InvalidWatcher_Reverts() public { + bytes32 digest = keccak256(abi.encode("test payload")); + + // Create signature with invalid private key (non-watcher) + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(evmxSwitchboard)), CHAIN_SLUG, digest) + ); + uint256 invalidPrivateKey = 0x2222222222222222222222222222222222222222222222222222222222222222; + bytes memory invalidSignature = createSignature(signatureDigest, invalidPrivateKey); + + vm.prank(vm.addr(invalidPrivateKey)); + vm.expectRevert(WatcherNotFound.selector); + evmxSwitchboard.attest(digest, invalidSignature); + } + + // ============================================ + // TESTS - EVMxSwitchboard AllowPayload Function + // ============================================ + + function test_AllowPayload_InvalidSource_Reverts() public { + bytes32 digest = keccak256(abi.encode("test")); + bytes32 appGatewayId = toBytes32Format(address(0x1234)); + + // Set up plug config with different appGatewayId + bytes memory plugConfig = abi.encode(toBytes32Format(address(0x5678))); + vm.prank(address(socket)); + evmxSwitchboard.updatePlugConfig(address(mockPlug), plugConfig); + + // Try to allow payload with wrong source + bytes memory source = abi.encode(appGatewayId); + vm.expectRevert(InvalidSource.selector); + evmxSwitchboard.allowPayload(digest, bytes32(0), address(mockPlug), source); + } + + function test_AllowPayload_ValidSource_ReturnsTrue() public { + bytes32 digest = keccak256(abi.encode("test")); + bytes32 appGatewayId = toBytes32Format(address(0x1234)); + + // Set up plug config + bytes memory plugConfig = abi.encode(appGatewayId); + vm.prank(address(socket)); + evmxSwitchboard.updatePlugConfig(address(mockPlug), plugConfig); + + // Attest the digest + bytes memory signature = _createAttestSignature(digest); + vm.prank(getWatcherAddress()); + evmxSwitchboard.attest(digest, signature); + + // Allow payload with correct source + bytes memory source = abi.encode(appGatewayId); + bool allowed = evmxSwitchboard.allowPayload(digest, bytes32(0), address(mockPlug), source); + assertTrue(allowed, "Payload should be allowed"); + } + + function test_AllowPayload_NotAttested_ReturnsFalse() public { + bytes32 digest = keccak256(abi.encode("test")); + bytes32 appGatewayId = toBytes32Format(address(0x1234)); + + // Set up plug config + bytes memory plugConfig = abi.encode(appGatewayId); + vm.prank(address(socket)); + evmxSwitchboard.updatePlugConfig(address(mockPlug), plugConfig); + + // Don't attest - just check allowPayload + bytes memory source = abi.encode(appGatewayId); + bool allowed = evmxSwitchboard.allowPayload(digest, bytes32(0), address(mockPlug), source); + assertFalse(allowed, "Payload should not be allowed when not attested"); + } + + // ============================================ + // TESTS - EVMxSwitchboard IncreaseFeesForPayload + // ============================================ + + function test_IncreaseFeesForPayload_Success() public { + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + + // Create payload + vm.prank(address(socket)); + bytes32 payloadId = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug), + payload, + overrides + ); + + // Increase fees + bytes memory feesData = abi.encode(uint256(0.1 ether)); + vm.expectEmit(true, true, false, true); + emit EVMxSwitchboard.FeesIncreased(payloadId, address(triggerPlug), feesData); + + vm.prank(address(socket)); + evmxSwitchboard.increaseFeesForPayload{value: 0}(payloadId, address(triggerPlug), feesData); + } + + function test_IncreaseFeesForPayload_InvalidPlug_Reverts() public { + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + + // Create payload + vm.prank(address(socket)); + bytes32 payloadId = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug), + payload, + overrides + ); + + // Try to increase fees with wrong plug address + bytes memory feesData = abi.encode(uint256(0.1 ether)); + vm.prank(address(socket)); + vm.expectRevert(InvalidSource.selector); + evmxSwitchboard.increaseFeesForPayload{value: 0}(payloadId, address(0x9999), feesData); + } + + // ============================================ + // TESTS - EVMxSwitchboard UpdatePlugConfig + // ============================================ + + function test_UpdatePlugConfig_Success() public { + bytes32 appGatewayId = toBytes32Format(address(0x1234)); + bytes memory plugConfig = abi.encode(appGatewayId); + + vm.expectEmit(true, false, false, true); + emit EVMxSwitchboard.PlugConfigUpdated(address(mockPlug), appGatewayId); + + vm.prank(address(socket)); + evmxSwitchboard.updatePlugConfig(address(mockPlug), plugConfig); + + // Verify config was set + bytes memory retrievedConfig = evmxSwitchboard.getPlugConfig(address(mockPlug), bytes("")); + bytes32 retrievedAppGatewayId = abi.decode(retrievedConfig, (bytes32)); + assertEq(retrievedAppGatewayId, appGatewayId, "AppGatewayId should match"); + } + + function test_UpdatePlugConfig_OnlySocket() public { + bytes32 appGatewayId = toBytes32Format(address(0x1234)); + bytes memory plugConfig = abi.encode(appGatewayId); + + vm.expectRevert(); + evmxSwitchboard.updatePlugConfig(address(mockPlug), plugConfig); + } + + // ============================================ + // TESTS - EVMxSwitchboard SetRevertingPayload + // ============================================ + + function test_SetRevertingPayload_Success() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + uint256 nonce = 1; + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + payloadId, + isReverting, + nonce + ) + ); + bytes memory signature = createSignature(digest, watcherPrivateKey); + + vm.expectEmit(true, false, false, true); + emit SwitchboardBase.RevertingPayloadIdset(payloadId, isReverting); + + vm.prank(getWatcherAddress()); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + + // Verify it was set (check via allowPayload or directly if there's a getter) + // Note: revertingPayloadIds is internal, so we can't directly check it + // But we can verify the event was emitted + } + + function test_SetRevertingPayload_OnlyOwner() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + + uint256 nonce = 1; + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + payloadId, + isReverting, + nonce + ) + ); + bytes memory signature = createSignature(digest, uint256(0x1234)); + + vm.expectRevert(); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + } + + // ============================================ + // TESTS - EVMxSwitchboard SetDefaultDeadline + // ============================================ + + function test_SetDefaultDeadline_Success() public { + uint256 newDeadline = 2 days; + + vm.expectEmit(true, false, false, true); + emit SwitchboardBase.DefaultDeadlineSet(newDeadline); + + vm.prank(owner); + evmxSwitchboard.setDefaultDeadline(newDeadline); + + assertEq( + evmxSwitchboard.defaultDeadline(), + newDeadline, + "Default deadline should be updated" + ); + } + + function test_SetDefaultDeadline_OnlyOwner() public { + uint256 newDeadline = 2 days; + + vm.prank(address(0x9999)); + vm.expectRevert(); + evmxSwitchboard.setDefaultDeadline(newDeadline); + } + + // ============================================ + // TESTS - EVMxSwitchboard GetPlugConfig + // ============================================ + + function test_GetPlugConfig_ReturnsConfig() public { + bytes32 appGatewayId = toBytes32Format(address(0x1234)); + bytes memory plugConfig = abi.encode(appGatewayId); + + // Set config + vm.prank(address(socket)); + evmxSwitchboard.updatePlugConfig(address(mockPlug), plugConfig); + + // Get config + bytes memory retrievedConfig = evmxSwitchboard.getPlugConfig(address(mockPlug), bytes("")); + bytes32 retrievedAppGatewayId = abi.decode(retrievedConfig, (bytes32)); + assertEq(retrievedAppGatewayId, appGatewayId, "AppGatewayId should match"); + } + + function test_GetPlugConfig_ReturnsZeroWhenNotSet() public { + // Get config for plug that hasn't been configured + bytes memory retrievedConfig = evmxSwitchboard.getPlugConfig(address(0x9999), bytes("")); + bytes32 retrievedAppGatewayId = abi.decode(retrievedConfig, (bytes32)); + assertEq(retrievedAppGatewayId, bytes32(0), "AppGatewayId should be zero when not set"); + } + + // ============================================ + // TESTS - EVMxSwitchboard ProcessPayload with Custom Deadline + // ============================================ + + function test_ProcessPayload_WithCustomDeadline() public { + MockPlug triggerPlug = _createTriggerPlug(); + bytes memory payload = abi.encode("test"); + + // Use custom deadline (not 0) + uint256 customDeadline = block.timestamp + 2 days; + bytes memory overrides = abi.encode(customDeadline); + + // Get counter before + uint64 counterBefore = evmxSwitchboard.payloadCounter(); + + // Call processPayload + vm.prank(address(socket)); + bytes32 payloadId = evmxSwitchboard.processPayload{value: 0}( + address(triggerPlug), + payload, + overrides + ); + + // Verify counter incremented + assertEq(evmxSwitchboard.payloadCounter(), counterBefore + 1); + + // Verify the event was emitted with custom deadline (not default) + // We can't directly verify the overrides in the event, but we can check the payloadId was created + assertTrue(payloadId != bytes32(0), "Payload ID should be created"); + } + + function test_ProcessPayload_WithZeroDeadline_UsesDefault() public { + MockPlug triggerPlug = _createTriggerPlug(); + bytes memory payload = abi.encode("test"); + + // Pass 0 as deadline - should use default + bytes memory overrides = abi.encode(uint256(0)); + + uint64 counterBefore = evmxSwitchboard.payloadCounter(); + bytes32 expectedPayloadId = createPayloadId( + CHAIN_SLUG, + switchboardId, + EVMX_CHAIN_SLUG, + WATCHER_ID, + counterBefore + ); + + uint256 expectedDeadline = block.timestamp + evmxSwitchboard.defaultDeadline(); + bytes memory expectedOverrides = abi.encode(expectedDeadline); + + vm.expectEmit(true, true, true, true); + emit EVMxSwitchboard.PayloadRequested( + expectedPayloadId, + address(triggerPlug), + switchboardId, + expectedOverrides, + payload + ); + + vm.prank(address(socket)); + evmxSwitchboard.processPayload{value: 0}(address(triggerPlug), payload, overrides); + } + + // ============================================ + // MISSING TESTS - Watcher Role Management + // ============================================ + + function test_grantWatcherRole_Success() public { + address newWatcher = address(0x5000); + vm.prank(owner); + evmxSwitchboard.grantWatcherRole(newWatcher); + + assertTrue(evmxSwitchboard.hasRole(WATCHER_ROLE, newWatcher)); + assertEq(evmxSwitchboard.totalWatchers(), 2); // 1 from setUp + 1 new + } + + function test_grantWatcherRole_WatcherFound_Reverts() public { + address existingWatcher = getWatcherAddress(); + vm.prank(owner); + vm.expectRevert(WatcherFound.selector); + evmxSwitchboard.grantWatcherRole(existingWatcher); + } + + function test_revokeWatcherRole_Success() public { + address watcherToRevoke = getWatcherAddress(); + uint256 totalBefore = evmxSwitchboard.totalWatchers(); + assertEq(totalBefore, 1); + + vm.prank(owner); + evmxSwitchboard.revokeWatcherRole(watcherToRevoke); + + assertFalse(evmxSwitchboard.hasRole(WATCHER_ROLE, watcherToRevoke)); + assertEq(evmxSwitchboard.totalWatchers(), 0); + } + + function test_revokeWatcherRole_WatcherNotFound_Reverts() public { + address nonWatcher = address(0x9999); + vm.prank(owner); + vm.expectRevert(WatcherNotFound.selector); + evmxSwitchboard.revokeWatcherRole(nonWatcher); + } + + function test_grantRole_WithValidRoles_Success() public { + address grantee = address(0x6000); + vm.prank(owner); + evmxSwitchboard.grantRole(GOVERNANCE_ROLE, grantee); + assertTrue(evmxSwitchboard.hasRole(GOVERNANCE_ROLE, grantee)); + + vm.prank(owner); + evmxSwitchboard.grantRole(RESCUE_ROLE, grantee); + assertTrue(evmxSwitchboard.hasRole(RESCUE_ROLE, grantee)); + } + + function test_grantRole_WithWatcherRole_Reverts() public { + address grantee = address(0x6000); + vm.prank(owner); + vm.expectRevert(InvalidRole.selector); + evmxSwitchboard.grantRole(WATCHER_ROLE, grantee); + } + + function test_revokeRole_WithValidRoles_Success() public { + address grantee = address(0x6000); + vm.startPrank(owner); + evmxSwitchboard.grantRole(GOVERNANCE_ROLE, grantee); + evmxSwitchboard.grantRole(RESCUE_ROLE, grantee); + vm.stopPrank(); + + vm.prank(owner); + evmxSwitchboard.revokeRole(GOVERNANCE_ROLE, grantee); + assertFalse(evmxSwitchboard.hasRole(GOVERNANCE_ROLE, grantee)); + + vm.prank(owner); + evmxSwitchboard.revokeRole(RESCUE_ROLE, grantee); + assertFalse(evmxSwitchboard.hasRole(RESCUE_ROLE, grantee)); + } + + function test_revokeRole_WithWatcherRole_Reverts() public { + address grantee = address(0x6000); + vm.prank(owner); + vm.expectRevert(InvalidRole.selector); + evmxSwitchboard.revokeRole(WATCHER_ROLE, grantee); + } + + // ============================================ + // MISSING TESTS - ProcessPayload Edge Cases + // ============================================ + + function test_ProcessPayload_EvmxConfigNotSet_Reverts() public { + // Deploy a new switchboard with evmxChainSlug = 0 + EVMxSwitchboard newSwitchboard = new EVMxSwitchboard( + CHAIN_SLUG, + socket, + owner, + address(0), + 0, // evmxChainSlug = 0 + WATCHER_ID + ); + + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + + vm.prank(address(socket)); + vm.expectRevert(EvmxConfigNotSet.selector); + newSwitchboard.processPayload{value: 0}(address(triggerPlug), payload, overrides); + } + + function test_ProcessPayload_EvmxWatcherIdZero_Reverts() public { + // Deploy a new switchboard with evmxWatcherId = 0 + EVMxSwitchboard newSwitchboard = new EVMxSwitchboard( + CHAIN_SLUG, + socket, + owner, + address(0), + EVMX_CHAIN_SLUG, + 0 // evmxWatcherId = 0 + ); + + MockPlug triggerPlug = _createTriggerPlug(); + (bytes memory payload, bytes memory overrides) = _createPayloadAndOverrides(); + + vm.prank(address(socket)); + vm.expectRevert(EvmxConfigNotSet.selector); + newSwitchboard.processPayload{value: 0}(address(triggerPlug), payload, overrides); + } + + // ============================================ + // MISSING TESTS - SetRevertingPayload Edge Cases + // ============================================ + + function test_SetRevertingPayload_NonceAlreadyUsed_Reverts() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + uint256 nonce = 1; + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(evmxSwitchboard)), + CHAIN_SLUG, + payloadId, + isReverting, + nonce + ) + ); + bytes memory signature = createSignature(digest, watcherPrivateKey); + + // First call succeeds + vm.prank(getWatcherAddress()); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + + // Second call with same nonce should revert + vm.prank(getWatcherAddress()); + vm.expectRevert(NonceAlreadyUsed.selector); + evmxSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + } + + // ============================================ + // MISSING TESTS - Attest with Multiple Watchers + // ============================================ + + function test_Attest_ReachesThreshold_SetsIsValid() public { + // Grant watcher role to 2 more watchers (total 3) + // Use addresses derived from private keys to match signatures + uint256 watcher2Key = 0x2222222222222222222222222222222222222222222222222222222222222222; + uint256 watcher3Key = 0x3333333333333333333333333333333333333333333333333333333333333333; + address watcher2 = vm.addr(watcher2Key); + address watcher3 = vm.addr(watcher3Key); + + vm.startPrank(owner); + evmxSwitchboard.grantWatcherRole(watcher2); + evmxSwitchboard.grantWatcherRole(watcher3); + vm.stopPrank(); + + assertEq(evmxSwitchboard.totalWatchers(), 3); + + bytes32 digest = keccak256(abi.encode("test payload")); + + // First attestation - should not set isValid yet + bytes memory signature1 = _createAttestSignature(digest); + vm.prank(getWatcherAddress()); + evmxSwitchboard.attest(digest, signature1); + assertFalse(evmxSwitchboard.isValid(digest)); // Not enough attestations + + // Second attestation - should not set isValid yet + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(evmxSwitchboard)), CHAIN_SLUG, digest) + ); + bytes memory signature2 = createSignature(signatureDigest, watcher2Key); + vm.prank(watcher2); + evmxSwitchboard.attest(digest, signature2); + assertFalse(evmxSwitchboard.isValid(digest)); // Still not enough + + // Third attestation - should set isValid to true + bytes memory signature3 = createSignature(signatureDigest, watcher3Key); + vm.prank(watcher3); + evmxSwitchboard.attest(digest, signature3); + assertTrue(evmxSwitchboard.isValid(digest)); // Now valid! + assertEq(evmxSwitchboard.attestations(digest), 3); + } +} diff --git a/test/protocol/switchboard/MessageSwitchboard.t.sol b/test/protocol/switchboard/MessageSwitchboard.t.sol new file mode 100644 index 00000000..1c842a2f --- /dev/null +++ b/test/protocol/switchboard/MessageSwitchboard.t.sol @@ -0,0 +1,2291 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.28; + +import "forge-std/Test.sol"; +import "../../Utils.t.sol"; +import "../../mocks/MockPlug.sol"; +import "../../../contracts/protocol/Socket.sol"; +import "../../../contracts/protocol/switchboard/MessageSwitchboard.sol"; +import "../../../contracts/protocol/switchboard/SwitchboardBase.sol"; +import "../../../contracts/utils/common/Structs.sol"; +import "../../../contracts/utils/common/Constants.sol"; +import "../../../contracts/utils/common/Converters.sol"; +import "../../../contracts/utils/common/IdUtils.sol"; +import "../../../contracts/utils/common/Errors.sol"; +import {WATCHER_ROLE, FEE_UPDATER_ROLE} from "../../../contracts/utils/common/AccessRoles.sol"; + +contract MessageSwitchboardTest is Test, Utils { + // Constants + uint32 constant SRC_CHAIN = 1; + uint32 constant DST_CHAIN = 2; + uint256 constant MIN_FEES = 0.001 ether; + + // Test addresses + address owner = address(0x1000); + address sponsor = address(0x3000); + address refundAddress = address(0x4000); + + // Private keys for signing + uint256 watcherPrivateKey = 0x1111111111111111111111111111111111111111111111111111111111111111; + uint256 feeUpdaterPrivateKey = + 0x5555555555555555555555555555555555555555555555555555555555555555; + + // Contracts + Socket socket; + MessageSwitchboard messageSwitchboard; + MockPlug srcPlug; + MockPlug dstPlug; + uint32 switchboardId; + + function setUp() public { + // Deploy actual Socket contract + socket = new Socket(SRC_CHAIN, owner, GAS_LIMIT_BUFFER, MAX_COPY_BYTES); + messageSwitchboard = new MessageSwitchboard(SRC_CHAIN, socket, owner); + + // Setup roles - grant watcher role to the address derived from watcherPrivateKey + address actualWatcherAddress = getWatcherAddress(); + address actualFeeUpdaterAddress = getFeeUpdaterAddress(); + + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + messageSwitchboard.grantRole(RESCUE_ROLE, owner); + messageSwitchboard.grantRole(FEE_UPDATER_ROLE, actualFeeUpdaterAddress); + messageSwitchboard.grantWatcherRole(SRC_CHAIN, actualWatcherAddress); + vm.stopPrank(); + + switchboardId = messageSwitchboard.switchboardId(); + // Now create plugs with the registered switchboard ID + srcPlug = new MockPlug(address(socket), switchboardId); + dstPlug = new MockPlug(address(socket), switchboardId); + } + + // Helper to get watcher address + function getWatcherAddress() public pure returns (address) { + return vm.addr(0x1111111111111111111111111111111111111111111111111111111111111111); + } + + // Helper to get fee updater address from private key + function getFeeUpdaterAddress() public view returns (address) { + return vm.addr(feeUpdaterPrivateKey); + } + + /** + * @dev Calculate digest based on MessageSwitchboard's _createDigest logic + * @param digestParams The digest parameters + * @return digest The calculated digest + */ + function calculateDigest(DigestParams memory digestParams) public pure returns (bytes32) { + bytes memory fixedPart = abi.encodePacked( + // Fixed-size fields + digestParams.socket, + digestParams.transmitter, + digestParams.payloadId, + digestParams.deadline, + digestParams.callType, + digestParams.gasLimit, + digestParams.value, + digestParams.target, + digestParams.prevBatchDigestHash + ); + + return + keccak256( + abi.encodePacked( + fixedPart, + uint32(digestParams.payload.length), + digestParams.payload, + uint32(digestParams.source.length), + digestParams.source, + uint32(digestParams.extraData.length), + digestParams.extraData + ) + ); + } + + /** + * @dev Setup sibling configuration (socket, switchboard, plug registration) + */ + function _setupSiblingConfig() internal { + _setupSiblingSocketConfig(); + _setupSiblingPlugConfig(); + } + + function _setupSiblingSocketConfig() internal { + // Setup sibling config BEFORE registering siblings + bytes32 siblingSocket = toBytes32Format(address(0x1234)); + bytes32 siblingSwitchboard = toBytes32Format(address(0x5678)); + uint32 siblingSwitchboardId = 1; // Mock switchboard ID for destination + vm.startPrank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + siblingSocket, + siblingSwitchboard, + siblingSwitchboardId + ); + // Also set config for reverse direction + messageSwitchboard.setSiblingConfig( + SRC_CHAIN, + toBytes32Format(address(socket)), + toBytes32Format(address(messageSwitchboard)), + uint32(messageSwitchboard.switchboardId()) + ); + vm.stopPrank(); + } + + function _setupSiblingPlugConfig() internal { + // Configure plugs in socket using new connect method + srcPlug.registerSibling(DST_CHAIN, address(dstPlug)); + dstPlug.registerSibling(SRC_CHAIN, address(srcPlug)); + } + + /** + * @dev Setup minimum fees for destination chain + */ + function _setupMinFees() internal { + uint32[] memory chainSlugs = new uint32[](1); + chainSlugs[0] = DST_CHAIN; + uint256[] memory minFees = new uint256[](1); + minFees[0] = MIN_FEES; + vm.prank(getFeeUpdaterAddress()); + bytes memory signature = _createSetMinMsgValueFeesBatchSignature(chainSlugs, minFees, 0); + messageSwitchboard.setMinMsgValueFeesBatch(chainSlugs, minFees, 0, signature); + } + + /** + * @dev Create a native payload via Socket's triggerAppGateway + * @param payloadData The payload data to encode + * @param msgValue The msg.value to send with the transaction + * @return payloadId The generated payload ID + */ + function _createNativePayload( + bytes memory payloadData, + uint256 msgValue + ) internal returns (bytes32 payloadId) { + bytes memory overrides = abi.encode( + uint8(1), // version + DST_CHAIN, + uint256(100000), // gasLimit + uint256(0), // value + refundAddress, // refundAddress + 86400 // deadline + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + bytes memory payload = abi.encode(payloadData); + + // Get counter before the call + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + // Use MockPlug to trigger Socket - this returns the payloadId + vm.deal(address(srcPlug), 10 ether); + payloadId = srcPlug.triggerSocket{value: msgValue}(payload); + + // Verify payloadId matches expected + bytes32 expectedPayloadId = _getLastPayloadId(payloadCounterBefore); + assertEq(payloadId, expectedPayloadId, "PayloadId mismatch"); + } + + /** + * @dev Create a sponsored payload via Socket's triggerAppGateway + * @param payloadData The payload data to encode + * @param maxFees The maximum fees for the sponsored transaction + * @return payloadId The generated payload ID + */ + function _createSponsoredPayload( + bytes memory payloadData, + uint256 maxFees + ) internal returns (bytes32 payloadId) { + bytes memory overrides = abi.encode( + uint8(2), // version + DST_CHAIN, + uint256(100000), // gasLimit + uint256(0), // value + maxFees, // maxFees + sponsor, // sponsor + 86400 // deadline + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + bytes memory payload = abi.encode(payloadData); + + // Get counter before the call + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + // Use MockPlug to trigger Socket - this returns the payloadId + payloadId = srcPlug.triggerSocket(payload); + + // Verify payloadId matches expected + bytes32 expectedPayloadId = _getLastPayloadId(payloadCounterBefore); + assertEq(payloadId, expectedPayloadId, "PayloadId mismatch"); + } + + /** + * @dev Create DigestParams for attestation with flexible parameters + * @param payloadId The payload ID + * @param payload The payload data + * @param gasLimit_ The gas limit (defaults to 100000) + * @param value_ The value (defaults to 0) + * @return digestParams The constructed DigestParams + */ + function _createDigestParams( + bytes32 payloadId, + bytes memory payload, + address, // Unused parameter, kept for compatibility + uint256 gasLimit_, + uint256 value_, + uint256 deadline + ) internal view returns (DigestParams memory) { + // Get sibling socket from switchboard (matches what contract uses) + bytes32 siblingSocket = messageSwitchboard.siblingSockets(DST_CHAIN); + bytes32 siblingPlug = messageSwitchboard.siblingPlugs(DST_CHAIN, address(srcPlug)); + + return + DigestParams({ + socket: siblingSocket, + transmitter: bytes32(0), + payloadId: payloadId, + deadline: deadline, + callType: WRITE, + gasLimit: gasLimit_, + value: value_, + target: siblingPlug, + prevBatchDigestHash: bytes32(0), // No longer using triggerId + payload: payload, + source: abi.encodePacked(SRC_CHAIN, toBytes32Format(address(srcPlug))), + extraData: bytes("") // Contract now sets extraData to empty + }); + } + + /** + * @dev Create DigestParams for attestation (simplified version with defaults) + * @param payloadId The payload ID + * @param payload The payload data + * @return digestParams The constructed DigestParams + */ + function _createDigestParams( + bytes32 payloadId, + bytes memory payload + ) internal view returns (DigestParams memory) { + return + _createDigestParams( + payloadId, + payload, + address(dstPlug), + 100000, + 0, + block.timestamp + 3600 + ); + } + + /** + * @dev Create expected DigestParams as the contract will create them in processPayload + * @param payloadId The payload ID + * @param dstChainSlug The destination chain slug + * @param plug_ The plug address + * @param gasLimit_ The gas limit + * @param value_ The value + * @param payload_ The payload data + * @param deadline_ The deadline from overrides (or 0 to use default) + * @return digestParams The expected DigestParams matching contract's _createDigestAndPayloadId + */ + function _createExpectedDigestParamsForProcessPayload( + bytes32 payloadId, + uint32 dstChainSlug, + address plug_, + uint256 gasLimit_, + uint256 value_, + bytes memory payload_, + uint256 deadline_ + ) internal view returns (DigestParams memory) { + bytes32 siblingSocket = messageSwitchboard.siblingSockets(dstChainSlug); + bytes32 siblingPlug = messageSwitchboard.siblingPlugs(dstChainSlug, plug_); + + // Contract uses overrides.deadline, or block.timestamp + defaultDeadline if deadline is 0 + uint256 deadline = block.timestamp + + (deadline_ > 0 ? deadline_ : messageSwitchboard.defaultDeadline()); + + return + DigestParams({ + socket: siblingSocket, + transmitter: bytes32(0), + payloadId: payloadId, + deadline: deadline, + callType: WRITE, + gasLimit: gasLimit_, + value: value_, + payload: payload_, + target: siblingPlug, + source: abi.encodePacked(SRC_CHAIN, toBytes32Format(plug_)), + prevBatchDigestHash: bytes32(0), + extraData: bytes("") + }); + } + + /** + * @dev Get the last created payload ID by reading payload counter before call + * @param payloadCounterBefore The payload counter before the call + * @return payloadId The calculated payload ID + */ + function _getLastPayloadId(uint64 payloadCounterBefore) internal view returns (bytes32) { + // Calculate payload ID using new structure + // Message payload: origin = (srcChainSlug, srcSwitchboardId), verification = (dstChainSlug, dstSwitchboardId) + uint32 dstSwitchboardId = messageSwitchboard.siblingSwitchboardIds(DST_CHAIN); + return + createPayloadId( + SRC_CHAIN, + uint32(messageSwitchboard.switchboardId()), + DST_CHAIN, + dstSwitchboardId, + payloadCounterBefore + ); + } + + /** + * @dev Create watcher signature for a given payload ID and nonce + * @param payloadId The payload ID to sign + * @param nonce The nonce to include in the signature + * @return signature The watcher signature + */ + function _createWatcherSignature( + bytes32 payloadId, + uint256 nonce + ) internal view returns (bytes memory) { + // markRefundEligible signs: keccak256(abi.encodePacked(switchboardAddress, chainSlug, payloadId, nonce)) + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + nonce + ) + ); + return createSignature(digest, watcherPrivateKey); + } + + /** + * @dev Create watcher signature for a given payload ID (backwards compatibility, uses nonce 0) + * @param payloadId The payload ID to sign + * @return signature The watcher signature + */ + function _createWatcherSignature(bytes32 payloadId) internal view returns (bytes memory) { + return _createWatcherSignature(payloadId, 0); + } + + function _createSetMinMsgValueFeesBatchSignature( + uint32[] memory chainSlugs, + uint256[] memory minFees, + uint256 nonce + ) internal view returns (bytes memory) { + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + uint32(chainSlugs.length), // Length prefix for array + chainSlugs, + uint32(minFees.length), // Length prefix for array + minFees, + nonce + ) + ); + return createSignature(digest, getFeeUpdaterPrivateKey()); + } + + /** + * @dev Approve plug for sponsor + */ + function _approvePlugForSponsor() internal { + vm.prank(sponsor); + address[] memory plugs = new address[](1); + plugs[0] = address(srcPlug); + messageSwitchboard.approvePlugs(plugs); + } + + /** + * @dev Complete setup for most tests (sibling config + min fees) + */ + function _setupCompleteNative() internal { + _setupSiblingConfig(); + _setupMinFees(); + } + + /** + * @dev Complete setup for sponsored tests (sibling config + sponsor approval) + */ + function _setupCompleteSponsored() internal { + _setupSiblingConfig(); + _approvePlugForSponsor(); + } + + function test_setup_Success() public view { + assertTrue(messageSwitchboard.chainSlug() == SRC_CHAIN); + assertTrue(messageSwitchboard.switchboardId() > 0); + assertTrue(messageSwitchboard.owner() == owner); + } + + // ============================================ + // CRITICAL TESTS - GROUP 1: Sibling Management + // ============================================ + + function test_setSiblingConfig_Success() public { + bytes32 siblingSocket = toBytes32Format(address(0x1234)); + bytes32 siblingSwitchboard = toBytes32Format(address(0x5678)); + + uint32 siblingSwitchboardId = 1; // Mock switchboard ID + + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.SiblingConfigSet(DST_CHAIN, siblingSocket, siblingSwitchboard); + + vm.prank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + siblingSocket, + siblingSwitchboard, + siblingSwitchboardId + ); + + assertEq(messageSwitchboard.siblingSockets(DST_CHAIN), siblingSocket); + assertEq(messageSwitchboard.siblingSwitchboards(DST_CHAIN), siblingSwitchboard); + assertEq(messageSwitchboard.siblingSwitchboardIds(DST_CHAIN), siblingSwitchboardId); + } + + function test_setSiblingConfig_NotOwner_Reverts() public { + vm.prank(address(0x9999)); + vm.expectRevert(); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + toBytes32Format(address(0x1234)), + toBytes32Format(address(0x5678)), + 1 // switchboardId + ); + } + + function test_registerSibling_Success() public { + _setupSiblingConfig(); + + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.PlugConfigUpdated( + address(srcPlug), + DST_CHAIN, + toBytes32Format(address(dstPlug)) + ); + srcPlug.registerSibling(DST_CHAIN, address(dstPlug)); + + bytes memory plugConfig = messageSwitchboard.getPlugConfig( + address(srcPlug), + abi.encode(DST_CHAIN) + ); + bytes32 siblingPlug = abi.decode(plugConfig, (bytes32)); + assertEq(siblingPlug, toBytes32Format(address(dstPlug))); + } + + function test_registerSibling_SiblingSocketNotFound_Reverts() public { + _setupSiblingConfig(); + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.registerSibling(999, address(0x9999)); + } + + // ============================================ + // CRITICAL TESTS - GROUP 2: processPayload - Native Flow + // ============================================ + + function test_processTrigger_Native_Success() public { + // Setup sibling config + _setupCompleteNative(); + + // Prepare overrides for version 1 (Native) + bytes memory overrides = abi.encode( + uint8(1), // version + DST_CHAIN, + uint256(100000), // gasLimit + uint256(0), // value + refundAddress, // refundAddress + 86400 // deadline + ); + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + bytes memory payload = abi.encode("test data"); + uint256 msgValue = MIN_FEES + 0.001 ether; + + // Get counter before the call + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + // Calculate expected payload ID using new structure + uint32 dstSwitchboardId = messageSwitchboard.siblingSwitchboardIds(DST_CHAIN); + bytes32 expectedPayloadId = createPayloadId( + SRC_CHAIN, + uint32(messageSwitchboard.switchboardId()), + DST_CHAIN, + dstSwitchboardId, + payloadCounterBefore + ); + + // Expect MessageOutbound event first (contract emits this before PayloadRequested) + // Calculate expected digestParams and digest + // Decode deadline from overrides (86400 in this test) + (, , , , , uint256 deadline) = abi.decode( + overrides, + (uint8, uint32, uint256, uint256, address, uint256) + ); + DigestParams memory expectedDigestParams = _createExpectedDigestParamsForProcessPayload( + expectedPayloadId, + DST_CHAIN, + address(srcPlug), + 100000, // gasLimit from overrides + 0, // value from overrides + payload, + deadline // deadline from overrides + ); + bytes32 expectedDigest = calculateDigest(expectedDigestParams); + + vm.expectEmit(true, true, false, true); + emit MessageSwitchboard.MessageOutbound( + expectedPayloadId, + DST_CHAIN, + expectedDigest, + expectedDigestParams, + false, // isSponsored + msgValue, + 0, + address(0) + ); + + // Expect PayloadRequested event second + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.PayloadRequested( + expectedPayloadId, + address(srcPlug), + switchboardId, + overrides, + payload + ); + + vm.deal(address(srcPlug), 10 ether); + bytes32 actualPayloadId = srcPlug.triggerSocket{value: msgValue}(payload); + + // Verify payload ID matches + assertEq(actualPayloadId, expectedPayloadId); + + // Verify payload counter increased + assertEq(messageSwitchboard.payloadCounter(), payloadCounterBefore + 1); + + // Verify fees stored + (, address storedRefundAddr, , , ) = messageSwitchboard.payloadFees(expectedPayloadId); + assertEq(storedRefundAddr, refundAddress); + } + + function test_processTrigger_Native_InsufficientValue_Reverts() public { + // Setup sibling config + _setupSiblingConfig(); + + // Set minimum fees + _setupMinFees(); + + // Try with insufficient value + bytes memory overrides = abi.encode( + uint8(1), // version + DST_CHAIN, + 100000, + 0, + refundAddress, + 86400 // deadline + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + vm.deal(address(srcPlug), 10 ether); + vm.prank(address(srcPlug)); + vm.expectRevert(InsufficientMsgValue.selector); + srcPlug.triggerSocket{value: MIN_FEES - 1}(abi.encode("test")); + } + + function test_processTrigger_Native_SiblingSocketNotFound_Reverts() public { + bytes memory overrides = abi.encode(uint8(1), DST_CHAIN, 100000, 0, refundAddress, 86400); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + vm.prank(address(srcPlug)); + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.triggerSocket(abi.encode("test")); + } + + // ============================================ + // CRITICAL TESTS - GROUP 3: processPayload - Sponsored Flow + // ============================================ + + function test_processTrigger_Sponsored_Success() public { + // Setup sibling config + _setupSiblingConfig(); + + // Sponsor approves plug + _approvePlugForSponsor(); + + // Prepare overrides for version 2 (Sponsored) + bytes memory overrides = abi.encode( + uint8(2), // version + DST_CHAIN, + uint256(100000), // gasLimit + uint256(0), // value + uint256(10 ether), // maxFees + sponsor, // sponsor + 86400 // deadline + ); + + bytes memory payload = abi.encode("sponsored test"); + + // Get counter before the call + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + // Calculate expected payload ID using new structure + uint32 dstSwitchboardId = messageSwitchboard.siblingSwitchboardIds(DST_CHAIN); + bytes32 expectedPayloadId = createPayloadId( + SRC_CHAIN, + uint32(messageSwitchboard.switchboardId()), + DST_CHAIN, + dstSwitchboardId, + payloadCounterBefore + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + // Expect MessageOutbound event first (contract emits this before PayloadRequested) + // Calculate expected digestParams and digest + // Decode deadline from overrides (version 2: uint8, uint32, uint256, uint256, uint256, address, uint256) + (, , , , , , uint256 deadline) = abi.decode( + overrides, + (uint8, uint32, uint256, uint256, uint256, address, uint256) + ); + DigestParams memory expectedDigestParams = _createExpectedDigestParamsForProcessPayload( + expectedPayloadId, + DST_CHAIN, + address(srcPlug), + 100000, // gasLimit from overrides + 0, // value from overrides + payload, + deadline // deadline from overrides + ); + bytes32 expectedDigest = calculateDigest(expectedDigestParams); + + vm.expectEmit(true, true, false, true); + emit MessageSwitchboard.MessageOutbound( + expectedPayloadId, + DST_CHAIN, + expectedDigest, + expectedDigestParams, + true, // isSponsored + 0, + 10 ether, + sponsor + ); + + // Expect PayloadRequested event second + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.PayloadRequested( + expectedPayloadId, + address(srcPlug), + switchboardId, + overrides, + payload + ); + + vm.prank(address(srcPlug)); + bytes32 actualPayloadId = srcPlug.triggerSocket(payload); + + // Verify payload ID matches + assertEq(actualPayloadId, expectedPayloadId); + + // Verify sponsored fees were stored + (uint256 maxFees, ) = messageSwitchboard.sponsoredPayloadFees(expectedPayloadId); + assertEq(maxFees, 10 ether); + } + + function test_processTrigger_Sponsored_NotApproved_Reverts() public { + // Setup sibling config + _setupSiblingConfig(); + + // Don't approve - try without approval + bytes memory overrides = abi.encode( + uint8(2), + DST_CHAIN, + 100000, + 0, + 10 ether, + sponsor, + 86400 + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + vm.prank(address(srcPlug)); + vm.expectRevert(PlugNotApprovedBySponsor.selector); + srcPlug.triggerSocket(abi.encode("test")); + } + + function test_processTrigger_UnsupportedVersion_Reverts() public { + bytes memory overrides = abi.encode(uint8(99), DST_CHAIN, 100000, 0, refundAddress, 86400); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + vm.prank(address(srcPlug)); + vm.expectRevert(UnsupportedOverrideVersion.selector); + srcPlug.triggerSocket(abi.encode("test")); + } + + // ============================================ + // CRITICAL TESTS - GROUP 4: Enhanced Attest + // ============================================ + + function test_attest_SuccessWithTargetVerification() public { + // Setup sibling config + _setupSiblingConfig(); + + // Create digest params (using any valid values since we're just testing attestation) + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x5678)); + + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + + // Calculate the actual digest from digestParams (as done in MessageSwitchboard._createDigest) + bytes32 digest = calculateDigest(digestParams); + + // Create watcher signature - attest signs: keccak256(abi.encodePacked(switchboardAddress, chainSlug, digest)) + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature = createSignature(signatureDigest, watcherPrivateKey); + + // Register this digest as attested (simulating the flow) + vm.prank(getWatcherAddress()); + vm.expectEmit(true, false, true, true); + emit SwitchboardBase.Attested(digest, getWatcherAddress()); + messageSwitchboard.attest(digest, signature); + + // Verify it's attested + assertTrue(messageSwitchboard.isValid(digest)); + } + + // NOTE: test_attest_InvalidTarget_Reverts() was removed because the attest() function + // no longer validates the target - target validation is now done during execution + + function test_attest_InvalidWatcher_Reverts() public { + // Setup sibling config + _setupSiblingConfig(); + + bytes32 payloadId = bytes32(uint256(0x5678)); + DigestParams memory digestParams = _createDigestParams(payloadId, abi.encode("test")); + + // Calculate the actual digest from digestParams + bytes32 digest = calculateDigest(digestParams); + + // Invalid signature from non-watcher (random private key) + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature = createSignature( + signatureDigest, + 0x2222222222222222222222222222222222222222222222222222222222222222 + ); // Random key + + vm.prank(address(0x9999)); + vm.expectRevert(WatcherNotFound.selector); + messageSwitchboard.attest(digest, signature); + } + + function test_attest_AlreadyAttested_Reverts() public { + // Setup sibling config + _setupSiblingConfig(); + + bytes32 payloadId = bytes32(uint256(0x5678)); + DigestParams memory digestParams = _createDigestParams(payloadId, abi.encode("test")); + + // Calculate the actual digest from digestParams + bytes32 digest = calculateDigest(digestParams); + + // Create watcher signature + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature = createSignature(signatureDigest, watcherPrivateKey); + + // First attest - should succeed + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(digest, signature); + + // Second attest - should revert + vm.prank(getWatcherAddress()); + vm.expectRevert(AlreadyAttested.selector); + messageSwitchboard.attest(digest, signature); + } + + // ============================================ + // IMPORTANT TESTS - GROUP 5: Sponsor Approvals + // ============================================ + + function test_approvePlugs_Batch_Success() public { + address[] memory plugs = new address[](2); + plugs[0] = address(srcPlug); + plugs[1] = address(dstPlug); + + vm.startPrank(sponsor); + vm.expectEmit(true, true, false, false); + emit MessageSwitchboard.PlugApproved(sponsor, address(srcPlug)); + + vm.expectEmit(true, true, false, false); + emit MessageSwitchboard.PlugApproved(sponsor, address(dstPlug)); + + messageSwitchboard.approvePlugs(plugs); + + assertTrue(messageSwitchboard.sponsorApprovals(sponsor, address(srcPlug))); + assertTrue(messageSwitchboard.sponsorApprovals(sponsor, address(dstPlug))); + + vm.stopPrank(); + } + + function test_revokePlugs_Batch_Success() public { + address[] memory plugs = new address[](2); + plugs[0] = address(srcPlug); + plugs[1] = address(dstPlug); + + vm.startPrank(sponsor); + messageSwitchboard.approvePlugs(plugs); + vm.stopPrank(); + + // Now revoke batch + vm.startPrank(sponsor); + vm.expectEmit(true, true, false, false); + emit MessageSwitchboard.PlugRevoked(sponsor, address(srcPlug)); + + vm.expectEmit(true, true, false, false); + emit MessageSwitchboard.PlugRevoked(sponsor, address(dstPlug)); + + messageSwitchboard.revokePlugs(plugs); + + assertFalse(messageSwitchboard.sponsorApprovals(sponsor, address(srcPlug))); + assertFalse(messageSwitchboard.sponsorApprovals(sponsor, address(dstPlug))); + + vm.stopPrank(); + } + + // ============================================ + // CRITICAL TESTS - GROUP 6: Refund Flow + // ============================================ + + function test_markRefundEligible_Success() public { + // Setup and create a payload + _setupCompleteNative(); + + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + + // Verify fees exist + (uint256 nativeFees, , , , ) = messageSwitchboard.payloadFees(payloadId); + assertEq(nativeFees, MIN_FEES); + + // Mark eligible with nonce + uint256 nonce = 1; + bytes memory signature = _createWatcherSignature(payloadId, nonce); + + vm.expectEmit(true, true, false, false); + emit MessageSwitchboard.RefundEligibilityMarked(payloadId, getWatcherAddress()); + + vm.prank(getWatcherAddress()); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + + // Verify marked eligible + (, , bool isEligible, , ) = messageSwitchboard.payloadFees(payloadId); + assertTrue(isEligible); + + // Verify nonce was used + uint256 namespacedNonce = uint256( + keccak256(abi.encodePacked(messageSwitchboard.markRefundEligible.selector, nonce)) + ); + assertTrue(messageSwitchboard.usedNonces(getWatcherAddress(), namespacedNonce)); + } + + function test_markRefundEligible_NoFeesToRefund_Reverts() public { + // Create a non-existent payloadId (one that was never created) + bytes32 payloadId = bytes32(uint256(0x9999)); + + // Create valid watcher signature (this will pass watcher check) + uint256 nonce = 1; + bytes memory signature = _createWatcherSignature(payloadId, nonce); + + // Should revert with NoFeesToRefund because payload doesn't exist + vm.prank(getWatcherAddress()); + vm.expectRevert(NoFeesToRefund.selector); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + } + + function test_markRefundEligible_AlreadyMarkedRefundEligible_Reverts() public { + // Setup and create a payload + _setupCompleteNative(); + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + + // Mark eligible first time + uint256 nonce = 1; + bytes memory signature = _createWatcherSignature(payloadId, nonce); + vm.prank(getWatcherAddress()); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + + // Try to mark eligible again with different nonce - should revert (already marked) + uint256 nonce2 = 2; + bytes memory signature2 = _createWatcherSignature(payloadId, nonce2); + vm.prank(getWatcherAddress()); + vm.expectRevert(AlreadyMarkedRefundEligible.selector); + messageSwitchboard.markRefundEligible(payloadId, nonce2, signature2); + } + + function test_markRefundEligible_NonceAlreadyUsed_Reverts() public { + // Setup and create two payloads + _setupCompleteNative(); + bytes32 payloadId1 = _createNativePayload("test1", MIN_FEES); + bytes32 payloadId2 = _createNativePayload("test2", MIN_FEES); + + // Mark first payload eligible with nonce + uint256 nonce = 1; + bytes memory signature1 = _createWatcherSignature(payloadId1, nonce); + vm.prank(getWatcherAddress()); + messageSwitchboard.markRefundEligible(payloadId1, nonce, signature1); + + // Try to use the same nonce again on a different payload - should revert with NonceAlreadyUsed + bytes memory signature2 = _createWatcherSignature(payloadId2, nonce); + vm.prank(getWatcherAddress()); + vm.expectRevert(NonceAlreadyUsed.selector); + messageSwitchboard.markRefundEligible(payloadId2, nonce, signature2); + } + + function test_markRefundEligible_AfterRefund_Reverts() public { + // Setup and create a payload + _setupCompleteNative(); + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + + // Mark eligible and refund + uint256 nonce = 1; + bytes memory signature = _createWatcherSignature(payloadId, nonce); + vm.prank(getWatcherAddress()); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + + vm.deal(address(messageSwitchboard), MIN_FEES); + vm.prank(refundAddress); + messageSwitchboard.refund(payloadId); + + // After refund, isRefundEligible is still true, so trying to mark eligible again + // will revert with AlreadyMarkedRefundEligible (line 429), not AlreadyRefunded (line 430) + uint256 nonce2 = 2; + bytes memory signature2 = _createWatcherSignature(payloadId, nonce2); + vm.prank(getWatcherAddress()); + vm.expectRevert(AlreadyMarkedRefundEligible.selector); + messageSwitchboard.markRefundEligible(payloadId, nonce2, signature2); + } + + function test_markRefundEligible_InvalidWatcher_Reverts() public { + // Setup and create a payload + _setupCompleteNative(); + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + + // Create signature from non-watcher (random private key) + uint256 nonWatcherPrivateKey = 0x9999999999999999999999999999999999999999999999999999999999999999; + address nonWatcher = vm.addr(nonWatcherPrivateKey); + + uint256 nonce = 1; + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + nonce + ) + ); + bytes memory signature = createSignature(digest, nonWatcherPrivateKey); + + // Should revert with WatcherNotFound at line 460 + vm.prank(nonWatcher); + vm.expectRevert(WatcherNotFound.selector); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + } + + function test_refund_Success() public { + // Setup and create payload + _setupCompleteNative(); + + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + + // Mark eligible + uint256 nonce = 1; + bytes memory signature = _createWatcherSignature(payloadId, nonce); + vm.prank(getWatcherAddress()); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + + // Refund + uint256 balanceBefore = refundAddress.balance; + vm.deal(address(messageSwitchboard), MIN_FEES); + + vm.expectEmit(true, true, false, true); + emit MessageSwitchboard.Refunded(payloadId, refundAddress, MIN_FEES); + + vm.prank(refundAddress); + messageSwitchboard.refund(payloadId); + + assertEq(refundAddress.balance, balanceBefore + MIN_FEES); + + // Verify marked as refunded + (, , , bool isRefunded, ) = messageSwitchboard.payloadFees(payloadId); + assertTrue(isRefunded); + } + + function test_refund_NotEligible_Reverts() public { + bytes32 payloadId = keccak256("test"); + + vm.prank(refundAddress); + vm.expectRevert(RefundNotEligible.selector); + messageSwitchboard.refund(payloadId); + } + + function test_refund_AlreadyRefunded_Reverts() public { + // Setup and create payload + _setupCompleteNative(); + bytes32 payloadId = _createNativePayload("test", MIN_FEES); + + // Mark eligible and refund once + uint256 nonce = 1; + bytes memory signature = _createWatcherSignature(payloadId, nonce); + vm.prank(getWatcherAddress()); + messageSwitchboard.markRefundEligible(payloadId, nonce, signature); + + vm.deal(address(messageSwitchboard), MIN_FEES); + vm.prank(refundAddress); + messageSwitchboard.refund(payloadId); + + // Try to refund again - should revert + vm.deal(address(messageSwitchboard), MIN_FEES); + vm.prank(refundAddress); + vm.expectRevert(AlreadyRefunded.selector); + messageSwitchboard.refund(payloadId); + } + + // ============================================ + // IMPORTANT TESTS - GROUP 8: increaseFeesForPayload + // ============================================ + + function test_increaseFeesForPayload_Native_Success() public { + // Setup sibling config and min fees + _setupCompleteNative(); + + bytes memory feesData = abi.encode(uint8(1)); // Native fees type + uint256 additionalFees = 0.01 ether; + uint256 initialFees = MIN_FEES + 0.001 ether; + + // First create a payload via processPayload + bytes memory overrides = abi.encode( + uint8(1), // version + DST_CHAIN, // dstChainSlug + uint256(100000), // gasLimit + uint256(0), // value + refundAddress, // refundAddress + uint256(0), // maxFees + address(0), // sponsor + false, // isSponsored + 86400 // deadline + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + // Get counter before creating payload + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + vm.deal(address(srcPlug), 1 ether); + vm.prank(address(srcPlug)); + bytes32 payloadId = srcPlug.triggerSocket{value: initialFees}(abi.encode("payload")); + + // Verify payloadId matches expected structure + uint32 dstSwitchboardId = messageSwitchboard.siblingSwitchboardIds(DST_CHAIN); + bytes32 expectedPayloadId = createPayloadId( + SRC_CHAIN, + uint32(messageSwitchboard.switchboardId()), + DST_CHAIN, + dstSwitchboardId, + payloadCounterBefore + ); + assertEq(payloadId, expectedPayloadId, "PayloadId mismatch"); + + // Verify initial fees were stored + (uint256 nativeFeesBefore, , , , ) = messageSwitchboard.payloadFees(payloadId); + assertEq(nativeFeesBefore, initialFees); + + // Now test fee increase + vm.expectEmit(true, true, false, true); + emit MessageSwitchboard.NativeFeesIncreased(payloadId, additionalFees, feesData); + + vm.prank(address(srcPlug)); + srcPlug.increaseFeesForPayload{value: additionalFees}(payloadId, feesData); + + // Verify fees increased + (uint256 nativeFeesAfter, , , , ) = messageSwitchboard.payloadFees(payloadId); + assertEq(nativeFeesAfter, initialFees + additionalFees); + } + + function test_increaseFeesForPayload_Sponsored_Success() public { + // Setup sibling config and sponsor approval + _setupCompleteSponsored(); + + uint256 newMaxFees = 0.05 ether; + bytes memory feesData = abi.encode(uint8(2), newMaxFees); // Sponsored fees type + new maxFees + + // First create a sponsored payload via processPayload + bytes memory overrides = abi.encode( + uint8(2), // version + DST_CHAIN, // dstChainSlug + uint256(100000), // gasLimit + uint256(0), // value + uint256(0.02 ether), // maxFees + sponsor, // sponsor + 86400 // deadline + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + // Get counter before creating payload + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + vm.prank(address(srcPlug)); + bytes32 payloadId = srcPlug.triggerSocket(abi.encode("payload")); + + // Verify payloadId matches expected structure + uint32 dstSwitchboardId = messageSwitchboard.siblingSwitchboardIds(DST_CHAIN); + bytes32 expectedPayloadId = createPayloadId( + SRC_CHAIN, + uint32(messageSwitchboard.switchboardId()), + DST_CHAIN, + dstSwitchboardId, + payloadCounterBefore + ); + assertEq(payloadId, expectedPayloadId, "PayloadId mismatch"); + + // Verify initial maxFees were stored + (uint256 maxFeesBefore, ) = messageSwitchboard.sponsoredPayloadFees(payloadId); + assertEq(maxFeesBefore, 0.02 ether); + + // Now test sponsored fee increase + vm.expectEmit(true, true, false, true); + emit MessageSwitchboard.SponsoredFeesIncreased(payloadId, newMaxFees, address(srcPlug)); + + vm.prank(address(srcPlug)); + srcPlug.increaseFeesForPayload(payloadId, feesData); + + // Verify maxFees updated + (uint256 maxFeesAfter, ) = messageSwitchboard.sponsoredPayloadFees(payloadId); + assertEq(maxFeesAfter, newMaxFees); + } + + function test_increaseFeesForPayload_Native_WithZeroValue() public { + // Setup sibling config and min fees + _setupCompleteNative(); + + bytes memory feesData = abi.encode(uint8(1)); // Native fees type + uint256 initialFees = MIN_FEES + 0.001 ether; + + // Create a payload + bytes memory overrides = abi.encode( + uint8(1), + DST_CHAIN, + uint256(100000), + uint256(0), + refundAddress, + uint256(0), + address(0), + false, + 86400 + ); + + srcPlug.setOverrides(overrides); + + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + vm.deal(address(srcPlug), 1 ether); + vm.prank(address(srcPlug)); + bytes32 payloadId = srcPlug.triggerSocket{value: initialFees}(abi.encode("payload")); + + // Increase fees with zero value - should still emit event but not add to fees + (uint256 nativeFeesBefore, , , , ) = messageSwitchboard.payloadFees(payloadId); + + vm.expectEmit(true, true, false, true); + emit MessageSwitchboard.NativeFeesIncreased(payloadId, 0, feesData); + + vm.prank(address(srcPlug)); + srcPlug.increaseFeesForPayload(payloadId, feesData); // No value sent + + // Verify fees didn't change + (uint256 nativeFeesAfter, , , , ) = messageSwitchboard.payloadFees(payloadId); + assertEq(nativeFeesAfter, nativeFeesBefore); + } + + function test_increaseFeesForPayload_UnauthorizedPlug_Reverts() public { + // Setup sibling config and min fees + _setupCompleteNative(); + + bytes memory feesData = abi.encode(uint8(1)); // Native fees type + uint256 additionalFees = 0.01 ether; + uint256 initialFees = MIN_FEES + 0.001 ether; + + // Create payload with srcPlug + bytes memory overrides = abi.encode( + uint8(1), // version + DST_CHAIN, // dstChainSlug + uint256(100000), // gasLimit + uint256(0), // value + refundAddress, // refundAddress + uint256(0), // maxFees + address(0), // sponsor + false, // isSponsored + 86400 // deadline + ); + + // Set overrides on the plug + srcPlug.setOverrides(overrides); + + // Get counter before creating payload + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + vm.deal(address(srcPlug), 1 ether); + vm.prank(address(srcPlug)); + bytes32 payloadId = srcPlug.triggerSocket{value: initialFees}(abi.encode("payload")); + + // Verify payloadId matches expected structure + uint32 dstSwitchboardId = messageSwitchboard.siblingSwitchboardIds(DST_CHAIN); + bytes32 expectedPayloadId = createPayloadId( + SRC_CHAIN, + uint32(messageSwitchboard.switchboardId()), + DST_CHAIN, + dstSwitchboardId, + payloadCounterBefore + ); + assertEq(payloadId, expectedPayloadId, "PayloadId mismatch"); + + // Try to increase fees with different plug - should revert because plug doesn't match + vm.deal(address(dstPlug), 1 ether); + vm.expectRevert(UnauthorizedFeeIncrease.selector); + vm.prank(address(dstPlug)); // Different plug (not the one that created the payload) + dstPlug.increaseFeesForPayload{value: additionalFees}(payloadId, feesData); + } + + function test_increaseFeesForPayload_Sponsored_UnauthorizedPlug_Reverts() public { + // Setup sibling config and sponsor approval + _setupCompleteSponsored(); + + uint256 newMaxFees = 0.05 ether; + bytes memory feesData = abi.encode(uint8(2), newMaxFees); + + // Create a sponsored payload with srcPlug + bytes memory overrides = abi.encode( + uint8(2), + DST_CHAIN, + uint256(100000), + uint256(0), + uint256(0.02 ether), + sponsor, + 86400 + ); + + srcPlug.setOverrides(overrides); + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + + vm.prank(address(srcPlug)); + bytes32 payloadId = srcPlug.triggerSocket(abi.encode("payload")); + + // Try to increase fees with different plug - should revert + vm.expectRevert(UnauthorizedFeeIncrease.selector); + vm.prank(address(dstPlug)); + dstPlug.increaseFeesForPayload(payloadId, feesData); + } + + function test_increaseFeesForPayload_InvalidFeesType_Reverts() public { + bytes memory feesData = abi.encode(uint8(3)); // Invalid fees type + uint256 additionalFees = 0.01 ether; + bytes32 payloadId = bytes32(uint256(0x9999)); // Non-existent payloadId + + // Socket's increaseFeesForPayload calls switchboard's increaseFeesForPayload with plug as msg.sender + // Switchboard will decode feesType and revert with InvalidFeesType before checking authorization + vm.deal(address(srcPlug), 1 ether); + vm.prank(address(srcPlug)); + vm.expectRevert(InvalidFeesType.selector); + srcPlug.increaseFeesForPayload{value: additionalFees}(payloadId, feesData); + } + + function test_increaseFeesForPayload_NotSocket_Reverts() public { + bytes32 payloadId = keccak256("payload"); + bytes memory feesData = abi.encode(uint8(1)); // Native fees type + uint256 additionalFees = 0.01 ether; + + vm.expectRevert(NotSocket.selector); + messageSwitchboard.increaseFeesForPayload{value: additionalFees}( + payloadId, + address(srcPlug), + feesData + ); + } + + // ============================================ + // MISSING TESTS - GROUP 9: setRevertingPayload + // ============================================ + + function test_setRevertingPayload_Success() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + uint256 nonce = 1; + + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + isReverting, + nonce + ) + ); + bytes memory signature = createSignature(digest, watcherPrivateKey); + + vm.expectEmit(true, true, false, true); + emit SwitchboardBase.RevertingPayloadIdset(payloadId, isReverting); + + vm.prank(getWatcherAddress()); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + + assertTrue(messageSwitchboard.revertingPayloadIds(payloadId)); + } + + function test_setRevertingPayload_NotOwner_Reverts() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bool isReverting = true; + uint256 nonce = 1; + + bytes32 digest = keccak256( + abi.encodePacked(address(messageSwitchboard), SRC_CHAIN, payloadId, isReverting, nonce) + ); + bytes memory signature = createSignature(digest, feeUpdaterPrivateKey); + + vm.expectRevert(); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + } + + function test_setRevertingPayload_SetToFalse() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + uint256 nonce = 1; + bool isReverting = true; + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + isReverting, + nonce + ) + ); + + bytes memory signature = createSignature(digest, watcherPrivateKey); + + // First set to true + vm.prank(getWatcherAddress()); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + assertTrue(messageSwitchboard.revertingPayloadIds(payloadId)); + + nonce++; + isReverting = false; + digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + payloadId, + isReverting, + nonce + ) + ); + + signature = createSignature(digest, watcherPrivateKey); + + // Then set to false + vm.expectEmit(true, false, false, true); + emit SwitchboardBase.RevertingPayloadIdset(payloadId, isReverting); + + vm.prank(getWatcherAddress()); + messageSwitchboard.setRevertingPayload(payloadId, isReverting, nonce, signature); + + assertFalse(messageSwitchboard.revertingPayloadIds(payloadId)); + } + + // ============================================ + // MISSING TESTS - GROUP 11: setMinMsgValueFeesBatch (with signature) + // ============================================ + + function test_setMinMsgValueFeesBatch_Success() public { + uint32[] memory chainSlugs_ = new uint32[](2); + chainSlugs_[0] = DST_CHAIN; + chainSlugs_[1] = 3; + + uint256[] memory minFees_ = new uint256[](2); + minFees_[0] = 0.001 ether; + minFees_[1] = 0.002 ether; + + uint256 nonce_ = 2; + + // Create signature from fee updater + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + uint32(chainSlugs_.length), + chainSlugs_, + uint32(minFees_.length), + minFees_, + nonce_ + ) + ); + bytes memory signature = createSignature(digest, getFeeUpdaterPrivateKey()); + + address actualFeeUpdater = getFeeUpdaterAddress(); + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.MinMsgValueFeesSet(chainSlugs_[0], minFees_[0], actualFeeUpdater); + + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.MinMsgValueFeesSet(chainSlugs_[1], minFees_[1], actualFeeUpdater); + + messageSwitchboard.setMinMsgValueFeesBatch(chainSlugs_, minFees_, nonce_, signature); + + assertEq(messageSwitchboard.minMsgValueFees(chainSlugs_[0]), minFees_[0]); + assertEq(messageSwitchboard.minMsgValueFees(chainSlugs_[1]), minFees_[1]); + } + + function test_setMinMsgValueFeesBatch_ArrayLengthMismatch_Reverts() public { + uint32[] memory chainSlugs_ = new uint32[](2); + chainSlugs_[0] = DST_CHAIN; + chainSlugs_[1] = 3; + + uint256[] memory minFees_ = new uint256[](1); // Length mismatch + minFees_[0] = 0.001 ether; + + uint256 nonce_ = 3; + + // Create signature from fee updater + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + uint32(chainSlugs_.length), + chainSlugs_, + uint32(minFees_.length), + minFees_, + nonce_ + ) + ); + bytes memory signature = createSignature(digest, getFeeUpdaterPrivateKey()); + + vm.expectRevert(ArrayLengthMismatch.selector); + messageSwitchboard.setMinMsgValueFeesBatch(chainSlugs_, minFees_, nonce_, signature); + } + + function test_setMinMsgValueFeesBatch_UnauthorizedFeeUpdater_Reverts() public { + uint32[] memory chainSlugs_ = new uint32[](1); + chainSlugs_[0] = DST_CHAIN; + + uint256[] memory minFees_ = new uint256[](1); + minFees_[0] = 0.001 ether; + + uint256 nonce_ = 4; + + // Create signature from non-fee-updater + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + uint32(chainSlugs_.length), + chainSlugs_, + uint32(minFees_.length), + minFees_, + nonce_ + ) + ); + bytes memory signature = createSignature(digest, watcherPrivateKey); + + vm.expectRevert(UnauthorizedFeeUpdater.selector); + messageSwitchboard.setMinMsgValueFeesBatch(chainSlugs_, minFees_, nonce_, signature); + } + + function test_setMinMsgValueFeesBatch_NonceAlreadyUsed_Reverts() public { + uint32[] memory chainSlugs_ = new uint32[](1); + chainSlugs_[0] = DST_CHAIN; + + uint256[] memory minFees_ = new uint256[](1); + minFees_[0] = 0.001 ether; + + uint256 nonce_ = 5; + + // Create signature from fee updater + bytes32 digest = keccak256( + abi.encodePacked( + toBytes32Format(address(messageSwitchboard)), + SRC_CHAIN, + uint32(chainSlugs_.length), + chainSlugs_, + uint32(minFees_.length), + minFees_, + nonce_ + ) + ); + bytes memory signature = createSignature(digest, getFeeUpdaterPrivateKey()); + + // First call succeeds + messageSwitchboard.setMinMsgValueFeesBatch(chainSlugs_, minFees_, nonce_, signature); + + // Second call with same nonce should revert + vm.expectRevert(NonceAlreadyUsed.selector); + messageSwitchboard.setMinMsgValueFeesBatch(chainSlugs_, minFees_, nonce_, signature); + } + + // ============================================ + // MISSING TESTS - GROUP 12: getPlugConfig and updatePlugConfig (direct tests) + // ============================================ + + function test_getPlugConfig_Success() public { + _setupSiblingConfig(); + + // Register sibling first + srcPlug.registerSibling(DST_CHAIN, address(dstPlug)); + + // Get plug config + bytes memory plugConfig = messageSwitchboard.getPlugConfig( + address(srcPlug), + abi.encode(DST_CHAIN) + ); + + bytes32 siblingPlug = abi.decode(plugConfig, (bytes32)); + assertEq(siblingPlug, toBytes32Format(address(dstPlug))); + } + + function test_getPlugConfig_NonExistentChain() public { + _setupSiblingConfig(); + + // Get plug config for non-existent chain + bytes memory plugConfig = messageSwitchboard.getPlugConfig( + address(srcPlug), + abi.encode(999) // Non-existent chain + ); + + bytes32 siblingPlug = abi.decode(plugConfig, (bytes32)); + assertEq(siblingPlug, bytes32(0)); + } + + function test_updatePlugConfig_Success() public { + _setupSiblingConfig(); + + // Update plug config directly via Socket + bytes memory plugConfig = abi.encode(SRC_CHAIN, toBytes32Format(address(dstPlug))); + + vm.expectEmit(true, true, true, true); + emit MessageSwitchboard.PlugConfigUpdated( + address(srcPlug), + SRC_CHAIN, + toBytes32Format(address(dstPlug)) + ); + + vm.prank(address(socket)); + messageSwitchboard.updatePlugConfig(address(srcPlug), plugConfig); + + bytes32 storedPlug = messageSwitchboard.siblingPlugs(SRC_CHAIN, address(srcPlug)); + assertEq(storedPlug, toBytes32Format(address(dstPlug))); + } + + function test_updatePlugConfig_NotSocket_Reverts() public { + _setupSiblingConfig(); + + bytes memory plugConfig = abi.encode(SRC_CHAIN, toBytes32Format(address(dstPlug))); + + vm.expectRevert(NotSocket.selector); + messageSwitchboard.updatePlugConfig(address(srcPlug), plugConfig); + } + + function test_updatePlugConfig_SiblingSocketNotFound_Reverts() public { + // Don't setup sibling config + bytes memory plugConfig = abi.encode(999, toBytes32Format(address(dstPlug))); + + vm.prank(address(socket)); + vm.expectRevert(SiblingSocketNotFound.selector); + messageSwitchboard.updatePlugConfig(address(srcPlug), plugConfig); + } + + // ============================================ + // Helper functions for signature-based tests + // ============================================ + + /** + * @dev Get private key for fee updater + * @return privateKey The private key for fee updater + */ + function getFeeUpdaterPrivateKey() public view returns (uint256) { + return feeUpdaterPrivateKey; + } + + // ============================================ + // MISSING TESTS - GROUP 13: _decodePackedSource and allowPayload + // ============================================ + + function test_allowPayload_Success() public { + _setupSiblingConfig(); + + // Create a digest and attest it + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x1234)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + bytes32 digest = calculateDigest(digestParams); + + // Attest the digest + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(digest, signature); + + // Create source bytes (packed format) + bytes memory source = abi.encodePacked(SRC_CHAIN, toBytes32Format(address(srcPlug))); + + // allowPayload should return true for attested digest with valid source + bool result = messageSwitchboard.allowPayload( + digest, + payloadId, + address(dstPlug), // target + source + ); + + assertTrue(result); + } + + function test_allowPayload_InvalidSource_Reverts() public { + _setupSiblingConfig(); + + // Create a digest and attest it + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x1234)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + bytes32 digest = calculateDigest(digestParams); + + // Attest the digest + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature = createSignature(signatureDigest, watcherPrivateKey); + + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(digest, signature); + + // Create source bytes with wrong plug address + bytes memory source = abi.encodePacked(SRC_CHAIN, toBytes32Format(address(0x9999))); + + // allowPayload should revert with InvalidSource + vm.expectRevert(InvalidSource.selector); + messageSwitchboard.allowPayload( + digest, + payloadId, + address(dstPlug), // target + source + ); + } + + function test_allowPayload_NotAttested_ReturnsFalse() public { + _setupSiblingConfig(); + + // Create a digest but don't attest it + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x1234)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + bytes32 digest = calculateDigest(digestParams); + + // Create source bytes + bytes memory source = abi.encodePacked(SRC_CHAIN, toBytes32Format(address(srcPlug))); + + // allowPayload should return false for non-attested digest + bool result = messageSwitchboard.allowPayload(digest, payloadId, address(dstPlug), source); + + assertFalse(result); + } + + function test_decodePackedSource_Success() public { + // Test _decodePackedSource indirectly through allowPayload + _setupSiblingConfig(); + + uint32 testChainSlug = SRC_CHAIN; + bytes32 testPlug = toBytes32Format(address(srcPlug)); + bytes memory packed = abi.encodePacked(testChainSlug, testPlug); + + // Create digest and attest + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x1234)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + // Override source to use packed format + digestParams.source = packed; + bytes32 digest = calculateDigest(digestParams); + + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(digest, signature); + + // allowPayload uses _decodePackedSource internally + bool result = messageSwitchboard.allowPayload(digest, payloadId, address(dstPlug), packed); + + assertTrue(result); + } + + function test_decodePackedSource_InvalidLength_Reverts() public { + _setupSiblingConfig(); + + // Create source with invalid length (less than 36 bytes) + bytes memory invalidSource = abi.encodePacked(uint32(1)); // Only 4 bytes + + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x1234)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + bytes32 digest = calculateDigest(digestParams); + + // allowPayload will call _decodePackedSource which should revert + vm.expectRevert("Invalid packed length"); + messageSwitchboard.allowPayload(digest, payloadId, address(dstPlug), invalidSource); + } + + // ============================================ + // MISSING TESTS - GROUP 14: Watcher Role Management + // ============================================ + + function test_grantWatcherRole_Success() public { + address newWatcher = address(0x5000); + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + vm.stopPrank(); + + vm.prank(owner); + messageSwitchboard.grantWatcherRole(SRC_CHAIN, newWatcher); + + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, SRC_CHAIN)); + assertTrue(messageSwitchboard.hasRole(role, newWatcher)); + assertEq(messageSwitchboard.totalWatchers(SRC_CHAIN), 2); // 1 from setUp + 1 new + } + + function test_grantWatcherRole_WatcherFound_Reverts() public { + address existingWatcher = getWatcherAddress(); + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + vm.stopPrank(); + + vm.prank(owner); + vm.expectRevert(WatcherFound.selector); + messageSwitchboard.grantWatcherRole(SRC_CHAIN, existingWatcher); + } + + function test_revokeWatcherRole_Success() public { + address watcherToRevoke = getWatcherAddress(); + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + vm.stopPrank(); + + uint256 totalBefore = messageSwitchboard.totalWatchers(SRC_CHAIN); + assertEq(totalBefore, 1); + + vm.prank(owner); + messageSwitchboard.revokeWatcherRole(SRC_CHAIN, watcherToRevoke); + + bytes32 role = keccak256(abi.encode(WATCHER_ROLE, SRC_CHAIN)); + assertFalse(messageSwitchboard.hasRole(role, watcherToRevoke)); + assertEq(messageSwitchboard.totalWatchers(SRC_CHAIN), 0); + } + + function test_revokeWatcherRole_WatcherNotFound_Reverts() public { + address nonWatcher = address(0x9999); + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + vm.stopPrank(); + + vm.prank(owner); + vm.expectRevert(WatcherNotFound.selector); + messageSwitchboard.revokeWatcherRole(SRC_CHAIN, nonWatcher); + } + + function test_grantRole_WithValidRoles_Success() public { + address grantee = address(0x6000); + vm.prank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, grantee); + assertTrue(messageSwitchboard.hasRole(GOVERNANCE_ROLE, grantee)); + + vm.prank(owner); + messageSwitchboard.grantRole(RESCUE_ROLE, grantee); + assertTrue(messageSwitchboard.hasRole(RESCUE_ROLE, grantee)); + + vm.prank(owner); + messageSwitchboard.grantRole(FEE_UPDATER_ROLE, grantee); + assertTrue(messageSwitchboard.hasRole(FEE_UPDATER_ROLE, grantee)); + } + + function test_grantRole_WithWatcherRole_Reverts() public { + address grantee = address(0x6000); + bytes32 watcherRole = keccak256(abi.encode(WATCHER_ROLE, SRC_CHAIN)); + vm.prank(owner); + vm.expectRevert(InvalidRole.selector); + messageSwitchboard.grantRole(watcherRole, grantee); + } + + function test_revokeRole_WithValidRoles_Success() public { + address grantee = address(0x6000); + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, grantee); + messageSwitchboard.grantRole(RESCUE_ROLE, grantee); + messageSwitchboard.grantRole(FEE_UPDATER_ROLE, grantee); + vm.stopPrank(); + + vm.prank(owner); + messageSwitchboard.revokeRole(GOVERNANCE_ROLE, grantee); + assertFalse(messageSwitchboard.hasRole(GOVERNANCE_ROLE, grantee)); + + vm.prank(owner); + messageSwitchboard.revokeRole(RESCUE_ROLE, grantee); + assertFalse(messageSwitchboard.hasRole(RESCUE_ROLE, grantee)); + + vm.prank(owner); + messageSwitchboard.revokeRole(FEE_UPDATER_ROLE, grantee); + assertFalse(messageSwitchboard.hasRole(FEE_UPDATER_ROLE, grantee)); + } + + function test_revokeRole_WithWatcherRole_Reverts() public { + address grantee = address(0x6000); + bytes32 watcherRole = keccak256(abi.encode(WATCHER_ROLE, SRC_CHAIN)); + vm.prank(owner); + vm.expectRevert(InvalidRole.selector); + messageSwitchboard.revokeRole(watcherRole, grantee); + } + + function test_setDefaultDeadline_Success() public { + uint256 newDeadline = 2 days; + vm.expectEmit(true, false, false, true); + emit SwitchboardBase.DefaultDeadlineSet(newDeadline); + + vm.prank(owner); + messageSwitchboard.setDefaultDeadline(newDeadline); + + assertEq(messageSwitchboard.defaultDeadline(), newDeadline); + } + + function test_processPayload_WithZeroDeadline_UsesDefault() public { + _setupCompleteNative(); + + bytes memory overrides = abi.encode( + uint8(1), // version + DST_CHAIN, + uint256(100000), // gasLimit + uint256(0), // value + refundAddress, // refundAddress + 0 // deadline = 0, should use defaultDeadline + ); + + srcPlug.setOverrides(overrides); + bytes memory payload = abi.encode("test data"); + uint256 msgValue = MIN_FEES + 0.001 ether; + + uint64 payloadCounterBefore = messageSwitchboard.payloadCounter(); + vm.deal(address(srcPlug), 10 ether); + bytes32 payloadId = srcPlug.triggerSocket{value: msgValue}(payload); + + // Verify payload was created (counter incremented) + assertEq(messageSwitchboard.payloadCounter(), payloadCounterBefore + 1); + + // Verify digest was created with default deadline + bytes32 digest = messageSwitchboard.payloadIdToDigest(payloadId); + assertTrue(digest != bytes32(0)); + } + + function test_attest_ReachesThreshold_SetsIsValid() public { + _setupSiblingConfig(); + + // Grant watcher role to 2 more watchers (total 3) + // Use addresses derived from private keys to match signatures + uint256 watcher2Key = 0x2222222222222222222222222222222222222222222222222222222222222222; + uint256 watcher3Key = 0x3333333333333333333333333333333333333333333333333333333333333333; + address watcher2 = vm.addr(watcher2Key); + address watcher3 = vm.addr(watcher3Key); + + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + messageSwitchboard.grantWatcherRole(SRC_CHAIN, watcher2); + messageSwitchboard.grantWatcherRole(SRC_CHAIN, watcher3); + vm.stopPrank(); + + assertEq(messageSwitchboard.totalWatchers(SRC_CHAIN), 3); + + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x5678)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + bytes32 digest = calculateDigest(digestParams); + + // First attestation - should not set isValid yet + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + bytes memory signature1 = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(digest, signature1); + assertFalse(messageSwitchboard.isValid(digest)); // Not enough attestations + + // Second attestation - should not set isValid yet + bytes memory signature2 = createSignature(signatureDigest, watcher2Key); + vm.prank(watcher2); + messageSwitchboard.attest(digest, signature2); + assertFalse(messageSwitchboard.isValid(digest)); // Still not enough + + // Third attestation - should set isValid to true + bytes memory signature3 = createSignature(signatureDigest, watcher3Key); + vm.prank(watcher3); + messageSwitchboard.attest(digest, signature3); + assertTrue(messageSwitchboard.isValid(digest)); // Now valid! + assertEq(messageSwitchboard.attestations(digest), 3); + } + + // ============================================ + // MISSING TESTS - GROUP 15: SwitchboardBase Functions + // ============================================ + + function test_getTransmitter_WithEmptySignature_ReturnsZero() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + address transmitter = messageSwitchboard.getTransmitter( + address(0), + payloadId, + bytes("") // Empty signature + ); + assertEq(transmitter, address(0)); + } + + function test_getTransmitter_WithSignature_ReturnsSigner() public { + bytes32 payloadId = bytes32(uint256(0x1234)); + bytes32 digest = keccak256(abi.encodePacked(address(socket), payloadId)); + bytes memory signature = createSignature(digest, watcherPrivateKey); + + address transmitter = messageSwitchboard.getTransmitter(address(0), payloadId, signature); + assertEq(transmitter, getWatcherAddress()); + } + + function test_rescueFunds_ERC20_Success() public { + // Deploy a mock ERC20 token + MockERC20 token = new MockERC20(); + address rescueTo = address(0x7000); + uint256 amount = 1000; + + // Transfer some tokens to the switchboard + token.mint(address(messageSwitchboard), amount); + + // Grant RESCUE_ROLE to owner + vm.startPrank(owner); + messageSwitchboard.grantRole(RESCUE_ROLE, owner); + vm.stopPrank(); + + uint256 balanceBefore = token.balanceOf(rescueTo); + + vm.prank(owner); + messageSwitchboard.rescueFunds(address(token), rescueTo, amount); + + assertEq(token.balanceOf(rescueTo), balanceBefore + amount); + assertEq(token.balanceOf(address(messageSwitchboard)), 0); + } + + function test_rescueFunds_ETH_Success() public { + address rescueTo = address(0x7000); + uint256 amount = 1 ether; + + // Send ETH to switchboard + vm.deal(address(messageSwitchboard), amount); + + // Grant RESCUE_ROLE to owner + vm.startPrank(owner); + messageSwitchboard.grantRole(RESCUE_ROLE, owner); + vm.stopPrank(); + + uint256 balanceBefore = rescueTo.balance; + + vm.prank(owner); + messageSwitchboard.rescueFunds(ETH_ADDRESS, rescueTo, amount); + + assertEq(rescueTo.balance, balanceBefore + amount); + } + + function test_rescueFunds_WithoutRole_Reverts() public { + address rescueTo = address(0x7000); + uint256 amount = 1 ether; + + vm.deal(address(messageSwitchboard), amount); + + vm.prank(address(0x9999)); + vm.expectRevert(); + messageSwitchboard.rescueFunds(ETH_ADDRESS, rescueTo, amount); + } + + // ============================================ + // MISSING TESTS - GROUP 16: _validateSibling Individual Branches + // ============================================ + + function test_processPayload_DstSocketZero_Reverts() public { + _setupSiblingConfig(); + _setupMinFees(); + + // Set dstSocket to zero while keeping others valid + vm.prank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + bytes32(0), // dstSocket = 0 + toBytes32Format(address(0x5678)), // dstSwitchboard valid + 1 // switchboardId valid + ); + + bytes memory overrides = abi.encode( + uint8(1), + DST_CHAIN, + uint256(100000), + uint256(0), + refundAddress, + 86400 + ); + srcPlug.setOverrides(overrides); + + vm.deal(address(srcPlug), 10 ether); + vm.prank(address(srcPlug)); + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.triggerSocket{value: MIN_FEES}(abi.encode("test")); + } + + function test_processPayload_DstSwitchboardZero_Reverts() public { + _setupSiblingConfig(); + _setupMinFees(); + + // Set dstSwitchboard to zero while keeping others valid + vm.prank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + toBytes32Format(address(0x1234)), // dstSocket valid + bytes32(0), // dstSwitchboard = 0 + 1 // switchboardId valid + ); + + bytes memory overrides = abi.encode( + uint8(1), + DST_CHAIN, + uint256(100000), + uint256(0), + refundAddress, + 86400 + ); + srcPlug.setOverrides(overrides); + + vm.deal(address(srcPlug), 10 ether); + vm.prank(address(srcPlug)); + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.triggerSocket{value: MIN_FEES}(abi.encode("test")); + } + + function test_processPayload_DstPlugZero_Reverts() public { + _setupSiblingSocketConfig(); + _setupMinFees(); + + // Don't register sibling plug (dstPlug will be zero) + // But register sibling socket config + bytes memory overrides = abi.encode( + uint8(1), + DST_CHAIN, + uint256(100000), + uint256(0), + refundAddress, + 86400 + ); + srcPlug.setOverrides(overrides); + + vm.deal(address(srcPlug), 10 ether); + vm.prank(address(srcPlug)); + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.triggerSocket{value: MIN_FEES}(abi.encode("test")); + } + + function test_processPayload_DstSwitchboardIdZero_Reverts() public { + _setupMinFees(); + + // Set sibling config but with switchboardId = 0 (not registered) + bytes32 siblingSocket = toBytes32Format(address(0x1234)); + bytes32 siblingSwitchboard = toBytes32Format(address(0x5678)); + vm.startPrank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + siblingSocket, + siblingSwitchboard, + 0 // switchboardId = 0, should cause revert at line 351 + ); + vm.stopPrank(); + + // Register sibling plug + srcPlug.registerSibling(DST_CHAIN, address(dstPlug)); + + bytes memory overrides = abi.encode( + uint8(1), + DST_CHAIN, + uint256(100000), + uint256(0), + refundAddress, + 86400 + ); + srcPlug.setOverrides(overrides); + + vm.deal(address(srcPlug), 10 ether); + vm.prank(address(srcPlug)); + // Should revert at line 351 in _createDigestAndPayloadId when dstSwitchboardId == 0 + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.triggerSocket{value: MIN_FEES}(abi.encode("test")); + } + + function test_processPayload_SiblingNotRegistered_Reverts() public { + _setupMinFees(); + + // Don't set sibling config at all - siblingSwitchboardIds[DST_CHAIN] will be 0 by default + // Register sibling plug (this will fail in updatePlugConfig, but let's test the processPayload path) + // Actually, we need to set socket/switchboard for updatePlugConfig to work, but not the switchboardId + bytes32 siblingSocket = toBytes32Format(address(0x1234)); + bytes32 siblingSwitchboard = toBytes32Format(address(0x5678)); + vm.startPrank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + siblingSocket, + siblingSwitchboard, + 0 // switchboardId = 0 (not registered) + ); + vm.stopPrank(); + + // Register sibling plug + srcPlug.registerSibling(DST_CHAIN, address(dstPlug)); + + bytes memory overrides = abi.encode( + uint8(1), + DST_CHAIN, + uint256(100000), + uint256(0), + refundAddress, + 86400 + ); + srcPlug.setOverrides(overrides); + + vm.deal(address(srcPlug), 10 ether); + vm.prank(address(srcPlug)); + // Should revert at line 351 when siblingSwitchboardIds[DST_CHAIN] == 0 + vm.expectRevert(SiblingSocketNotFound.selector); + srcPlug.triggerSocket{value: MIN_FEES}(abi.encode("test")); + } + + function test_updatePlugConfig_DstSocketZero_Reverts() public { + _setupSiblingSocketConfig(); + + // Set dstSocket to zero + vm.prank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + bytes32(0), // dstSocket = 0 + toBytes32Format(address(0x5678)), + 1 + ); + + bytes memory plugConfig = abi.encode(DST_CHAIN, toBytes32Format(address(dstPlug))); + vm.prank(address(socket)); + vm.expectRevert(SiblingSocketNotFound.selector); + messageSwitchboard.updatePlugConfig(address(srcPlug), plugConfig); + } + + function test_updatePlugConfig_DstSwitchboardZero_Reverts() public { + _setupSiblingSocketConfig(); + + // Set dstSwitchboard to zero + vm.prank(owner); + messageSwitchboard.setSiblingConfig( + DST_CHAIN, + toBytes32Format(address(0x1234)), + bytes32(0), // dstSwitchboard = 0 + 1 + ); + + bytes memory plugConfig = abi.encode(DST_CHAIN, toBytes32Format(address(dstPlug))); + vm.prank(address(socket)); + vm.expectRevert(SiblingSocketNotFound.selector); + messageSwitchboard.updatePlugConfig(address(srcPlug), plugConfig); + } + + function test_attest_DoesNotReachThreshold_IsValidStaysFalse() public { + _setupSiblingConfig(); + + // Grant watcher role to 1 more watcher (total 2) + uint256 watcher2Key = 0x2222222222222222222222222222222222222222222222222222222222222222; + address watcher2 = vm.addr(watcher2Key); + + vm.startPrank(owner); + messageSwitchboard.grantRole(GOVERNANCE_ROLE, owner); + messageSwitchboard.grantWatcherRole(SRC_CHAIN, watcher2); + vm.stopPrank(); + + assertEq(messageSwitchboard.totalWatchers(SRC_CHAIN), 2); + + bytes memory payload = abi.encode("test"); + bytes32 payloadId = bytes32(uint256(0x5678)); + DigestParams memory digestParams = _createDigestParams(payloadId, payload); + bytes32 digest = calculateDigest(digestParams); + + bytes32 signatureDigest = keccak256( + abi.encodePacked(toBytes32Format(address(messageSwitchboard)), SRC_CHAIN, digest) + ); + + // First attestation - should not set isValid (need 2, only have 1) + bytes memory signature1 = createSignature(signatureDigest, watcherPrivateKey); + vm.prank(getWatcherAddress()); + messageSwitchboard.attest(digest, signature1); + assertFalse(messageSwitchboard.isValid(digest)); // Not enough attestations + assertEq(messageSwitchboard.attestations(digest), 1); + } + + // ============================================ + // MISSING TESTS - GROUP 17: SwitchboardBase Constructor Edge Cases + // ============================================ + + function test_SwitchboardBase_Constructor_InvalidChainSlug_Reverts() public { + vm.expectRevert(InvalidChainSlug.selector); + new MessageSwitchboard(0, socket, owner); // chainSlug = 0 + } + + function test_SwitchboardBase_Constructor_InvalidSocket_Reverts() public { + vm.expectRevert(InvalidSocket.selector); + new MessageSwitchboard(SRC_CHAIN, ISocket(address(0)), owner); // socket = address(0) + } + + function test_SwitchboardBase_Constructor_InvalidOwner_Reverts() public { + vm.expectRevert(InvalidOwner.selector); + new MessageSwitchboard(SRC_CHAIN, socket, address(0)); // owner = address(0) + } +} + +// Mock ERC20 for testing rescueFunds +contract MockERC20 { + mapping(address => uint256) public balanceOf; + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + } + + function transfer(address to, uint256 amount) external returns (bool) { + balanceOf[msg.sender] -= amount; + balanceOf[to] += amount; + return true; + } +}