// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; import {PaymentChannelManager} from "../../contracts/channels/PaymentChannelManager.sol"; import {IPaymentChannelManager} from "../../contracts/channels/IPaymentChannelManager.sol"; contract PaymentChannelManagerTest is Test { PaymentChannelManager public manager; address public admin; address public alice; address public bob; uint256 public alicePk; uint256 public bobPk; uint256 constant CHALLENGE_WINDOW = 1 hours; function setUp() public { admin = address(0x1); (alice, alicePk) = makeAddrAndKey("alice"); (bob, bobPk) = makeAddrAndKey("bob"); manager = new PaymentChannelManager(admin, CHALLENGE_WINDOW); vm.deal(alice, 100 ether); vm.deal(bob, 100 ether); } function _signState( uint256 channelId, uint256 nonce, uint256 balanceA, uint256 balanceB, uint256 pk ) internal view returns (uint8 v, bytes32 r, bytes32 s) { bytes32 stateHash = keccak256(abi.encodePacked(channelId, nonce, balanceA, balanceB)); bytes32 ethSigned = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", stateHash)); (v, r, s) = vm.sign(pk, ethSigned); } function test_openChannel() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); assertEq(channelId, 0); IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId); assertEq(ch.participantA, alice); assertEq(ch.participantB, bob); assertEq(ch.depositA, 10 ether); assertEq(ch.depositB, 0); assertEq(uint256(ch.status), uint256(IPaymentChannelManager.ChannelStatus.Open)); assertEq(address(manager).balance, 10 ether); } function test_openChannel_revertsZeroParticipant() public { vm.prank(alice); vm.expectRevert("zero participant"); manager.openChannel(address(0)); } function test_openChannel_revertsZeroDeposit() public { vm.prank(alice); vm.expectRevert("zero deposit"); manager.openChannel(bob); } function test_fundChannel() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); vm.prank(bob); manager.fundChannel{value: 10 ether}(channelId); IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId); assertEq(ch.depositA, 10 ether); assertEq(ch.depositB, 10 ether); assertEq(address(manager).balance, 20 ether); } function test_closeChannelCooperative() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); vm.prank(bob); manager.fundChannel{value: 10 ether}(channelId); uint256 balanceA = 15 ether; uint256 balanceB = 5 ether; uint256 nonce = 1; (uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, nonce, balanceA, balanceB, alicePk); (uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, nonce, balanceA, balanceB, bobPk); uint256 aliceBefore = alice.balance; uint256 bobBefore = bob.balance; vm.prank(alice); manager.closeChannelCooperative(channelId, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB); assertEq(alice.balance, aliceBefore + balanceA); assertEq(bob.balance, bobBefore + balanceB); assertEq(uint256(manager.getChannel(channelId).status), uint256(IPaymentChannelManager.ChannelStatus.Closed)); } function test_submitCloseAndFinalize() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); vm.prank(bob); manager.fundChannel{value: 10 ether}(channelId); uint256 balanceA = 12 ether; uint256 balanceB = 8 ether; uint256 nonce = 1; (uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, nonce, balanceA, balanceB, alicePk); (uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, nonce, balanceA, balanceB, bobPk); vm.prank(alice); manager.submitClose(channelId, nonce, balanceA, balanceB, vA, rA, sA, vB, rB, sB); IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId); assertEq(uint256(ch.status), uint256(IPaymentChannelManager.ChannelStatus.Dispute)); assertEq(ch.disputeNonce, nonce); assertEq(ch.disputeBalanceA, balanceA); assertEq(ch.disputeBalanceB, balanceB); vm.warp(block.timestamp + CHALLENGE_WINDOW + 1); uint256 aliceBefore = alice.balance; uint256 bobBefore = bob.balance; vm.prank(bob); manager.finalizeClose(channelId); assertEq(alice.balance, aliceBefore + balanceA); assertEq(bob.balance, bobBefore + balanceB); assertEq(uint256(manager.getChannel(channelId).status), uint256(IPaymentChannelManager.ChannelStatus.Closed)); } function test_challengeClose_newestStateWins() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); vm.prank(bob); manager.fundChannel{value: 10 ether}(channelId); uint256 balanceA1 = 12 ether; uint256 balanceB1 = 8 ether; (uint8 vA1, bytes32 rA1, bytes32 sA1) = _signState(channelId, 1, balanceA1, balanceB1, alicePk); (uint8 vB1, bytes32 rB1, bytes32 sB1) = _signState(channelId, 1, balanceA1, balanceB1, bobPk); vm.prank(alice); manager.submitClose(channelId, 1, balanceA1, balanceB1, vA1, rA1, sA1, vB1, rB1, sB1); uint256 balanceA2 = 5 ether; uint256 balanceB2 = 15 ether; (uint8 vA2, bytes32 rA2, bytes32 sA2) = _signState(channelId, 2, balanceA2, balanceB2, alicePk); (uint8 vB2, bytes32 rB2, bytes32 sB2) = _signState(channelId, 2, balanceA2, balanceB2, bobPk); vm.prank(bob); manager.challengeClose(channelId, 2, balanceA2, balanceB2, vA2, rA2, sA2, vB2, rB2, sB2); IPaymentChannelManager.Channel memory ch = manager.getChannel(channelId); assertEq(ch.disputeNonce, 2); assertEq(ch.disputeBalanceA, balanceA2); assertEq(ch.disputeBalanceB, balanceB2); vm.warp(block.timestamp + CHALLENGE_WINDOW + 1); uint256 aliceBefore = alice.balance; uint256 bobBefore = bob.balance; vm.prank(alice); manager.finalizeClose(channelId); assertEq(alice.balance, aliceBefore + balanceA2); assertEq(bob.balance, bobBefore + balanceB2); } function test_challengeClose_revertsOlderState() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); vm.prank(bob); manager.fundChannel{value: 10 ether}(channelId); uint256 balanceA1 = 12 ether; uint256 balanceB1 = 8 ether; (uint8 vA1, bytes32 rA1, bytes32 sA1) = _signState(channelId, 1, balanceA1, balanceB1, alicePk); (uint8 vB1, bytes32 rB1, bytes32 sB1) = _signState(channelId, 1, balanceA1, balanceB1, bobPk); vm.prank(alice); manager.submitClose(channelId, 1, balanceA1, balanceB1, vA1, rA1, sA1, vB1, rB1, sB1); uint256 balanceA0 = 20 ether; uint256 balanceB0 = 0; (uint8 vA0, bytes32 rA0, bytes32 sA0) = _signState(channelId, 0, balanceA0, balanceB0, alicePk); (uint8 vB0, bytes32 rB0, bytes32 sB0) = _signState(channelId, 0, balanceA0, balanceB0, bobPk); vm.prank(bob); vm.expectRevert("not newer"); manager.challengeClose(channelId, 0, balanceA0, balanceB0, vA0, rA0, sA0, vB0, rB0, sB0); } function test_finalizeClose_revertsBeforeDeadline() public { vm.prank(alice); uint256 channelId = manager.openChannel{value: 10 ether}(bob); vm.prank(bob); manager.fundChannel{value: 10 ether}(channelId); uint256 balanceA = 10 ether; uint256 balanceB = 10 ether; (uint8 vA, bytes32 rA, bytes32 sA) = _signState(channelId, 1, balanceA, balanceB, alicePk); (uint8 vB, bytes32 rB, bytes32 sB) = _signState(channelId, 1, balanceA, balanceB, bobPk); vm.prank(alice); manager.submitClose(channelId, 1, balanceA, balanceB, vA, rA, sA, vB, rB, sB); vm.expectRevert("window open"); manager.finalizeClose(channelId); } function test_pauseBlocksOpen() public { vm.prank(admin); manager.pause(); vm.prank(alice); vm.expectRevert("paused"); manager.openChannel(bob); } function test_getChannelCount() public { assertEq(manager.getChannelCount(), 0); vm.prank(alice); manager.openChannel{value: 1 ether}(bob); assertEq(manager.getChannelCount(), 1); } }