diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f7233e..028617a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - name: Run Forge tests run: | - forge test --fork-url https://rpc.ankr.com/eth_goerli -vvvv --fork-block-number 8897000 + forge test --fork-url https://rpc.ankr.com/eth_goerli -vvvv --fork-block-number 8897777 id: test - name: Initialise status badge diff --git a/src/CCIP2ETH.sol b/src/CCIP2ETH.sol index 0278495..56ad83f 100644 --- a/src/CCIP2ETH.sol +++ b/src/CCIP2ETH.sol @@ -62,7 +62,7 @@ contract CCIP2ETH is iCCIP2ETH { /// @dev - Constructor constructor(address _gateway) { gateway = iGatewayManager(_gateway); - chainID = gateway.uintToString(block.chainid); + chainID = block.chainid == 1 ? "1" : "5"; // mainnet or goerli /// @dev - Sets ENS Mainnet wrapper as Wrapper isWrapper[0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401] = true; emit UpdatedWrapper(0xD4416b13d2b3a9aBae7AcD5D6C2BbDBE25686401, true); @@ -82,14 +82,6 @@ contract CCIP2ETH is iCCIP2ETH { supportsInterface[iCallbackType.signedRedirect.selector] = true; } - /// Note - Checks for admin privileges - modifier OnlyDev() { - if (msg.sender != gateway.owner()) { - revert NotAuthorised("NOT_DEV"); - } - _; - } - /** * @dev Gets recordhash for a node * @param _node - Namehash of domain.eth, or bytes32(address _Owner) @@ -268,9 +260,6 @@ contract CCIP2ETH is iCCIP2ETH { } if (_recordhash.length == 0) { _recordhash = recordhash[bytes32(uint256(uint160(_owner)))]; - if (_recordhash.length == 0) { - _recordhash = abi.encodePacked("https://ccip.namesys.xyz"); // Web2 fallback - } } string memory _recType = gateway.funcToJson(request); // Filename for the requested record bytes32 _checkhash = @@ -312,14 +301,14 @@ contract CCIP2ETH is iCCIP2ETH { string memory _domain, // String-formatted complete 'a.b.c.domain.eth' string memory _recType, // Record type , // Complete reverse-DNS path for __fallback() - , // DNS-encoded domain.eth + , // DNS-encoded full domain.eth bytes memory _request // Format: + + ) = abi.decode(extradata, (bytes32, uint256, bytes32, string, string, string, bytes, bytes)); address _owner = ENS.owner(_node); if (isWrapper[_owner]) { _owner = iToken(_owner).ownerOf(uint256(_node)); } - /// @dev - Timeout in 4 blocks (must be < 256 blocks) + /// @dev - Timeout in 4 blocks if (block.number > _blocknumber + 5) { revert InvalidRequest("BLOCK_TIMEOUT"); } @@ -382,7 +371,7 @@ contract CCIP2ETH is iCCIP2ETH { "Requesting Signature To Install dApp Service\n", "\nOrigin: ", _domain, // e.g. ens.domain.eth - "\ndApp: ", + "\nDApp: ", _redirectDomain, // e.g. app.ens.eth "\nExtradata: 0x", gateway.bytesToHexString(abi.encodePacked(keccak256(result)), 0), @@ -568,21 +557,29 @@ contract CCIP2ETH is iCCIP2ETH { /// @dev : Management functions + /// @dev : Checks for admin privileges + modifier OnlyDev() { + if (msg.sender != gateway.owner()) { + revert NotAuthorised("NOT_DEV"); + } + _; + } + /// @dev - Returns owner of the contract function owner() public view returns (address) { return gateway.owner(); } - /// @dev - Updates ChainID in case of a hardfork + /// @dev - Updates ChainID in case of a hardfork function updateChainID() public { chainID = gateway.uintToString(block.chainid); } + /** * @dev Sets fees for ownerhash * Note - Set to 0 at launch * @param _wei - Fees in WEI per EOA */ - function updateOwnerhashFees(uint256 _wei) external OnlyDev { ownerhashFees = _wei; } diff --git a/src/GatewayManager.sol b/src/GatewayManager.sol index 4503178..7d94ef7 100644 --- a/src/GatewayManager.sol +++ b/src/GatewayManager.sol @@ -12,14 +12,16 @@ import "./Interface.sol"; */ contract GatewayManager is iERC173, iGatewayManager { /// @dev - Events - event AddGateway(string indexed domain); - event RemoveGateway(string indexed domain); - event UpdateFuncFile(bytes4 _func, string _name); - + event Web3GatewayUpdated(string indexed domain); + event Web3GatewayRemoved(string indexed domain); + event FuncMapUpdated(bytes4 _func, string _name); + event Web2GatewayUpdated(string indexed domain); + event Web2GatewayRemoved(string indexed domain); /// @dev - Errors + error ContenthashNotImplemented(bytes1 _type); error InvalidRequest(string _message); - error UnimplementedFeature(bytes4 func); + error FeatureNotImplemented(bytes4 func); /// @dev - Contract owner/multisig address address public owner; @@ -33,8 +35,11 @@ contract GatewayManager is iERC173, iGatewayManager { address immutable THIS = address(this); /// @dev - Primary IPFS gateway domain, ipfs2.eth.limo string public PrimaryGateway = "ipfs2.eth.limo"; - /// @dev - List of secondary gateway domains - string[] public Gateways; + /// @dev - List of secondary gateway domains (default) + string[] public Web3Gateways; + /// @dev - List of web2/L2 service gateway domains (fallback) + string[] public Web2Gateways; + /// @dev - Resolver function bytes4 selector → Off-chain record filename .json mapping(bytes4 => string) public funcMap; @@ -49,10 +54,13 @@ contract GatewayManager is iERC173, iGatewayManager { funcMap[iResolver.contenthash.selector] = "contenthash"; funcMap[iResolver.zonehash.selector] = "dns/zonehash"; /// @dev - Set initial list of secondary gateways - Gateways.push("dweb.link"); - emit AddGateway("dweb.link"); - Gateways.push("ipfs.io"); - emit AddGateway("ipfs.io"); + Web3Gateways.push("dweb.link"); + emit Web3GatewayUpdated("dweb.link"); + Web3Gateways.push("ipfs.io"); + emit Web3GatewayUpdated("ipfs.io"); + + Web2Gateways.push("ccip.namesys.xyz"); + emit Web2GatewayUpdated("ccip.namesys.xyz"); } /** @@ -69,20 +77,29 @@ contract GatewayManager is iERC173, iGatewayManager { returns (string[] memory gateways) { /// @dev Filter recordhash vs. web2 gateway - if (_recordhash.length == 32) { - // Short IPNS hash - _recordhash = abi.encodePacked(hex"e5010172002408011220", _recordhash); - } else if (iGatewayManager(this).isWeb2(_recordhash)) { - // Web2 fallback + if (_recordhash.length == 0) { + // Default L2/Web2 service fallback + uint256 gateLen = Web2Gateways.length; + gateways = new string[](gateLen); + while (gateLen > 0) { + --gateLen; + gateways[gateLen] = string.concat(string("https://"), Web2Gateways[gateLen], _path, ".json?t={data}"); + } + return gateways; + } else if (_recordhash[0] == bytes1("h") && iGatewayManager(this).isWeb2(_recordhash)) { + // Web2 set by owner gateways = new string[](1); gateways[0] = string.concat(string(_recordhash), _path, ".json?t={data}"); return gateways; + } else if (_recordhash.length == 32) { + // Short IPNS hash + _recordhash = abi.encodePacked(hex"e5010172002408011220", _recordhash); } unchecked { - uint256 gLen = Gateways.length; + uint256 gLen = Web3Gateways.length; uint256 len = (gLen / 2) + 2; - if (len > 4) len = 4; - gateways = new string[](len); + if (len > 3) len = 3; + gateways = new string[](len+1); uint256 i; if (bytes(PrimaryGateway).length > 0) { gateways[i++] = string.concat( @@ -116,8 +133,9 @@ contract GatewayManager is iERC173, iGatewayManager { } while (i < len) { seed = uint256(keccak256(abi.encodePacked(block.number * i, seed))); - gateways[i++] = string.concat("https://", Gateways[seed % gLen], _fullPath); + gateways[i++] = string.concat("https://", Web3Gateways[seed % gLen], _fullPath); } + gateways[len] = string.concat("https://", Web2Gateways[0], _fullPath); // fallback, } } @@ -128,7 +146,7 @@ contract GatewayManager is iERC173, iGatewayManager { this; response; extradata; - revert UnimplementedFeature(iGatewayManager.__fallback.selector); + revert FeatureNotImplemented(iGatewayManager.__fallback.selector); } /** @@ -170,7 +188,7 @@ contract GatewayManager is iERC173, iGatewayManager { } _jsonPath = string.concat("dns/", uintToString(resource)); } else { - revert UnimplementedFeature(func); + revert FeatureNotImplemented(func); } } @@ -281,35 +299,73 @@ contract GatewayManager is iERC173, iGatewayManager { */ function addFuncMap(bytes4 _func, string calldata _name) external onlyDev { funcMap[_func] = _name; - emit UpdateFuncFile(_func, _name); + emit FuncMapUpdated(_func, _name); } /** - * @dev Shows list of all available gateways - * @return list - List of gateways + * @dev Shows list of web3 gateways + * @return List of gateways */ - function listGateways() external view returns (string[] memory list) { - return Gateways; + function listWeb3Gateways() external view returns (string[] memory) { + return Web3Gateways; } /** * @dev Add a single gateway * @param _domain - New gateway domain to add */ - function addGateway(string calldata _domain) external onlyDev { - Gateways.push(_domain); - emit AddGateway(_domain); + function addWeb3Gateway(string calldata _domain) external onlyDev { + Web3Gateways.push(_domain); + emit Web3GatewayUpdated(_domain); + } + + /** + * @dev Remove a single gateway + * @param _index - Gateway index to remove + */ + function removeWeb3Gateway(uint256 _index) external onlyDev { + require(Web3Gateways.length > 1, "Last Gateway"); + emit Web3GatewayRemoved(Web3Gateways[_index]); + Web3Gateways[_index] = Web3Gateways[Web3Gateways.length - 1]; + Web3Gateways.pop(); } + /** + * @dev Replace a single gateway + * @param _index : Gateway index to replace + * @param _domain : New gateway domain.tld + */ + function replaceWeb3Gateway(uint256 _index, string calldata _domain) external onlyDev { + emit Web3GatewayRemoved(Web3Gateways[_index]); + Web3Gateways[_index] = _domain; + emit Web3GatewayUpdated(_domain); + } + + /** + * @dev Shows list of web2 gateways + * @return List of gateways + */ + function listWeb2Gateways() external view returns (string[] memory) { + return Web2Gateways; + } + + /** + * @dev Add a single gateway + * @param _domain - New gateway domain to add + */ + function addWeb2Gateway(string calldata _domain) external onlyDev { + Web2Gateways.push(_domain); + emit Web2GatewayUpdated(_domain); + } /** * @dev Remove a single gateway * @param _index - Gateway index to remove */ - function removeGateway(uint256 _index) external onlyDev { - require(Gateways.length > 1, "Last Gateway"); - emit RemoveGateway(Gateways[_index]); - Gateways[_index] = Gateways[Gateways.length - 1]; - Gateways.pop(); + function removeWeb2Gateway(uint256 _index) external onlyDev { + require(Web2Gateways.length > 1, "Last Gateway"); + emit Web2GatewayRemoved(Web2Gateways[_index]); + Web2Gateways[_index] = Web2Gateways[Web2Gateways.length - 1]; + Web2Gateways.pop(); } /** @@ -317,10 +373,10 @@ contract GatewayManager is iERC173, iGatewayManager { * @param _index : Gateway index to replace * @param _domain : New gateway domain.tld */ - function replaceGateway(uint256 _index, string calldata _domain) external onlyDev { - emit RemoveGateway(Gateways[_index]); - Gateways[_index] = _domain; - emit AddGateway(_domain); + function replaceWeb2Gateway(uint256 _index, string calldata _domain) external onlyDev { + emit Web2GatewayRemoved(Web2Gateways[_index]); + Web2Gateways[_index] = _domain; + emit Web2GatewayUpdated(_domain); } /** diff --git a/src/Interface.sol b/src/Interface.sol index 06d7bc1..c510270 100644 --- a/src/Interface.sol +++ b/src/Interface.sol @@ -58,18 +58,24 @@ interface iGatewayManager is iERC173 { function bytesToHexString(bytes calldata _buffer, uint256 _start) external pure returns (string memory); function bytes32ToHexString(bytes32 _buffer) external pure returns (string memory); function funcToJson(bytes calldata _request) external view returns (string memory _jsonPath); - function listGateways() external view returns (string[] memory list); function toChecksumAddress(address _addr) external pure returns (string memory); function __fallback(bytes calldata response, bytes calldata extradata) external view returns (bytes memory result); function addFuncMap(bytes4 _func, string calldata _name) external; - function addGateway(string calldata _domain) external; - function removeGateway(uint256 _index) external; - function replaceGateway(uint256 _index, string calldata _domain) external; function formatSubdomain(bytes calldata _recordhash) external pure returns (string memory result); function isWeb2(bytes calldata _recordhash) external pure returns (bool); + + function listWeb2Gateways() external view returns (string[] memory list); + function addWeb2Gateway(string calldata _domain) external; + function removeWeb2Gateway(uint256 _index) external; + function replaceWeb2Gateway(uint256 _index, string calldata _domain) external; + + function listWeb3Gateways() external view returns (string[] memory list); + function addWeb3Gateway(string calldata _domain) external; + function removeWeb3Gateway(uint256 _index) external; + function replaceWeb3Gateway(uint256 _index, string calldata _domain) external; } interface iResolver { diff --git a/test/GatewayManager.t.sol b/test/GatewayManager.t.sol index 08498ed..f52c255 100644 --- a/test/GatewayManager.t.sol +++ b/test/GatewayManager.t.sol @@ -119,6 +119,112 @@ contract GatewayManagerTest is Test { bBytes = hex"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; assertEq(bStr, gateway.bytesToHexString(bBytes, 0)); } + + function test6_Web2Gateways() public { + string[] memory gateways = gateway.listWeb2Gateways(); + assertEq("ccip.namesys.xyz", gateways[0]); + assertEq(1, gateways.length); + + vm.expectRevert("Last Gateway"); + gateway.removeWeb2Gateway(0); + + gateway.addWeb2Gateway("ccip2.namesys.xyz"); + gateways = gateway.listWeb2Gateways(); + assertEq("ccip.namesys.xyz", gateways[0]); + assertEq("ccip2.namesys.xyz", gateways[1]); + assertEq(2, gateways.length); + + gateway.replaceWeb2Gateway(1, "ccipx.namesys.xyz"); + gateways = gateway.listWeb2Gateways(); + assertEq("ccip.namesys.xyz", gateways[0]); + assertEq("ccipx.namesys.xyz", gateways[1]); + assertEq(2, gateways.length); + + gateway.removeWeb2Gateway(0); + gateways = gateway.listWeb2Gateways(); + assertEq("ccipx.namesys.xyz", gateways[0]); + assertEq(1, gateways.length); + + gateway.addWeb2Gateway("ccip2.namesys.xyz"); + gateway.replaceWeb2Gateway(0, "ccip.namesys.xyz"); + gateway.removeWeb2Gateway(1); + gateways = gateway.listWeb2Gateways(); + assertEq("ccip.namesys.xyz", gateways[0]); + assertEq(1, gateways.length); + + } + + + function test7_Web3Gateways() public { + string[] memory gateways = gateway.listWeb3Gateways(); + assertEq("dweb.link", gateways[0]); + assertEq("ipfs.io", gateways[1]); + assertEq(2, gateways.length); + + gateway.removeWeb3Gateway(0); + vm.expectRevert("Last Gateway"); + gateway.removeWeb3Gateway(0); + + gateway.addWeb3Gateway("dweb.link"); + gateways = gateway.listWeb3Gateways(); + assertEq("dweb.link", gateways[1]); + assertEq("ipfs.io", gateways[0]); + assertEq(2, gateways.length); + + gateway.replaceWeb3Gateway(0, "ipfs.namesys.xyz"); + gateways = gateway.listWeb3Gateways(); + assertEq("dweb.link", gateways[1]); + assertEq("ipfs.namesys.xyz", gateways[0]); + assertEq(2, gateways.length); + + gateway.removeWeb3Gateway(0); + gateways = gateway.listWeb3Gateways(); + assertEq("dweb.link", gateways[0]); + assertEq(1, gateways.length); + + gateway.addWeb3Gateway("ipfs.namesys.xyz"); + gateway.replaceWeb3Gateway(0, "ipfs.io"); + gateway.removeWeb3Gateway(0); + gateways = gateway.listWeb3Gateways(); + assertEq("ipfs.namesys.xyz", gateways[0]); + assertEq(1, gateways.length); + } + + function test8_RandomGateway() public{ + string memory _path = "/.well-known/eth/freetib/contenthash"; + bytes memory _recordhash; + string[] memory gateways = gateway.randomGateways(_recordhash, _path, 0); + assertEq(gateways[0], "https://ccip.namesys.xyz/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(1, gateways.length); + gateway.addWeb2Gateway("ccip2.namesys.xyz"); + gateways = gateway.randomGateways(_recordhash, _path, 0); + assertEq(2, gateways.length); + assertEq(gateways[0], "https://ccip.namesys.xyz/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[1], "https://ccip2.namesys.xyz/.well-known/eth/freetib/contenthash.json?t={data}"); + + _recordhash = bytes("https://ccip.namesys.xyz"); + gateways = gateway.randomGateways(_recordhash, _path, 0); + assertEq(1, gateways.length); + assertEq(gateways[0], "https://ccip.namesys.xyz/.well-known/eth/freetib/contenthash.json?t={data}"); + + gateway.addWeb3Gateway("ipfs.namesys.xyz"); + gateway.addWeb3Gateway("ipfs2.namesys.xyz"); + _recordhash = hex"3c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c"; + gateways = gateway.randomGateways(_recordhash, _path, 0); + assertEq(4, gateways.length); + assertEq(gateways[0], "https://e5010172002408011220.3c5aba6c9b5055a5fa12281c486188ed.8ae2b6ef394b3d981b00d17a4b51735c.ipfs2.eth.limo/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[1], "https://dweb.link/ipns/f01720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[2], "https://ipfs.namesys.xyz/ipns/f01720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[3], "https://ccip.namesys.xyz/ipns/f01720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c/.well-known/eth/freetib/contenthash.json?t={data}"); + + _recordhash = hex"e50101720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c"; + gateways = gateway.randomGateways(_recordhash, _path, 0); + assertEq(4, gateways.length); + assertEq(gateways[0], "https://e5010172002408011220.3c5aba6c9b5055a5fa12281c486188ed.8ae2b6ef394b3d981b00d17a4b51735c.ipfs2.eth.limo/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[1], "https://dweb.link/ipns/f01720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[2], "https://ipfs.namesys.xyz/ipns/f01720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c/.well-known/eth/freetib/contenthash.json?t={data}"); + assertEq(gateways[3], "https://ccip.namesys.xyz/ipns/f01720024080112203c5aba6c9b5055a5fa12281c486188ed8ae2b6ef394b3d981b00d17a4b51735c/.well-known/eth/freetib/contenthash.json?t={data}"); + } } /// @dev Utility functions diff --git a/test/goerli.sh b/test/goerli.sh index fc6bfe2..5865e33 100755 --- a/test/goerli.sh +++ b/test/goerli.sh @@ -1 +1 @@ -forge fmt && source .env && forge test --fork-url $GOERLI_RPC_URL -vvv --fork-block-number 8897777 --gas-report --watch +forge fmt && source .env && forge test --fork-url $GOERLI_RPC_URL -vvv --fork-block-number 8897777 -vvvv --gas-report --watch