From cae579dd32743725ea5016866ee53f470934e793 Mon Sep 17 00:00:00 2001 From: Zehui Zheng Date: Fri, 26 Apr 2024 18:52:48 +0800 Subject: [PATCH] feat: ENS portal --- .changeset/proud-badgers-prove.md | 6 + contracts/common/InputEncoding.sol | 18 +++ contracts/delegatecall/AssetTransferToENS.sol | 18 ++- contracts/library/LibENS.sol | 21 ++++ contracts/portals/ENSPortal.sol | 63 ++++++++++ contracts/portals/IENSPortal.sol | 36 ++++++ test/foundry/portals/ENSPortal.t.sol | 119 ++++++++++++++++++ 7 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 .changeset/proud-badgers-prove.md create mode 100644 contracts/library/LibENS.sol create mode 100644 contracts/portals/ENSPortal.sol create mode 100644 contracts/portals/IENSPortal.sol create mode 100644 test/foundry/portals/ENSPortal.t.sol diff --git a/.changeset/proud-badgers-prove.md b/.changeset/proud-badgers-prove.md new file mode 100644 index 00000000..24c6d19b --- /dev/null +++ b/.changeset/proud-badgers-prove.md @@ -0,0 +1,6 @@ +--- +"@cartesi/rollups": major +--- + +Added ENS Portal. +Added a new input encoding for ENS inputs. diff --git a/contracts/common/InputEncoding.sol b/contracts/common/InputEncoding.sol index a41be2fb..94c78877 100644 --- a/contracts/common/InputEncoding.sol +++ b/contracts/common/InputEncoding.sol @@ -134,4 +134,22 @@ library InputEncoding { data // arbitrary size ); } + + /// @notice Encode an ENS input. + /// @param node The ENS node + /// @param name The ENS name + /// @param execLayerData Additional data to be interpreted by the execution layer + /// @return The encoded input payload + function encodeENSInput( + bytes32 node, + bytes calldata name, + bytes calldata execLayerData + ) internal pure returns (bytes memory) { + return + abi.encode( + node, // 32B + name, // arbitrary size + execLayerData // arbitrary size + ); + } } diff --git a/contracts/delegatecall/AssetTransferToENS.sol b/contracts/delegatecall/AssetTransferToENS.sol index 07363824..c012bea4 100644 --- a/contracts/delegatecall/AssetTransferToENS.sol +++ b/contracts/delegatecall/AssetTransferToENS.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; -import {AddrResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -12,10 +11,12 @@ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; import {LibAddress} from "../library/LibAddress.sol"; +import {LibENS} from "../library/LibENS.sol"; contract AssetTransferToENS { using LibAddress for address; using SafeERC20 for IERC20; + using LibENS for ENS; ENS immutable _ens; @@ -28,7 +29,7 @@ contract AssetTransferToENS { uint256 value, bytes memory payload ) external { - address recipient = _resolveENS(node); + address recipient = _ens.resolveToAddress(node); recipient.safeCall(value, payload); } @@ -37,7 +38,7 @@ contract AssetTransferToENS { bytes32 node, uint256 value ) external { - address recipient = _resolveENS(node); + address recipient = _ens.resolveToAddress(node); token.safeTransfer(recipient, value); } @@ -47,7 +48,7 @@ contract AssetTransferToENS { uint256 tokenId, bytes calldata data ) external { - address recipient = _resolveENS(node); + address recipient = _ens.resolveToAddress(node); token.safeTransferFrom(address(this), recipient, tokenId, data); } @@ -58,7 +59,7 @@ contract AssetTransferToENS { uint256 value, bytes calldata data ) external { - address recipient = _resolveENS(node); + address recipient = _ens.resolveToAddress(node); token.safeTransferFrom(address(this), recipient, id, value, data); } @@ -69,7 +70,7 @@ contract AssetTransferToENS { uint256[] memory values, bytes calldata data ) external { - address recipient = _resolveENS(node); + address recipient = _ens.resolveToAddress(node); token.safeBatchTransferFrom( address(this), recipient, @@ -78,9 +79,4 @@ contract AssetTransferToENS { data ); } - - function _resolveENS(bytes32 node) internal view returns (address) { - AddrResolver resolver = AddrResolver(_ens.resolver(node)); - return resolver.addr(node); - } } diff --git a/contracts/library/LibENS.sol b/contracts/library/LibENS.sol new file mode 100644 index 00000000..c0c7161d --- /dev/null +++ b/contracts/library/LibENS.sol @@ -0,0 +1,21 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; +import {AddrResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol"; + +library LibENS { + /// @notice Resolve ENS node to address + /// @param ens The ENS registry + /// @param node The ENS node + /// @return The address that ENS node resolves to + function resolveToAddress( + ENS ens, + bytes32 node + ) internal view returns (address) { + AddrResolver resolver = AddrResolver(ens.resolver(node)); + return resolver.addr(node); + } +} diff --git a/contracts/portals/ENSPortal.sol b/contracts/portals/ENSPortal.sol new file mode 100644 index 00000000..469f2de4 --- /dev/null +++ b/contracts/portals/ENSPortal.sol @@ -0,0 +1,63 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {IENSPortal} from "./IENSPortal.sol"; +import {Portal} from "./Portal.sol"; +import {IInputBox} from "../inputs/IInputBox.sol"; +import {InputEncoding} from "../common/InputEncoding.sol"; +import {LibENS} from "../library/LibENS.sol"; + +/// @title ENS Portal +/// +/// @notice This contract allows anyone to send input to the InputBox with ENS +contract ENSPortal is IENSPortal, Portal { + using LibENS for ENS; + + ENS immutable _ens; + + /// @notice Constructs the portal. + /// @param inputBox The input box used by the portal + /// @param ens The ENS registry + constructor(IInputBox inputBox, ENS ens) Portal(inputBox) { + _ens = ens; + } + + function sendInputWithENS( + address appContract, + bytes32 node, + bytes calldata name, + bytes calldata execLayerData + ) external override { + address resolution = _ens.resolveToAddress(node); + + if (resolution != msg.sender) { + revert AddressResolutionMismatch(resolution, msg.sender); + } + + bytes memory payload = InputEncoding.encodeENSInput( + node, + name, + execLayerData + ); + + _inputBox.addInput(appContract, payload); + } + + function getENS() external view override returns (ENS) { + return _ens; + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, Portal) returns (bool) { + return + interfaceId == type(IENSPortal).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/contracts/portals/IENSPortal.sol b/contracts/portals/IENSPortal.sol new file mode 100644 index 00000000..4c668713 --- /dev/null +++ b/contracts/portals/IENSPortal.sol @@ -0,0 +1,36 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {IPortal} from "./IPortal.sol"; +import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; + +/// @title ENS Portal interface +interface IENSPortal is IPortal { + // Errors + + /// @notice The provided ENS node does not resolve to the sender address. + /// @param resolution The address that the ENS node resolves to + /// @param sender The sender address + error AddressResolutionMismatch(address resolution, address sender); + + // Permissionless functions + + /// @notice Send input to InputBox with ENS. + /// @param appContract The application contract address + /// @param node The ENS node + /// @param name The ENS name + /// @param execLayerData Additional data to be interpreted by + /// the execution layer. The data may include the ENS name + function sendInputWithENS( + address appContract, + bytes32 node, + bytes calldata name, + bytes calldata execLayerData + ) external; + + /// @notice Get the ENS registry used by this portal. + /// @return The ENS registry + function getENS() external view returns (ENS); +} diff --git a/test/foundry/portals/ENSPortal.t.sol b/test/foundry/portals/ENSPortal.t.sol new file mode 100644 index 00000000..13b9d9d5 --- /dev/null +++ b/test/foundry/portals/ENSPortal.t.sol @@ -0,0 +1,119 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.22; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {ENS} from "@ensdomains/ens-contracts/contracts/registry/ENS.sol"; +import {AddrResolver} from "@ensdomains/ens-contracts/contracts/resolvers/profiles/AddrResolver.sol"; + +import {ENSPortal} from "contracts/portals/ENSPortal.sol"; +import {IENSPortal} from "contracts/portals/IENSPortal.sol"; +import {IInputBox} from "contracts/inputs/IInputBox.sol"; +import {IPortal} from "contracts/portals/IPortal.sol"; +import {InputEncoding} from "contracts/common/InputEncoding.sol"; + +import {ERC165Test} from "../util/ERC165Test.sol"; + +contract ENSPortalTest is ERC165Test { + IInputBox _inputBox; + IENSPortal _portal; + ENS _ens; + AddrResolver _resolver; + + address _inputSender; + address _appContract; + bytes4[] _interfaceIds; + + bytes32 constant _node = keccak256("user.eth"); + + function setUp() public { + _inputSender = _newAddr(); + _appContract = _newAddr(); + _inputBox = IInputBox(_newAddr()); + _ens = ENS(_newAddr()); + _portal = new ENSPortal(_inputBox, _ens); + _resolver = AddrResolver(_newAddr()); + + vm.mockCall( + address(_ens), + abi.encodeCall(ENS.resolver, (_node)), + abi.encode(_resolver) + ); + vm.mockCall( + address(_resolver), + abi.encodeWithSignature("addr(bytes32)", (_node)), + abi.encode(_inputSender) + ); + + _interfaceIds.push(type(IENSPortal).interfaceId); + _interfaceIds.push(type(IPortal).interfaceId); + } + + function getERC165Contract() public view override returns (IERC165) { + return _portal; + } + + function getSupportedInterfaces() + public + view + override + returns (bytes4[] memory) + { + return _interfaceIds; + } + + function testGetInputBox() public view { + assertEq(address(_portal.getInputBox()), address(_inputBox)); + } + + function testGetENS() public view { + assertEq(address(_portal.getENS()), address(_ens)); + } + + function testAddressResolutionMismatch( + address incorrectSender, + bytes calldata name, + bytes calldata execLayerData + ) public { + vm.assume(incorrectSender != _inputSender); + + vm.expectRevert( + abi.encodeWithSelector( + IENSPortal.AddressResolutionMismatch.selector, + _inputSender, + incorrectSender + ) + ); + vm.prank(incorrectSender); + _portal.sendInputWithENS(_appContract, _node, name, execLayerData); + } + + function testSendInput( + bytes calldata name, + bytes calldata execLayerData + ) public { + bytes memory payload = _encodePayload(_node, name, execLayerData); + bytes memory addInput = _encodeAddInput(payload); + + vm.mockCall(address(_inputBox), addInput, abi.encode(bytes32(0))); + vm.expectCall(address(_inputBox), addInput, 1); + vm.prank(_inputSender); + _portal.sendInputWithENS(_appContract, _node, name, execLayerData); + } + + function _encodePayload( + bytes32 node, + bytes calldata name, + bytes calldata execLayerData + ) internal pure returns (bytes memory) { + return InputEncoding.encodeENSInput(node, name, execLayerData); + } + + function _encodeAddInput( + bytes memory payload + ) internal view returns (bytes memory) { + return abi.encodeCall(IInputBox.addInput, (_appContract, payload)); + } +}