diff --git a/.gitmodules b/.gitmodules index 86d572d1..e230d117 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "lib/PermitC"] path = lib/PermitC url = https://github.com/limitbreakinc/PermitC +[submodule "lib/lxly-bridge-and-call"] + path = lib/lxly-bridge-and-call + url = https://github.com/agglayer/lxly-bridge-and-call diff --git a/lib/lxly-bridge-and-call b/lib/lxly-bridge-and-call new file mode 160000 index 00000000..ccd84985 --- /dev/null +++ b/lib/lxly-bridge-and-call @@ -0,0 +1 @@ +Subproject commit ccd84985f39a320a5d7775e0092822dd89afb4cb diff --git a/remappings.txt b/remappings.txt index 413cecc2..22272ee9 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,3 +5,5 @@ forge-std/=lib/forge-std/src/ @erc721a-upgradeable/=lib/ERC721A-Upgradeable/contracts/ @limitbreak/creator-token-standards/=lib/creator-token-standards/src/ @limitbreak/permit-c/=lib/PermitC/src/ +@lxly-bridge-and-call/=lib/lxly-bridge-and-call/src/ +@zkevm-contracts/=lib/lxly-bridge-and-call/lib/zkevm-contracts/contracts/ diff --git a/script/agglayer/Agglayer.s.sol b/script/agglayer/Agglayer.s.sol new file mode 100644 index 00000000..e62e05b8 --- /dev/null +++ b/script/agglayer/Agglayer.s.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {TestNFT} from "./TestNFT.sol"; + +import {OwnableRoles} from "@solady/auth/OwnableRoles.sol"; +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Role} from "src/Role.sol"; +import {ERC20Core} from "src/core/token/ERC20Core.sol"; +import {AgglayerCrossChain} from "src/module/token/crosschain/Agglayer.sol"; +import {MintableERC20} from "src/module/token/minting/MintableERC20.sol"; + +contract DeployTestNFT is Script { + + TestNFT public testNFT; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + testNFT = new TestNFT("TestNFT", "TNFT"); + console.log("TestNFT deployed to:", address(testNFT)); + + vm.stopBroadcast(); + } + +} + +interface IBridge { + + function bridge() external view returns (address); + +} + +contract MintTestNFT is Script { + + TestNFT public testNFT; + MintableERC20 public mintableModule; + AgglayerCrossChain public agglayer; + ERC20Core public core; + + address agglayerBridgeExtension = 0x2311BFA86Ae27FC10E1ad3f805A2F9d22Fc8a6a1; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployerAddress = vm.addr(deployerPrivateKey); + // address coreAddress = vm.envAddress("TEST_TOKEN_ADDRESS"); + address testNFTAddress = vm.envAddress("TEST_NFT_ADDRESS"); + uint64 destinationNetwork = 1; + vm.startBroadcast(deployerPrivateKey); + + address[] memory modules = new address[](2); + bytes[] memory moduleData = new bytes[](2); + + mintableModule = new MintableERC20(address(0x0)); + agglayer = new AgglayerCrossChain(); + console.log("mintableModule deployed to:", address(mintableModule)); + console.log("agglayer deployed to:", address(agglayer)); + + bytes memory mintableEncodedInstallParams = abi.encode(deployerAddress); + bytes memory agglayerEncodedInstallParams = abi.encode(agglayerBridgeExtension); + console.log("agglayerEncodedInstallParams"); + console.logBytes(agglayerEncodedInstallParams); + console.log(IBridge(agglayerBridgeExtension).bridge()); + + modules[0] = address(mintableModule); + modules[1] = address(agglayer); + + moduleData[0] = mintableEncodedInstallParams; + moduleData[1] = agglayerEncodedInstallParams; + + console.log("moduleData"); + console.logBytes(moduleData[0]); + console.logBytes(moduleData[1]); + + console.log("modules"); + console.logAddress(modules[0]); + console.logAddress(modules[1]); + + core = new ERC20Core("test", "TEST", "", deployerAddress, modules, moduleData); + console.log("core deployed to:", address(core)); + + core.grantRoles(deployerAddress, Role._MINTER_ROLE); + core.mint(deployerAddress, 100, ""); + console.log("Minted test tokens to deployer"); + + core.approve(address(core), 100); + console.log("Approved core"); + + bytes memory extraArgs = abi.encode(deployerAddress, true, address(core), 100, ""); + bytes memory payload = abi.encodeWithSelector(bytes4(keccak256("mint(address,uint256)")), deployerAddress, 1); + + AgglayerCrossChain(address(core)).sendCrossChainTransaction( + destinationNetwork, testNFTAddress, payload, extraArgs + ); + + vm.stopBroadcast(); + } + +} diff --git a/script/agglayer/TestNFT.sol b/script/agglayer/TestNFT.sol new file mode 100644 index 00000000..60df5a0d --- /dev/null +++ b/script/agglayer/TestNFT.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {ERC721A} from "@erc721a/ERC721A.sol"; + +contract TestNFT is ERC721A { + + constructor(string memory _name, string memory _symbol) ERC721A(_name, _symbol) {} + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + +} diff --git a/src/module/token/crosschain/Agglayer.sol b/src/module/token/crosschain/Agglayer.sol new file mode 100644 index 00000000..1fa5e819 --- /dev/null +++ b/src/module/token/crosschain/Agglayer.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {Module} from "../../../Module.sol"; +import {Role} from "../../../Role.sol"; +import {CrossChain} from "./CrossChain.sol"; +import {IPolygonZkEVMBridge} from "@zkevm-contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {IERC20} from "src/interface/IERC20.sol"; + +interface IBridge is IPolygonZkEVMBridge { + + function networkID() external view returns (uint32); + +} + +interface IBridgeExtension { + + function bridge() external view returns (address); + + function bridgeAndCall( + address token, + uint256 amount, + bytes calldata permitData, + uint32 destinationNetwork, + address callAddress, + address fallbackAddress, + bytes calldata callData, + bool forceUpdateGlobalExitRoot + ) external payable; + +} + +library AgglayerCrossChainStorage { + + /// @custom:storage-location erc7201:token.bridgeAndCall + bytes32 public constant BRIDGE_AND_CALL_STORAGE_POSITION = + keccak256(abi.encode(uint256(keccak256("token.bridgeAndCall")) - 1)) & ~bytes32(uint256(0xff)); + + struct Data { + address router; + address bridge; + uint32 networkId; + } + + function data() internal pure returns (Data storage data_) { + bytes32 position = BRIDGE_AND_CALL_STORAGE_POSITION; + assembly { + data_.slot := position + } + } + +} + +contract AgglayerCrossChain is Module, CrossChain { + + /*////////////////////////////////////////////////////////////// + EXTENSION CONFIG + //////////////////////////////////////////////////////////////*/ + + /// @notice Returns all implemented callback and fallback functions. + function getModuleConfig() external pure override returns (ModuleConfig memory config) { + config.fallbackFunctions = new FallbackFunction[](4); + + config.fallbackFunctions[0] = FallbackFunction({selector: this.getRouter.selector, permissionBits: 0}); + config.fallbackFunctions[1] = + FallbackFunction({selector: this.setRouter.selector, permissionBits: Role._MANAGER_ROLE}); + config.fallbackFunctions[2] = + FallbackFunction({selector: this.sendCrossChainTransaction.selector, permissionBits: 0}); + config.fallbackFunctions[3] = FallbackFunction({selector: this.bridgeTokens.selector, permissionBits: 0}); + + config.registerInstallationCallback = true; + } + + /// @dev Called by a Core into an Module during the installation of the Module. + function onInstall(bytes calldata data) external { + (address router) = abi.decode(data, (address)); + address bridge = IBridgeExtension(router).bridge(); + + _agglayerStorage().router = router; + _agglayerStorage().bridge = bridge; + _agglayerStorage().networkId = IBridge(bridge).networkID(); + } + + /// @dev Called by a Core into an Module during the uninstallation of the Module. + function onUninstall(bytes calldata data) external {} + + /// @dev Returns bytes encoded install params, to be sent to `onInstall` function + function encodeBytesOnInstall(address router) external pure returns (bytes memory) { + return abi.encode(router); + } + + /// @dev Returns bytes encoded uninstall params, to be sent to `onUninstall` function + function encodeBytesOnUninstall() external pure returns (bytes memory) { + return ""; + } + + /*////////////////////////////////////////////////////////////// + FALLBACK FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function getRouter() external view override returns (address) { + return _agglayerStorage().router; + } + + function setRouter(address router) external override { + _agglayerStorage().router = router; + } + + function sendCrossChainTransaction( + uint64 _destinationNetwork, + address _callAddress, + bytes calldata _payload, + bytes calldata _extraArgs + ) external payable override { + ( + address _fallbackAddress, + bool _forceUpdateGlobalExitRoot, + address _token, + uint256 _amount, + bytes memory permitData + ) = abi.decode(_extraArgs, (address, bool, address, uint256, bytes)); + + if (_token == address(0) && _amount == 0) { + _bridgeMessage(uint32(_destinationNetwork), _callAddress, _forceUpdateGlobalExitRoot, _payload); + } else if (_payload.length == 0) { + _bridgeAsset( + uint32(_destinationNetwork), _callAddress, _amount, _token, _forceUpdateGlobalExitRoot, permitData + ); + } else { + _bridgeAndCall( + _token, + _amount, + permitData, + uint32(_destinationNetwork), + _callAddress, + _fallbackAddress, + _payload, + _forceUpdateGlobalExitRoot + ); + } + + onCrossChainTransactionSent(_destinationNetwork, _callAddress, _payload, _extraArgs); + } + + function bridgeTokens(uint64 _destinationNetwork, address _callAddress, uint256 _amount) + external + payable + { + _bridgeAsset(uint32(_destinationNetwork), _callAddress, _amount, address(this), true, ""); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function _bridgeMessage( + uint32 _destinationNetwork, + address _callAddress, + bool _forceUpdateGlobalExitRoot, + bytes memory _payload + ) internal { + IBridge(_agglayerStorage().bridge).bridgeMessage( + uint32(_destinationNetwork), _callAddress, _forceUpdateGlobalExitRoot, _payload + ); + } + + function _bridgeAsset( + uint32 _destinationNetwork, + address _callAddress, + uint256 _amount, + address _token, + bool _forceUpdateGlobalExitRoot, + bytes memory permitData + ) internal { + address bridge = _agglayerStorage().bridge; + IERC20(_token).transferFrom(msg.sender, address(this), _amount); + IERC20(_token).approve(bridge, _amount); + + IBridge(bridge).bridgeAsset( + uint32(_destinationNetwork), _callAddress, _amount, _token, _forceUpdateGlobalExitRoot, permitData + ); + } + + function _bridgeAndCall( + address _token, + uint256 _amount, + bytes memory permitData, + uint32 _destinationNetwork, + address _callAddress, + address _fallbackAddress, + bytes memory _payload, + bool _forceUpdateGlobalExitRoot + ) internal { + address router = _agglayerStorage().router; + IERC20(_token).transferFrom(msg.sender, address(this), _amount); + IERC20(_token).approve(router, _amount); + + IBridgeExtension(router).bridgeAndCall( + _token, + _amount, + permitData, + _destinationNetwork, + _callAddress, + _fallbackAddress, + _payload, + _forceUpdateGlobalExitRoot + ); + } + + function onCrossChainTransactionSent( + uint64 _destinationNetwork, + address _callAddress, + bytes calldata _payload, + bytes calldata _extraArgs + ) internal override { + /// post cross chain transaction sent logic goes here + } + + function onCrossChainTransactionReceived( + uint64 _sourceChain, + address _sourceAddress, + bytes memory _payload, + bytes memory _extraArgs + ) internal override { + /// post cross chain transaction received logic goes here + } + + function _agglayerStorage() internal pure returns (AgglayerCrossChainStorage.Data storage) { + return AgglayerCrossChainStorage.data(); + } + +}