From f2cc98d9286e8e62a50e7bde08f5dddb760b5dc8 Mon Sep 17 00:00:00 2001 From: Ilja von Hoessle Date: Thu, 8 Aug 2024 13:57:22 +0200 Subject: [PATCH] add (WIP): Extend State and Params structs to support cross-chain Payment Channels Signed-off-by: Ilja von Hoessle --- contracts/Adjudicator.sol | 237 +++++++++++++++++++++++++------------- contracts/AssetHolder.sol | 87 +++++++++----- contracts/Channel.sol | 68 ++++++----- 3 files changed, 262 insertions(+), 130 deletions(-) diff --git a/contracts/Adjudicator.sol b/contracts/Adjudicator.sol index 2adf43e..489e0f5 100644 --- a/contracts/Adjudicator.sol +++ b/contracts/Adjudicator.sol @@ -1,4 +1,4 @@ -// Copyright 2019 - See NOTICE file for copyright holders. +// Copyright 2024 - See NOTICE file for copyright holders. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -39,7 +39,11 @@ contract Adjudicator { * In the FORCEEXEC phase, the smart contract is executed on-chain. * In the CONCLUDED phase, the channel is considered finalized. */ - enum DisputePhase { DISPUTE, FORCEEXEC, CONCLUDED } + enum DisputePhase { + DISPUTE, + FORCEEXEC, + CONCLUDED + } struct Dispute { uint64 timeout; @@ -62,7 +66,12 @@ contract Adjudicator { * @param phase The dispute phase of the channel. * @param timeout The dispute phase timeout. */ - event ChannelUpdate(bytes32 indexed channelID, uint64 version, uint8 phase, uint64 timeout); + event ChannelUpdate( + bytes32 indexed channelID, + uint64 version, + uint8 phase, + uint64 timeout + ); // SignedState is a combination of params, state, and signatures. struct SignedState { @@ -79,9 +88,7 @@ contract Adjudicator { function register( SignedState memory channel, SignedState[] memory subChannels - ) - external - { + ) external { require(channel.params.ledgerChannel, "not ledger"); registerRecursive(channel, subChannels, 0); } @@ -98,10 +105,8 @@ contract Adjudicator { function registerRecursive( SignedState memory channel, SignedState[] memory subChannels, - uint startIndex) - internal - returns (uint256[] memory outcome, uint nextIndex) - { + uint startIndex + ) internal returns (uint256[] memory outcome, uint nextIndex) { nextIndex = startIndex; Channel.Allocation memory alloc = channel.state.outcome; Channel.Asset[] memory assets = alloc.assets; @@ -116,12 +121,18 @@ contract Adjudicator { require(locked.length <= subChannels.length, "subChannels: too short"); for (uint s = 0; s < locked.length; s++) { SignedState memory _channel = subChannels[nextIndex++]; - (Channel.SubAlloc memory subAlloc, Channel.State memory _state) = - (locked[s], _channel.state); + (Channel.SubAlloc memory subAlloc, Channel.State memory _state) = ( + locked[s], + _channel.state + ); require(subAlloc.ID == _state.channelID, "invalid sub-channel id"); uint256[] memory _outcome; - (_outcome, nextIndex) = registerRecursive(_channel, subChannels, nextIndex); + (_outcome, nextIndex) = registerRecursive( + _channel, + subChannels, + nextIndex + ); Channel.requireEqualAssetArray(assets, _state.outcome.assets); Array.requireEqualUint256Array(subAlloc.balances, _outcome); @@ -134,20 +145,21 @@ contract Adjudicator { * (not registered before) or (newer version and within refutation period). * Registration is skipped if the channel state is already registered. */ - function registerSingle( - SignedState memory channel - ) - internal - { - (Channel.Params memory params, Channel.State memory state) = - (channel.params, channel.state); + function registerSingle(SignedState memory channel) internal { + (Channel.Params memory params, Channel.State memory state) = ( + channel.params, + channel.state + ); requireValidParams(params, state); Channel.validateSignatures(params, state, channel.sigs); if (params.virtualChannel) { require(!Channel.hasApp(params), "cannot have app"); - require(state.outcome.locked.length == 0, "cannot have locked funds"); + require( + state.outcome.locked.length == 0, + "cannot have locked funds" + ); } // If registered, require newer version and refutation timeout not passed. @@ -159,9 +171,15 @@ contract Adjudicator { return; } require(dispute.version < state.version, "invalid version"); - require(dispute.phase == uint8(DisputePhase.DISPUTE), "incorrect phase"); + require( + dispute.phase == uint8(DisputePhase.DISPUTE), + "incorrect phase" + ); // solhint-disable-next-line not-rely-on-time - require(block.timestamp < dispute.timeout, "refutation timeout passed"); + require( + block.timestamp < dispute.timeout, + "refutation timeout passed" + ); } // Write state. @@ -187,11 +205,10 @@ contract Adjudicator { Channel.State memory stateOld, Channel.State memory state, uint256 actorIdx, - bytes memory sig) - external - { + bytes memory sig + ) external { Dispute memory dispute = requireGetDispute(state.channelID); - if(dispute.phase == uint8(DisputePhase.DISPUTE)) { + if (dispute.phase == uint8(DisputePhase.DISPUTE)) { // solhint-disable-next-line not-rely-on-time require(block.timestamp >= dispute.timeout, "timeout not passed"); } else if (dispute.phase == uint8(DisputePhase.FORCEEXEC)) { @@ -205,7 +222,14 @@ contract Adjudicator { require(actorIdx < params.participants.length, "actorIdx out of range"); requireValidParams(params, state); require(dispute.stateHash == hashState(stateOld), "wrong old state"); - require(Sig.verify(Channel.encodeState(state), sig, params.participants[actorIdx]), "invalid signature"); + require( + Sig.verify( + Channel.encodeState(state), + sig, + params.participants[actorIdx].ethAddress + ), + "invalid signature" + ); requireValidTransition(params, stateOld, state, actorIdx); storeChallenge(params, state, DisputePhase.FORCEEXEC); @@ -223,15 +247,23 @@ contract Adjudicator { function conclude( Channel.Params memory params, Channel.State memory state, - Channel.State[] memory subStates) - external - { + Channel.State[] memory subStates + ) external { require(params.ledgerChannel, "not ledger"); requireValidParams(params, state); - + concludeSingle(state); - (uint256[][] memory outcome,) = forceConcludeRecursive(state, subStates, 0); - pushOutcome(state.channelID, state.outcome.assets, params.participants, outcome); + (uint256[][] memory outcome, ) = forceConcludeRecursive( + state, + subStates, + 0 + ); + pushOutcome( + state.channelID, + state.outcome.assets, + params.participants, + outcome + ); } /** @@ -249,9 +281,8 @@ contract Adjudicator { function concludeFinal( Channel.Params memory params, Channel.State memory state, - bytes[] memory sigs) - external - { + bytes[] memory sigs + ) external { require(params.ledgerChannel, "not ledger"); require(state.isFinal == true, "state not final"); require(state.outcome.locked.length == 0, "cannot have sub-channels"); @@ -261,11 +292,19 @@ contract Adjudicator { // If registered, require not concluded. (Dispute memory dispute, bool registered) = getDispute(state.channelID); if (registered) { - require(dispute.phase != uint8(DisputePhase.CONCLUDED), "channel already concluded"); + require( + dispute.phase != uint8(DisputePhase.CONCLUDED), + "channel already concluded" + ); } storeChallenge(params, state, DisputePhase.CONCLUDED); - pushOutcome(state.channelID, state.outcome.assets, params.participants, state.outcome.balances); + pushOutcome( + state.channelID, + state.outcome.assets, + params.participants, + state.outcome.balances + ); } /** @@ -273,7 +312,9 @@ contract Adjudicator { * @param params The parameters of the channel. * @return The ID of the channel. */ - function channelID(Channel.Params memory params) public pure returns (bytes32) { + function channelID( + Channel.Params memory params + ) public pure returns (bytes32) { return keccak256(Channel.encodeParams(params)); } @@ -282,7 +323,9 @@ contract Adjudicator { * @param state The state to hash. * @return The hash of the state. */ - function hashState(Channel.State memory state) public pure returns (bytes32) { + function hashState( + Channel.State memory state + ) public pure returns (bytes32) { return keccak256(Channel.encodeState(state)); } @@ -293,8 +336,8 @@ contract Adjudicator { */ function requireValidParams( Channel.Params memory params, - Channel.State memory state) - internal pure { + Channel.State memory state + ) internal pure { require(state.channelID == channelID(params), "invalid params"); } @@ -308,11 +351,10 @@ contract Adjudicator { function storeChallenge( Channel.Params memory params, Channel.State memory state, - DisputePhase disputePhase) - internal - { + DisputePhase disputePhase + ) internal { (Dispute memory dispute, bool registered) = getDispute(state.channelID); - + dispute.challengeDuration = uint64(params.challengeDuration); dispute.version = state.version; dispute.hasApp = Channel.hasApp(params); @@ -324,10 +366,14 @@ contract Adjudicator { // Make channel concludable if state is final. // solhint-disable-next-line not-rely-on-time dispute.timeout = uint64(block.timestamp); - } else if (!registered || dispute.phase == uint8(DisputePhase.FORCEEXEC)) { + } else if ( + !registered || dispute.phase == uint8(DisputePhase.FORCEEXEC) + ) { // Increment timeout if channel is not registered or in phase FORCEEXEC. // solhint-disable-next-line not-rely-on-time - dispute.timeout = uint64(block.timestamp).add(dispute.challengeDuration); + dispute.timeout = uint64(block.timestamp).add( + dispute.challengeDuration + ); } setDispute(state.channelID, dispute); @@ -346,12 +392,18 @@ contract Adjudicator { Channel.Params memory params, Channel.State memory from, Channel.State memory to, - uint256 actorIdx) - internal pure - { - require(to.version == from.version + 1, "version must increment by one"); + uint256 actorIdx + ) internal pure { + require( + to.version == from.version + 1, + "version must increment by one" + ); require(from.isFinal == false, "cannot progress from final state"); - requireAssetPreservation(from.outcome, to.outcome, params.participants.length); + requireAssetPreservation( + from.outcome, + to.outcome, + params.participants.length + ); App app = App(params.app); app.validTransition(params, from, to, actorIdx); } @@ -366,18 +418,29 @@ contract Adjudicator { function requireAssetPreservation( Channel.Allocation memory oldAlloc, Channel.Allocation memory newAlloc, - uint256 numParts) - internal pure - { - require(oldAlloc.balances.length == newAlloc.balances.length, "balances length mismatch"); - require(oldAlloc.assets.length == newAlloc.assets.length, "assets length mismatch"); + uint256 numParts + ) internal pure { + require( + oldAlloc.balances.length == newAlloc.balances.length, + "balances length mismatch" + ); + require( + oldAlloc.assets.length == newAlloc.assets.length, + "assets length mismatch" + ); Channel.requireEqualSubAllocArray(oldAlloc.locked, newAlloc.locked); for (uint256 i = 0; i < newAlloc.assets.length; i++) { Channel.requireEqualAsset(oldAlloc.assets[i], newAlloc.assets[i]); uint256 sumOld = 0; uint256 sumNew = 0; - require(oldAlloc.balances[i].length == numParts, "old balances length mismatch"); - require(newAlloc.balances[i].length == numParts, "new balances length mismatch"); + require( + oldAlloc.balances[i].length == numParts, + "old balances length mismatch" + ); + require( + newAlloc.balances[i].length == numParts, + "new balances length mismatch" + ); for (uint256 k = 0; k < numParts; k++) { sumOld = sumOld.add(oldAlloc.balances[i][k]); sumNew = sumNew.add(newAlloc.balances[i][k]); @@ -394,7 +457,10 @@ contract Adjudicator { function concludeSingle(Channel.State memory state) internal { Dispute memory dispute = requireGetDispute(state.channelID); require(dispute.stateHash == hashState(state), "invalid channel state"); - require(dispute.phase != uint8(DisputePhase.CONCLUDED), "channel already concluded"); + require( + dispute.phase != uint8(DisputePhase.CONCLUDED), + "channel already concluded" + ); // If still in phase DISPUTE and the channel has an app, increase the // timeout by one duration to account for phase FORCEEXEC. @@ -419,10 +485,8 @@ contract Adjudicator { function forceConcludeRecursive( Channel.State memory state, Channel.State[] memory subStates, - uint256 startIndex) - internal - returns (uint256[][] memory outcome, uint nextIndex) - { + uint256 startIndex + ) internal returns (uint256[][] memory outcome, uint nextIndex) { forceConcludeSingle(state); // Initialize with outcome of channel. @@ -445,11 +509,15 @@ contract Adjudicator { require(subAlloc.ID == subState.channelID, "invalid subchannel id"); uint256[][] memory subOutcome; - (subOutcome, nextIndex) = forceConcludeRecursive(subState, subStates, nextIndex); + (subOutcome, nextIndex) = forceConcludeRecursive( + subState, + subStates, + nextIndex + ); // Add outcome of subchannels. uint16[] memory indexMap = subAlloc.indexMap; - for (uint a = 0; a < assets.length; a++) { + for (uint a = 0; a < assets.length; a++) { for (uint p = 0; p < indexMap.length; p++) { uint256 _subOutcome = subOutcome[a][p]; uint16 _p = indexMap[p]; @@ -478,15 +546,21 @@ contract Adjudicator { function pushOutcome( bytes32 channel, Channel.Asset[] memory assets, - address[] memory participants, - uint256[][] memory outcome) - internal - { + Channel.Participant[] memory participants, + uint256[][] memory outcome + ) internal { for (uint a = 0; a < assets.length; a++) { Channel.Asset memory asset = assets[a]; if (asset.chainID == block.chainid) { - //slither-disable-next-line calls-loop - AssetHolder(asset.holder).setOutcome(channel, participants, outcome[a]); + if (asset.ethHolder != address(0)) { + // solhint-disable-next-line calls-loop + AssetHolder(asset.ethHolder).setOutcome( + channel, + participants, + outcome[a] + ); + } + // //slither-disable-next-line calls-loop } } } @@ -495,7 +569,9 @@ contract Adjudicator { * @dev Returns the dispute state for the given channelID. The second return * value indicates whether the given channel has been registered yet. */ - function getDispute(bytes32 chID) internal view returns (Dispute memory, bool) { + function getDispute( + bytes32 chID + ) internal view returns (Dispute memory, bool) { Dispute memory dispute = disputes[chID]; return (dispute, dispute.stateHash != bytes32(0)); } @@ -504,7 +580,9 @@ contract Adjudicator { * @dev Returns the dispute state for the given channelID. Reverts if the * channel has not been registered yet. */ - function requireGetDispute(bytes32 chID) internal view returns (Dispute memory) { + function requireGetDispute( + bytes32 chID + ) internal view returns (Dispute memory) { (Dispute memory dispute, bool registered) = getDispute(chID); require(registered, "not registered"); return dispute; @@ -516,6 +594,11 @@ contract Adjudicator { */ function setDispute(bytes32 chID, Dispute memory dispute) internal { disputes[chID] = dispute; - emit ChannelUpdate(chID, dispute.version, dispute.phase, dispute.timeout); + emit ChannelUpdate( + chID, + dispute.version, + dispute.phase, + dispute.timeout + ); } } diff --git a/contracts/AssetHolder.sol b/contracts/AssetHolder.sol index eed132f..f5b590b 100644 --- a/contracts/AssetHolder.sol +++ b/contracts/AssetHolder.sol @@ -1,4 +1,4 @@ -// Copyright 2019 - See NOTICE file for copyright holders. +// Copyright 2024 - See NOTICE file for copyright holders. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ pragma experimental ABIEncoderV2; import "../vendor/openzeppelin-contracts/contracts/utils/math/SafeMath.sol"; import "./Sig.sol"; +import "./Channel.sol"; /** * @title The Perun AssetHolder @@ -33,14 +34,18 @@ abstract contract AssetHolder { */ struct WithdrawalAuth { bytes32 channelID; - address participant; // The account used to sign the authorization which is debited. + Channel.Participant participant; // The account used to sign the authorization which is debited. address payable receiver; // The receiver of the authorization. uint256 amount; // The amount that can be withdrawn. } event OutcomeSet(bytes32 indexed channelID); event Deposited(bytes32 indexed fundingID, uint256 amount); - event Withdrawn(bytes32 indexed fundingID, uint256 amount, address receiver); + event Withdrawn( + bytes32 indexed fundingID, + uint256 amount, + address receiver + ); /** * @notice This mapping stores the balances of participants to their fundingID. @@ -63,8 +68,11 @@ abstract contract AssetHolder { /** * @notice The onlyAdjudicator modifier specifies functions that can only be called from the adjudicator contract. */ - modifier onlyAdjudicator { - require(msg.sender == adjudicator, "can only be called by the adjudicator"); // solhint-disable-line reason-string + modifier onlyAdjudicator() { + require( + msg.sender == adjudicator, + "can only be called by the adjudicator" + ); // solhint-disable-line reason-string _; } @@ -85,11 +93,17 @@ abstract contract AssetHolder { */ function setOutcome( bytes32 channelID, - address[] calldata parts, - uint256[] calldata newBals) - external onlyAdjudicator { - require(parts.length == newBals.length, "participants length should equal balances"); // solhint-disable-line reason-string - require(settled[channelID] == false, "trying to set already settled channel"); // solhint-disable-line reason-string + Channel.Participant[] calldata parts, + uint256[] calldata newBals + ) external onlyAdjudicator { + require( + parts.length == newBals.length, + "participants length should equal balances" + ); // solhint-disable-line reason-string + require( + settled[channelID] == false, + "trying to set already settled channel" + ); // solhint-disable-line reason-string // The channelID itself might already be funded uint256 sumHeld = holdings[channelID]; @@ -98,7 +112,7 @@ abstract contract AssetHolder { bytes32[] memory fundingIDs = new bytes32[](parts.length); for (uint256 i = 0; i < parts.length; i++) { - bytes32 id = calcFundingID(channelID, parts[i]); + bytes32 id = calcFundingID(channelID, parts[i].ethAddress); // Save calculated ids to save gas. fundingIDs[i] = id; // Compute old balances. @@ -133,7 +147,7 @@ abstract contract AssetHolder { function deposit(bytes32 fundingID, uint256 amount) external payable { depositCheck(fundingID, amount); holdings[fundingID] = holdings[fundingID].add(amount); - depositEnact(fundingID, amount); + depositEnact(fundingID, amount); emit Deposited(fundingID, amount); } @@ -153,10 +167,23 @@ abstract contract AssetHolder { * what amounf of asset from which channel participant. * @param signature Signature on the withdrawal authorization. */ - function withdraw(WithdrawalAuth calldata authorization, bytes calldata signature) external { + function withdraw( + WithdrawalAuth calldata authorization, + bytes calldata signature + ) external { require(settled[authorization.channelID], "channel not settled"); - require(Sig.verify(abi.encode(authorization), signature, authorization.participant), "signature verification failed"); - bytes32 id = calcFundingID(authorization.channelID, authorization.participant); + require( + Sig.verify( + abi.encode(authorization), + signature, + authorization.participant.ethAddress + ), + "signature verification failed" + ); + bytes32 id = calcFundingID( + authorization.channelID, + authorization.participant.ethAddress + ); require(holdings[id] >= authorization.amount, "insufficient funds"); withdrawCheck(authorization, signature); holdings[id] = holdings[id].sub(authorization.amount); @@ -172,8 +199,10 @@ abstract contract AssetHolder { * call it via `super`. */ //slither-disable-next-line dead-code - function depositCheck(bytes32 fundingID, uint256 amount) internal view virtual - {} // solhint-disable no-empty-blocks + function depositCheck( + bytes32 fundingID, + uint256 amount + ) internal view virtual {} // solhint-disable no-empty-blocks /** * @notice Enacts a deposit or reverts otherwise. @@ -182,8 +211,7 @@ abstract contract AssetHolder { * This function is empty by default and the overrider does not need to * call it via `super`. */ - function depositEnact(bytes32 fundingID, uint256 amount) internal virtual - {} // solhint-disable no-empty-blocks + function depositEnact(bytes32 fundingID, uint256 amount) internal virtual {} // solhint-disable no-empty-blocks /** * @notice Checks a withdrawal for validity and reverts otherwise. @@ -192,8 +220,10 @@ abstract contract AssetHolder { * This function is empty by default and the overrider does not need to * call it via `super`. */ - function withdrawCheck(WithdrawalAuth calldata authorization, bytes calldata signature) internal view virtual - {} // solhint-disable no-empty-blocks + function withdrawCheck( + WithdrawalAuth calldata authorization, + bytes calldata signature + ) internal view virtual {} // solhint-disable no-empty-blocks /** * @notice Enacts a withdrawal or reverts otherwise. @@ -203,16 +233,21 @@ abstract contract AssetHolder { * call it via `super`. */ //slither-disable-next-line dead-code - function withdrawEnact(WithdrawalAuth calldata authorization, bytes calldata signature) internal virtual - {} // solhint-disable no-empty-blocks + function withdrawEnact( + WithdrawalAuth calldata authorization, + bytes calldata signature + ) internal virtual {} // solhint-disable no-empty-blocks /** * @notice Internal helper function that calculates the fundingID. * @param channelID ID of the channel. - * @param participant Address of a participant in the channel. + * @param ethAddress Ethereum address of a participant in the channel. * @return The funding ID, an identifier used for indexing. */ - function calcFundingID(bytes32 channelID, address participant) internal pure returns (bytes32) { - return keccak256(abi.encode(channelID, participant)); + function calcFundingID( + bytes32 channelID, + address ethAddress + ) internal pure returns (bytes32) { + return keccak256(abi.encode(channelID, ethAddress)); } } diff --git a/contracts/Channel.sol b/contracts/Channel.sol index a93e06e..b4e93ea 100644 --- a/contracts/Channel.sol +++ b/contracts/Channel.sol @@ -1,4 +1,4 @@ -// Copyright 2019 - See NOTICE file for copyright holders. +// Copyright 2024 - See NOTICE file for copyright holders. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,12 +24,19 @@ library Channel { struct Params { uint256 challengeDuration; uint256 nonce; - address[] participants; + Participant[] participants; address app; bool ledgerChannel; bool virtualChannel; } + struct Participant { + address ethAddress; + bytes ethPubKey; + bytes ccAddress; + bytes ccPubKey; + } + struct State { bytes32 channelID; uint64 version; @@ -40,7 +47,8 @@ library Channel { struct Asset { uint256 chainID; - address holder; + address ethHolder; + bytes ccHolder; } struct Allocation { @@ -71,21 +79,34 @@ library Channel { function validateSignatures( Params memory params, State memory state, - bytes[] memory sigs) - internal pure - { + bytes[] memory sigs + ) internal pure { bytes memory encodedState = encodeState(state); - require(params.participants.length == sigs.length, "signatures length mismatch"); + require( + params.participants.length == sigs.length, + "signatures length mismatch" + ); for (uint256 i = 0; i < sigs.length; i++) { - require(Sig.verify(encodedState, sigs[i], params.participants[i]), "invalid signature"); + require( + Sig.verify( + encodedState, + sigs[i], + params.participants[i].ethAddress + ), + "invalid signature" + ); } } - function encodeParams(Params memory params) internal pure returns (bytes memory) { + function encodeParams( + Params memory params + ) internal pure returns (bytes memory) { return abi.encode(params); } - function encodeState(State memory state) internal pure returns (bytes memory) { + function encodeState( + State memory state + ) internal pure returns (bytes memory) { return abi.encode(state); } @@ -93,9 +114,7 @@ library Channel { function requireEqualSubAllocArray( SubAlloc[] memory a, SubAlloc[] memory b - ) - internal pure - { + ) internal pure { require(a.length == b.length, "SubAlloc[]: unequal length"); for (uint i = 0; i < a.length; i++) { requireEqualSubAlloc(a[i], b[i]); @@ -106,32 +125,27 @@ library Channel { function requireEqualSubAlloc( SubAlloc memory a, SubAlloc memory b - ) - internal pure - { + ) internal pure { require(a.ID == b.ID, "SubAlloc: unequal ID"); Array.requireEqualUint256Array(a.balances, b.balances); Array.requireEqualUint16Array(a.indexMap, b.indexMap); } /// @dev Asserts that a and b are equal. - function requireEqualAsset( - Asset memory a, - Asset memory b - ) - internal pure - { + function requireEqualAsset(Asset memory a, Asset memory b) internal pure { require(a.chainID == b.chainID, "Asset: unequal chainID"); - require(a.holder == b.holder, "Asset: unequal holder"); + require(a.ethHolder == b.ethHolder, "Asset: unequal ethHolder"); + require( + keccak256(a.ccHolder) == keccak256(b.ccHolder), + "Asset: unequal ccHolder" + ); } /// @dev Asserts that a and b are equal. function requireEqualAssetArray( Channel.Asset[] memory a, Channel.Asset[] memory b - ) - internal pure - { + ) internal pure { require(a.length == b.length, "Asset[]: unequal length"); for (uint i = 0; i < a.length; i++) { requireEqualAsset(a[i], b[i]); @@ -141,5 +155,5 @@ library Channel { /// @dev Returns whether the channel has an app. function hasApp(Params memory params) internal pure returns (bool) { return params.app != address(0); - } + } }