diff --git a/audit/auditLog.json b/audit/auditLog.json index c14ba974f..50e1354a0 100644 --- a/audit/auditLog.json +++ b/audit/auditLog.json @@ -7,6 +7,13 @@ "auditReportPath": "./audit/reports/2024.08.14_StargateFacetV2_ReAudit.pdf", "auditCommitHash": "d622002440317580b5d0fb90ef22b839d84957e2" }, + "audit20240902": { + "auditCompletedOn": "02.09.2024", + "auditedBy": "Sujith Somraaj (individual security researcher)", + "auditorGitHandle": "sujithsomraaj", + "auditReportPath": "./audit/reports/2024.09.02_CalldataVerificationFacet.pdf", + "auditCommitHash": "374d066a2d4ffd98eb6042cac7a2dd4aa0bf2ff8" + }, "audit20240913": { "auditCompletedOn": "13.09.2024", "auditedBy": "Sujith Somraaj (individual security researcher)", @@ -137,6 +144,11 @@ "audit20241206" ] }, + "CalldataVerificationFacet": { + "1.2.0": [ + "audit20240902" + ] + }, "DeBridgeDlnFacet": { "1.0.0": [ "audit20241205" @@ -181,6 +193,11 @@ "audit20250110_1" ] }, + "IAcrossSpokePool": { + "1.0.0": [ + "audit20250106" + ] + }, "IGasZip": { "1.0.0": [ "audit20241107" diff --git a/audit/reports/2024.09.02_CalldataVerificationFacet.pdf b/audit/reports/2024.09.02_CalldataVerificationFacet.pdf new file mode 100644 index 000000000..453844524 Binary files /dev/null and b/audit/reports/2024.09.02_CalldataVerificationFacet.pdf differ diff --git a/src/Facets/CalldataVerificationFacet.sol b/src/Facets/CalldataVerificationFacet.sol index fd87f9554..b664ed231 100644 --- a/src/Facets/CalldataVerificationFacet.sol +++ b/src/Facets/CalldataVerificationFacet.sol @@ -4,16 +4,20 @@ pragma solidity ^0.8.17; import { ILiFi } from "../Interfaces/ILiFi.sol"; import { LibSwap } from "../Libraries/LibSwap.sol"; import { AmarokFacet } from "./AmarokFacet.sol"; +import { AcrossFacetV3 } from "./AcrossFacetV3.sol"; +import { StargateFacetV2 } from "./StargateFacetV2.sol"; import { StargateFacet } from "./StargateFacet.sol"; import { AcrossFacetV3 } from "./AcrossFacetV3.sol"; import { CelerIMFacetBase, CelerIM } from "lifi/Helpers/CelerIMFacetBase.sol"; import { StandardizedCallFacet } from "lifi/Facets/StandardizedCallFacet.sol"; import { LibBytes } from "../Libraries/LibBytes.sol"; +import { GenericSwapFacetV3 } from "lifi/Facets/GenericSwapFacetV3.sol"; +import { InvalidCallData } from "../Errors/GenericErrors.sol"; /// @title CalldataVerificationFacet /// @author LI.FI (https://li.fi) /// @notice Provides functionality for verifying calldata -/// @custom:version 1.1.2 +/// @custom:version 1.2.0 contract CalldataVerificationFacet { using LibBytes for bytes; @@ -67,7 +71,7 @@ contract CalldataVerificationFacet { function extractMainParameters( bytes calldata data ) - public + external pure returns ( string memory bridge, @@ -101,24 +105,25 @@ contract CalldataVerificationFacet { ); } - // @notice Extracts the non-EVM address from the calldata - // @param data The calldata to extract the non-EVM address from - // @return nonEVMAddress The non-EVM address extracted from the calldata + /// @notice Extracts the non-EVM address from the calldata + /// @param data The calldata to extract the non-EVM address from + /// @return nonEVMAddress The non-EVM address extracted from the calldata function extractNonEVMAddress( bytes calldata data ) external pure returns (bytes32 nonEVMAddress) { - bytes memory callData = data; - ILiFi.BridgeData memory bridgeData = _extractBridgeData(data); + bytes memory callData; if ( bytes4(data[:4]) == StandardizedCallFacet.standardizedCall.selector ) { // standardizedCall callData = abi.decode(data[4:], (bytes)); + } else { + callData = data; } // Non-EVM address is always the first parameter of bridge specific data - if (bridgeData.hasSourceSwaps) { + if (_extractBridgeData(data).hasSourceSwaps) { assembly { let offset := mload(add(callData, 0x64)) // Get the offset of the bridge specific data nonEVMAddress := mload(add(callData, add(offset, 0x24))) // Get the non-EVM address @@ -151,30 +156,63 @@ contract CalldataVerificationFacet { uint256 receivingAmount ) { + // valid callData for a genericSwap call should have at least 484 bytes: + // Function selector: 4 bytes + // _transactionId: 32 bytes + // _integrator: 64 bytes + // _referrer: 64 bytes + // _receiver: 32 bytes + // _minAmountOut: 32 bytes + // _swapData: 256 bytes + if (data.length <= 484) { + revert InvalidCallData(); + } + LibSwap.SwapData[] memory swapData; - bytes memory callData = data; + bytes memory callData; + bytes4 functionSelector = bytes4(data[:4]); + // check if this is a call via StandardizedCallFacet if ( - bytes4(data[:4]) == StandardizedCallFacet.standardizedCall.selector + functionSelector == StandardizedCallFacet.standardizedCall.selector ) { - // standardizedCall - callData = abi.decode(data[4:], (bytes)); + // extract nested function selector and calldata + // will always start at position 68 + functionSelector = bytes4(data[68:72]); + callData = data[68:]; + // callData = abi.decode(data[4:], (bytes)); // this one is also valid, even though the calldata differs slightly (add. padding) + } else { + callData = data; + } + + if ( + functionSelector == + GenericSwapFacetV3.swapTokensSingleV3ERC20ToERC20.selector || + functionSelector == + GenericSwapFacetV3.swapTokensSingleV3ERC20ToNative.selector || + functionSelector == + GenericSwapFacetV3.swapTokensSingleV3NativeToERC20.selector + ) { + // single swap + swapData = new LibSwap.SwapData[](1); + + // extract parameters from calldata + (, , , receiver, receivingAmount, swapData[0]) = abi.decode( + callData.slice(4, callData.length - 4), + (bytes32, string, string, address, uint256, LibSwap.SwapData) + ); + } else { + // multi swap or GenericSwap V1 call + (, , , receiver, receivingAmount, swapData) = abi.decode( + callData.slice(4, callData.length - 4), + (bytes32, string, string, address, uint256, LibSwap.SwapData[]) + ); } - (, , , receiver, receivingAmount, swapData) = abi.decode( - callData.slice(4, callData.length - 4), - (bytes32, string, string, address, uint256, LibSwap.SwapData[]) - ); + // extract missing return parameters from swapData sendingAssetId = swapData[0].sendingAssetId; amount = swapData[0].fromAmount; receivingAssetId = swapData[swapData.length - 1].receivingAssetId; - return ( - sendingAssetId, - amount, - receiver, - receivingAssetId, - receivingAmount - ); } /// @notice Validates the calldata @@ -189,7 +227,7 @@ contract CalldataVerificationFacet { /// or type(uint256).max to ignore /// @param hasSourceSwaps Whether the calldata has source swaps /// @param hasDestinationCall Whether the calldata has a destination call - /// @return isValid Whether the calldata is validate + /// @return isValid Returns true if the calldata is valid function validateCalldata( bytes calldata data, string calldata bridge, @@ -200,22 +238,14 @@ contract CalldataVerificationFacet { bool hasSourceSwaps, bool hasDestinationCall ) external pure returns (bool isValid) { - ILiFi.BridgeData memory bridgeData; - ( - bridgeData.bridge, - bridgeData.sendingAssetId, - bridgeData.receiver, - bridgeData.minAmount, - bridgeData.destinationChainId, - bridgeData.hasSourceSwaps, - bridgeData.hasDestinationCall - ) = extractMainParameters(data); + ILiFi.BridgeData memory bridgeData = _extractBridgeData(data); + + bytes32 bridgeNameHash = keccak256(abi.encodePacked(bridge)); return // Check bridge - (keccak256(abi.encodePacked(bridge)) == - keccak256(abi.encodePacked("")) || + (bridgeNameHash == keccak256(abi.encodePacked("")) || keccak256(abi.encodePacked(bridgeData.bridge)) == - keccak256(abi.encodePacked(bridge))) && + bridgeNameHash) && // Check sendingAssetId (sendingAssetId == 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF || bridgeData.sendingAssetId == sendingAssetId) && @@ -235,9 +265,9 @@ contract CalldataVerificationFacet { /// @notice Validates the destination calldata /// @param data The calldata to validate - /// @param callTo The call to address to validate + /// @param callTo The callTo address to validate /// @param dstCalldata The destination calldata to validate - /// @return isValid Returns true if the calldata matches with the provided parameters + /// @return isValid Returns true if the calldata is valid function validateDestinationCalldata( bytes calldata data, bytes calldata callTo, @@ -254,6 +284,7 @@ contract CalldataVerificationFacet { bytes4 selector = abi.decode(callData, (bytes4)); + // --------------------------------------- // Case: Amarok if (selector == AmarokFacet.startBridgeTokensViaAmarok.selector) { (, AmarokFacet.AmarokData memory amarokData) = abi.decode( @@ -277,6 +308,7 @@ contract CalldataVerificationFacet { abi.decode(callTo, (address)) == amarokData.callTo; } + // --------------------------------------- // Case: Stargate if (selector == StargateFacet.startBridgeTokensViaStargate.selector) { (, StargateFacet.StargateData memory stargateData) = abi.decode( @@ -303,6 +335,51 @@ contract CalldataVerificationFacet { keccak256(dstCalldata) == keccak256(stargateData.callData) && keccak256(callTo) == keccak256(stargateData.callTo); } + + // --------------------------------------- + // Case: StargateV2 + + if ( + selector == StargateFacetV2.startBridgeTokensViaStargate.selector + ) { + (, StargateFacetV2.StargateData memory stargateDataV2) = abi + .decode( + callData.slice(4, callData.length - 4), + (ILiFi.BridgeData, StargateFacetV2.StargateData) + ); + + return + keccak256(dstCalldata) == + keccak256(stargateDataV2.sendParams.composeMsg) && + _compareBytesToBytes32CallTo( + callTo, + stargateDataV2.sendParams.to + ); + } + if ( + selector == + StargateFacetV2.swapAndStartBridgeTokensViaStargate.selector + ) { + (, , StargateFacetV2.StargateData memory stargateDataV2) = abi + .decode( + callData.slice(4, callData.length - 4), + ( + ILiFi.BridgeData, + LibSwap.SwapData[], + StargateFacetV2.StargateData + ) + ); + + return + keccak256(dstCalldata) == + keccak256(stargateDataV2.sendParams.composeMsg) && + _compareBytesToBytes32CallTo( + callTo, + stargateDataV2.sendParams.to + ); + } + + // --------------------------------------- // Case: Celer if ( selector == CelerIMFacetBase.startBridgeTokensViaCelerIM.selector @@ -357,6 +434,37 @@ contract CalldataVerificationFacet { keccak256(abi.encode(acrossV3Data.receiverAddress)); } + // --------------------------------------- + // Case: AcrossV3 + if (selector == AcrossFacetV3.startBridgeTokensViaAcrossV3.selector) { + (, AcrossFacetV3.AcrossV3Data memory acrossV3Data) = abi.decode( + callData.slice(4, callData.length - 4), + (ILiFi.BridgeData, AcrossFacetV3.AcrossV3Data) + ); + + return + keccak256(dstCalldata) == keccak256(acrossV3Data.message) && + keccak256(callTo) == + keccak256(abi.encode(acrossV3Data.receiverAddress)); + } + if ( + selector == + AcrossFacetV3.swapAndStartBridgeTokensViaAcrossV3.selector + ) { + (, , AcrossFacetV3.AcrossV3Data memory acrossV3Data) = abi.decode( + callData.slice(4, callData.length - 4), + ( + ILiFi.BridgeData, + LibSwap.SwapData[], + AcrossFacetV3.AcrossV3Data + ) + ); + return + keccak256(dstCalldata) == keccak256(acrossV3Data.message) && + keccak256(callTo) == + keccak256(abi.encode(acrossV3Data.receiverAddress)); + } + // All other cases return false; } @@ -407,4 +515,27 @@ contract CalldataVerificationFacet { (ILiFi.BridgeData, LibSwap.SwapData[]) ); } + + function _compareBytesToBytes32CallTo( + bytes memory callTo, + bytes32 callToBytes32 + ) private pure returns (bool) { + require( + callTo.length >= 20, + "Invalid callTo length; expected at least 20 bytes" + ); + + // Convert bytes to address type from callTo + address callToAddress; + assembly { + callToAddress := mload(add(callTo, 32)) + } + + // Convert callToBytes32 to address type and compare them + address callToAddressFromBytes32 = address( + uint160(uint256(callToBytes32)) + ); + + return callToAddress == callToAddressFromBytes32; + } } diff --git a/src/Interfaces/IAcrossSpokePool.sol b/src/Interfaces/IAcrossSpokePool.sol index aef0cfc3f..1f793d64c 100644 --- a/src/Interfaces/IAcrossSpokePool.sol +++ b/src/Interfaces/IAcrossSpokePool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -/// @custom:version 1.0.0 pragma solidity ^0.8.17; +/// @custom:version 1.0.0 interface IAcrossSpokePool { function deposit( address recipient, // Recipient address diff --git a/test/solidity/Facets/CalldataVerificationFacet.t.sol b/test/solidity/Facets/CalldataVerificationFacet.t.sol index 27bc2d6ae..1637190ac 100644 --- a/test/solidity/Facets/CalldataVerificationFacet.t.sol +++ b/test/solidity/Facets/CalldataVerificationFacet.t.sol @@ -4,21 +4,33 @@ pragma solidity ^0.8.17; import { CalldataVerificationFacet } from "lifi/Facets/CalldataVerificationFacet.sol"; import { AmarokFacet } from "lifi/Facets/AmarokFacet.sol"; import { MayanFacet } from "lifi/Facets/MayanFacet.sol"; -import { StargateFacet } from "lifi/Facets/StargateFacet.sol"; import { AcrossFacetV3 } from "lifi/Facets/AcrossFacetV3.sol"; +import { StargateFacet } from "lifi/Facets/StargateFacet.sol"; +import { StargateFacetV2 } from "lifi/Facets/StargateFacetV2.sol"; +import { IStargate } from "lifi/Interfaces/IStargate.sol"; import { StandardizedCallFacet } from "lifi/Facets/StandardizedCallFacet.sol"; import { CelerIM, CelerIMFacetBase } from "lifi/Helpers/CelerIMFacetBase.sol"; import { GenericSwapFacet } from "lifi/Facets/GenericSwapFacet.sol"; +import { GenericSwapFacetV3 } from "lifi/Facets/GenericSwapFacetV3.sol"; import { ILiFi } from "lifi/Interfaces/ILiFi.sol"; import { LibSwap } from "lifi/Libraries/LibSwap.sol"; import { TestBase } from "../utils/TestBase.sol"; +import { LibBytes } from "lifi/Libraries/LibBytes.sol"; + import { MsgDataTypes } from "celer-network/contracts/message/libraries/MessageSenderLib.sol"; -import "forge-std/console.sol"; +import { console } from "forge-std/console.sol"; +import { InvalidCallData } from "lifi/Errors/GenericErrors.sol"; +import { OFTComposeMsgCodec } from "lifi/Periphery/ReceiverStargateV2.sol"; contract CalldataVerificationFacetTest is TestBase { + using LibBytes for bytes; + using OFTComposeMsgCodec for address; + CalldataVerificationFacet internal calldataVerificationFacet; function setUp() public { + customBlockNumberForForking = 19979843; + initTestBase(); calldataVerificationFacet = new CalldataVerificationFacet(); bridgeData = ILiFi.BridgeData({ transactionId: keccak256("id"), @@ -44,6 +56,16 @@ contract CalldataVerificationFacetTest is TestBase { requiresDeposit: true }) ); + + // set facet address in TestBase + setFacetAddressInTestBase( + address(calldataVerificationFacet), + "CalldataVerificationFacet" + ); + } + + function test_DeploysWithoutErrors() public { + calldataVerificationFacet = new CalldataVerificationFacet(); } function test_IgnoresExtraBytes() public view { @@ -278,9 +300,129 @@ contract CalldataVerificationFacetTest is TestBase { assertEq(hasDestinationCall, bridgeData.hasDestinationCall); } - function test_CanExtractGenericSwapParameters() public { + function test_RevertsOnInvalidGenericSwapCallData() public { + // prepare minimum callData + swapData[0] = LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: address(123), + receivingAssetId: address(456), + fromAmount: 1, + callData: "", + requiresDeposit: false + }); + bytes memory callData = abi.encodeWithSelector( + GenericSwapFacetV3.swapTokensSingleV3ERC20ToERC20.selector, + keccak256(""), + "", + "", + payable(address(1234)), + 1, + swapData[0] + ); + + // reduce calldata to 483 bytes to not meet min calldata length threshold + callData = callData.slice(0, 483); + + vm.expectRevert(InvalidCallData.selector); + + calldataVerificationFacet.extractGenericSwapParameters(callData); + } + + function test_CanExtractGenericSwapMinCallData() public { + swapData[0] = LibSwap.SwapData({ + callTo: address(uniswap), + approveTo: address(uniswap), + sendingAssetId: address(123), + receivingAssetId: address(456), + fromAmount: 1, + callData: "", + requiresDeposit: false + }); + bytes memory callData = abi.encodeWithSelector( + GenericSwapFacetV3.swapTokensSingleV3ERC20ToERC20.selector, + keccak256(""), + "", + "", + payable(address(1234)), + 1, + swapData[0] + ); + + ( + address sendingAssetId, + uint256 amount, + address receiver, + address receivingAssetId, + uint256 receivingAmount + ) = calldataVerificationFacet.extractGenericSwapParameters(callData); + + assertEq(sendingAssetId, swapData[0].sendingAssetId); + assertEq(amount, swapData[0].fromAmount); + assertEq(receiver, address(1234)); + assertEq( + receivingAssetId, + swapData[swapData.length - 1].receivingAssetId + ); + assertEq(receivingAmount, 1); + } + + function test_CanExtractGenericSwapV3SingleParameters() public { + bytes memory callData = abi.encodeWithSelector( + GenericSwapFacetV3.swapTokensSingleV3ERC20ToERC20.selector, + keccak256("id"), + "acme", + "acme", + payable(address(1234)), + 1 ether, + swapData[0] + ); + + ( + address sendingAssetId, + uint256 amount, + address receiver, + address receivingAssetId, + uint256 receivingAmount + ) = calldataVerificationFacet.extractGenericSwapParameters(callData); + + assertEq(sendingAssetId, swapData[0].sendingAssetId); + assertEq(amount, swapData[0].fromAmount); + assertEq(receiver, address(1234)); + assertEq( + receivingAssetId, + swapData[swapData.length - 1].receivingAssetId + ); + assertEq(receivingAmount, 1 ether); + + // StandardizedCall + bytes memory standardizedCallData = abi.encodeWithSelector( + StandardizedCallFacet.standardizedCall.selector, + callData + ); + ( + sendingAssetId, + amount, + receiver, + receivingAssetId, + receivingAmount + ) = calldataVerificationFacet.extractGenericSwapParameters( + standardizedCallData + ); + + assertEq(sendingAssetId, swapData[0].sendingAssetId); + assertEq(amount, swapData[0].fromAmount); + assertEq(receiver, address(1234)); + assertEq( + receivingAssetId, + swapData[swapData.length - 1].receivingAssetId + ); + assertEq(receivingAmount, 1 ether); + } + + function test_CanExtractGenericSwapV3MultipleParameters() public { bytes memory callData = abi.encodeWithSelector( - GenericSwapFacet.swapTokensGeneric.selector, + GenericSwapFacetV3.swapTokensMultipleV3ERC20ToERC20.selector, keccak256("id"), "acme", "acme", @@ -602,6 +744,100 @@ contract CalldataVerificationFacetTest is TestBase { assertFalse(badCall); } + function test_CanValidateStargateV2DestinationCalldata() public { + uint16 ASSET_ID_USDC = 1; + address STARGATE_POOL_USDC = 0xc026395860Db2d07ee33e05fE50ed7bD583189C7; + + StargateFacetV2.StargateData memory stargateData = StargateFacetV2 + .StargateData({ + assetId: ASSET_ID_USDC, + sendParams: IStargate.SendParam({ + dstEid: 30150, + to: USER_RECEIVER.addressToBytes32(), + amountLD: defaultUSDCAmount, + minAmountLD: (defaultUSDCAmount * 9e4) / 1e5, + extraOptions: "", + composeMsg: bytes("foobarbytes"), + oftCmd: OftCmdHelper.bus() + }), + fee: IStargate.MessagingFee({ nativeFee: 0, lzTokenFee: 0 }), + refundAddress: payable(USER_REFUND) + }); + + // get quote and update fee information in stargateData + IStargate.MessagingFee memory fees = IStargate(STARGATE_POOL_USDC) + .quoteSend(stargateData.sendParams, false); + stargateData.fee = fees; + + bytes memory callData = abi.encodeWithSelector( + StargateFacetV2.startBridgeTokensViaStargate.selector, + bridgeData, + stargateData + ); + + bytes memory callDataWithSwap = abi.encodeWithSelector( + StargateFacetV2.swapAndStartBridgeTokensViaStargate.selector, + bridgeData, + swapData, + stargateData + ); + + bool validCall = calldataVerificationFacet.validateDestinationCalldata( + callData, + abi.encode(USER_RECEIVER), + bytes("foobarbytes") + ); + bool validCallWithSwap = calldataVerificationFacet + .validateDestinationCalldata( + callDataWithSwap, + abi.encode(USER_RECEIVER), + bytes("foobarbytes") + ); + + bool badCall = calldataVerificationFacet.validateDestinationCalldata( + callData, + abi.encode(USER_RECEIVER), + bytes("badbytes") + ); + + assertTrue(validCall); + assertTrue(validCallWithSwap); + assertFalse(badCall); + + // StandardizedCall + bytes memory standardizedCallData = abi.encodeWithSelector( + StandardizedCallFacet.standardizedCall.selector, + callData + ); + + bytes memory standardizedCallDataWithSwap = abi.encodeWithSelector( + StandardizedCallFacet.standardizedCall.selector, + callDataWithSwap + ); + + validCall = calldataVerificationFacet.validateDestinationCalldata( + standardizedCallData, + abi.encode(USER_RECEIVER), + bytes("foobarbytes") + ); + validCallWithSwap = calldataVerificationFacet + .validateDestinationCalldata( + standardizedCallDataWithSwap, + abi.encode(USER_RECEIVER), + bytes("foobarbytes") + ); + + badCall = calldataVerificationFacet.validateDestinationCalldata( + standardizedCallData, + abi.encode(USER_RECEIVER), + bytes("badbytes") + ); + + assertTrue(validCall); + assertTrue(validCallWithSwap); + assertFalse(badCall); + } + function test_CanValidateCelerIMDestinationCalldata() public { CelerIM.CelerIMData memory cimData = CelerIM.CelerIMData({ maxSlippage: 1, @@ -765,6 +1001,31 @@ contract CalldataVerificationFacetTest is TestBase { assertFalse(badCall); } + function test_RevertsOnDestinationCalldataWithInvalidSelector() public { + CelerIM.CelerIMData memory cimData = CelerIM.CelerIMData({ + maxSlippage: 1, + nonce: 2, + callTo: abi.encode(USER_RECEIVER), + callData: bytes("foobarbytes"), + messageBusFee: 3, + bridgeType: MsgDataTypes.BridgeSendType.Liquidity + }); + + bytes memory callData = abi.encodeWithSelector( + GenericSwapFacet.swapTokensGeneric.selector, // wrong selector, does not support destination calls + bridgeData, + cimData + ); + + bool validCall = calldataVerificationFacet.validateDestinationCalldata( + callData, + abi.encode(USER_RECEIVER), + bytes("foobarbytes") + ); + + assertFalse(validCall); + } + function checkBridgeData(ILiFi.BridgeData memory data) internal { assertTrue(data.transactionId == bridgeData.transactionId); assertEq(data.bridge, bridgeData.bridge); @@ -778,3 +1039,13 @@ contract CalldataVerificationFacetTest is TestBase { assertTrue(data[0].receivingAssetId == swapData[0].receivingAssetId); } } + +library OftCmdHelper { + function taxi() internal pure returns (bytes memory) { + return ""; + } + + function bus() internal pure returns (bytes memory) { + return new bytes(1); + } +}