import { expectRevert as innerExpectRevert } from '@statechannels/devtools' ; import { constants , Contract , Wallet } from 'ethers' ; import { AllocationType } from '@statechannels/exit-format' ; import embeddedApplicationArtifact from '../../../../artifacts/contracts/examples/EmbeddedApplication.sol/EmbeddedApplication.json' ; import { convertAddressToBytes32 , getChannelId , signState } from '../../../../src' ; import { encodeOutcome } from '../../../../src/contract/outcome' ; import { getFixedPart , getVariablePart , State , VariablePart } from '../../../../src/contract/state' ; import { AlreadyMoved , encodeEmbeddedApplicationData , SupportProof , } from '../../../../src/contract/embedded-application' ; import { getTestProvider , setupContract } from '../../../test-helpers' ; import { MAGIC_ADDRESS_INDICATING_ETH } from '../../../../src/transactions' ; import { Outcome } from '../../../../lib/src' ; type RevertReason = 'destinations may not change' | 'p2.amt !constant' | 'total allocation changed' | 'incorrect move from None' | 'inferior support proof' | 'incorrect move from A' | 'incorrect move from B' | 'move from None,A,B only' | 'X / J outcome mismatch' | 'X.appDefinition changed' | 'X.challengeDuration changed' | '1 or 2 states required' | 'sig0 !by participant0' | 'sig1 !by participant1' | 'sig0 on state0 !by participant0' | 'sig0 on state1 !by participant1' | 'sig0 on state1 !by participant0' | 'sig0 on state0 !by participant1' | 'invalid whoSignedWhat' | 'invalid transition in X' ; async function expectRevert ( fn , reason ) { return innerExpectRevert ( fn , reason ) ; } function absorbOutcomeOfXIntoJ ( xOutcome ) { return [ { asset : xOutcome [ 0 ] . asset , metadata : '0x' , allocations : [ { destination : xOutcome [ 0 ] . allocations [ 0 ] . destination , amount : xOutcome [ 0 ] . allocations [ 0 ] . amount , metadata : '0x' , allocationType : AllocationType . simple , } , { destination : xOutcome [ 0 ] . allocations [ 1 ] . destination , amount : xOutcome [ 0 ] . allocations [ 1 ] . amount , metadata : '0x' , allocationType : AllocationType . simple , } , { destination : convertAddressToBytes32 ( Irene . address ) , amount : '0xa' , metadata : '0x' , allocationType : AllocationType . simple , } , ] , } , ] ; } let embeddedApplication ; const Alice = Wallet . createRandom ( ) ; const Bob = Wallet . createRandom ( ) ; const Irene = Wallet . createRandom ( ) ; const sixFour = [ { asset : MAGIC_ADDRESS_INDICATING_ETH , metadata : '0x' , allocations : [ { destination : convertAddressToBytes32 ( Alice . address ) , amount : '0x6' , metadata : '0x' , allocationType : AllocationType . simple , } , { destination : convertAddressToBytes32 ( Bob . address ) , amount : '0x4' , metadata : '0x' , allocationType : AllocationType . simple , } , ] , } , ] ; const fourSix = [ { asset : MAGIC_ADDRESS_INDICATING_ETH , metadata : '0x' , allocations : [ { destination : convertAddressToBytes32 ( Alice . address ) , amount : '0x4' , metadata : '0x' , allocationType : AllocationType . simple , } , { destination : convertAddressToBytes32 ( Bob . address ) , amount : '0x6' , metadata : '0x' , allocationType : AllocationType . simple , } , ] , } , ] ; const stateForX = { turnNum : 4 , isFinal : false , channel : { chainId : '0x1' , participants : [ Alice . address , Bob . address ] , channelNonce : 87 , } , challengeDuration : 0 , outcome : sixFour , appDefinition : constants . AddressZero , appData : '0x' , } ; const greaterStateForX = { turnNum : 5 , isFinal : false , channel : { chainId : '0x1' , participants : [ Alice . address , Bob . address ] , channelNonce : 87 , } , challengeDuration : 0 , outcome : fourSix , appDefinition : constants . AddressZero , appData : '0x' , } ; const supportProofForX = stateForX => ( { fixedPart : getFixedPart ( stateForX ) , variableParts : [ getVariablePart ( stateForX ) ] , turnNumTo : stateForX . turnNum , sigs : [ signState ( stateForX , Alice . privateKey ) . signature , signState ( stateForX , Bob . privateKey ) . signature , ] , whoSignedWhat : [ 0 , 0 ] , } ) ; const NoneVariablePartForJ = { outcome : encodeOutcome ( absorbOutcomeOfXIntoJ ( stateForX . outcome as Outcome ) ) , appData : encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . None , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( stateForX ) , } ) , } ; const AvariablePartForJ = { outcome : encodeOutcome ( absorbOutcomeOfXIntoJ ( stateForX . outcome as Outcome ) ) , appData : encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( stateForX ) , } ) , } ; const BvariablePartForJ = { outcome : encodeOutcome ( absorbOutcomeOfXIntoJ ( stateForX . outcome as Outcome ) ) , appData : encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . B , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( stateForX ) , } ) , } ; const ABvariablePartForJ = { outcome : encodeOutcome ( absorbOutcomeOfXIntoJ ( greaterStateForX . outcome as Outcome ) ) , appData : encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . AB , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( greaterStateForX ) , } ) , } ; const provider = getTestProvider ( ) ; beforeAll ( async ( ) => { embeddedApplication = setupContract ( provider , embeddedApplicationArtifact , process . env . EMBEDDED_APPLICATION_ADDRESS ) ; } ) ; const turnNumTo = 0 ; const nParticipants = 0 ; const signedByFrom = 0b00 ; describe ( 'EmbeddedApplication: named state transitions' , ( ) => { it ( 'returns true / reverts for a correct / incorrect None => A transition' , async ( ) => { const result = await embeddedApplication . validTransition ( NoneVariablePartForJ , AvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b01 ) ; expect ( result ) . toBe ( true ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , AvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b10 ) , 'incorrect move from None' ) ; } ) ; it ( 'returns true / reverts for a correct / incorrect None => B transition' , async ( ) => { const result = await embeddedApplication . validTransition ( NoneVariablePartForJ , BvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b10 ) ; expect ( result ) . toBe ( true ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , BvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'incorrect move from None' ) ; } ) ; it ( 'returns true / reverts for a correct / incorrect A => AB transition' , async ( ) => { const result = await embeddedApplication . validTransition ( AvariablePartForJ , ABvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b10 ) ; expect ( result ) . toBe ( true ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( AvariablePartForJ , ABvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'incorrect move from A' ) ; } ) ; it ( 'returns true / reverts for a correct / incorrect B => AB transition' , async ( ) => { const result = await embeddedApplication . validTransition ( BvariablePartForJ , ABvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b01 ) ; expect ( result ) . toBe ( true ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( BvariablePartForJ , ABvariablePartForJ , turnNumTo , nParticipants , signedByFrom , 0b10 ) , 'incorrect move from B' ) ; } ) ; it ( 'reverts when trying to move from AB' , async ( ) => { const evenGreaterSupportProofForX = { ... ABvariablePartForJ , appData : encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( { ... greaterStateForX , turnNum : 99 } ) , } ) , } ; await expectRevert ( ( ) => embeddedApplication . validTransition ( ABvariablePartForJ , evenGreaterSupportProofForX , turnNumTo , nParticipants , signedByFrom , 0b10 ) , 'move from None,A,B only' ) ; } ) ; } ) ; describe ( 'EmbeddedApplication: reversions' , ( ) => { it ( 'reverts if destinations change' , async ( ) => { const maliciousOutcome = absorbOutcomeOfXIntoJ ( stateForX . outcome as Outcome ) ; maliciousOutcome [ 0 ] . allocations [ 2 ] . destination = convertAddressToBytes32 ( Alice . address ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , { ... AvariablePartForJ , outcome : encodeOutcome ( maliciousOutcome ) } , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'destinations may not change' ) ; } ) ; it ( 'reverts if Irene`s balance changes' , async ( ) => { const maliciousOutcome = absorbOutcomeOfXIntoJ ( stateForX . outcome as Outcome ) ; maliciousOutcome [ 0 ] . allocations [ 2 ] . amount = '0x0' ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , { ... AvariablePartForJ , outcome : encodeOutcome ( maliciousOutcome ) } , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'p2.amt !constant' ) ; } ) ; it ( 'reverts if the total amount allocated changes' , async ( ) => { const maliciousOutcome = absorbOutcomeOfXIntoJ ( stateForX . outcome as Outcome ) ; maliciousOutcome [ 0 ] . allocations [ 1 ] . amount = '0xaaa' ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , { ... AvariablePartForJ , outcome : encodeOutcome ( maliciousOutcome ) } , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'total allocation changed' ) ; } ) ; it ( 'reverts if an inferior support proof is provided' , async ( ) => { const inferiorSupportProof = { ... ABvariablePartForJ } ; ( inferiorSupportProof . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . AB , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( stateForX ) , } ) ) , await expectRevert ( ( ) => embeddedApplication . validTransition ( AvariablePartForJ , inferiorSupportProof , turnNumTo , nParticipants , signedByFrom , 0b10 ) , 'inferior support proof' ) ; } ) ; it ( 'reverts if J does not absorb X' , async ( ) => { const notProperlyAbsorbed = { ... AvariablePartForJ } ; notProperlyAbsorbed . outcome = encodeOutcome ( absorbOutcomeOfXIntoJ ( fourSix ) ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , notProperlyAbsorbed , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'X / J outcome mismatch' ) ; } ) ; it ( 'reverts if X.appDefinition changes' , async ( ) => { const appDefinitionChanged = { ... AvariablePartForJ } ; appDefinitionChanged . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( { ... stateForX , appDefinition : Alice . address } ) , } ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , appDefinitionChanged , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'X.appDefinition changed' ) ; } ) ; it ( 'reverts if X.challengeDuration changes' , async ( ) => { const challengeDurationChanged = { ... AvariablePartForJ } ; challengeDurationChanged . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : supportProofForX ( { ... stateForX , challengeDuration : 7 } ) , } ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , challengeDurationChanged , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'X.challengeDuration changed' ) ; } ) ; it ( 'reverts if no states in support proof' , async ( ) => { const malicious = { ... AvariablePartForJ } ; malicious . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : { ... supportProofForX ( stateForX ) , variableParts : [ ] as any } , } ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , malicious , turnNumTo , nParticipants , signedByFrom , 0b01 ) , '1 or 2 states required' ) ; } ) ; it ( 'reverts if whoSignedWhat is invalid' , async ( ) => { const malicious = { ... AvariablePartForJ } ; malicious . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : { ... supportProofForX ( stateForX ) , whoSignedWhat : [ 9 , 9 ] } , } ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , malicious , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'invalid whoSignedWhat' ) ; } ) ; it ( 'reverts if sig0 is corrupted' , async ( ) => { const malicious = { ... AvariablePartForJ } ; malicious . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : { ... supportProofForX ( stateForX ) , sigs : [ supportProofForX ( stateForX ) . sigs [ 1 ] , supportProofForX ( stateForX ) . sigs [ 1 ] ] , } , } ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , malicious , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'sig0 !by participant0' ) ; } ) ; it ( 'reverts if sig1 is corrupted' , async ( ) => { const malicious = { ... AvariablePartForJ } ; malicious . appData = encodeEmbeddedApplicationData ( { alreadyMoved : AlreadyMoved . A , channelIdForX : getChannelId ( stateForX . channel ) , supportProofForX : { ... supportProofForX ( stateForX ) , sigs : [ supportProofForX ( stateForX ) . sigs [ 0 ] , supportProofForX ( stateForX ) . sigs [ 0 ] ] , } , } ) ; await expectRevert ( ( ) => embeddedApplication . validTransition ( NoneVariablePartForJ , malicious , turnNumTo , nParticipants , signedByFrom , 0b01 ) , 'sig1 !by participant1' ) ; } ) ; } ) ;