packages/contracts-core/test/suite/Summit.t.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {CallerNotInbox, DisputeTimeoutNotOver, NotaryInDispute} from "../../contracts/libs/Errors.sol";
import {IAgentSecured} from "../../contracts/interfaces/IAgentSecured.sol";
import {InterfaceDestination} from "../../contracts/interfaces/InterfaceDestination.sol";
import {ISnapshotHub} from "../../contracts/interfaces/ISnapshotHub.sol";
import {DISPUTE_TIMEOUT_NOTARY, SNAPSHOT_TREE_HEIGHT} from "../../contracts/libs/Constants.sol";
import {MerkleMath} from "../../contracts/libs/merkle/MerkleMath.sol";
import {InterfaceSummit} from "../../contracts/Summit.sol";
import {Versioned} from "../../contracts/base/Version.sol";
import {AgentFlag, AgentStatus, Summit, SynapseTest} from "../utils/SynapseTest.t.sol";
import {State, RawAttestation, RawSnapshot, RawState, RawStateIndex} from "../utils/libs/SynapseStructs.t.sol";
import {Random} from "../utils/libs/Random.t.sol";
import {AgentSecuredTest} from "./hubs/ExecutionHub.t.sol";
// solhint-disable func-name-mixedcase
// solhint-disable no-empty-blocks
// solhint-disable code-complexity
contract SummitTest is AgentSecuredTest {
struct SignedSnapshot {
bytes snapshot;
bytes signature;
}
uint256 internal constant STATES = 10;
mapping(uint256 => mapping(uint256 => RawState)) internal guardStates;
mapping(uint256 => SignedSnapshot) internal guardSnapshots;
mapping(uint256 => RawAttestation) internal notaryAttestations;
// Deploy Production version of Summit and mocks for everything else
constructor() SynapseTest(DEPLOY_PROD_SUMMIT) {}
function setUp() public virtual override {
RawAttestation memory empty = RawAttestation({
snapRoot: 0,
dataHash: 0,
_agentRoot: 0,
_snapGasHash: 0,
nonce: 0,
blockNumber: uint40(block.number),
timestamp: uint40(block.timestamp)
});
empty.setDataHash();
notaryAttestations[0] = empty;
super.setUp();
}
function test_setupCorrectly() public {
// Check Agents
// Summit should know about agents from all domains, including Guards
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(summit).agentStatus(agent), AgentFlag.Active);
}
}
// Check version
assertEq(Versioned(summit).version(), LATEST_VERSION, "!version");
// Check attestation getter for zero nonce
(bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) = ISnapshotHub(summit).getAttestation(0);
assertEq(attPayload, notaryAttestations[0].formatAttestation(), "!attPayload");
assertEq(agentRoot, 0, "!agentRoot");
assertEq(snapGas.length, 0, "!snapGas");
}
function test_constructor_revert_chainIdOverflow() public {
vm.chainId(2 ** 32);
vm.expectRevert("SafeCast: value doesn't fit in 32 bits");
new Summit({synapseDomain_: 1, agentManager_: address(2), inbox_: address(3)});
}
function test_cleanSetup(Random memory random) public override {
uint32 domain = DOMAIN_SYNAPSE;
vm.chainId(domain);
address agentManager = random.nextAddress();
address inbox_ = random.nextAddress();
address caller = random.nextAddress();
Summit cleanContract = new Summit(domain, agentManager, inbox_);
vm.prank(caller);
cleanContract.initialize();
assertEq(cleanContract.owner(), caller, "!owner");
assertEq(cleanContract.agentManager(), agentManager, "!agentManager");
assertEq(cleanContract.inbox(), inbox_, "!inbox");
assertEq(cleanContract.localDomain(), domain, "!localDomain");
}
function initializeLocalContract() public override {
Summit(localContract()).initialize();
}
function test_acceptGuardSnapshot_revert_notInbox(address caller) public {
vm.assume(caller != localInbox());
vm.expectRevert(CallerNotInbox.selector);
vm.prank(caller);
InterfaceSummit(summit).acceptGuardSnapshot(0, 0, "");
}
function test_acceptNotarySnapshot_revert_notInbox(address caller) public {
vm.assume(caller != localInbox());
vm.expectRevert(CallerNotInbox.selector);
vm.prank(caller);
InterfaceSummit(summit).acceptNotarySnapshot(0, 0, 0, "");
}
function test_acceptNotarySnapshot_revert_blockTimestampOverflow() public {
address notary = domains[DOMAIN_LOCAL].agent;
address guard = domains[0].agent;
Random memory random = Random("salt");
RawSnapshot memory rawSnap = random.nextSnapshot();
// Another Guard signs the snapshot
(bytes memory snapPayload, bytes memory guardSignature) = signSnapshot(guard, rawSnap);
bytes memory notarySig = signSnapshot(notary, snapPayload);
inbox.submitSnapshot(snapPayload, guardSignature);
vm.warp(2 ** 40);
vm.expectRevert("SafeCast: value doesn't fit in 40 bits");
inbox.submitSnapshot(snapPayload, notarySig);
}
function test_verifyAttestation_existingNonce(Random memory random, uint256 mask) public {
test_notarySnapshots(random);
// Restrict nonce to existing ones
uint32 nonce = uint32(bound(random.nextUint32(), 0, DOMAIN_AGENTS));
// Attestation is valid if and only if all four fields match
(bool isValid, RawAttestation memory ra) = notaryAttestations[nonce].modifyAttestation(mask);
verifyAttestation(random, ra, isValid);
}
function test_verifyAttestation_unknownNonce(Random memory random, RawAttestation memory ra) public {
test_notarySnapshots(random);
// Restrict nonce to non-existing ones
ra.nonce = uint32(bound(ra.nonce, DOMAIN_AGENTS + 1, type(uint32).max));
verifyAttestation(random, ra, false);
}
function verifyAttestation(Random memory random, RawAttestation memory ra, bool isValid) public {
// Pick random domain expect for 0
uint256 domainIndex = bound(random.nextUint256(), 1, allDomains.length - 1);
uint32 domain = allDomains[domainIndex];
// Pick random Notary
uint256 notaryIndex = bound(random.nextUint256(), 0, DOMAIN_AGENTS - 1);
address notary = domains[domain].agents[notaryIndex];
(bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
if (!isValid) {
// Expect Events to be emitted
vm.expectEmit(true, true, true, true);
emit InvalidAttestation(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(inbox.verifyAttestation(attPayload, attSig), isValid, "!returnValue");
if (isValid) {
assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
}
// Verify report on constructed attestation
verifyAttestationReport(random, ra, isValid);
}
function verifyAttestationReport(Random memory random, RawAttestation memory ra, bool isAttestationValid) public {
// Report is considered invalid, if reported attestation is valid
bool isValid = !isAttestationValid;
// Pick random Guard
uint256 guardIndex = bound(random.nextUint256(), 0, DOMAIN_AGENTS - 1);
address guard = domains[0].agents[guardIndex];
(bytes memory attPayload, bytes memory arSig) = signAttestationReport(guard, ra);
if (!isValid) {
// Expect Events to be emitted
vm.expectEmit(true, true, true, true);
emit InvalidAttestationReport(attPayload, arSig);
// TODO: check that anyone could make the call
expectStatusUpdated(AgentFlag.Fraudulent, 0, guard);
expectDisputeResolved(0, guard, address(0), address(this));
}
vm.recordLogs();
assertEq(inbox.verifyAttestationReport(attPayload, arSig), isValid, "!returnValue");
if (isValid) {
assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
}
}
function test_guardSnapshots(Random memory random) public {
// Every Guard submits a snapshot with a random state for domains in [1 .. DOMAINS] range
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
State[] memory states = new State[](STATES);
for (uint32 j = 0; j < STATES; ++j) {
// Use random non-zero nonce for every state
uint32 nonce = uint32(bound(random.nextUint32(), 1, type(uint32).max));
guardStates[i][j] = random.nextState({origin: j + 1, nonce: nonce});
states[j] = guardStates[i][j].castToState();
}
address guard = domains[0].agents[i];
(bytes memory snapPayload, bytes memory snapSig) = signSnapshot(guard, states);
guardSnapshots[i] = SignedSnapshot(snapPayload, snapSig);
}
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
// Check that every State is saved
for (uint256 j = 0; j < STATES; ++j) {
vm.expectEmit(true, true, true, true);
emit StateSaved(guardStates[i][j].formatState());
}
vm.expectEmit(true, true, true, true);
emit SnapshotAccepted(0, domains[0].agents[i], guardSnapshots[i].snapshot, guardSnapshots[i].signature);
(bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) =
inbox.submitSnapshot(guardSnapshots[i].snapshot, guardSnapshots[i].signature);
assertEq(attPayload, "", "Guard: non-empty attestation");
assertEq(agentRoot, bytes32(0), "Guard: non-empty agent root");
assertEq(snapGas.length, 0, "Guard: non-empty snap gas data");
// Check latest Guard States
for (uint32 j = 0; j < STATES; ++j) {
assertEq(
ISnapshotHub(summit).getLatestAgentState(j + 1, domains[0].agents[i]),
guardStates[i][j].formatState(),
"!latestState: guard"
);
}
}
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
(bytes memory snapPayload, bytes memory snapSignature) = ISnapshotHub(summit).getGuardSnapshot(i);
assertEq(snapPayload, guardSnapshots[i].snapshot, "!snapshot");
assertEq(snapSignature, guardSnapshots[i].signature, "!signature");
}
// Check global latest state
checkLatestState();
}
function test_notarySnapshots(Random memory random) public {
// Every Guard submits a snapshot with a random state for domains in [1 .. DOMAINS] range
test_guardSnapshots(random);
bytes[] memory snapPayloads = new bytes[](DOMAIN_AGENTS);
bytes[] memory snapSignatures = new bytes[](DOMAIN_AGENTS);
// Every Notary submits a snapshot with a random Guard state for all domains
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
// Set random timestamp and block height
RawAttestation memory ra;
ra.blockNumber = random.nextUint40();
ra.timestamp = random.nextUint40();
vm.roll(ra.blockNumber);
vm.warp(ra.timestamp);
RawSnapshot memory rs;
rs.states = new RawState[](STATES);
for (uint256 j = 0; j < STATES; ++j) {
// Pick a random Guard to choose their state for domain (J+1)
// To ensure that all Notary snapshots are different pick Guard
// with the same index as Notary for the first state
uint256 guardIndex = j == 0 ? i : random.nextUint256() % DOMAIN_AGENTS;
rs.states[j] = guardStates[guardIndex][j];
}
// Calculate root and height using AttestationProofGenerator
acceptSnapshot(rs);
ra.snapRoot = getSnapshotRoot();
ra._agentRoot = getAgentRoot();
ra._snapGasHash = rs.snapGasHash();
ra.setDataHash();
// This is i-th submitted attestation so far, but attestation nonce starts from 1
ra.nonce = i + 1;
notaryAttestations[ra.nonce] = ra;
bytes memory attestation = ra.formatAttestation();
address notary = domains[DOMAIN_LOCAL].agents[i];
(snapPayloads[i], snapSignatures[i]) = signSnapshot(notary, rs);
// Nothing should be saved before Notary submitted their first snapshot
(bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) =
ISnapshotHub(summit).getLatestNotaryAttestation(notary);
assertEq(attPayload, "");
assertEq(agentRoot, bytes32(0));
assertEq(snapGas.length, 0);
vm.expectEmit(true, true, true, true);
emit AttestationSaved(attestation);
vm.expectEmit(true, true, true, true);
emit SnapshotAccepted(DOMAIN_LOCAL, notary, snapPayloads[i], snapSignatures[i]);
// Should pass to Destination: acceptAttestation(status, sigIndex, attestation, agentRoot, snapGas)
vm.expectCall(
destinationSynapse,
abi.encodeWithSelector(
InterfaceDestination.acceptAttestation.selector,
agentIndex[notary],
type(uint256).max,
attestation,
ra._agentRoot,
rs.snapGas()
)
);
(attPayload, agentRoot, snapGas) = inbox.submitSnapshot(snapPayloads[i], snapSignatures[i]);
assertEq(attPayload, attestation, "Notary: incorrect attestation");
assertEq(agentRoot, ra._agentRoot, "Notary: incorrect agent root");
assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "Notary: incorrect snap gas hash");
// Check attestation getter
(attPayload, agentRoot, snapGas) = ISnapshotHub(summit).getAttestation(ra.nonce);
assertEq(attPayload, attestation, "!getAttestation");
assertEq(agentRoot, ra._agentRoot, "!getAttestation: agent root");
assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "!getAttestation: gas hash");
(attPayload, agentRoot, snapGas) = ISnapshotHub(summit).getLatestNotaryAttestation(notary);
assertEq(attPayload, attestation, "!latestAttestation");
assertEq(agentRoot, ra._agentRoot, "!latestAttestation: agent root");
assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "!latestAttestation: gas hash");
// Check proofs for every State in the Notary snapshot
for (uint8 j = 0; j < STATES; ++j) {
bytes32[] memory snapProof = ISnapshotHub(summit).getSnapshotProof(ra.nonce, j);
// Item to prove is State's "left sub-leaf"
(bytes32 item,) = rs.states[j].castToState().subLeafs();
// Item index is twice the state index (since it's a left child)
assertEq(
MerkleMath.proofRoot(2 * j, item, snapProof, SNAPSHOT_TREE_HEIGHT), ra.snapRoot, "!getSnapshotProof"
);
}
// Check latest Notary States
for (uint32 j = 0; j < STATES; ++j) {
assertEq(
ISnapshotHub(summit).getLatestAgentState(j + 1, notary),
rs.states[j].formatState(),
"!latestState: notary"
);
}
}
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
(bytes memory snapPayload, bytes memory snapSignature) = ISnapshotHub(summit).getNotarySnapshot(i);
assertEq(snapPayload, snapPayloads[i], "!payload");
assertEq(snapSignature, snapSignatures[i], "!signature");
}
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
address guard = domains[0].agents[i];
// No Attestations should be saved for Guards
(bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) =
ISnapshotHub(summit).getLatestNotaryAttestation(guard);
assertEq(attPayload, "");
assertEq(agentRoot, bytes32(0));
assertEq(snapGas.length, 0);
}
// Check global latest state
checkLatestState();
}
function test_getLatestState_empty(uint32 domain) public {
assertEq(InterfaceSummit(summit).getLatestState(domain), "");
}
function checkLatestState() public {
// Check global latest state
for (uint32 j = 0; j < STATES; ++j) {
RawState memory latestState;
for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
if (guardStates[i][j].nonce > latestState.nonce) {
latestState = guardStates[i][j];
}
}
assertEq(InterfaceSummit(summit).getLatestState(j + 1), latestState.formatState(), "!getLatestState");
}
}
// ══════════════════════════════════════════ TESTS: WHILE IN DISPUTE ══════════════════════════════════════════════
function test_submitSnapshot_revert_notaryInDispute(RawState memory rs) public {
(uint256 domainId, RawStateIndex memory rsi) = (1, RawStateIndex(0, 1));
uint32 domain = allDomains[domainId];
(address guard0, address guard1, address notary) =
(domains[0].agents[0], domains[0].agents[1], domains[domain].agents[0]);
// Put Notary 0 and Guard 0 in dispute
openDispute({guard: guard0, notary: notary});
// Make sure state nonce is non-zero
if (rs.nonce == 0) rs.nonce = 1;
// Create Guard 1 snapshot
(bytes memory snapPayload, bytes memory guardSig) = createSignedSnapshot(guard1, rs, rsi);
// Guard 1 submits snapshot
inbox.submitSnapshot(snapPayload, guardSig);
// Notary 0 signs the same snapshot
bytes memory notarySig = signSnapshot(notary, snapPayload);
vm.expectRevert(NotaryInDispute.selector);
inbox.submitSnapshot(snapPayload, notarySig);
}
function test_submitSnapshot_success_guardInDispute(RawState memory rs) public {
(uint256 domainId, RawStateIndex memory rsi) = (1, RawStateIndex(0, 1));
uint32 domain = allDomains[domainId];
(address guard, address notary) = (domains[0].agents[0], domains[domain].agents[0]);
// Put Notary 0 and Guard 0 in dispute
openDispute({guard: guard, notary: notary});
// Make sure state nonce is non-zero
if (rs.nonce == 0) rs.nonce = 1;
(bytes memory snapPayload, bytes memory guardSig) = createSignedSnapshot(guard, rs, rsi);
// Guard 0 submits snapshot - being in dispute does not interfere with future snapshots
vm.expectEmit();
emit SnapshotAccepted(0, guard, snapPayload, guardSig);
inbox.submitSnapshot(snapPayload, guardSig);
}
// ═════════════════════════════════════════ TESTS: NOTARY WON DISPUTE ═════════════════════════════════════════════
function prepareSubmitSnapshotDisputeTest() internal returns (bytes memory snapPayload, bytes memory notarySig) {
address notary = domains[DOMAIN_LOCAL].agent;
address reportGuard = domains[0].agent;
address snapshotGuard = domains[0].agents[1];
Random memory random = Random("salt");
RawSnapshot memory rawSnap = random.nextSnapshot();
// Another Guard signs the snapshot
bytes memory guardSignature;
(snapPayload, guardSignature) = signSnapshot(snapshotGuard, rawSnap);
notarySig = signSnapshot(notary, snapPayload);
inbox.submitSnapshot(snapPayload, guardSignature);
openTestDispute({guardIndex: agentIndex[reportGuard], notaryIndex: agentIndex[notary]});
}
/// @dev Resolves test dispute above in favor of the Notary.
function prepareNotaryWonDisputeTest() internal {
address notary = domains[DOMAIN_LOCAL].agent;
address guard = domains[0].agent;
resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
}
function test_submitSnapshot_revert_notaryWonDisputeTimeout() public {
(bytes memory snapPayload, bytes memory notarySig) = prepareSubmitSnapshotDisputeTest();
skip(7 days);
prepareNotaryWonDisputeTest();
skip(DISPUTE_TIMEOUT_NOTARY - 1);
vm.expectRevert(DisputeTimeoutNotOver.selector);
inbox.submitSnapshot(snapPayload, notarySig);
}
function test_submitSnapshot_afterNotaryDisputeTimeout() public {
address notary = domains[DOMAIN_LOCAL].agent;
(bytes memory snapPayload, bytes memory notarySig) = prepareSubmitSnapshotDisputeTest();
skip(7 days);
prepareNotaryWonDisputeTest();
skip(DISPUTE_TIMEOUT_NOTARY);
inbox.submitSnapshot(snapPayload, notarySig);
(bytes memory snapPayload_,) = ISnapshotHub(summit).getNotarySnapshot(0);
assertEq(snapPayload_, snapPayload);
}
// ═════════════════════════════════════════════════ OVERRIDES ═════════════════════════════════════════════════════
function localContract() public view override returns (address) {
return summit;
}
/// @notice Returns local domain for the tested contract
function localDomain() public pure override returns (uint32) {
return DOMAIN_SYNAPSE;
}
}