diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 7725282..974ed87 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -23,13 +23,24 @@ jobs: - run: npm ci - run: npm run coverage:sol + - uses: hrishikesh-kadam/setup-lcov@v1 + - name: Filter LCOV + run: | + lcov --remove lcov.info \ + 'test/*' \ + 'script/*' \ + 'src/proto/**' \ + 'src/core/PacketHandler.sol' \ + 'src/core/ContractRegistry.sol' \ + -o lcov.filtered.info + - name: Report code coverage uses: zgosalvez/github-actions-report-lcov@v5 with: - coverage-files: lcov.info - minimum-coverage: 0 # TODO: Once new tests are added, increase minimum-coverage from 0 to a realistic gate (e.g., 5–10%), and keep bumping it as coverage improves. + coverage-files: lcov.filtered.info + minimum-coverage: 100 artifact-name: code-coverage-report github-token: ${{ secrets.GITHUB_TOKEN }} update-comment: true diff --git a/script/DeployAll.s.sol b/script/DeployAll.s.sol index c1e0e77..888db14 100644 --- a/script/DeployAll.s.sol +++ b/script/DeployAll.s.sol @@ -41,7 +41,6 @@ import {ILightClient} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/02 contract DeployAll is Script, Config { // === Deployment artifacts (written back to deployments.toml) === IBCHandler public ibcHandler; - MockCrossContract public mockApp; CrossSimpleModule public crossSimpleModule; MockClient public mockClient; @@ -92,57 +91,67 @@ contract DeployAll is Script, Config { console2.log(" Initialized. port=%s, clientType=%s", portCross, mockClientType); } - // ---------- entry ---------- - function run() external { - // 1) Load config with write-back enabled (stores results after deployment) - _loadConfig( - "./deployments.toml", - /*writeBack=*/ - true - ); + function _readConfig() + internal + returns ( + string memory mnemonic, + uint32 mnemonicIndex, + bool debugMode, + string memory portCross, + string memory mockClientType + ) + { + string memory m = config.get("mnemonic").toString(); + uint256 idxU256 = config.get("mnemonic_index").toUint256(); + require(idxU256 < 2 ** 32, "mnemonic_index too large"); + uint32 idx = uint32(idxU256); + bool dbg = config.get("debug_mode").toBool(); + string memory port = config.get("port_cross").toString(); + string memory cli = config.get("mock_client_type").toString(); + return (m, idx, dbg, port, cli); + } - uint256 chainId = block.chainid; + function _logConfig( + uint256 chainId, + bool debugMode, + string memory portCross, + string memory mockClientType, + uint32 mnemonicIndex + ) internal { console2.log("Deploying to chain:", chainId); - - // 2) Read configuration values (resolved for the current chain) - // - Required: - // string: mnemonic - // uint: mnemonic_index - // bool: debug_mode - // string: port_cross - // string: mock_client_type - string memory mnemonic = config.get("mnemonic").toString(); - uint256 mnemonicIndexU256 = config.get("mnemonic_index").toUint256(); - // solhint-disable-next-line gas-strict-inequalities - require(mnemonicIndexU256 <= type(uint32).max, "mnemonic_index too large"); - uint32 mnemonicIndex = uint32(mnemonicIndexU256); - - bool debugMode = config.get("debug_mode").toBool(); - string memory portCross = config.get("port_cross").toString(); - string memory mockClientType = config.get("mock_client_type").toString(); - console2.log("Config:"); console2.log(" debug_mode :", debugMode); console2.log(" port_cross :", portCross); console2.log(" mock_client_type:", mockClientType); console2.log(" mnemonic_index :", mnemonicIndex); + } - // 3) Derive deployer private key from mnemonic + index (Foundry cheatcode) - uint256 deployerPk = vm.deriveKey(mnemonic, mnemonicIndex); - address deployer = vm.addr(deployerPk); - console2.log("Deployer:", deployer); + function _deriveDeployer(string memory mnemonic, uint32 mnemonicIndex) + internal + returns (uint256 deployerPk, address deployer) + { + uint256 pk = vm.deriveKey(mnemonic, mnemonicIndex); + address addr = vm.addr(pk); + console2.log("Deployer:", addr); + return (pk, addr); + } - // 4) Deploy + Initialize (single broadcast session) + function _broadcastDeployAndInit( + uint256 deployerPk, + bool debugMode, + string memory portCross, + string memory mockClientType + ) internal { vm.startBroadcast(deployerPk); - ibcHandler = _deployCore(); (mockApp, crossSimpleModule, mockClient) = _deployApp(ibcHandler, debugMode); _initialize(ibcHandler, crossSimpleModule, portCross, mockClientType, mockClient); - vm.stopBroadcast(); + } - // 5) Write back: save addresses & metadata to deployments.toml - // (addresses go under .address.*, meta under .meta.*) + function _writeBack(address deployer) internal { + // save addresses & metadata to deployments.toml + // (addresses go under .address.*, meta under .meta.*) config.set("ibc_handler", address(ibcHandler)); config.set("mock_cross_contract", address(mockApp)); config.set("cross_simple_module", address(crossSimpleModule)); @@ -150,7 +159,28 @@ contract DeployAll is Script, Config { // Meta config.set("deployer", deployer); - console2.log("\nDeployment complete! Addresses saved to deployments.toml"); } + + // ---------- entry ---------- + function run() external { + _loadConfig("./deployments.toml", true); + + uint256 chainId = block.chainid; + ( + string memory mnemonic, + uint32 mnemonicIndex, + bool debugMode, + string memory portCross, + string memory mockClientType + ) = _readConfig(); + + _logConfig(chainId, debugMode, portCross, mockClientType, mnemonicIndex); + + (uint256 deployerPk, address deployer) = _deriveDeployer(mnemonic, mnemonicIndex); + + _broadcastDeployAndInit(deployerPk, debugMode, portCross, mockClientType); + + _writeBack(deployer); + } } diff --git a/test/CrossModule.t.sol b/test/CrossModule.t.sol new file mode 100644 index 0000000..a8d8095 --- /dev/null +++ b/test/CrossModule.t.sol @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +// solhint-disable one-contract-per-file, func-name-mixedcase +pragma solidity ^0.8.20; + +import "forge-std/src/Test.sol"; + +import "../src/core/CrossModule.sol"; +import { + IIBCModule, + IIBCModuleInitializer +} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/26-router/IIBCModule.sol"; +import {IIBCHandler} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/25-handler/IIBCHandler.sol"; +import {Packet} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/04-channel/IIBCChannel.sol"; + +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; + +contract DummyHandler {} + +contract TestableCrossModule is CrossModule { + uint256 public recvCount; + uint256 public ackCount; + uint256 public timeoutCount; + bytes public lastAckArg; + + constructor(IIBCHandler h) CrossModule(h) {} + + function handlePacket( + Packet memory /*packet*/ + ) + internal + virtual + override + returns (bytes memory) + { + ++recvCount; + return bytes("ack-ok"); + } + + function handleAcknowledgement( + Packet memory, + /*packet*/ + bytes memory acknowledgement + ) + internal + virtual + override + { + ++ackCount; + lastAckArg = acknowledgement; + } + + function handleTimeout( + Packet calldata /*packet*/ + ) + internal + virtual + override + { + ++timeoutCount; + } +} + +contract CrossModuleTest is Test { + DummyHandler private handler; + TestableCrossModule private mod; + Packet internal _emptyPacket; + + function setUp() public { + handler = new DummyHandler(); + mod = new TestableCrossModule(IIBCHandler(address(handler))); + } + + function test_constructor_GrantsIbcRoleToHandler() public { + assertTrue(mod.hasRole(mod.IBC_ROLE(), address(handler))); + } + + function test_supportsInterface_ReturnsTrueForIIBCAndIAccessControlAndFalseForUnsupported() public view { + assertTrue(mod.supportsInterface(type(IIBCModule).interfaceId)); + assertTrue(mod.supportsInterface(type(IIBCModuleInitializer).interfaceId)); + assertTrue(mod.supportsInterface(type(IAccessControl).interfaceId)); + assertFalse(mod.supportsInterface(0xDEADBEEF)); + } + + function test_onRecvPacket_CallsHandlerAndReturnsAckWhenCallerHasRole() public { + vm.prank(address(handler)); + bytes memory ack = mod.onRecvPacket(_emptyPacket, address(0)); + assertEq(ack, bytes("ack-ok")); + assertEq(mod.recvCount(), 1); + } + + function test_onRecvPacket_RevertWhen_CallerLacksIbcRole() public { + vm.expectRevert(); + mod.onRecvPacket(_emptyPacket, address(0)); + } + + function test_onAcknowledgementPacket_CallsHandlerWhenCallerHasRole() public { + vm.prank(address(handler)); + mod.onAcknowledgementPacket(_emptyPacket, bytes("ack123"), address(0)); + assertEq(mod.ackCount(), 1); + assertEq(mod.lastAckArg(), bytes("ack123")); + } + + function test_onAcknowledgementPacket_RevertWhen_CallerLacksIbcRole() public { + vm.expectRevert(); + mod.onAcknowledgementPacket(_emptyPacket, bytes("ack"), address(0)); + } + + function test_onTimeoutPacket_CallsHandlerWhenCallerHasRole() public { + vm.prank(address(handler)); + mod.onTimeoutPacket(_emptyPacket, address(0)); + assertEq(mod.timeoutCount(), 1); + } + + function test_onTimeoutPacket_RevertWhen_CallerLacksIbcRole() public { + vm.expectRevert(); + mod.onTimeoutPacket(_emptyPacket, address(0)); + } + + function test_onChanOpenInit_ReturnsSelfAndVersionWhenCallerHasRole() public { + IIBCModuleInitializer.MsgOnChanOpenInit memory m; + m.version = "v1"; + vm.prank(address(handler)); + (address moduleAddr, string memory version) = mod.onChanOpenInit(m); + assertEq(moduleAddr, address(mod)); + assertEq(version, "v1"); + } + + function test_onChanOpenInit_RevertWhen_CallerLacksIbcRole() public { + IIBCModuleInitializer.MsgOnChanOpenInit memory m; + m.version = "v1"; + vm.expectRevert(); + mod.onChanOpenInit(m); + } + + function test_onChanOpenTry_ReturnsSelfAndCounterpartyVersionWhenCallerHasRole() public { + IIBCModuleInitializer.MsgOnChanOpenTry memory m; + m.counterpartyVersion = "cp-v1"; + vm.prank(address(handler)); + (address moduleAddr, string memory version) = mod.onChanOpenTry(m); + assertEq(moduleAddr, address(mod)); + assertEq(version, "cp-v1"); + } + + function test_onChanOpenTry_RevertWhen_CallerLacksIbcRole() public { + IIBCModuleInitializer.MsgOnChanOpenTry memory m; + m.counterpartyVersion = "cp-v1"; + vm.expectRevert(); + mod.onChanOpenTry(m); + } + + function test_onChanOpenAck_SucceedsWhenCallerHasRole() public { + IIBCModule.MsgOnChanOpenAck memory m; + vm.prank(address(handler)); + mod.onChanOpenAck(m); // no revert + } + + function test_onChanOpenAck_RevertWhen_CallerLacksIbcRole() public { + IIBCModule.MsgOnChanOpenAck memory m; + vm.expectRevert(); + mod.onChanOpenAck(m); + } + + function test_onChanOpenConfirm_SucceedsWhenCallerHasRole() public { + IIBCModule.MsgOnChanOpenConfirm memory m; + vm.prank(address(handler)); + mod.onChanOpenConfirm(m); // no revert + } + + function test_onChanOpenConfirm_RevertWhen_CallerLacksIbcRole() public { + IIBCModule.MsgOnChanOpenConfirm memory m; + vm.expectRevert(); + mod.onChanOpenConfirm(m); + } + + function test_onChanCloseInit_SucceedsWhenCallerHasRole() public { + IIBCModule.MsgOnChanCloseInit memory m; + vm.prank(address(handler)); + mod.onChanCloseInit(m); // no revert + } + + function test_onChanCloseInit_RevertWhen_CallerLacksIbcRole() public { + IIBCModule.MsgOnChanCloseInit memory m; + vm.expectRevert(); + mod.onChanCloseInit(m); + } + + function test_onChanCloseConfirm_SucceedsWhenCallerHasRole() public { + IIBCModule.MsgOnChanCloseConfirm memory m; + vm.prank(address(handler)); + mod.onChanCloseConfirm(m); // no revert + } + + function test_onChanCloseConfirm_RevertWhen_CallerLacksIbcRole() public { + IIBCModule.MsgOnChanCloseConfirm memory m; + vm.expectRevert(); + mod.onChanCloseConfirm(m); + } +} diff --git a/test/CrossSimpleModule.t.sol b/test/CrossSimpleModule.t.sol new file mode 100644 index 0000000..41ed24c --- /dev/null +++ b/test/CrossSimpleModule.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// solhint-disable one-contract-per-file, func-name-mixedcase +pragma solidity ^0.8.20; + +import "forge-std/src/Test.sol"; + +import "../src/core/CrossSimpleModule.sol"; +import "../src/core/IContractModule.sol"; +import "../src/core/TxAtomicSimple.sol"; +import {IIBCHandler} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/25-handler/IIBCHandler.sol"; +import {Packet} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/04-channel/IIBCChannel.sol"; + +contract DummyHandler {} + +contract DummyModule is IContractModule { + // do nothing implementation + function onContractCall( + CrossContext calldata, + /*context*/ + bytes calldata /*callInfo*/ + ) + external + pure + returns (bytes memory) + { + return ""; + } +} + +contract CrossSimpleModuleHarness is CrossSimpleModule { + constructor(IIBCHandler h, IContractModule m, bool debugMode) CrossSimpleModule(h, m, debugMode) {} + + function exposed_getModule(Packet calldata p) external returns (IContractModule) { + return getModule(p); + } + + function exposed_registerModule(IContractModule m) external { + registerModule(m); + } + + function workaround_hasIbcRole(address a) external view returns (bool) { + return hasRole(IBC_ROLE, a); + } +} + +contract CrossSimpleModuleTest is Test { + DummyHandler private handler; + DummyModule private moduleImpl; + Packet internal _emptyPacket; + + function setUp() public { + handler = new DummyHandler(); + moduleImpl = new DummyModule(); + } + + function test_constructor_RegistersModuleAndGetReturnsSameAddress() public { + CrossSimpleModuleHarness harness = + new CrossSimpleModuleHarness(IIBCHandler(address(handler)), IContractModule(address(moduleImpl)), false); + + IContractModule got = harness.exposed_getModule(_emptyPacket); + assertEq(address(got), address(moduleImpl), "must register"); + } + + function test_constructor_GrantsIbcRoleWhenDebugModeTrue() public { + CrossSimpleModuleHarness harness = + new CrossSimpleModuleHarness(IIBCHandler(address(handler)), IContractModule(address(moduleImpl)), true); + + assertTrue(harness.workaround_hasIbcRole(address(this)), "role on debug"); + } + + function test_constructor_DoesNotGrantIbcRoleWhenDebugModeFalse() public { + CrossSimpleModuleHarness harness = + new CrossSimpleModuleHarness(IIBCHandler(address(handler)), IContractModule(address(moduleImpl)), false); + + assertFalse(harness.workaround_hasIbcRole(address(this)), "no role on debug"); + } + + function test_register_RevertOn_SecondInitialization() public { + CrossSimpleModuleHarness harness = + new CrossSimpleModuleHarness(IIBCHandler(address(handler)), IContractModule(address(moduleImpl)), false); + + vm.expectRevert(SimpleContractRegistry.ModuleAlreadyInitialized.selector); + harness.exposed_registerModule(IContractModule(address(moduleImpl))); + } + + function test_getPacketAcknowledgementCall_DifferentStatusesProduceDifferentBytes() public { + CrossSimpleModuleHarness harness = + new CrossSimpleModuleHarness(IIBCHandler(address(handler)), IContractModule(address(moduleImpl)), false); + + bytes memory a = harness.getPacketAcknowledgementCall(PacketAcknowledgementCall.CommitStatus.COMMIT_STATUS_OK); + bytes memory b = + harness.getPacketAcknowledgementCall(PacketAcknowledgementCall.CommitStatus.COMMIT_STATUS_FAILED); + + assertGt(a.length, 0, "ack nonempty"); + assertGt(b.length, 0, "ack nonempty"); + assertFalse(keccak256(a) == keccak256(b), "diff enc"); + } +} diff --git a/test/IBCKeeper.t.sol b/test/IBCKeeper.t.sol index 97db50d..ac7cba3 100644 --- a/test/IBCKeeper.t.sol +++ b/test/IBCKeeper.t.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 +// solhint-disable one-contract-per-file, func-name-mixedcase pragma solidity ^0.8.20; import "forge-std/src/Test.sol"; @@ -6,30 +7,30 @@ import "../src/core/IBCKeeper.sol"; contract DummyHandler {} -contract TestableIBCKeeper is IBCKeeper { +contract IBCKeeperHarness is IBCKeeper { constructor(IIBCHandler h) IBCKeeper(h) {} - function handlerAddr() external view returns (address) { + function exposed_getIBCHandler() external view returns (address) { return address(getIBCHandler()); } } contract IBCKeeperTest is Test { - DummyHandler dummy; - TestableIBCKeeper keeper; + DummyHandler private dummy; + IBCKeeperHarness private keeper; function setUp() public { dummy = new DummyHandler(); - keeper = new TestableIBCKeeper(IIBCHandler(address(dummy))); + keeper = new IBCKeeperHarness(IIBCHandler(address(dummy))); } - function test_GetIBCHandler_GetIBCHandlerReturnsSameAddress() public view { - address h = keeper.handlerAddr(); + function test_getIBCHandler_ReturnsSameAddress() public view { + address h = keeper.exposed_getIBCHandler(); assertEq(h, address(dummy)); } - function test_Constructor_AllowsZeroAddress() public { - TestableIBCKeeper k = new TestableIBCKeeper(IIBCHandler(address(0))); - assertEq(k.handlerAddr(), address(0)); + function test_constructor_AllowsZeroAddress() public { + IBCKeeperHarness k = new IBCKeeperHarness(IIBCHandler(address(0))); + assertEq(k.exposed_getIBCHandler(), address(0)); } } diff --git a/test/MockCrossContract.t.sol b/test/MockCrossContract.t.sol new file mode 100644 index 0000000..a2d3154 --- /dev/null +++ b/test/MockCrossContract.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// solhint-disable one-contract-per-file, func-name-mixedcase +pragma solidity ^0.8.20; + +import "forge-std/src/Test.sol"; + +import "../src/example/MockCrossContract.sol"; +import "../src/core/IContractModule.sol"; + +import {Account as AuthAccount, AuthType} from "../src/proto/cross/core/auth/Auth.sol"; +import {GoogleProtobufAny as Any} from "@hyperledger-labs/yui-ibc-solidity/contracts/proto/GoogleProtobufAny.sol"; + +contract MockCrossContractTest is Test { + MockCrossContract private mock; + + function setUp() public { + mock = new MockCrossContract(); + } + + function _mkSigner(bytes memory id, AuthType.AuthMode mode) internal pure returns (AuthAccount.Data memory s) { + Any.Data memory emptyOpt = Any.Data({type_url: "", value: ""}); + AuthType.Data memory at = AuthType.Data({mode: mode, option: emptyOpt}); + s = AuthAccount.Data({id: id, auth_type: at}); + } + + function _mkContextSingle(bytes memory id, AuthType.AuthMode mode) internal pure returns (CrossContext memory ctx) { + AuthAccount.Data[] memory signers = new AuthAccount.Data[](1); + signers[0] = _mkSigner(id, mode); + + ctx = CrossContext({txId: hex"11", txIndex: 1, signers: signers}); + } + + function test_onContractCall_ReturnsExpectedBytes() public { + CrossContext memory ctx = _mkContextSingle(bytes("tester"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + bytes memory callInfo = hex"01"; + + bytes memory ret = mock.onContractCall(ctx, callInfo); + + assertEq(ret, bytes("mock call succeed")); + } + + function test_onContractCall_RevertWhen_SignersLenIsZero() public { + AuthAccount.Data[] memory signers = new AuthAccount.Data[](0); + CrossContext memory ctx = CrossContext({txId: hex"22", txIndex: 1, signers: signers}); + + vm.expectRevert(bytes("signers length must be 1")); + mock.onContractCall(ctx, hex"01"); + } + + function test_onContractCall_RevertWhen_SignersLenIsTwo() public { + AuthAccount.Data[] memory signers = new AuthAccount.Data[](2); + signers[0] = _mkSigner(bytes("tester"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + signers[1] = _mkSigner(bytes("tester"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + + CrossContext memory ctx = CrossContext({txId: hex"33", txIndex: 1, signers: signers}); + + vm.expectRevert(bytes("signers length must be 1")); + mock.onContractCall(ctx, hex"01"); + } + + function test_onContractCall_RevertWhen_AuthModeIsNotChannel() public { + CrossContext memory ctx = _mkContextSingle(bytes("tester"), AuthType.AuthMode.AUTH_MODE_EXTENSION); + + vm.expectRevert(bytes("auth mode must be CHANNEL")); + mock.onContractCall(ctx, hex"01"); + } + + function test_onContractCall_RevertWhen_UnexpectedAccountId() public { + CrossContext memory ctx = _mkContextSingle(bytes("hacker"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + + vm.expectRevert(bytes("unexpected account ID")); + mock.onContractCall(ctx, hex"01"); + } + + function test_onContractCall_RevertWhen_CallInfoLenIsZero() public { + CrossContext memory ctx = _mkContextSingle(bytes("tester"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + + vm.expectRevert(bytes("the length of callInfo must be 1")); + mock.onContractCall(ctx, bytes("")); + } + + function test_onContractCall_RevertWhen_CallInfoLenIsTwo() public { + CrossContext memory ctx = _mkContextSingle(bytes("tester"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + + bytes memory callInfo = new bytes(2); + callInfo[0] = 0x01; + callInfo[1] = 0x02; + + vm.expectRevert(bytes("the length of callInfo must be 1")); + mock.onContractCall(ctx, callInfo); + } + + function test_onContractCall_RevertWhen_CallInfoIsNot0x01() public { + CrossContext memory ctx = _mkContextSingle(bytes("tester"), AuthType.AuthMode.AUTH_MODE_CHANNEL); + + vm.expectRevert(bytes("callInfo must be 0x01")); + mock.onContractCall(ctx, hex"02"); + } +} diff --git a/test/SimpleContractRegistry.t.sol b/test/SimpleContractRegistry.t.sol new file mode 100644 index 0000000..8cfc741 --- /dev/null +++ b/test/SimpleContractRegistry.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// solhint-disable one-contract-per-file, func-name-mixedcase +pragma solidity ^0.8.20; + +import "forge-std/src/Test.sol"; + +import "../src/core/SimpleContractRegistry.sol"; +import "../src/core/IContractModule.sol"; +import {Packet} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/04-channel/IIBCChannel.sol"; + +contract DummyModule is IContractModule { + // do nothing implementation + function onContractCall( + CrossContext calldata, + /*context*/ + bytes calldata /*callInfo*/ + ) + external + pure + returns (bytes memory) + { + return ""; + } +} + +contract SimpleContractRegistryHarness is SimpleContractRegistry { + function exposed_registerModule(IContractModule m) external { + registerModule(m); + } + + function exposed_getModule(Packet calldata p) external returns (IContractModule) { + return getModule(p); + } + + function workaround_moduleAddr() external view returns (address) { + return address(contractModule); + } +} + +contract SimpleContractRegistryTest is Test { + DummyModule private dummy; + SimpleContractRegistryHarness private registry; + + Packet internal _emptyPacket; + + function setUp() public { + dummy = new DummyModule(); + registry = new SimpleContractRegistryHarness(); + } + + function test_get_RevertWhen_NotInitialized() public { + vm.expectRevert(SimpleContractRegistry.ModuleNotInitialized.selector); + registry.exposed_getModule(_emptyPacket); + } + + function test_register_ThenGetReturnsSameAddress() public { + IContractModule m = IContractModule(address(dummy)); + + registry.exposed_registerModule(m); + + IContractModule got = registry.exposed_getModule(_emptyPacket); + assertEq(address(got), address(m)); + assertEq(registry.workaround_moduleAddr(), address(m)); + } + + function test_register_RevertOn_SecondInitialization() public { + IContractModule m = IContractModule(address(dummy)); + + registry.exposed_registerModule(m); + + vm.expectRevert(SimpleContractRegistry.ModuleAlreadyInitialized.selector); + registry.exposed_registerModule(m); + } + + function testFuzz_register_ThenGetReturnsSameAddress(address anyAddr) public { + vm.assume(anyAddr != address(0)); + IContractModule m = IContractModule(anyAddr); + + registry.exposed_registerModule(m); + + IContractModule got = registry.exposed_getModule(_emptyPacket); + assertEq(address(got), anyAddr); + } +} diff --git a/test/TxAtomicSimple.t.sol b/test/TxAtomicSimple.t.sol new file mode 100644 index 0000000..40c886a --- /dev/null +++ b/test/TxAtomicSimple.t.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// solhint-disable one-contract-per-file, func-name-mixedcase +pragma solidity ^0.8.20; + +import "forge-std/src/Test.sol"; + +import "../src/core/TxAtomicSimple.sol"; +import "../src/core/IContractModule.sol"; +import "../src/core/IBCKeeper.sol"; +import {IIBCHandler} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/25-handler/IIBCHandler.sol"; +import {Packet} from "@hyperledger-labs/yui-ibc-solidity/contracts/core/04-channel/IIBCChannel.sol"; + +import "../src/proto/cross/core/atomic/simple/AtomicSimple.sol"; +import {GoogleProtobufAny as Any} from "@hyperledger-labs/yui-ibc-solidity/contracts/proto/GoogleProtobufAny.sol"; +import {Account as AuthAccount} from "../src/proto/cross/core/auth/Auth.sol"; + +contract DummyHandler {} + +contract SuccessModule is IContractModule { + bytes public retBytes; + + constructor(bytes memory r) { + retBytes = r; + } + + function onContractCall( + CrossContext calldata, + /*context*/ + bytes calldata /*callInfo*/ + ) + external + view + returns (bytes memory) + { + return retBytes; + } +} + +contract RevertingModule is IContractModule { + function onContractCall( + CrossContext calldata, + /*context*/ + bytes calldata /*callInfo*/ + ) + external + pure + returns (bytes memory) + { + revert("boom"); + } +} + +contract TxAtomicSimpleHarness is TxAtomicSimple { + IContractModule internal _module; + + constructor(IIBCHandler h) IBCKeeper(h) {} + + function registerModule(IContractModule module) internal override { + _module = module; + } + + function getModule( + Packet memory /*packet*/ + ) + internal + override + returns (IContractModule) + { + return _module; + } + + function exposed_handlePacket(Packet calldata p) external returns (bytes memory ack) { + return handlePacket(p); + } + + function exposed_handleAcknowledgement(Packet calldata p, bytes calldata ackBytes) external { + handleAcknowledgement(p, ackBytes); + } + + function exposed_handleTimeout(Packet calldata p) external { + handleTimeout(p); + } + + function workaround_setModule(IContractModule m) external { + _module = m; + } +} + +contract TxAtomicSimpleTest is Test { + DummyHandler private handler; + TxAtomicSimpleHarness private harness; + + event OnContractCall(bytes indexed txId, uint8 indexed txIndex, bool indexed success, bytes ret); + + function setUp() public { + handler = new DummyHandler(); + harness = new TxAtomicSimpleHarness(IIBCHandler(address(handler))); + } + + function _mkPacketWithCall(bytes memory txId, bytes memory callInfo) internal pure returns (Packet memory p) { + Any.Data memory emptyAny = Any.Data({type_url: "", value: ""}); + ReturnValue.Data memory emptyRet = ReturnValue.Data({value: ""}); + + AuthAccount.Data[] memory signers; + Any.Data[] memory objects; + + PacketDataCallResolvedContractTransaction.Data memory txResolved = + PacketDataCallResolvedContractTransaction.Data({ + cross_chain_channel: emptyAny, + signers: signers, + call_info: callInfo, + return_value: emptyRet, + objects: objects + }); + + PacketDataCall.Data memory callData = PacketDataCall.Data({tx_id: txId, tx: txResolved}); + + bytes memory anyPayload = Any.encode( + // solhint-disable-next-line gas-small-strings + Any.Data({type_url: "/cross.core.atomic.simple.PacketDataCall", value: PacketDataCall.encode(callData)}) + ); + + HeaderField.Data[] memory fields; + bytes memory packetDataBytes = + PacketData.encode(PacketData.Data({header: Header.Data({fields: fields}), payload: anyPayload})); + + p.data = packetDataBytes; + } + + function _decodeAckStatus(bytes memory ack) internal pure returns (PacketAcknowledgementCall.CommitStatus) { + Acknowledgement.Data memory ackData = Acknowledgement.decode(ack); + PacketData.Data memory pd = PacketData.decode(ackData.result); + Any.Data memory any_ = Any.decode(pd.payload); + PacketAcknowledgementCall.Data memory pac = PacketAcknowledgementCall.decode(any_.value); + return pac.status; + } + + function test_handlePacket_ReturnsOkAndEmitsEventWhenModuleSucceeds() public { + bytes memory ret = hex"010203"; + harness.workaround_setModule(new SuccessModule(ret)); + + bytes memory txId = hex"deadbeef"; + bytes memory callInfo = hex"c0ffee"; + Packet memory packet = _mkPacketWithCall(txId, callInfo); + + vm.expectEmit(address(harness)); + emit OnContractCall(txId, 1, true, ret); + + bytes memory ack = harness.exposed_handlePacket(packet); + + assertEq( + uint256(_decodeAckStatus(ack)), + uint256(PacketAcknowledgementCall.CommitStatus.COMMIT_STATUS_OK), + "ACK should be OK" + ); + } + + function test_handlePacket_ReturnsFailedAndEmitsEventWhenModuleReverts() public { + harness.workaround_setModule(new RevertingModule()); + + bytes memory txId = hex"bead"; + bytes memory callInfo = hex"00"; + Packet memory packet = _mkPacketWithCall(txId, callInfo); + + vm.expectEmit(address(harness)); + emit OnContractCall(txId, 1, false, ""); + + bytes memory ack = harness.exposed_handlePacket(packet); + + assertEq( + uint256(_decodeAckStatus(ack)), + uint256(PacketAcknowledgementCall.CommitStatus.COMMIT_STATUS_FAILED), + "ACK should be FAILED" + ); + } + + function test_handlePacket_RevertWhen_PayloadEmpty() public { + HeaderField.Data[] memory fields; + bytes memory packetDataBytes = + PacketData.encode(PacketData.Data({header: Header.Data({fields: fields}), payload: bytes("")})); + Packet memory p; + p.data = packetDataBytes; + + vm.expectRevert(TxAtomicSimple.PayloadDecodeFailed.selector); + harness.exposed_handlePacket(p); + } + + function test_handlePacket_RevertWhen_TypeURLUnexpected() public { + bytes memory bogus = Any.encode(Any.Data({type_url: "/not.expected", value: hex"01"})); + HeaderField.Data[] memory fields; + bytes memory packetDataBytes = + PacketData.encode(PacketData.Data({header: Header.Data({fields: fields}), payload: bogus})); + Packet memory p; + p.data = packetDataBytes; + + vm.expectRevert(TxAtomicSimple.UnexpectedTypeURL.selector); + harness.exposed_handlePacket(p); + } + + function test_handleAcknowledgement_RevertOn_NotImplemented() public { + Packet memory p; + vm.expectRevert(TxAtomicSimple.NotImplemented.selector); + harness.exposed_handleAcknowledgement(p, hex""); + } + + function test_handleTimeout_RevertOn_NotImplemented() public { + Packet memory p; + vm.expectRevert(TxAtomicSimple.NotImplemented.selector); + harness.exposed_handleTimeout(p); + } +}