193 lines
8.7 KiB
Solidity
193 lines
8.7 KiB
Solidity
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
pragma solidity ^0.8.19;
|
||
|
|
|
||
|
|
import { Test } from "forge-std/Test.sol";
|
||
|
|
import { GenericStateChannelManager } from "../../contracts/channels/GenericStateChannelManager.sol";
|
||
|
|
import { IGenericStateChannelManager } from "../../contracts/channels/IGenericStateChannelManager.sol";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Full E2E tests for GenericStateChannelManager: deploy, open, fund, cooperative close
|
||
|
|
* with stateHash, unilateral close (submit -> finalize), and challenge (newest state wins).
|
||
|
|
*/
|
||
|
|
contract GenericStateChannelsE2ETest is Test {
|
||
|
|
GenericStateChannelManager public manager;
|
||
|
|
|
||
|
|
address public admin;
|
||
|
|
address public alice;
|
||
|
|
address public bob;
|
||
|
|
address public carol;
|
||
|
|
uint256 public alicePk;
|
||
|
|
uint256 public bobPk;
|
||
|
|
uint256 public carolPk;
|
||
|
|
|
||
|
|
uint256 constant CHALLENGE_WINDOW = 1 hours;
|
||
|
|
|
||
|
|
function _signState(
|
||
|
|
uint256 channelId,
|
||
|
|
bytes32 stateHash,
|
||
|
|
uint256 nonce,
|
||
|
|
uint256 balanceA,
|
||
|
|
uint256 balanceB,
|
||
|
|
uint256 pk
|
||
|
|
) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
|
||
|
|
bytes32 stateHashInner = keccak256(abi.encodePacked(channelId, stateHash, nonce, balanceA, balanceB));
|
||
|
|
bytes32 ethSigned = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", stateHashInner));
|
||
|
|
(v, r, s) = vm.sign(pk, ethSigned);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setUp() public {
|
||
|
|
admin = address(0x1);
|
||
|
|
(alice, alicePk) = makeAddrAndKey("alice");
|
||
|
|
(bob, bobPk) = makeAddrAndKey("bob");
|
||
|
|
(carol, carolPk) = makeAddrAndKey("carol");
|
||
|
|
|
||
|
|
manager = new GenericStateChannelManager(admin, CHALLENGE_WINDOW);
|
||
|
|
vm.deal(alice, 100 ether);
|
||
|
|
vm.deal(bob, 100 ether);
|
||
|
|
vm.deal(carol, 100 ether);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// E2E: Open (A+B fund) -> sign state with stateHash -> cooperative close -> assert balances
|
||
|
|
function testE2E_CooperativeClose_WithStateHash() public {
|
||
|
|
vm.prank(alice);
|
||
|
|
uint256 channelId = manager.openChannel{ value: 10 ether }(bob);
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.fundChannel{ value: 10 ether }(channelId);
|
||
|
|
|
||
|
|
bytes32 stateHash = keccak256("game-result-v1");
|
||
|
|
uint256 balanceA = 12 ether;
|
||
|
|
uint256 balanceB = 8 ether;
|
||
|
|
uint256 nonce = 1;
|
||
|
|
(uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, stateHash, nonce, balanceA, balanceB, alicePk);
|
||
|
|
(uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, stateHash, nonce, balanceA, balanceB, bobPk);
|
||
|
|
|
||
|
|
uint256 aliceBefore = alice.balance;
|
||
|
|
uint256 bobBefore = bob.balance;
|
||
|
|
vm.prank(alice);
|
||
|
|
manager.closeChannelCooperative(channelId, stateHash, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);
|
||
|
|
|
||
|
|
assertEq(alice.balance, aliceBefore + balanceA, "alice balance");
|
||
|
|
assertEq(bob.balance, bobBefore + balanceB, "bob balance");
|
||
|
|
assertEq(uint256(manager.getChannel(channelId).status), uint256(IGenericStateChannelManager.ChannelStatus.Closed), "status closed");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// E2E: Open -> fund -> submitClose (stateHash) -> warp past deadline -> finalizeClose -> assert balances
|
||
|
|
function testE2E_UnilateralClose_WithStateHash() public {
|
||
|
|
vm.prank(alice);
|
||
|
|
uint256 channelId = manager.openChannel{ value: 10 ether }(bob);
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.fundChannel{ value: 10 ether }(channelId);
|
||
|
|
|
||
|
|
bytes32 stateHash = keccak256("attestation");
|
||
|
|
uint256 balanceA = 6 ether;
|
||
|
|
uint256 balanceB = 14 ether;
|
||
|
|
uint256 nonce = 1;
|
||
|
|
(uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, stateHash, nonce, balanceA, balanceB, alicePk);
|
||
|
|
(uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, stateHash, nonce, balanceA, balanceB, bobPk);
|
||
|
|
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.submitClose(channelId, stateHash, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB);
|
||
|
|
|
||
|
|
vm.warp(block.timestamp + CHALLENGE_WINDOW + 1);
|
||
|
|
uint256 aliceBefore = alice.balance;
|
||
|
|
uint256 bobBefore = bob.balance;
|
||
|
|
vm.prank(carol);
|
||
|
|
manager.finalizeClose(channelId);
|
||
|
|
|
||
|
|
assertEq(alice.balance, aliceBefore + balanceA, "alice balance");
|
||
|
|
assertEq(bob.balance, bobBefore + balanceB, "bob balance");
|
||
|
|
assertEq(uint256(manager.getChannel(channelId).status), uint256(IGenericStateChannelManager.ChannelStatus.Closed), "status closed");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// E2E: submitClose (old state) -> challengeClose (newer state with different stateHash) -> finalize -> newest state wins
|
||
|
|
function testE2E_Challenge_NewestStateWins_WithStateHash() public {
|
||
|
|
vm.prank(alice);
|
||
|
|
uint256 channelId = manager.openChannel{ value: 10 ether }(bob);
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.fundChannel{ value: 10 ether }(channelId);
|
||
|
|
|
||
|
|
bytes32 stateHash1 = keccak256("state-v1");
|
||
|
|
uint256 balanceA1 = 15 ether;
|
||
|
|
uint256 balanceB1 = 5 ether;
|
||
|
|
(uint8 vA1, bytes32 rA1, bytes32 sA1) = _signState(channelId, stateHash1, 1, balanceA1, balanceB1, alicePk);
|
||
|
|
(uint8 vB1, bytes32 rB1, bytes32 sB1) = _signState(channelId, stateHash1, 1, balanceA1, balanceB1, bobPk);
|
||
|
|
vm.prank(alice);
|
||
|
|
manager.submitClose(channelId, stateHash1, 1, balanceA1, balanceB1, vA1, rA1, sA1, vB1, rB1, sB1);
|
||
|
|
|
||
|
|
bytes32 stateHash2 = keccak256("state-v2");
|
||
|
|
uint256 balanceA2 = 4 ether;
|
||
|
|
uint256 balanceB2 = 16 ether;
|
||
|
|
(uint8 vA2, bytes32 rA2, bytes32 sA2) = _signState(channelId, stateHash2, 2, balanceA2, balanceB2, alicePk);
|
||
|
|
(uint8 vB2, bytes32 rB2, bytes32 sB2) = _signState(channelId, stateHash2, 2, balanceA2, balanceB2, bobPk);
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.challengeClose(channelId, stateHash2, 2, balanceA2, balanceB2, vA2, rA2, sA2, vB2, rB2, sB2);
|
||
|
|
|
||
|
|
vm.warp(block.timestamp + CHALLENGE_WINDOW + 1);
|
||
|
|
uint256 aliceBefore = alice.balance;
|
||
|
|
uint256 bobBefore = bob.balance;
|
||
|
|
manager.finalizeClose(channelId);
|
||
|
|
|
||
|
|
assertEq(alice.balance, aliceBefore + balanceA2, "alice gets newer state share");
|
||
|
|
assertEq(bob.balance, bobBefore + balanceB2, "bob gets newer state share");
|
||
|
|
assertEq(uint256(manager.getChannel(channelId).status), uint256(IGenericStateChannelManager.ChannelStatus.Closed), "closed");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// E2E: Pause blocks open/fund; existing channel can still be closed cooperatively
|
||
|
|
function testE2E_Pause_AllowsCloseNotOpen() public {
|
||
|
|
vm.prank(alice);
|
||
|
|
uint256 channelId = manager.openChannel{ value: 10 ether }(bob);
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.fundChannel{ value: 10 ether }(channelId);
|
||
|
|
|
||
|
|
vm.prank(admin);
|
||
|
|
manager.pause();
|
||
|
|
|
||
|
|
vm.prank(alice);
|
||
|
|
vm.expectRevert("paused");
|
||
|
|
manager.openChannel(carol);
|
||
|
|
|
||
|
|
bytes32 stateHash = keccak256("final");
|
||
|
|
uint256 balanceA = 10 ether;
|
||
|
|
uint256 balanceB = 10 ether;
|
||
|
|
(uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, stateHash, 1, balanceA, balanceB, alicePk);
|
||
|
|
(uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, stateHash, 1, balanceA, balanceB, bobPk);
|
||
|
|
vm.prank(alice);
|
||
|
|
manager.closeChannelCooperative(channelId, stateHash, 1, balanceA, balanceB, vA, rA, sA, vB, rB, sB);
|
||
|
|
|
||
|
|
assertEq(uint256(manager.getChannel(channelId).status), uint256(IGenericStateChannelManager.ChannelStatus.Closed), "closed while paused");
|
||
|
|
}
|
||
|
|
|
||
|
|
/// E2E: Two channels; close first cooperatively (stateHash), second via submit -> finalize
|
||
|
|
function testE2E_MultipleChannels_WithStateHash() public {
|
||
|
|
vm.prank(alice);
|
||
|
|
uint256 id1 = manager.openChannel{ value: 5 ether }(bob);
|
||
|
|
vm.prank(bob);
|
||
|
|
manager.fundChannel{ value: 5 ether }(id1);
|
||
|
|
|
||
|
|
vm.prank(alice);
|
||
|
|
uint256 id2 = manager.openChannel{ value: 3 ether }(carol);
|
||
|
|
|
||
|
|
bytes32 sh1 = keccak256("ch1");
|
||
|
|
uint256 a1 = 7 ether;
|
||
|
|
uint256 b1 = 3 ether;
|
||
|
|
(uint8 vA1, bytes32 rA1, bytes32 sA1) = _signState(id1, sh1, 1, a1, b1, alicePk);
|
||
|
|
(uint8 vB1, bytes32 rB1, bytes32 sB1) = _signState(id1, sh1, 1, a1, b1, bobPk);
|
||
|
|
vm.prank(alice);
|
||
|
|
manager.closeChannelCooperative(id1, sh1, 1, a1, b1, vA1, rA1, sA1, vB1, rB1, sB1);
|
||
|
|
|
||
|
|
bytes32 sh2 = keccak256("ch2");
|
||
|
|
uint256 a2 = 1 ether;
|
||
|
|
uint256 c2 = 2 ether;
|
||
|
|
(uint8 vA2, bytes32 rA2, bytes32 sA2) = _signState(id2, sh2, 1, a2, c2, alicePk);
|
||
|
|
(uint8 vC2, bytes32 rC2, bytes32 sC2) = _signState(id2, sh2, 1, a2, c2, carolPk);
|
||
|
|
vm.prank(carol);
|
||
|
|
manager.submitClose(id2, sh2, 1, a2, c2, vA2, rA2, sA2, vC2, rC2, sC2);
|
||
|
|
vm.warp(block.timestamp + CHALLENGE_WINDOW + 1);
|
||
|
|
manager.finalizeClose(id2);
|
||
|
|
|
||
|
|
assertEq(uint256(manager.getChannel(id1).status), uint256(IGenericStateChannelManager.ChannelStatus.Closed), "ch1 closed");
|
||
|
|
assertEq(uint256(manager.getChannel(id2).status), uint256(IGenericStateChannelManager.ChannelStatus.Closed), "ch2 closed");
|
||
|
|
assertEq(manager.getChannelCount(), 2, "two channels");
|
||
|
|
}
|
||
|
|
}
|