diff --git a/benchmarks/clearing/benchmarkYellowAdjudicator.ts b/benchmarks/clearing/benchmarkYellowAdjudicator.ts new file mode 100644 index 0000000..407e96d --- /dev/null +++ b/benchmarks/clearing/benchmarkYellowAdjudicator.ts @@ -0,0 +1,45 @@ +import { writeFileSync } from 'node:fs'; + +import { gasUsed } from '../helpers'; + +import { + deployYellowAdjudicator, + depositForSwaps, + getSwapParams, + randomWallet, + setSeed, +} from './fixtures'; +import { BENCHMARK_STEPS, emptyYellowAdjudicatorGasResults } from './yellowAdjudicatorGas'; + +async function main(): Promise { + setSeed(42); + + const gasResults = emptyYellowAdjudicatorGasResults; + + await Promise.all( + BENCHMARK_STEPS.map(async (stepNum) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const yellowAdjudicator = await deployYellowAdjudicator(); + const brokerA = randomWallet(); + const brokerB = randomWallet(); + + const assets = await depositForSwaps(yellowAdjudicator, brokerA, brokerB, stepNum * 2); + + gasResults.swap[`swaps_${stepNum}`] = await gasUsed( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + await yellowAdjudicator.swap( + ...(await getSwapParams(brokerA, brokerB, assets, stepNum * 2)), + { gasLimit: 30_000_000 }, + ), + ); + }), + ); + + writeFileSync(__dirname + '/gasResults.json', JSON.stringify(gasResults, undefined, 2)); + console.log('Benchmark results updated successfully!'); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/benchmarks/clearing/fixtures.ts b/benchmarks/clearing/fixtures.ts new file mode 100644 index 0000000..826c12d --- /dev/null +++ b/benchmarks/clearing/fixtures.ts @@ -0,0 +1,200 @@ +import { ethers } from 'hardhat'; +import { + State, + bindSignatures, + getFixedPart, + getVariablePart, + signStates, +} from '@statechannels/nitro-protocol'; +import { ParamType, entropyToMnemonic } from 'ethers/lib/utils'; + +import type { Wallet } from 'ethers'; +import type { + FixedPart, + SignedVariablePart, + VariablePart, +} from '@statechannels/nitro-protocol/dist/src/contract/state'; //TODO: remove explicit import path when implicit becomes available +import type { YellowAdjudicator } from '../../typechain'; + +function _randomUnprefixedByte(): string { + return randomNum(10, 99).toString(); +} + +function randomBytes(num: number): string { + let bytes = '0x'; + + for (let i = 0; i < num; i++) { + bytes += _randomUnprefixedByte(); + } + + return bytes; +} + +export function randomWallet(): Wallet { + const entropy = randomBytes(16); + const mnemonic = entropyToMnemonic(entropy); + return ethers.Wallet.fromMnemonic(mnemonic); +} + +function randomAddress(): string { + return randomWallet().address; +} +function prng(seed: number): () => number { + if (typeof seed == 'undefined') { + return () => 0; + } + + let l = seed % 2_147_483_647; + if (l <= 0) l += 2_147_483_646; + + return () => (l = ((l * 16_807) % 2_147_483_647) - 1) / 2_147_483_646; +} + +let randomPartial = prng(Date.now()); + +export function setSeed(seed: number): void { + randomPartial = prng(seed); +} + +function randomNum(min: number, max: number): number { + return Math.floor(randomPartial() * (max - min + 1)) + min; +} + +interface Liability { + asset: string; + amount: number; +} + +interface SwapSpecs { + brokerA: string; + brokerB: string; + swaps: [Liability, Liability][]; + swapSpecsFinalizationTimestamp: number; +} + +const MIN_DEPOSIT_AMOUNT = 100; +const MAX_DEPOSIT_AMOUNT = 1000; + +const MAX_SWAP_AMOUNT = 100; + +function randomDepositAmount(): number { + return randomNum(MIN_DEPOSIT_AMOUNT, MAX_DEPOSIT_AMOUNT); +} + +function randomSwapAmount(): number { + return randomNum(1, MAX_SWAP_AMOUNT); +} + +function encodeSwapSpecs(swapSpecs: SwapSpecs): string { + return ethers.utils.defaultAbiCoder.encode( + [ + { + type: 'tuple', + components: [ + { name: 'swapSpecsFinalizationTimestamp', type: 'uint64' }, + { name: 'brokerA', type: 'address' }, + { name: 'brokerB', type: 'address' }, + { + type: 'tuple[2][]', + name: 'swaps', + components: [ + { + name: 'asset', + type: 'address', + }, + { + name: 'amount', + type: 'uint256', + }, + ], + } as ParamType, + ], + } as ParamType, + ], + [swapSpecs], + ); +} + +export async function depositForSwaps( + yellowAdjudicator: YellowAdjudicator, + brokerA: Wallet, + brokerB: Wallet, + swapNum: number, +): Promise { + // create assets + const assets = Array.from({ length: swapNum }, () => randomAddress()); + + // set assets to brokers + await Promise.all( + assets.map(async (asset) => { + const amount1 = randomDepositAmount(); + await yellowAdjudicator.setDeposit(brokerA.address, asset, amount1); + + const amount2 = randomDepositAmount(); + await yellowAdjudicator.setDeposit(brokerB.address, asset, amount2); + }), + ); + + return assets; +} + +export async function getSwapParams( + brokerA: Wallet, + brokerB: Wallet, + assets: string[], + swapNum: number, +): Promise<[FixedPart, VariablePart, SignedVariablePart]> { + // construct swaps + const swaps = [] as [Liability, Liability][]; + + for (let i = 0; i < swapNum; i += 2) { + swaps.push([ + { asset: assets[i], amount: randomSwapAmount() }, + { asset: assets[i + 1], amount: randomSwapAmount() }, + ]); + } + + const swapSpecsFinalizationTimestamp = Math.round(Date.now() / 1000) - 600; + + // construct SwapSpecs + const swapSpecs: SwapSpecs = { + brokerA: brokerA.address, + brokerB: brokerB.address, + swaps, + swapSpecsFinalizationTimestamp, + }; + + // construct State + const state: State = { + chainId: '0x01', + participants: [brokerA.address, brokerB.address], + channelNonce: '0x01', + appDefinition: randomAddress(), + challengeDuration: 0xff_ff, + turnNum: 1, + isFinal: false, + outcome: [], + appData: encodeSwapSpecs(swapSpecs), + }; + + // construct swap parameters + const fixedPart = getFixedPart(state); + const variablePart = getVariablePart(state); + + const sigs = await signStates([state], [brokerA, brokerB], [0, 0]); + + const signedVariableParts = bindSignatures([variablePart], sigs, [0, 0]); + + // NOTE: as there are no checks on preSwap variable part yet, we can provide any variable part + return [fixedPart, variablePart, signedVariableParts[0]]; +} + +export async function deployYellowAdjudicator(): Promise { + const YAFactory = await ethers.getContractFactory('YellowAdjudicator'); + // TODO: fix https://stackoverflow.com/questions/74773005/ts-export-declare-namespace-shadows-already-exported-variable + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const YA = (await YAFactory.deploy()) as YellowAdjudicator; + await YA.deployed(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return YA; +} diff --git a/benchmarks/clearing/gasResults.json b/benchmarks/clearing/gasResults.json new file mode 100644 index 0000000..59e2eef --- /dev/null +++ b/benchmarks/clearing/gasResults.json @@ -0,0 +1,14 @@ +{ + "swap": { + "swaps_1": 93243, + "swaps_10": 322477, + "swaps_100": 2623728, + "swaps_200": 5199965, + "swaps_300": 7797538, + "swaps_400": 10415163, + "swaps_500": 13054208, + "swaps_650": 17050363, + "swaps_800": 21093696, + "swaps_1000": 26556298 + } +} \ No newline at end of file diff --git a/benchmarks/clearing/yellowAdjudicatorGas.ts b/benchmarks/clearing/yellowAdjudicatorGas.ts new file mode 100644 index 0000000..f2c34ce --- /dev/null +++ b/benchmarks/clearing/yellowAdjudicatorGas.ts @@ -0,0 +1,10 @@ +// export const BENCHMARK_STEPS = [1000] as const; +export const BENCHMARK_STEPS = [1, 10, 100, 200, 300, 400, 500, 650, 800, 1000] as const; + +export interface YellowAdjudicatorGasResults { + swap: Record; +} + +export const emptyYellowAdjudicatorGasResults: YellowAdjudicatorGasResults = { + swap: {}, +}; diff --git a/contracts/clearing/VirtualMarginApp.sol b/contracts/clearing/VirtualMarginApp.sol new file mode 100644 index 0000000..8582daf --- /dev/null +++ b/contracts/clearing/VirtualMarginApp.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; +pragma experimental ABIEncoderV2; + +import '@statechannels/nitro-protocol/contracts/interfaces/IForceMoveApp.sol'; +import '@statechannels/nitro-protocol/contracts/libraries/NitroUtils.sol'; +import '@statechannels/nitro-protocol/contracts/interfaces/INitroTypes.sol'; +import {ExitFormat as Outcome} from '@statechannels/exit-format/contracts/ExitFormat.sol'; + +// NOTE: Attack: +// Bob can submit a convenient candidate, when Alice in trouble (Way back machine attack) + +// Possible solutions: +// 1: Alice does checkpoint periodically +// 2: Alice hire a WatchTower, which replicates Alice's states, +// and challenge in the case of challenge event and missing heartbeat + +/** + * @dev The VirtualMarginApp contract complies with the ForceMoveApp interface and allows payments to be made virtually from Initiator to Receiver (participants[0] to participants[n+1], where n is the number of intermediaries). + */ +contract VirtualMarginApp is IForceMoveApp { + struct MarginProof { + INitroTypes.Signature leaderSignature; + INitroTypes.Signature receiverSignature; + uint256 version; + uint256 leaderMargin; + uint256 receiverMargin; + } + + enum AllocationIndices { + Initiator, + Receiver + } + + /** + * @notice Encodes application-specific rules for a particular ForceMove-compliant state channel. + * @dev Encodes application-specific rules for a particular ForceMove-compliant state channel. + * @param fixedPart Fixed part of the state channel. + * @param proof Array of recovered variable parts which constitutes a support proof for the candidate. + * @param candidate Recovered variable part the proof was supplied for. + */ + function requireStateSupported( + FixedPart calldata fixedPart, + RecoveredVariablePart[] calldata proof, + RecoveredVariablePart calldata candidate + ) external pure override { + // This channel has only 4 states which can be supported: + // 0 prefund + // 1 postfund + // 2+ margin call + // 3+ final + + uint8 nParticipants = uint8(fixedPart.participants.length); + + // states 0,1,3+: + + if (proof.length == 0) { + require( + NitroUtils.getClaimedSignersNum(candidate.signedBy) == nParticipants, + '!unanimous' + ); + + if (candidate.variablePart.turnNum == 0) return; // prefund + if (candidate.variablePart.turnNum == 1) return; // postfund + + // final + if (candidate.variablePart.turnNum >= 3) { + // final (note: there is a core protocol escape hatch for this, too, so it could be removed) + require(candidate.variablePart.isFinal, '!final; turnNum>=3 && |proof|=0'); + return; + } + + revert('bad candidate turnNum; |proof|=0'); + } + + // state 2+ requires postfund state to be supplied + + if (proof.length == 1) { + _requireProofOfUnanimousConsensusOnPostFund(proof[0], nParticipants); + + require(candidate.variablePart.turnNum >= 2, 'turnNum < 2; |proof|=1'); + + // supplied state must be signed by either party + require( + NitroUtils.isClaimedSignedBy(candidate.signedBy, 0) || + NitroUtils.isClaimedSignedBy(candidate.signedBy, nParticipants - 1), + 'no identity proof on margin call state' + ); + + _requireValidMarginProof(fixedPart, candidate.variablePart); + + _requireValidOutcomeTransition( + proof[0].variablePart.outcome, + candidate.variablePart.outcome, + fixedPart.participants[0], + fixedPart.participants[nParticipants - 1], + nParticipants + ); + return; + } + revert('bad proof length'); + } + + function _requireProofOfUnanimousConsensusOnPostFund( + RecoveredVariablePart memory rVP, + uint256 numParticipants + ) internal pure { + require(rVP.variablePart.turnNum == 1, 'bad proof[0].turnNum; |proof|=1'); + require( + NitroUtils.getClaimedSignersNum(rVP.signedBy) == numParticipants, + 'postfund !unanimous; |proof|=1' + ); + } + + function _requireValidMarginProof( + FixedPart memory fixedPart, + VariablePart memory variablePart + ) internal pure returns (uint256 leaderMargin, uint256 receiverMargin) { + MarginProof memory marginProof = abi.decode(variablePart.appData, (MarginProof)); + + // correct margin version + require(marginProof.version == variablePart.turnNum, 'version != turnNum'); + + // correct margin signatures + address recoveredLeader = NitroUtils.recoverSigner( + keccak256(abi.encode(NitroUtils.getChannelId(fixedPart), marginProof.version)), + marginProof.leaderSignature + ); + require(recoveredLeader == fixedPart.participants[0], 'invalid signature for voucher'); // could be incorrect channelId or incorrect signature + + address recoveredReceiver = NitroUtils.recoverSigner( + keccak256(abi.encode(NitroUtils.getChannelId(fixedPart), marginProof.version)), + marginProof.receiverSignature + ); + require( + recoveredReceiver == fixedPart.participants[fixedPart.participants.length - 1], + 'invalid signature for voucher' + ); // could be incorrect channelId or incorrect signature + + // correct outcome adjustments + require( + variablePart.outcome[0].allocations[0].amount == leaderMargin, + 'incorrect leader margin' + ); + require( + variablePart.outcome[0].allocations[1].amount == receiverMargin, + 'incorrect receiver margin' + ); + } + + function _requireValidOutcomeTransition( + Outcome.SingleAssetExit[] memory oldOutcome, + Outcome.SingleAssetExit[] memory newOutcome, + address Leader, + address Receiver, + uint8 nParticipants + ) internal pure { + // NOTE: do we need such strict rules? + // is there a scenario they can be broken in a malicious way? + + // only 1 collateral asset (USDT) for now, 2 later (+ YellowToken) + require(oldOutcome.length == 1 && newOutcome.length == 1, 'invalid number of assets'); + + // only 2 allocations + require( + oldOutcome[0].allocations.length == 2 && newOutcome[0].allocations.length == 2, + 'invalid number of allocations' + ); + + // TODO: allocations are to Leader and Receiver + // require( + // oldOutcome[0].allocations[0].destination == Leader && + // oldOutcome[0].allocations[1].destination == Receiver && + // newOutcome[0].allocations[0].destination == Leader && + // newOutcome[0].allocations[1].destination == Receiver, + // 'invalid number of allocations' + // ); + + // TODO: Add getter and setter, for Fee and collateral currencies + // newOutcome[0].asset == ASSET_FEE_ADDRESS && + // newOutcome[1].asset == ASSET_COLLATERAL_ADDRESS, + + // equal sums + uint256 oldAllocationSum; + uint256 newAllocationSum; + for (uint256 i = 0; i < nParticipants; i++) { + oldAllocationSum += oldOutcome[0].allocations[i].amount; + newAllocationSum += newOutcome[0].allocations[i].amount; + } + require(oldAllocationSum == newAllocationSum, 'total allocated cannot change'); + } +} diff --git a/contracts/clearing/YellowAdjudicator.sol b/contracts/clearing/YellowAdjudicator.sol new file mode 100644 index 0000000..bad4b14 --- /dev/null +++ b/contracts/clearing/YellowAdjudicator.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; +pragma experimental ABIEncoderV2; + +import '@statechannels/nitro-protocol/contracts/NitroAdjudicator.sol'; +import '@statechannels/nitro-protocol/contracts/libraries/NitroUtils.sol'; + +contract YellowAdjudicator is NitroAdjudicator { + struct Liability { + address asset; + uint256 amount; + } + + struct SwapSpecs { + uint64 swapSpecsFinalizationTimestamp; // guarantees swaps with equal amounts between same brokers are distinguishable + address brokerA; + address brokerB; + Liability[2][] swaps; + } + + // broker => asset => balance + mapping(address => mapping(address => uint256)) public deposits; + + // keep track of performed swaps to prevent using the same signatures twice + // REVIEW: hashedPostSwapSpecs => swapWasPerformed + mapping(bytes32 => bool) internal _swapPerformed; + + // 21_400 gas for empty function + function swap( + FixedPart calldata fixedPart, + VariablePart calldata preSwapVP, + SignedVariablePart calldata postSwapSVP + ) external { + // 12_400 gas for supplying arguments + // 1700 gas + SwapSpecs memory postSwapSpecs = abi.decode(postSwapSVP.variablePart.appData, (SwapSpecs)); + // check this swap has not been performed + // 3000 gas + require( + _swapPerformed[keccak256(abi.encode(postSwapSpecs))] == false, + 'swap already performed' + ); + // check finalizationTimestamp is < now and != 0 + // 50 gas + require(postSwapSpecs.swapSpecsFinalizationTimestamp != 0, 'swap specs not finalized yet'); + // 20 gas + require( + postSwapSpecs.swapSpecsFinalizationTimestamp < block.timestamp, + 'future swap specs finalized' + ); + + // REVIEW: what check on outcome (margin) should be performed (check outcome sums equal) + // REVIEW: should we check if guarantee allocations (margin) exist? + + // check sigs on postSwapSpecs + // 2200 gas + bytes32 postSwapSpecsHash = NitroUtils.hashState(fixedPart, postSwapSVP.variablePart); + address brokerA = postSwapSpecs.brokerA; + address brokerB = postSwapSpecs.brokerB; + + // 4000 gas + require( + NitroUtils.isSignedBy(postSwapSpecsHash, postSwapSVP.sigs[0], brokerA), + 'not signed by brokerA' + ); + // 4000 gas + require( + NitroUtils.isSignedBy(postSwapSpecsHash, postSwapSVP.sigs[1], brokerB), + 'not signed by brokerB' + ); + + // mark swap as performed + // 20000 gas + _swapPerformed[keccak256(abi.encode(postSwapSpecs))] = true; + + // perform swap + // REVIEW: how to improve readability? + for (uint256 i = 0; i < postSwapSpecs.swaps.length; i++) { + // 300 gas + address assetA = postSwapSpecs.swaps[i][0].asset; + address assetB = postSwapSpecs.swaps[i][1].asset; + uint256 amountA = postSwapSpecs.swaps[i][0].amount; + uint256 amountB = postSwapSpecs.swaps[i][1].amount; + + // broker1 - 55 WETH + // broker1 - 5 WBTC + // broker2 - 10 WETH + // broker2 - 77 WBTC + // swap: broker1 2 WBTC + // broker2 15 WETH + // broker1 - 40 WETH + // broker1 - 7 WBTC + // broker2 - 25 WETH + // broker2 - 75 WBTC + + // 5500 gas each + // 22000 gas + deposits[brokerA][assetA] -= amountB; + deposits[brokerA][assetB] += amountA; + deposits[brokerB][assetA] += amountB; + deposits[brokerB][assetB] -= amountA; + } + } + + function setDeposit(address broker, address asset, uint256 amount) external { + deposits[broker][asset] = amount; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index a44484d..7e12226 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -27,6 +27,16 @@ const config: HardhatUserConfig = { }, }, }, + { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 20_000, + }, + viaIR: true, + }, + }, ], }, typechain: { diff --git a/package-lock.json b/package-lock.json index 39c3995..d36e7e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@openzeppelin/contracts-upgradeable": "4.7.3", "@openzeppelin/hardhat-upgrades": "1.21.0", "@openzeppelin/test-helpers": "0.5.16", + "@statechannels/nitro-protocol": "^2.0.0-alpha.2", "@tsconfig/node18-strictest-esm": "1.0.1", "@typechain/ethers-v5": "10.1.0", "@typechain/hardhat": "6.1.3", @@ -3822,6 +3823,83 @@ "antlr4ts": "^0.5.0-alpha.4" } }, + "node_modules/@statechannels/exit-format": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@statechannels/exit-format/-/exit-format-0.0.6.tgz", + "integrity": "sha512-tzZqWENtSQLNEV7btigvnFnB7PRkwFagm+7tYgqbXrsHudRGjtetLAGGsJcrwGXxx1qqC2uZuDCvVdz0w2y8Nw==", + "dev": true, + "dependencies": { + "ethers": "^5.1.4", + "lodash": "^4.17.21" + } + }, + "node_modules/@statechannels/nitro-protocol": { + "version": "2.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/@statechannels/nitro-protocol/-/nitro-protocol-2.0.0-alpha.2.tgz", + "integrity": "sha512-qJ0Mvse3+VIOh58iMgcVg6/p4ZMvQFb1aoJiJnwCJHKEB03Ej6PGyYzyFs7zX7xeTrh59DLq3jQuIPaqkGFHyg==", + "dev": true, + "dependencies": { + "@openzeppelin/contracts": "^4.7.3", + "@statechannels/exit-format": "^0.0.6", + "@typechain/ethers-v5": "^9.0.0" + } + }, + "node_modules/@statechannels/nitro-protocol/node_modules/@typechain/ethers-v5": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-9.0.0.tgz", + "integrity": "sha512-bAanuPl1L2itaUdMvor/QvwnIH+TM/CmG00q17Ilv3ZZMeJ2j8HcarhgJUZ9pBY1teBb85P8cC03dz3mSSx+tQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + }, + "peerDependencies": { + "@ethersproject/abi": "^5.0.0", + "@ethersproject/bytes": "^5.0.0", + "@ethersproject/providers": "^5.0.0", + "ethers": "^5.1.3", + "typechain": "^7.0.0", + "typescript": ">=4.0.0" + } + }, + "node_modules/@statechannels/nitro-protocol/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@statechannels/nitro-protocol/node_modules/typechain": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-7.0.1.tgz", + "integrity": "sha512-4c+ecLW4mTiKwTDdofiN8ToDp7TkFC2Bzp2Pt/+qeKzkmELWzy2eDjCiv0IWHswAZhE2y9KXBhTmShzhIzD+LQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/prettier": "^2.1.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.0", + "glob": "^7.1.6", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.1.2", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + }, + "bin": { + "typechain": "dist/cli/cli.js" + }, + "peerDependencies": { + "typescript": ">=4.1.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -36173,6 +36251,65 @@ "antlr4ts": "^0.5.0-alpha.4" } }, + "@statechannels/exit-format": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@statechannels/exit-format/-/exit-format-0.0.6.tgz", + "integrity": "sha512-tzZqWENtSQLNEV7btigvnFnB7PRkwFagm+7tYgqbXrsHudRGjtetLAGGsJcrwGXxx1qqC2uZuDCvVdz0w2y8Nw==", + "dev": true, + "requires": { + "ethers": "^5.1.4", + "lodash": "^4.17.21" + } + }, + "@statechannels/nitro-protocol": { + "version": "2.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/@statechannels/nitro-protocol/-/nitro-protocol-2.0.0-alpha.2.tgz", + "integrity": "sha512-qJ0Mvse3+VIOh58iMgcVg6/p4ZMvQFb1aoJiJnwCJHKEB03Ej6PGyYzyFs7zX7xeTrh59DLq3jQuIPaqkGFHyg==", + "dev": true, + "requires": { + "@openzeppelin/contracts": "^4.7.3", + "@statechannels/exit-format": "^0.0.6", + "@typechain/ethers-v5": "^9.0.0" + }, + "dependencies": { + "@typechain/ethers-v5": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@typechain/ethers-v5/-/ethers-v5-9.0.0.tgz", + "integrity": "sha512-bAanuPl1L2itaUdMvor/QvwnIH+TM/CmG00q17Ilv3ZZMeJ2j8HcarhgJUZ9pBY1teBb85P8cC03dz3mSSx+tQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15", + "ts-essentials": "^7.0.1" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "peer": true + }, + "typechain": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/typechain/-/typechain-7.0.1.tgz", + "integrity": "sha512-4c+ecLW4mTiKwTDdofiN8ToDp7TkFC2Bzp2Pt/+qeKzkmELWzy2eDjCiv0IWHswAZhE2y9KXBhTmShzhIzD+LQ==", + "dev": true, + "peer": true, + "requires": { + "@types/prettier": "^2.1.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.0", + "glob": "^7.1.6", + "js-sha3": "^0.8.0", + "lodash": "^4.17.15", + "mkdirp": "^1.0.4", + "prettier": "^2.1.2", + "ts-command-line-args": "^2.2.0", + "ts-essentials": "^7.0.1" + } + } + } + }, "@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", diff --git a/package.json b/package.json index ee31dfc..1ea9fa3 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@openzeppelin/contracts-upgradeable": "4.7.3", "@openzeppelin/hardhat-upgrades": "1.21.0", "@openzeppelin/test-helpers": "0.5.16", + "@statechannels/nitro-protocol": "^2.0.0-alpha.2", "@tsconfig/node18-strictest-esm": "1.0.1", "@typechain/ethers-v5": "10.1.0", "@typechain/hardhat": "6.1.3",