packages/contracts-core/contracts/inbox/StatementInbox.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {Attestation, AttestationLib} from "../libs/memory/Attestation.sol";
import {
AgentNotGuard,
AgentNotNotary,
IncorrectAgentDomain,
IncorrectSnapshotProof,
IncorrectSnapshotRoot,
IncorrectState,
IndexOutOfRange
} from "../libs/Errors.sol";
import {Receipt, ReceiptLib} from "../libs/memory/Receipt.sol";
import {Snapshot, SnapshotLib} from "../libs/memory/Snapshot.sol";
import {State, StateLib} from "../libs/memory/State.sol";
import {AgentStatus} from "../libs/Structures.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {MessagingBase} from "../base/MessagingBase.sol";
import {StatementInboxEvents} from "../events/StatementInboxEvents.sol";
import {IAgentManager} from "../interfaces/IAgentManager.sol";
import {IExecutionHub} from "../interfaces/IExecutionHub.sol";
import {IStateHub} from "../interfaces/IStateHub.sol";
import {IStatementInbox} from "../interfaces/IStatementInbox.sol";
// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/// @notice `StatementInbox` is the entry point for all agent-signed statements. It verifies the
/// agent signatures, and passes the unsigned statements to the contract to consume it via `acceptX` functions. Is is
/// also used to verify the agent-signed statements and initiate the agent slashing, should the statement be invalid.
/// `StatementInbox` is responsible for the following:
/// - Accepting State and Receipt Reports to initiate a dispute between Guard and Notary.
/// - Storing all the Guard Reports with the Guard signature leading to a dispute.
/// - Verifying State/State Reports referencing the local chain and slashing the signer if statement is invalid.
/// - Verifying Receipt/Receipt Reports referencing the local chain and slashing the signer if statement is invalid.
abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatementInbox {
using AttestationLib for bytes;
using ReceiptLib for bytes;
using StateLib for bytes;
using SnapshotLib for bytes;
struct StoredReport {
uint256 sigIndex;
bytes statementPayload;
}
// ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════
address public agentManager;
address public origin;
address public destination;
// TODO: optimize this
bytes[] internal _storedSignatures;
StoredReport[] internal _storedReports;
/// @dev gap for upgrade safety
uint256[45] private __GAP; // solhint-disable-line var-name-mixedcase
// ════════════════════════════════════════════════ INITIALIZER ════════════════════════════════════════════════════
/// @dev Initializes the contract:
/// - Sets up `msg.sender` as the owner of the contract.
/// - Sets up `agentManager`, `origin`, and `destination`.
// solhint-disable-next-line func-name-mixedcase
function __StatementInbox_init(address agentManager_, address origin_, address destination_)
internal
onlyInitializing
{
agentManager = agentManager_;
origin = origin_;
destination = destination_;
__Ownable2Step_init();
}
// ══════════════════════════════════════════ SUBMIT AGENT STATEMENTS ══════════════════════════════════════════════
/// @inheritdoc IStatementInbox
// solhint-disable-next-line ordering
function submitStateReportWithSnapshot(
uint8 stateIndex,
bytes memory srSignature,
bytes memory snapPayload,
bytes memory snapSignature
) external returns (bool wasAccepted) {
// This will revert if payload is not a snapshot
Snapshot snapshot = snapPayload.castToSnapshot();
// This will revert if the snapshot signer is not a known Notary
(AgentStatus memory notaryStatus,) =
_verifySnapshot({snapshot: snapshot, snapSignature: snapSignature, verifyNotary: true});
// Notary needs to be Active/Unstaking
notaryStatus.verifyActiveUnstaking();
// Check if Notary is active on this chain
_verifyNotaryDomain(notaryStatus.domain);
// This will revert if state index is out of range
State state = snapshot.state(stateIndex);
// This will revert if the report signer is not an known Guard
(AgentStatus memory guardStatus,) = _verifyStateReport(state, srSignature);
// Check that Guard is active
guardStatus.verifyActive();
_saveReport(state.unwrap().clone(), srSignature);
// This will revert if either actor is already in dispute
IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index);
return true;
}
/// @inheritdoc IStatementInbox
function submitStateReportWithAttestation(
uint8 stateIndex,
bytes memory srSignature,
bytes memory snapPayload,
bytes memory attPayload,
bytes memory attSignature
) external returns (bool wasAccepted) {
// This will revert if payload is not a snapshot
Snapshot snapshot = snapPayload.castToSnapshot();
// This will revert if state index is out of range
State state = snapshot.state(stateIndex);
// This will revert if the report signer is not an known Guard
(AgentStatus memory guardStatus,) = _verifyStateReport(state, srSignature);
// Check that Guard is active
guardStatus.verifyActive();
// This will revert if payload is not an attestation
Attestation att = attPayload.castToAttestation();
// This will revert if signer is not an known Notary
(AgentStatus memory notaryStatus,) = _verifyAttestation(att, attSignature);
// Notary needs to be Active/Unstaking
notaryStatus.verifyActiveUnstaking();
// Check if Notary is active on this chain
_verifyNotaryDomain(notaryStatus.domain);
if (snapshot.calculateRoot() != att.snapRoot()) revert IncorrectSnapshotRoot();
_saveReport(state.unwrap().clone(), srSignature);
// This will revert if either actor is already in dispute
IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index);
return true;
}
/// @inheritdoc IStatementInbox
function submitStateReportWithSnapshotProof(
uint8 stateIndex,
bytes memory statePayload,
bytes memory srSignature,
bytes32[] memory snapProof,
bytes memory attPayload,
bytes memory attSignature
) external returns (bool wasAccepted) {
// This will revert if payload is not a state
State state = statePayload.castToState();
// This will revert if the report signer is not an known Guard
(AgentStatus memory guardStatus,) = _verifyStateReport(state, srSignature);
// Check that Guard is active
guardStatus.verifyActive();
// This will revert if payload is not an attestation
Attestation att = attPayload.castToAttestation();
// This will revert if signer is not a known Notary
(AgentStatus memory notaryStatus,) = _verifyAttestation(att, attSignature);
// Notary needs to be Active/Unstaking
notaryStatus.verifyActiveUnstaking();
// Check if Notary is active on this chain
_verifyNotaryDomain(notaryStatus.domain);
// This will revert if any of these is true:
// - Attestation root is not equal to Merkle Root derived from State and Snapshot Proof.
// - Snapshot Proof's first element does not match the State metadata.
// - Snapshot Proof length exceeds Snapshot tree Height.
// - State index is out of range.
_verifySnapshotMerkle(att, stateIndex, state, snapProof);
_saveReport(statePayload, srSignature);
// This will revert if either actor is already in dispute
IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index);
return true;
}
// ══════════════════════════════════════════ VERIFY AGENT STATEMENTS ══════════════════════════════════════════════
/// @inheritdoc IStatementInbox
function verifyReceipt(bytes memory rcptPayload, bytes memory rcptSignature)
external
returns (bool isValidReceipt)
{
// This will revert if payload is not a receipt
Receipt rcpt = rcptPayload.castToReceipt();
// This will revert if the attestation signer is not a known Notary
(AgentStatus memory status, address notary) = _verifyReceipt(rcpt, rcptSignature);
// Notary needs to be Active/Unstaking
status.verifyActiveUnstaking();
isValidReceipt = IExecutionHub(destination).isValidReceipt(rcptPayload);
if (!isValidReceipt) {
emit InvalidReceipt(rcptPayload, rcptSignature);
IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender);
}
}
/// @inheritdoc IStatementInbox
function verifyReceiptReport(bytes memory rcptPayload, bytes memory rrSignature)
external
returns (bool isValidReport)
{
// This will revert if payload is not a receipt
Receipt rcpt = rcptPayload.castToReceipt();
// This will revert if the report signer is not a known Guard
(AgentStatus memory status, address guard) = _verifyReceiptReport(rcpt, rrSignature);
// Guard needs to be Active/Unstaking
status.verifyActiveUnstaking();
// Report is valid IF AND ONLY IF the reported receipt in invalid
isValidReport = !IExecutionHub(destination).isValidReceipt(rcptPayload);
if (!isValidReport) {
emit InvalidReceiptReport(rcptPayload, rrSignature);
IAgentManager(agentManager).slashAgent(status.domain, guard, msg.sender);
}
}
/// @inheritdoc IStatementInbox
function verifyStateWithAttestation(
uint8 stateIndex,
bytes memory snapPayload,
bytes memory attPayload,
bytes memory attSignature
) external returns (bool isValidState) {
// This will revert if payload is not an attestation
Attestation att = attPayload.castToAttestation();
// This will revert if the attestation signer is not a known Notary
(AgentStatus memory status, address notary) = _verifyAttestation(att, attSignature);
// Notary needs to be Active/Unstaking
status.verifyActiveUnstaking();
// This will revert if payload is not a snapshot
Snapshot snapshot = snapPayload.castToSnapshot();
if (snapshot.calculateRoot() != att.snapRoot()) revert IncorrectSnapshotRoot();
// This will revert if state does not refer to this chain
bytes memory statePayload = snapshot.state(stateIndex).unwrap().clone();
isValidState = IStateHub(origin).isValidState(statePayload);
if (!isValidState) {
emit InvalidStateWithAttestation(stateIndex, statePayload, attPayload, attSignature);
IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender);
}
}
/// @inheritdoc IStatementInbox
function verifyStateWithSnapshotProof(
uint8 stateIndex,
bytes memory statePayload,
bytes32[] memory snapProof,
bytes memory attPayload,
bytes memory attSignature
) external returns (bool isValidState) {
// This will revert if payload is not an attestation
Attestation att = attPayload.castToAttestation();
// This will revert if the attestation signer is not a known Notary
(AgentStatus memory status, address notary) = _verifyAttestation(att, attSignature);
// Notary needs to be Active/Unstaking
status.verifyActiveUnstaking();
// This will revert if payload is not a state
State state = statePayload.castToState();
// This will revert if any of these is true:
// - Attestation root is not equal to Merkle Root derived from State and Snapshot Proof.
// - Snapshot Proof's first element does not match the State metadata.
// - Snapshot Proof length exceeds Snapshot tree Height.
// - State index is out of range.
_verifySnapshotMerkle(att, stateIndex, state, snapProof);
// This will revert if state does not refer to this chain
isValidState = IStateHub(origin).isValidState(statePayload);
if (!isValidState) {
emit InvalidStateWithAttestation(stateIndex, statePayload, attPayload, attSignature);
IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender);
}
}
/// @inheritdoc IStatementInbox
function verifyStateWithSnapshot(uint8 stateIndex, bytes memory snapPayload, bytes memory snapSignature)
external
returns (bool isValidState)
{
// This will revert if payload is not a snapshot
Snapshot snapshot = snapPayload.castToSnapshot();
// This will revert if the snapshot signer is not a known Guard/Notary
(AgentStatus memory status, address agent) =
_verifySnapshot({snapshot: snapshot, snapSignature: snapSignature, verifyNotary: false});
// Agent needs to be Active/Unstaking
status.verifyActiveUnstaking();
// This will revert if state does not refer to this chain
isValidState = IStateHub(origin).isValidState(snapshot.state(stateIndex).unwrap().clone());
if (!isValidState) {
emit InvalidStateWithSnapshot(stateIndex, snapPayload, snapSignature);
IAgentManager(agentManager).slashAgent(status.domain, agent, msg.sender);
}
}
/// @inheritdoc IStatementInbox
function verifyStateReport(bytes memory statePayload, bytes memory srSignature)
external
returns (bool isValidReport)
{
// This will revert if payload is not a state
State state = statePayload.castToState();
// This will revert if the report signer is not a known Guard
(AgentStatus memory status, address guard) = _verifyStateReport(state, srSignature);
// Guard needs to be Active/Unstaking
status.verifyActiveUnstaking();
// Report is valid IF AND ONLY IF the reported state in invalid
// This will revert if the reported state does not refer to this chain
isValidReport = !IStateHub(origin).isValidState(statePayload);
if (!isValidReport) {
emit InvalidStateReport(statePayload, srSignature);
IAgentManager(agentManager).slashAgent(status.domain, guard, msg.sender);
}
}
// ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════
/// @inheritdoc IStatementInbox
function getReportsAmount() external view returns (uint256) {
return _storedReports.length;
}
/// @inheritdoc IStatementInbox
function getGuardReport(uint256 index)
external
view
returns (bytes memory statementPayload, bytes memory reportSignature)
{
if (index >= _storedReports.length) revert IndexOutOfRange();
StoredReport memory storedReport = _storedReports[index];
statementPayload = storedReport.statementPayload;
reportSignature = _storedSignatures[storedReport.sigIndex];
}
/// @inheritdoc IStatementInbox
function getStoredSignature(uint256 index) external view returns (bytes memory) {
return _storedSignatures[index];
}
// ══════════════════════════════════════════════ INTERNAL LOGIC ═══════════════════════════════════════════════════
/// @dev Saves the statement reported by Guard as invalid and the Guard Report signature.
function _saveReport(bytes memory statementPayload, bytes memory reportSignature) internal {
uint256 sigIndex = _saveSignature(reportSignature);
_storedReports.push(StoredReport(sigIndex, statementPayload));
}
/// @dev Saves the signature and returns its index.
function _saveSignature(bytes memory signature) internal returns (uint256 sigIndex) {
sigIndex = _storedSignatures.length;
_storedSignatures.push(signature);
}
// ═══════════════════════════════════════════════ AGENT CHECKS ════════════════════════════════════════════════════
/**
* @dev Recovers a signer from a hashed message, and a EIP-191 signature for it.
* Will revert, if the signer is not a known agent.
* @dev Agent flag could be any of these: Active/Unstaking/Resting/Fraudulent/Slashed
* Further checks need to be performed in a caller function.
* @param hashedStatement Hash of the statement that was signed by an Agent
* @param signature Agent signature for the hashed statement
* @return status Struct representing agent status:
* - flag Unknown/Active/Unstaking/Resting/Fraudulent/Slashed
* - domain Domain where agent is/was active
* - index Index of agent in the Agent Merkle Tree
* @return agent Agent that signed the statement
*/
function _recoverAgent(bytes32 hashedStatement, bytes memory signature)
internal
view
returns (AgentStatus memory status, address agent)
{
bytes32 ethSignedMsg = ECDSA.toEthSignedMessageHash(hashedStatement);
agent = ECDSA.recover(ethSignedMsg, signature);
status = IAgentManager(agentManager).agentStatus(agent);
// Discard signature of unknown agents.
// Further flag checks are supposed to be performed in a caller function.
status.verifyKnown();
}
/// @dev Verifies that Notary signature is active on local domain.
function _verifyNotaryDomain(uint32 notaryDomain) internal view {
// Notary needs to be from the local domain (if contract is not deployed on Synapse Chain).
// Or Notary could be from any domain (if contract is deployed on Synapse Chain).
if (notaryDomain != localDomain && localDomain != synapseDomain) revert IncorrectAgentDomain();
}
// ════════════════════════════════════════ ATTESTATION RELATED CHECKS ═════════════════════════════════════════════
/**
* @dev Internal function to verify the signed attestation payload.
* Reverts if any of these is true:
* - Attestation signer is not a known Notary.
* @param att Typed memory view over attestation payload
* @param attSignature Notary signature for the attestation
* @return status Struct representing agent status, see {_recoverAgent}
* @return notary Notary that signed the snapshot
*/
function _verifyAttestation(Attestation att, bytes memory attSignature)
internal
view
returns (AgentStatus memory status, address notary)
{
// This will revert if signer is not a known agent
(status, notary) = _recoverAgent(att.hashValid(), attSignature);
// Attestation signer needs to be a Notary, not a Guard
if (status.domain == 0) revert AgentNotNotary();
}
/**
* @dev Internal function to verify the signed attestation report payload.
* Reverts if any of these is true:
* - Report signer is not a known Guard.
* @param att Typed memory view over attestation payload that Guard reports as invalid
* @param arSignature Guard signature for the "invalid attestation" report
* @return status Struct representing guard status, see {_recoverAgent}
* @return guard Guard that signed the report
*/
function _verifyAttestationReport(Attestation att, bytes memory arSignature)
internal
view
returns (AgentStatus memory status, address guard)
{
// This will revert if signer is not a known agent
(status, guard) = _recoverAgent(att.hashInvalid(), arSignature);
// Report signer needs to be a Guard, not a Notary
if (status.domain != 0) revert AgentNotGuard();
}
// ══════════════════════════════════════════ RECEIPT RELATED CHECKS ═══════════════════════════════════════════════
/**
* @dev Internal function to verify the signed receipt payload.
* Reverts if any of these is true:
* - Receipt signer is not a known Notary.
* @param rcpt Typed memory view over receipt payload
* @param rcptSignature Notary signature for the receipt
* @return status Struct representing agent status, see {_recoverAgent}
* @return notary Notary that signed the snapshot
*/
function _verifyReceipt(Receipt rcpt, bytes memory rcptSignature)
internal
view
returns (AgentStatus memory status, address notary)
{
// This will revert if signer is not a known agent
(status, notary) = _recoverAgent(rcpt.hashValid(), rcptSignature);
// Receipt signer needs to be a Notary, not a Guard
if (status.domain == 0) revert AgentNotNotary();
}
/**
* @dev Internal function to verify the signed receipt report payload.
* Reverts if any of these is true:
* - Report signer is not a known Guard.
* @param rcpt Typed memory view over receipt payload that Guard reports as invalid
* @param rrSignature Guard signature for the "invalid receipt" report
* @return status Struct representing guard status, see {_recoverAgent}
* @return guard Guard that signed the report
*/
function _verifyReceiptReport(Receipt rcpt, bytes memory rrSignature)
internal
view
returns (AgentStatus memory status, address guard)
{
// This will revert if signer is not a known agent
(status, guard) = _recoverAgent(rcpt.hashInvalid(), rrSignature);
// Report signer needs to be a Guard, not a Notary
if (status.domain != 0) revert AgentNotGuard();
}
// ═══════════════════════════════════════ STATE/SNAPSHOT RELATED CHECKS ═══════════════════════════════════════════
/**
* @dev Internal function to verify the signed snapshot report payload.
* Reverts if any of these is true:
* - Report signer is not a known Guard.
* @param state Typed memory view over state payload that Guard reports as invalid
* @param srSignature Guard signature for the report
* @return status Struct representing guard status, see {_recoverAgent}
* @return guard Guard that signed the report
*/
function _verifyStateReport(State state, bytes memory srSignature)
internal
view
returns (AgentStatus memory status, address guard)
{
// This will revert if signer is not a known agent
(status, guard) = _recoverAgent(state.hashInvalid(), srSignature);
// Report signer needs to be a Guard, not a Notary
if (status.domain != 0) revert AgentNotGuard();
}
/**
* @dev Internal function to verify the signed snapshot payload.
* Reverts if any of these is true:
* - Snapshot signer is not a known Agent.
* - Snapshot signer is not a Notary (if verifyNotary is true).
* @param snapshot Typed memory view over snapshot payload
* @param snapSignature Agent signature for the snapshot
* @param verifyNotary If true, snapshot signer needs to be a Notary, not a Guard
* @return status Struct representing agent status, see {_recoverAgent}
* @return agent Agent that signed the snapshot
*/
function _verifySnapshot(Snapshot snapshot, bytes memory snapSignature, bool verifyNotary)
internal
view
returns (AgentStatus memory status, address agent)
{
// This will revert if signer is not a known agent
(status, agent) = _recoverAgent(snapshot.hashValid(), snapSignature);
// If requested, snapshot signer needs to be a Notary, not a Guard
if (verifyNotary && status.domain == 0) revert AgentNotNotary();
}
// ═══════════════════════════════════════════ MERKLE RELATED CHECKS ═══════════════════════════════════════════════
/**
* @dev Internal function to verify that snapshot roots match.
* Reverts if any of these is true:
* - Attestation root is not equal to Merkle Root derived from State and Snapshot Proof.
* - Snapshot Proof's first element does not match the State metadata.
* - Snapshot Proof length exceeds Snapshot tree Height.
* - State index is out of range.
* @param att Typed memory view over Attestation
* @param stateIndex Index of state in the snapshot
* @param state Typed memory view over the provided state payload
* @param snapProof Raw payload with snapshot data
*/
function _verifySnapshotMerkle(Attestation att, uint8 stateIndex, State state, bytes32[] memory snapProof)
internal
pure
{
// Snapshot proof first element should match State metadata (aka "right sub-leaf")
(, bytes32 rightSubLeaf) = state.subLeafs();
if (snapProof[0] != rightSubLeaf) revert IncorrectSnapshotProof();
// Reconstruct Snapshot Merkle Root using the snapshot proof
// This will revert if:
// - State index is out of range.
// - Snapshot Proof length exceeds Snapshot tree Height.
bytes32 snapshotRoot = SnapshotLib.proofSnapRoot(state.root(), state.origin(), snapProof, stateIndex);
// Snapshot root should match the attestation root
if (att.snapRoot() != snapshotRoot) revert IncorrectSnapshotRoot();
}
}