From d87fce03465e9b17b2c2e9a843dbbd81a3db6074 Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Wed, 11 Oct 2023 23:27:22 +0200 Subject: [PATCH] feat(evm): introduce v2 of CometblsClient --- evm/contracts/clients/CometblsClientV2.sol | 393 ++++++++++++++++++ .../{DevnetVerifier.sol => Verifier.sol} | 80 ++-- evm/contracts/core/IZKVerifierV2.sol | 11 + evm/contracts/lib/CometblsHelp.sol | 34 ++ evm/evm.nix | 6 +- 5 files changed, 474 insertions(+), 50 deletions(-) create mode 100644 evm/contracts/clients/CometblsClientV2.sol rename evm/contracts/clients/{DevnetVerifier.sol => Verifier.sol} (63%) create mode 100644 evm/contracts/core/IZKVerifierV2.sol diff --git a/evm/contracts/clients/CometblsClientV2.sol b/evm/contracts/clients/CometblsClientV2.sol new file mode 100644 index 0000000000..9e09b1a58c --- /dev/null +++ b/evm/contracts/clients/CometblsClientV2.sol @@ -0,0 +1,393 @@ +pragma solidity ^0.8.18; + +import "../core/02-client/ILightClient.sol"; +import "../core/02-client/IBCHeight.sol"; +import "../proto/ibc/core/client/v1/client.sol"; +import "../proto/ibc/lightclients/tendermint/v1/tendermint.sol"; +import "../proto/cosmos/ics23/v1/proofs.sol"; +import "../proto/tendermint/types/types.sol"; +import "../proto/tendermint/types/canonical.sol"; +import "../proto/union/ibc/lightclients/cometbls/v1/cometbls.sol"; +import "../proto/ibc/lightclients/wasm/v1/wasm.sol"; +import {GoogleProtobufAny as Any} from "../proto/GoogleProtobufAny.sol"; +import "solidity-bytes-utils/BytesLib.sol"; +import "../lib/CometblsHelp.sol"; +import "../lib/ICS23.sol"; +import "../core/IZKVerifierV2.sol"; +import "../core/IMembershipVerifier.sol"; + +contract CometblsClient is ILightClient { + using BytesLib for bytes; + using IBCHeight for IbcCoreClientV1Height.Data; + using CometblsHelp for TendermintTypesHeader.Data; + using CometblsHelp for TendermintTypesCommit.Data; + using CometblsHelp for UnionIbcLightclientsCometblsV1ConsensusState.Data; + using CometblsHelp for UnionIbcLightclientsCometblsV1ClientState.Data; + using CometblsHelp for OptimizedConsensusState; + using CometblsHelp for bytes; + using CometblsHelp for IZKVerifierV2; + + // OptimizedConsensusState + mapping(string => IbcCoreClientV1Height.Data) internal latestHeights; + mapping(string => bytes) internal codeIds; + mapping(string => UnionIbcLightclientsCometblsV1ClientState.Data) + internal clientStates; + mapping(bytes32 => OptimizedConsensusState) internal consensusStates; + mapping(bytes32 => ProcessedMoment) internal processedMoments; + + address internal ibcHandler; + IZKVerifierV2 internal zkVerifier; + IMembershipVerifier internal membershipVerifier; + + constructor( + address ibcHandler_, + IZKVerifierV2 zkVerifier_, + IMembershipVerifier membershipVerifier_ + ) { + ibcHandler = ibcHandler_; + zkVerifier = zkVerifier_; + membershipVerifier = membershipVerifier_; + } + + function stateIndex( + string calldata clientId, + uint128 height + ) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(clientId, height)); + } + + function createClient( + string calldata clientId, + bytes calldata clientStateBytes, + bytes calldata consensusStateBytes + ) + external + override + onlyIBC + returns ( + bytes32 clientStateCommitment, + ConsensusStateUpdate memory update, + bool ok + ) + { + ( + UnionIbcLightclientsCometblsV1ClientState.Data memory clientState, + IbcCoreClientV1Height.Data memory latestHeight, + bytes memory codeId + ) = clientStateBytes.unmarshalClientStateFromProto(); + ( + UnionIbcLightclientsCometblsV1ConsensusState.Data + memory consensusState, + uint64 timestamp + ) = consensusStateBytes.unmarshalConsensusStateFromProto(); + + if (latestHeight.revision_height == 0 || timestamp == 0) { + return (clientStateCommitment, update, false); + } + + clientStates[clientId] = clientState; + latestHeights[clientId] = latestHeight; + codeIds[clientId] = codeId; + OptimizedConsensusState memory optimizedConsensusState = consensusState + .toOptimizedConsensusState(timestamp); + consensusStates[ + stateIndex(clientId, latestHeight.toUint128()) + ] = optimizedConsensusState; + return ( + clientState.marshalToCommitment(latestHeight, codeId), + ConsensusStateUpdate({ + consensusStateCommitment: optimizedConsensusState + .marshalToCommitment(), + height: latestHeight + }), + true + ); + } + + function getTimestampAtHeight( + string calldata clientId, + IbcCoreClientV1Height.Data calldata height + ) external view override returns (uint64, bool) { + OptimizedConsensusState memory consensusState = consensusStates[ + stateIndex(clientId, height.toUint128()) + ]; + return (consensusState.timestamp, true); + } + + function getLatestHeight( + string calldata clientId + ) external view override returns (IbcCoreClientV1Height.Data memory, bool) { + return (latestHeights[clientId], true); + } + + function updateClient( + string calldata clientId, + bytes calldata clientMessageBytes + ) + external + override + onlyIBC + returns (bytes32, ConsensusStateUpdate[] memory, bool) + { + UnionIbcLightclientsCometblsV1Header.Data + memory header = clientMessageBytes.unmarshalHeaderEthABI(); + UnionIbcLightclientsCometblsV1ClientState.Data + storage clientState = clientStates[clientId]; + OptimizedConsensusState storage consensusState = consensusStates[ + stateIndex(clientId, header.trusted_height.toUint128()) + ]; + + require( + consensusState.timestamp != 0, + "LC: trusted height does not exists" + ); + + uint64 untrustedHeightNumber = uint64( + header.signed_header.commit.height + ); + uint64 trustedHeightNumber = header.trusted_height.revision_height; + require( + untrustedHeightNumber > trustedHeightNumber, + "LC: header height <= consensus state height" + ); + + uint64 trustedTimestamp = consensusState.timestamp; + uint64 untrustedTimestamp = uint64( + header.signed_header.header.time.secs + ); + require( + untrustedTimestamp > trustedTimestamp, + "LC: header time <= consensus state time" + ); + + GoogleProtobufDuration.Data memory currentTime = GoogleProtobufDuration + .Data({Seconds: int64(uint64(block.timestamp)), nanos: 0}); + require( + !CometblsHelp.isExpired( + header.signed_header.header.time, + clientState.trusting_period, + currentTime + ), + "LC: header expired" + ); + + uint64 maxClockDrift = uint64( + currentTime.Seconds + clientState.max_clock_drift.Seconds + ); + require( + untrustedTimestamp < maxClockDrift, + "LC: header back to the future" + ); + + /* + We want to verify that 1/3 of trusted valset & 2/3 of untrusted valset signed. + In adjacent verification, trusted vals = untrusted vals. + In non adjacent verification, untrusted vals are coming from the untrusted header. + */ + bytes32 trustedValidatorsHash = consensusState.nextValidatorsHash; + bytes32 untrustedValidatorsHash; + bool adjacent = untrustedHeightNumber == trustedHeightNumber + 1; + if (adjacent) { + untrustedValidatorsHash = trustedValidatorsHash; + } else { + untrustedValidatorsHash = header + .signed_header + .header + .validators_hash + .toBytes32(0); + } + + bytes32 expectedBlockHash = header.signed_header.header.merkleRoot(); + + require( + header.signed_header.commit.block_id.hash.toBytes32(0) == + expectedBlockHash, + "LC: commit.block_id.hash != header.root()" + ); + + TendermintTypesCanonicalVote.Data memory vote = header + .signed_header + .commit + .toCanonicalVote(clientState.chain_id, expectedBlockHash); + bytes memory signedVote = Encoder.encodeDelim( + TendermintTypesCanonicalVote.encode(vote) + ); + + bool ok = zkVerifier.verifyZKP( + trustedValidatorsHash, + untrustedValidatorsHash, + signedVote, + header.zero_knowledge_proof + ); + require(ok, "LC: invalid ZKP"); + + IbcCoreClientV1Height.Data + memory untrustedHeight = IbcCoreClientV1Height.Data({ + revision_number: header.trusted_height.revision_number, + revision_height: untrustedHeightNumber + }); + + // Update states + IbcCoreClientV1Height.Data storage latestHeight = latestHeights[ + clientId + ]; + if (untrustedHeightNumber > latestHeight.revision_height) { + latestHeight.revision_height = untrustedHeightNumber; + } + + uint128 newHeightIdx = untrustedHeight.toUint128(); + + consensusState = consensusStates[stateIndex(clientId, newHeightIdx)]; + consensusState.timestamp = uint64( + header.signed_header.header.time.secs + ); + consensusState.root = header.signed_header.header.app_hash.toBytes32(0); + consensusState.nextValidatorsHash = untrustedValidatorsHash; + + ConsensusStateUpdate[] memory updates = new ConsensusStateUpdate[](1); + updates[0] = ConsensusStateUpdate({ + consensusStateCommitment: consensusState.marshalToCommitment(), + height: untrustedHeight + }); + + processedMoments[stateIndex(clientId, newHeightIdx)] = ProcessedMoment({ + timestamp: uint128(block.timestamp), + height: uint128(block.number) + }); + + return ( + clientState.marshalToCommitment(latestHeight, codeIds[clientId]), + updates, + true + ); + } + + function verifyMembership( + string calldata clientId, + IbcCoreClientV1Height.Data calldata height, + uint64 delayTimePeriod, + uint64 delayBlockPeriod, + bytes calldata proof, + bytes memory prefix, + bytes calldata path, + bytes calldata value + ) external view override returns (bool) { + OptimizedConsensusState memory consensusState = consensusStates[ + stateIndex(clientId, height.toUint128()) + ]; + require( + consensusState.timestamp != 0, + "LC: verifyMembership: consensusState does not exist" + ); + if ( + !validateDelayPeriod( + clientId, + height, + delayTimePeriod, + delayBlockPeriod + ) + ) { + revert("LC: delayPeriod expired"); + } + return + membershipVerifier.verifyMembership( + abi.encodePacked(consensusState.root), + proof, + prefix, + path, + value + ); + } + + function verifyNonMembership( + string calldata clientId, + IbcCoreClientV1Height.Data calldata height, + uint64 delayTimePeriod, + uint64 delayBlockPeriod, + bytes calldata proof, + bytes calldata prefix, + bytes calldata path + ) external returns (bool) { + OptimizedConsensusState memory consensusState = consensusStates[ + stateIndex(clientId, height.toUint128()) + ]; + require( + consensusState.timestamp != 0, + "LC: verifyNonMembership: consensusState does not exist" + ); + if ( + !validateDelayPeriod( + clientId, + height, + delayTimePeriod, + delayBlockPeriod + ) + ) { + revert("LC: delayPeriod expired"); + } + return + membershipVerifier.verifyNonMembership( + abi.encodePacked(consensusState.root), + proof, + prefix, + path + ); + } + + function validateDelayPeriod( + string calldata clientId, + IbcCoreClientV1Height.Data calldata height, + uint64 delayPeriodTime, + uint64 delayPeriodBlocks + ) public view returns (bool) { + uint128 heightU128 = height.toUint128(); + uint64 currentTime = uint64(block.timestamp); + ProcessedMoment memory moment = processedMoments[ + stateIndex(clientId, heightU128) + ]; + uint64 validTime = uint64(moment.timestamp) + delayPeriodTime; + if (delayPeriodTime != 0 && currentTime < validTime) { + return false; + } + uint64 currentHeight = uint64(block.number); + uint64 validHeight = uint64(moment.height) + delayPeriodBlocks; + if (delayPeriodBlocks != 0 && currentHeight < validHeight) { + return false; + } + return true; + } + + function getClientState( + string calldata clientId + ) external view returns (bytes memory, bool) { + bytes memory codeId = codeIds[clientId]; + if (codeId.length == 0) { + return (bytes(""), false); + } + return ( + clientStates[clientId].marshalToProto( + latestHeights[clientId], + codeId + ), + true + ); + } + + function getConsensusState( + string calldata clientId, + IbcCoreClientV1Height.Data calldata height + ) external view returns (bytes memory, bool) { + OptimizedConsensusState memory consensusState = consensusStates[ + stateIndex(clientId, height.toUint128()) + ]; + if (consensusState.timestamp == 0) { + return (bytes(""), false); + } + return (consensusState.marshalToProto(), true); + } + + modifier onlyIBC() { + require(msg.sender == ibcHandler); + _; + } +} diff --git a/evm/contracts/clients/DevnetVerifier.sol b/evm/contracts/clients/Verifier.sol similarity index 63% rename from evm/contracts/clients/DevnetVerifier.sol rename to evm/contracts/clients/Verifier.sol index d0e1cf1bed..c84f23b30e 100644 --- a/evm/contracts/clients/DevnetVerifier.sol +++ b/evm/contracts/clients/Verifier.sol @@ -1,9 +1,9 @@ pragma solidity ^0.8.21; import "../lib/Pairing.sol"; -import "../core/IZKVerifier.sol"; +import "../core/IZKVerifierV2.sol"; -contract DevnetVerifier is IZKVerifier { +contract Verifier is IZKVerifierV2 { using Pairing for *; uint256 constant SNARK_SCALAR_FIELD = @@ -28,63 +28,63 @@ contract DevnetVerifier is IZKVerifier { function verifyingKey() internal pure returns (VerifyingKey memory vk) { vk.alfa1 = Pairing.G1Point( uint256( - 9974399132350238449672423145167802132344597176432790937987673566759904354712 + 2550450311668052934365492757435655272086248044926623610031063320730829667555 ), uint256( - 10396217607362300103655122228113983820745493114140883199476303464408811706471 + 8703042035341333540858177383145184333521890832996178936720364709928918567324 ) ); vk.beta2 = Pairing.G2Point( [ uint256( - 20043334460449324572644561653520106968487299991365945714189067590923833559557 + 12087544808749353461394614734663969995991213332932322707094948493323523587075 ), uint256( - 3782843380964690766572041754552260909078546283792951053210110465664576118592 + 5609784734408442342743459772838249525064711794655914581473900530365072178092 ) ], [ uint256( - 4546441854933490265510538123407299251387870046105247930781926195493537303978 + 3620271307123320035058604702546780432892661890675938601781290078906891602600 ), uint256( - 19728170969753285624598425791670262520539566544285475380089632156164753610432 + 12561749020549188140474766670178579480395789909983438907997458237645471245637 ) ] ); vk.gamma2 = Pairing.G2Point( [ uint256( - 15890984819252760833184574925585572560291816058221856734884092043888365097798 + 16152858558628938416519886954198273278207481765134586011504238844337112405492 ), uint256( - 13558421301005029939663494790802233493306340917537858200716018199215933051901 + 1666473556802178727950421359638955559724204816316988428054967258707482978937 ) ], [ uint256( - 8951430351447595274973237553867518771312837295026859105316664000150429223102 + 11671729608220716829286816261707767434127200387281583853687528523414420405187 ), uint256( - 9774001800913153454819154173343364291874345033268265728436390595601923216347 + 11529896311426611528742842264352476302384772641871656355304253747472884088839 ) ] ); vk.delta2 = Pairing.G2Point( [ uint256( - 12986197120328725341178217804701057807111123287171378211441714126957192190146 + 1978394675920979727010360476867770152431151269630391960275282752505331298298 ), uint256( - 6358811827968308311530932341706580062890352807488954904621603172031504605990 + 8057837855735530867774111909354816419959310377834316597048249385367299183616 ) ], [ uint256( - 11008158088064515525514471643307844090914721261889690007083299787176620957920 + 617817208407359897149661494204930461401582985150453668270265136103096748375 ), uint256( - 1693569334322206029064251989727378784021135124521538321367277282710375215047 + 20112109139090099709565952821220817370470570950311800907839011307723047316048 ) ] ); @@ -119,7 +119,8 @@ contract DevnetVerifier is IZKVerifier { uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c, - uint256[9] calldata input + uint256[5] calldata input, + uint256[2] memory proofCommitment ) public view returns (bool r) { Proof memory proof; proof.A = Pairing.G1Point(a[0], a[1]); @@ -162,73 +163,58 @@ contract DevnetVerifier is IZKVerifier { // temporary point to avoid extra allocations in accumulate Pairing.G1Point memory q = Pairing.G1Point(0, 0); + vk_x.X = uint256( - 19536632576810938663755749582603546087222638703180042738513910916616519682978 + 19784694582259872339546543192603616109827470778459686418308124157049352412391 ); // vk.K[0].X vk_x.Y = uint256( - 6698373882991209028988452302062701111718632869665993606888642342207051892975 + 8050675694081171924276577880525281636790024000613356369940397902103623997045 ); // vk.K[0].Y add_input[0] = vk_x.X; add_input[1] = vk_x.Y; - add_input[2] = input[7]; - add_input[3] = input[8]; + add_input[2] = proofCommitment[0]; + add_input[3] = proofCommitment[1]; Pairing.plus_raw(add_input, vk_x); mul_input[0] = uint256( - 2175229953105907030386086995813356912474746827628735806700482499160750847843 + 11971204246816059209275294976507473955184279486240207887069942049166626280793 ); // vk.K[1].X mul_input[1] = uint256( - 19823529752927772060409556160428145736017998454909758668189685129708026335065 + 17504727181738195812829690872641417282627885783178145006033783186285339904875 ); // vk.K[1].Y mul_input[2] = input[0]; accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[1] * input[0] mul_input[0] = uint256( - 219999636782629863970338640713754993296807671982705311132408472476488701731 + 11657740610894456804295347994135540407849394439286467178511726083546573935190 ); // vk.K[2].X mul_input[1] = uint256( - 11582191598571666113262523487623760501658738560317219321241346601375876165826 + 15322460840551705023577477711103730789391402917721093431052634068176417470830 ); // vk.K[2].Y mul_input[2] = input[1]; accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[2] * input[1] mul_input[0] = uint256( - 914394202216898966177299917746741778977940677187377639141420924936000943248 + 18128705705780345472226931423901518026915070087276737749651398231700428372595 ); // vk.K[3].X mul_input[1] = uint256( - 8726710514357051704626909121942479242019757832647898014481949563241929367905 + 3028226418589796602201694314311238770080143896157409106468825846675506179552 ); // vk.K[3].Y mul_input[2] = input[2]; accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[3] * input[2] mul_input[0] = uint256( - 410530762185814800540583115824275203642834613850491151197240739569603959187 + 4197945774141365264567395738908678390136055541810363021270211823598396886106 ); // vk.K[4].X mul_input[1] = uint256( - 5236570818789858673799951129197614899816105385688852546833382795763613513196 + 784961274727179063355557729271941546761163202680786306491941279774542819927 ); // vk.K[4].Y mul_input[2] = input[3]; accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[4] * input[3] mul_input[0] = uint256( - 13915041915789362048532482320640272960446035437675260680928350524425298814782 + 8766677265255536993942124635649738274909387559882289319459250062942891332208 ); // vk.K[5].X mul_input[1] = uint256( - 4402873937379531482689066168118493057889537848402358898771477872149907606547 + 4058621307841112497722744752034571789854603453063040480920996807162994795066 ); // vk.K[5].Y mul_input[2] = input[4]; accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[5] * input[4] - mul_input[0] = uint256( - 13581186194999365488187594952848464234662365346750156291065587645871146629135 - ); // vk.K[6].X - mul_input[1] = uint256( - 3315129916730707978366419309543430205662621536944423844626436472087928543555 - ); // vk.K[6].Y - mul_input[2] = input[5]; - accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[6] * input[5] - mul_input[0] = uint256( - 13424886217355741741339780135239743700963066884441662888102595067007931455321 - ); // vk.K[7].X - mul_input[1] = uint256( - 5117715757204980109056335794282910227777795377124703228976341447232892223753 - ); // vk.K[7].Y - mul_input[2] = input[6]; - accumulate(mul_input, q, add_input, vk_x); // vk_x += vk.K[7] * input[6] return Pairing.pairing( diff --git a/evm/contracts/core/IZKVerifierV2.sol b/evm/contracts/core/IZKVerifierV2.sol new file mode 100644 index 0000000000..10699c6a8a --- /dev/null +++ b/evm/contracts/core/IZKVerifierV2.sol @@ -0,0 +1,11 @@ +pragma solidity ^0.8.18; + +interface IZKVerifierV2 { + function verifyProof( + uint256[2] memory a, + uint256[2][2] memory b, + uint256[2] memory c, + uint256[5] calldata input, + uint256[2] memory proofCommitment + ) external view returns (bool r); +} diff --git a/evm/contracts/lib/CometblsHelp.sol b/evm/contracts/lib/CometblsHelp.sol index f191d5d6c9..02968d29c1 100644 --- a/evm/contracts/lib/CometblsHelp.sol +++ b/evm/contracts/lib/CometblsHelp.sol @@ -10,6 +10,7 @@ import "../proto/tendermint/types/canonical.sol"; import "./Encoder.sol"; import "./MerkleTree.sol"; import "../core/IZKVerifier.sol"; +import "../core/IZKVerifierV2.sol"; import "solidity-bytes-utils/BytesLib.sol"; import "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {GoogleProtobufAny as Any} from "../proto/GoogleProtobufAny.sol"; @@ -116,6 +117,39 @@ library CometblsHelp { return verifier.verifyProof(a, b, c, inputs); } + function verifyZKP( + IZKVerifierV2 verifier, + bytes32 trustedValidatorsHash, + bytes32 untrustedValidatorsHash, + bytes memory message, + bytes memory zkp + ) internal view returns (bool) { + (uint256 messageX, uint256 messageY) = hashToField2(message); + + ( + uint256[2] memory a, + uint256[2][2] memory b, + uint256[2] memory c, + uint256 commitmentHash, + uint256[2] memory proofCommitment + ) = abi.decode( + zkp, + (uint256[2], uint256[2][2], uint256[2], uint256, uint256[2]) + ); + + uint256[5] memory inputs = [ + uint256(trustedValidatorsHash), + uint256(untrustedValidatorsHash), + messageX, + messageY, + // Gnark commitment API extend internal inputs with the following commitment hash and proof commitment + // See https://github.com/ConsenSys/gnark/issues/652 + commitmentHash + ]; + + return verifier.verifyProof(a, b, c, inputs, proofCommitment); + } + function isExpired( GoogleProtobufTimestamp.Data memory headerTime, GoogleProtobufDuration.Data memory trustingPeriod, diff --git a/evm/evm.nix b/evm/evm.nix index c4bfd41e59..5a598a11b4 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -110,13 +110,13 @@ network = "devnet"; rpc-url = "http://localhost:8545"; private-key = builtins.readFile ./../networks/genesis/devnet-evm/dev-key0.prv; - zkp-verifier-prefix = "Devnet"; + zkp-verifier-prefix = ""; } { network = "testnet"; rpc-url = "https://rpc-sepolia.rockx.com/"; private-key = ''"$1"''; - zkp-verifier-prefix = "Testnet"; + zkp-verifier-prefix = ""; } ]; @@ -160,7 +160,7 @@ { path = "clients/${zkp-verifier-prefix}Verifier.sol"; name = "${zkp-verifier-prefix}Verifier"; } { path = "clients/ICS23MembershipVerifier.sol"; name = "ICS23MembershipVerifier"; } - { path = "clients/CometblsClient.sol"; name = "CometblsClient"; args = ''--constructor-args "$DEVNETOWNABLEIBCHANDLER" "''$${pkgs.lib.strings.toUpper zkp-verifier-prefix}VERIFIER" "$ICS23MEMBERSHIPVERIFIER"''; } + { path = "clients/CometblsClientV2.sol"; name = "CometblsClient"; args = ''--constructor-args "$DEVNETOWNABLEIBCHANDLER" "''$${pkgs.lib.strings.toUpper zkp-verifier-prefix}VERIFIER" "$ICS23MEMBERSHIPVERIFIER"''; } { path = "apps/ucs/01-relay/Relay.sol"; name = "UCS01Relay"; args = ''--constructor-args "$DEVNETOWNABLEIBCHANDLER" "1"'';} ]}