// SPDX-License-Identifier: MIT pragma solidity 0.8.17; import {IAgentSecured} from "../../contracts/interfaces/IAgentSecured.sol"; import {InterfaceGasOracle} from "../../contracts/interfaces/InterfaceGasOracle.sol"; import {IStateHub} from "../../contracts/interfaces/IStateHub.sol"; import { EthTransferFailed, IncorrectDestinationDomain, InsufficientEthBalance, TipsValueTooLow } from "../../contracts/libs/Errors.sol"; import {SNAPSHOT_MAX_STATES} from "../../contracts/libs/Constants.sol"; import {TipsLib} from "../../contracts/libs/stack/Tips.sol"; import {InterfaceOrigin} from "../../contracts/Origin.sol"; import {Versioned} from "../../contracts/base/Version.sol"; import {RevertingApp} from "../harnesses/client/RevertingApp.t.sol"; import {console, stdJson} from "forge-std/Script.sol"; import {fakeState, fakeSnapshot} from "../utils/libs/FakeIt.t.sol"; import {Random} from "../utils/libs/Random.t.sol"; import { MessageFlag, RawAttestation, RawBaseMessage, RawGasData, RawHeader, RawMessage, RawRequest, RawSnapshot, RawState, RawStateIndex, RawTips } from "../utils/libs/SynapseStructs.t.sol"; import {AgentFlag, GasOracle, Origin, SynapseTest} from "../utils/SynapseTest.t.sol"; import {AgentSecuredTest} from "./base/AgentSecured.t.sol"; // solhint-disable func-name-mixedcase // solhint-disable no-empty-blocks // solhint-disable ordering contract OriginTest is AgentSecuredTest { address public sender = makeAddr("Sender"); address public recipient = makeAddr("Recipient"); uint32 public period = 1 minutes; RawTips public tips = RawTips(0, 0, 0, 0); RawRequest public request = RawRequest({gasLimit: 100_000, gasDrop: 0, version: 0}); // Deploy Production version of Origin and mocks for everything else constructor() SynapseTest(DEPLOY_PROD_ORIGIN) {} function test_setupCorrectly() public { // Check Agents: currently all Agents are known in LightManager for (uint256 d = 0; d < allDomains.length; ++d) { uint32 domain = allDomains[d]; for (uint256 i = 0; i < domains[domain].agents.length; ++i) { address agent = domains[domain].agents[i]; checkAgentStatus(agent, IAgentSecured(origin).agentStatus(agent), AgentFlag.Active); } } // Check version assertEq(Versioned(origin).version(), LATEST_VERSION, "!version"); } function test_constructor_revert_chainIdOverflow() public { vm.chainId(2 ** 32); vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); new Origin({synapseDomain_: 1, agentManager_: address(2), inbox_: address(3), gasOracle_: address(4)}); } function test_cleanSetup(Random memory random) public override { uint32 domain = uint32(block.chainid); address caller = random.nextAddress(); address agentManager = random.nextAddress(); address inbox_ = random.nextAddress(); address gasOracle_ = address(new GasOracle(DOMAIN_SYNAPSE, random.nextAddress())); Origin cleanContract = new Origin(DOMAIN_SYNAPSE, agentManager, inbox_, gasOracle_); vm.prank(caller); cleanContract.initialize(); assertEq(cleanContract.owner(), caller, "!owner"); assertEq(cleanContract.localDomain(), domain, "!localDomain"); assertEq(cleanContract.agentManager(), agentManager, "!agentManager"); assertEq(cleanContract.inbox(), inbox_, "!inbox"); assertEq(cleanContract.gasOracle(), gasOracle_, "!gasOracle"); assertEq(cleanContract.statesAmount(), 1, "!statesAmount"); } function initializeLocalContract() public override { Origin(localContract()).initialize(); } function test_sendBaseMessage_revert_blockTimestampOverflow() public { vm.warp(2 ** 40); vm.prank(sender); vm.expectRevert("SafeCast: value doesn't fit in 40 bits"); InterfaceOrigin(origin).sendBaseMessage( DOMAIN_REMOTE, addressToBytes32(recipient), period, request.encodeRequest(), "test content" ); } function test_sendBaseMessage_revert_blockNumberOverflow() public { vm.roll(2 ** 40); vm.prank(sender); vm.expectRevert("SafeCast: value doesn't fit in 40 bits"); InterfaceOrigin(origin).sendBaseMessage( DOMAIN_REMOTE, addressToBytes32(recipient), period, request.encodeRequest(), "test content" ); } function test_sendBaseMessage_revert_tipsTooLow(RawTips memory minTips, uint256 msgValue) public { minTips.boundTips(1 ** 32); minTips.floorTips(1); msgValue = msgValue % minTips.castToTips().value(); // Force gasOracle.getMinimumTips(DOMAIN_REMOTE, *, *) to return minTips vm.mockCall( gasOracle, abi.encodeWithSelector(InterfaceGasOracle.getMinimumTips.selector, DOMAIN_REMOTE), abi.encode(minTips.encodeTips()) ); deal(sender, msgValue); vm.expectRevert(TipsValueTooLow.selector); vm.prank(sender); InterfaceOrigin(origin).sendBaseMessage{value: msgValue}( DOMAIN_REMOTE, addressToBytes32(recipient), period, request.encodeRequest(), "test content" ); } function test_sendBaseMessage_revert_sameDestination() public { vm.expectRevert(IncorrectDestinationDomain.selector); vm.prank(sender); InterfaceOrigin(origin).sendBaseMessage( localDomain(), addressToBytes32(recipient), period, request.encodeRequest(), "test content" ); } function test_sendManagementMessage_revert_sameDestination() public { vm.expectRevert(IncorrectDestinationDomain.selector); vm.prank(localAgentManager()); InterfaceOrigin(origin).sendManagerMessage(localDomain(), period, "test payload"); } function test_getMinimumTipsValue( uint32 destination_, uint256 paddedRequest, uint256 contentLength, RawTips memory minTips ) public { minTips.boundTips(1 ** 32); // Force gasOracle.getMinimumTips(destination_, *, *) to return minTips vm.mockCall( gasOracle, abi.encodeWithSelector(InterfaceGasOracle.getMinimumTips.selector, destination_), abi.encode(minTips.encodeTips()) ); vm.expectCall( address(gasOracle), abi.encodeWithSelector( InterfaceGasOracle.getMinimumTips.selector, destination_, paddedRequest, contentLength ) ); assertEq( InterfaceOrigin(origin).getMinimumTipsValue(destination_, paddedRequest, contentLength), minTips.castToTips().value(), "!getMinimumTipsValue" ); } function test_sendMessages(RawGasData memory rgd) public { // Force gasOracle.getGasData() to return rgd vm.mockCall( gasOracle, abi.encodeWithSelector(InterfaceGasOracle.getGasData.selector), abi.encode(rgd.encodeGasData()) ); uint192 encodedRequest = request.encodeRequest(); bytes memory content = "test content"; bytes memory body = RawBaseMessage({ tips: tips, sender: addressToBytes32(sender), recipient: addressToBytes32(recipient), request: request, content: content }).formatBaseMessage(); bytes[] memory messages = new bytes[](MESSAGES); bytes32[] memory leafs = new bytes32[](MESSAGES); bytes32[] memory roots = new bytes32[](MESSAGES); for (uint32 i = 0; i < MESSAGES; ++i) { RawMessage memory rm = RawMessage( RawHeader({ flag: uint8(MessageFlag.Base), origin: DOMAIN_LOCAL, nonce: i + 1, destination: DOMAIN_REMOTE, optimisticPeriod: period }), body ); messages[i] = rm.formatMessage(); leafs[i] = rm.castToMessage().leaf(); insertMessage(leafs[i]); roots[i] = getRoot(i + 1); } for (uint32 i = 0; i < MESSAGES; ++i) { // Expect Origin Events RawState memory rs = RawState({ root: roots[i], origin: DOMAIN_LOCAL, nonce: i + 1, blockNumber: uint40(block.number), timestamp: uint40(block.timestamp), gasData: rgd }); bytes memory state = rs.formatState(); vm.expectEmit(); emit StateSaved(state); vm.expectEmit(); emit Sent(leafs[i], i + 1, DOMAIN_REMOTE, messages[i]); vm.expectCall(gasOracle, abi.encodeCall(InterfaceGasOracle.updateGasData, (DOMAIN_REMOTE))); vm.prank(sender); (uint32 messageNonce, bytes32 messageHash) = InterfaceOrigin(origin).sendBaseMessage( DOMAIN_REMOTE, addressToBytes32(recipient), period, encodedRequest, content ); // Check return values assertEq(messageNonce, i + 1, "!messageNonce"); assertEq(messageHash, leafs[i], "!messageHash"); skipBlock(); } } function test_states(RawGasData memory rgd) public { IStateHub hub = IStateHub(origin); // Check initial States assertEq(hub.statesAmount(), 1, "!initial statesAmount"); // Initial state was saved "1 block ago" RawState memory rs; rs.origin = DOMAIN_LOCAL; rs.blockNumber = uint40(block.number - 1); rs.timestamp = uint40(block.timestamp - BLOCK_TIME); bytes memory state = rs.formatState(); assertEq(hub.suggestState(0), state, "!state: 0"); assertEq(hub.suggestState(0), hub.suggestLatestState(), "!latest state: 0"); // Send some messages test_sendMessages(rgd); // Check saved States assertEq(hub.statesAmount(), MESSAGES + 1, "!statesAmount"); assertEq(hub.suggestState(0), state, "!suggestState: 0"); for (uint32 i = 0; i < MESSAGES; ++i) { rs.nonce += 1; rs.root = getRoot(rs.nonce); rs.blockNumber += 1; rs.timestamp += uint40(BLOCK_TIME); rs.gasData = rgd; state = rs.formatState(); assertEq(hub.suggestState(i + 1), state, "!suggestState"); } assertEq(hub.suggestLatestState(), state, "!suggestLatestState"); } function test_verifySnapshot_valid(uint32 nonce, RawGasData memory rgd, RawStateIndex memory rsi) public { // Use empty mutation mask test_verifySnapshot_existingNonce(nonce, 0, rgd, rsi); } function test_verifySnapshot_existingNonce( uint32 nonce, uint256 mask, RawGasData memory rgd, RawStateIndex memory rsi ) public boundIndex(rsi) { (bool isValid, RawState memory rs) = _prepareExistingState(rgd, nonce, mask); _verifySnapshot(rs, isValid, rsi); } function test_verifySnapshot_unknownNonce(RawState memory rs, RawStateIndex memory rsi) public boundIndex(rsi) { // Restrict nonce to non-existing ones rs.nonce = uint32(bound(rs.nonce, MESSAGES + 1, type(uint32).max)); rs.origin = DOMAIN_LOCAL; // Remaining fields are fuzzed _verifySnapshot(rs, false, rsi); } function test_verifyAttestation_valid(Random memory random, uint32 nonce) public { test_verifyAttestation_existingNonce(random, nonce, 0); } function test_verifyAttestation_existingNonce(Random memory random, uint32 nonce, uint256 mask) public { (bool isValid, RawState memory rs) = _prepareExistingState(random.nextGasData(), nonce, mask); _verifyAttestation(random, rs, isValid); } function test_verifyAttestation_unknownNonce(Random memory random, RawState memory rs) public { // Restrict nonce to non-existing ones rs.nonce = uint32(bound(rs.nonce, MESSAGES + 1, type(uint32).max)); rs.origin = DOMAIN_LOCAL; // Remaining fields are fuzzed _verifyAttestation(random, rs, false); } function test_verifyAttestationWithProof_valid(Random memory random, uint32 nonce) public { // Use empty mutation mask test_verifyAttestationWithProof_existingNonce(random, nonce, 0); } function test_verifyAttestationWithProof_existingNonce(Random memory random, uint32 nonce, uint256 mask) public { (bool isValid, RawState memory rs) = _prepareExistingState(random.nextGasData(), nonce, mask); _verifyAttestationWithProof(random, rs, isValid); } function test_verifyAttestationWithProof_unknownNonce(Random memory random, RawState memory rs) public { // Restrict nonce to non-existing ones rs.nonce = uint32(bound(rs.nonce, MESSAGES + 1, type(uint32).max)); rs.origin = DOMAIN_LOCAL; // Remaining fields are fuzzed _verifyAttestationWithProof(random, rs, false); } // ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════ function _prepareExistingState(RawGasData memory rgd, uint32 nonce, uint256 mask) internal returns (bool isValid, RawState memory rs) { uint40 initialBN = uint40(block.number - 1); uint40 initialTS = uint40(block.timestamp - BLOCK_TIME); test_sendMessages(rgd); // State is valid if and only if all three fields match isValid = mask & 7 == 0; // Restrict nonce to existing ones nonce = uint32(bound(nonce, 0, MESSAGES)); rs.origin = DOMAIN_LOCAL; rs.nonce = nonce; rs.root = getRoot(nonce) ^ bytes32(mask & 1); rs.blockNumber = (initialBN + nonce) ^ uint40(mask & 2); rs.timestamp = uint40(initialTS + nonce * BLOCK_TIME) ^ uint40(mask & 4); } function _prepareAttestation(Random memory random, RawState memory rawState) internal returns ( uint32 domain, address notary, RawStateIndex memory rsi, bytes memory snapshot, RawAttestation memory ra ) { // Pick random domain expect for 0 uint256 domainIndex = bound(random.nextUint256(), 1, allDomains.length - 1); domain = allDomains[domainIndex]; // Pick random Notary uint256 notaryIndex = bound(random.nextUint256(), 0, DOMAIN_AGENTS - 1); notary = domains[domain].agents[notaryIndex]; // Fuzz the position of invalid state in the snapshot rsi = random.nextStateIndex(); RawSnapshot memory rawSnap = fakeSnapshot(rawState, rsi); snapshot = rawSnap.formatSnapshot(); // Use random metadata ra = random.nextAttestation(rawSnap, random.nextUint32()); // Save snapshot for Snapshot Proof generation acceptSnapshot(rawSnap); } function _verifyAttestation(Random memory random, RawState memory rawState, bool isValid) internal { (uint32 domain, address notary, RawStateIndex memory rsi, bytes memory snapshot, RawAttestation memory ra) = _prepareAttestation(random, rawState); bytes memory state = rawState.formatState(); (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra); if (!isValid) { // Expect Events to be emitted vm.expectEmit(true, true, true, true); emit InvalidStateWithAttestation(rsi.stateIndex, state, attPayload, attSig); // TODO: check that anyone could make the call expectStatusUpdated(AgentFlag.Fraudulent, domain, notary); expectDisputeResolved(0, notary, address(0), address(this)); } vm.recordLogs(); assertEq( lightInbox.verifyStateWithAttestation(rsi.stateIndex, snapshot, attPayload, attSig), isValid, "!returnValue" ); if (isValid) { assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't"); } } function _verifyAttestationWithProof(Random memory random, RawState memory rawState, bool isValid) internal { (uint32 domain, address notary, RawStateIndex memory rsi,, RawAttestation memory ra) = _prepareAttestation(random, rawState); bytes32[] memory snapProof = genSnapshotProof(rsi.stateIndex); bytes memory state = rawState.formatState(); (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra); if (!isValid) { // Expect Events to be emitted vm.expectEmit(true, true, true, true); emit InvalidStateWithAttestation(rsi.stateIndex, state, attPayload, attSig); // TODO: check that anyone could make the call expectStatusUpdated(AgentFlag.Fraudulent, domain, notary); expectDisputeResolved(0, notary, address(0), address(this)); } vm.recordLogs(); assertEq( lightInbox.verifyStateWithSnapshotProof(rsi.stateIndex, state, snapProof, attPayload, attSig), isValid, "!returnValue" ); if (isValid) { assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't"); } } function _verifySnapshot(RawState memory rawState, bool isValid, RawStateIndex memory rsi) internal { address notary = domains[DOMAIN_REMOTE].agent; RawSnapshot memory rawSnap = fakeSnapshot(rawState, rsi); (bytes memory snapPayload, bytes memory snapSig) = signSnapshot(notary, rawSnap); vm.recordLogs(); if (!isValid) { // Expect Events to be emitted vm.expectEmit(true, true, true, true); emit InvalidStateWithSnapshot(rsi.stateIndex, snapPayload, snapSig); // TODO: check that anyone could make the call expectStatusUpdated(AgentFlag.Fraudulent, DOMAIN_REMOTE, notary); expectDisputeResolved(0, notary, address(0), address(this)); } assertEq(lightInbox.verifyStateWithSnapshot(rsi.stateIndex, snapPayload, snapSig), isValid, "!returnValue"); if (isValid) { assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't"); } _verifyStateReport(rawState, isValid); } function _verifyStateReport(RawState memory rawState, bool isStateValid) internal { // Report is valid only if reported state is invalid bool isValid = !isStateValid; address guard = domains[0].agent; (bytes memory statePayload, bytes memory srSig) = signStateReport(guard, rawState); if (!isValid) { // Expect Events to be emitted vm.expectEmit(true, true, true, true); emit InvalidStateReport(statePayload, srSig); // TODO: check that anyone could make the call expectStatusUpdated(AgentFlag.Fraudulent, 0, guard); expectDisputeResolved(0, guard, address(0), address(this)); } vm.recordLogs(); assertEq(lightInbox.verifyStateReport(statePayload, srSig), isValid, "!returnValue"); if (isValid) { assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't"); } } // ════════════════════════════════════════════ TEST: WITHDRAW TIPS ════════════════════════════════════════════════ function test_withdrawTips(uint256 amount) public { vm.deal(origin, amount); vm.expectEmit(origin); emit TipWithdrawalCompleted(recipient, amount); vm.prank(address(lightManager)); InterfaceOrigin(origin).withdrawTips(recipient, amount); assertEq(recipient.balance, amount); } function test_remoteWithdrawTips_revert_insufficientBalance(uint256 balance, uint256 amount) public { amount = bound(amount, 1, type(uint256).max); balance = balance % amount; vm.deal(origin, balance); vm.expectRevert(InsufficientEthBalance.selector); vm.prank(address(lightManager)); InterfaceOrigin(origin).withdrawTips(recipient, amount); } function test_withdrawTips_revert_recipientReverted(uint256 amount) public { address revertingRecipient = address(new RevertingApp()); vm.deal(origin, amount); vm.expectRevert(EthTransferFailed.selector); vm.prank(address(lightManager)); InterfaceOrigin(origin).withdrawTips(revertingRecipient, amount); } // ═════════════════════════════════════════════════ OVERRIDES ═════════════════════════════════════════════════════ /// @notice Returns local domain for the tested contract function localDomain() public pure override returns (uint32) { return DOMAIN_LOCAL; } /// @notice Returns address of the tested contract function localContract() public view override returns (address) { return localOrigin(); } }