diff --git a/packages/contracts-core/contracts/Destination.sol b/packages/contracts-core/contracts/Destination.sol index 158d61459b..95b90d9e58 100644 --- a/packages/contracts-core/contracts/Destination.sol +++ b/packages/contracts-core/contracts/Destination.sol @@ -8,6 +8,7 @@ import {AGENT_ROOT_OPTIMISTIC_PERIOD} from "./libs/Constants.sol"; import {IndexOutOfRange, NotaryInDispute, OutdatedNonce} from "./libs/Errors.sol"; import {ChainGas, GasData} from "./libs/stack/GasData.sol"; import {AgentStatus, DestinationStatus} from "./libs/Structures.sol"; +import {ChainContext} from "./libs/ChainContext.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentSecured} from "./base/AgentSecured.sol"; import {DestinationEvents} from "./events/DestinationEvents.sol"; @@ -78,7 +79,7 @@ contract Destination is ExecutionHub, DestinationEvents, InterfaceDestination { if (localDomain != synapseDomain) { _nextAgentRoot = agentRoot; InterfaceLightManager(address(agentManager)).setAgentRoot(agentRoot); - destStatus.agentRootTime = uint40(block.timestamp); + destStatus.agentRootTime = ChainContext.blockTimestamp(); } // No need to do anything on Synapse Chain, as the agent root is set in BondingManager } @@ -198,12 +199,12 @@ contract Destination is ExecutionHub, DestinationEvents, InterfaceDestination { { status = destStatus; // Update the timestamp for the latest snapshot root - status.snapRootTime = uint40(block.timestamp); + status.snapRootTime = ChainContext.blockTimestamp(); // No need to save agent roots on Synapse Chain, as they could be accessed via BondingManager // Don't update agent root, if there is already a pending one // Update the data for latest agent root only if it differs from the saved one if (localDomain != synapseDomain && !rootPending && _nextAgentRoot != agentRoot) { - status.agentRootTime = uint40(block.timestamp); + status.agentRootTime = ChainContext.blockTimestamp(); status.notaryIndex = notaryIndex; _nextAgentRoot = agentRoot; emit AgentRootAccepted(agentRoot); @@ -224,7 +225,7 @@ contract Destination is ExecutionHub, DestinationEvents, InterfaceDestination { if (GasData.unwrap(gasData) == GasData.unwrap(storedGasData.gasData)) continue; // Save the gas data _storedGasData[domain] = - StoredGasData({gasData: gasData, notaryIndex: notaryIndex, submittedAt: uint40(block.timestamp)}); + StoredGasData({gasData: gasData, notaryIndex: notaryIndex, submittedAt: ChainContext.blockTimestamp()}); } } } diff --git a/packages/contracts-core/contracts/Summit.sol b/packages/contracts-core/contracts/Summit.sol index f1ca2b0fc4..57f6a2de8a 100644 --- a/packages/contracts-core/contracts/Summit.sol +++ b/packages/contracts-core/contracts/Summit.sol @@ -10,6 +10,7 @@ import {Receipt, ReceiptLib} from "./libs/memory/Receipt.sol"; import {Snapshot, SnapshotLib} from "./libs/memory/Snapshot.sol"; import {AgentFlag, AgentStatus, DisputeFlag, MessageStatus} from "./libs/Structures.sol"; import {Tips, TipsLib} from "./libs/stack/Tips.sol"; +import {ChainContext} from "./libs/ChainContext.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentSecured} from "./base/AgentSecured.sol"; import {SummitEvents} from "./events/SummitEvents.sol"; @@ -276,7 +277,7 @@ contract Summit is SnapshotHub, SummitEvents, InterfaceSummit { pending: true, tipsAwarded: savedRcpt.tipsAwarded, receiptNotaryIndex: rcptNotaryIndex, - submittedAt: uint40(block.timestamp) + submittedAt: ChainContext.blockTimestamp() }); // Save receipt tips _receiptTips[messageHash] = ReceiptTips({ diff --git a/packages/contracts-core/contracts/base/MessagingBase.sol b/packages/contracts-core/contracts/base/MessagingBase.sol index b0785c1075..10d0021dab 100644 --- a/packages/contracts-core/contracts/base/MessagingBase.sol +++ b/packages/contracts-core/contracts/base/MessagingBase.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; +// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════ +import {ChainContext} from "../libs/ChainContext.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {MultiCallable} from "./MultiCallable.sol"; import {Versioned} from "./Version.sol"; @@ -27,8 +29,7 @@ abstract contract MessagingBase is MultiCallable, Versioned, Ownable2StepUpgrade uint256[50] private __GAP; // solhint-disable-line var-name-mixedcase constructor(string memory version_, uint32 synapseDomain_) Versioned(version_) { - // TODO: do we want to/need to check for overflow? - localDomain = uint32(block.chainid); + localDomain = ChainContext.chainId(); synapseDomain = synapseDomain_; } diff --git a/packages/contracts-core/contracts/events/StatementInboxEvents.sol b/packages/contracts-core/contracts/events/StatementInboxEvents.sol index a5888509da..99abf8da93 100644 --- a/packages/contracts-core/contracts/events/StatementInboxEvents.sol +++ b/packages/contracts-core/contracts/events/StatementInboxEvents.sol @@ -36,7 +36,7 @@ abstract contract StatementInboxEvents { * @param attPayload Raw payload with Attestation data for snapshot * @param attSignature Notary signature for the attestation */ - event InvalidStateWithAttestation(uint256 stateIndex, bytes statePayload, bytes attPayload, bytes attSignature); + event InvalidStateWithAttestation(uint8 stateIndex, bytes statePayload, bytes attPayload, bytes attSignature); /** * @notice Emitted when a proof of invalid state in the signed snapshot is submitted. @@ -44,7 +44,7 @@ abstract contract StatementInboxEvents { * @param snapPayload Raw payload with snapshot data * @param snapSignature Agent signature for the snapshot */ - event InvalidStateWithSnapshot(uint256 stateIndex, bytes snapPayload, bytes snapSignature); + event InvalidStateWithSnapshot(uint8 stateIndex, bytes snapPayload, bytes snapSignature); /** * @notice Emitted when a proof of invalid state report is submitted. diff --git a/packages/contracts-core/contracts/hubs/ExecutionHub.sol b/packages/contracts-core/contracts/hubs/ExecutionHub.sol index 300452061c..fec576fcdb 100644 --- a/packages/contracts-core/contracts/hubs/ExecutionHub.sol +++ b/packages/contracts-core/contracts/hubs/ExecutionHub.sol @@ -27,6 +27,7 @@ import {Request} from "../libs/stack/Request.sol"; import {SnapshotLib} from "../libs/memory/Snapshot.sol"; import {AgentFlag, AgentStatus, MessageStatus} from "../libs/Structures.sol"; import {Tips} from "../libs/stack/Tips.sol"; +import {ChainContext} from "../libs/ChainContext.sol"; import {TypeCasts} from "../libs/TypeCasts.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentSecured} from "../base/AgentSecured.sol"; @@ -36,6 +37,7 @@ import {IExecutionHub} from "../interfaces/IExecutionHub.sol"; import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol"; // ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; /// @notice `ExecutionHub` is a parent contract for `Destination`. It is responsible for the following: @@ -52,6 +54,7 @@ abstract contract ExecutionHub is AgentSecured, ReentrancyGuardUpgradeable, Exec using MessageLib for bytes; using ReceiptLib for bytes; using SafeCall for address; + using SafeCast for uint256; using TypeCasts for bytes32; /// @notice Struct representing stored data for the snapshot root @@ -115,7 +118,7 @@ abstract contract ExecutionHub is AgentSecured, ReentrancyGuardUpgradeable, Exec bytes memory msgPayload, bytes32[] calldata originProof, bytes32[] calldata snapProof, - uint256 stateIndex, + uint8 stateIndex, uint64 gasLimit ) external nonReentrant { // This will revert if payload is not a formatted message payload @@ -150,7 +153,7 @@ abstract contract ExecutionHub is AgentSecured, ReentrancyGuardUpgradeable, Exec // This is the first valid attempt to execute the message => save origin and snapshot proof rcptData.origin = header.origin(); rcptData.rootIndex = rootData.index; - rcptData.stateIndex = uint8(stateIndex); + rcptData.stateIndex = stateIndex; if (success) { // This is the successful attempt to execute the message => save the executor rcptData.executor = msg.sender; @@ -284,13 +287,14 @@ abstract contract ExecutionHub is AgentSecured, ReentrancyGuardUpgradeable, Exec function _saveAttestation(Attestation att, uint32 notaryIndex, uint256 sigIndex) internal { bytes32 root = att.snapRoot(); if (_rootData[root].submittedAt != 0) revert DuplicatedSnapshotRoot(); + // TODO: consider using more than 32 bits for the root index _rootData[root] = SnapRootData({ notaryIndex: notaryIndex, attNonce: att.nonce(), attBN: att.blockNumber(), attTS: att.timestamp(), - index: uint32(_roots.length), - submittedAt: uint40(block.timestamp), + index: _roots.length.toUint32(), + submittedAt: ChainContext.blockTimestamp(), sigIndex: sigIndex }); _roots.push(root); @@ -347,7 +351,7 @@ abstract contract ExecutionHub is AgentSecured, ReentrancyGuardUpgradeable, Exec bytes32 msgLeaf, bytes32[] calldata originProof, bytes32[] calldata snapProof, - uint256 stateIndex + uint8 stateIndex ) internal view returns (SnapRootData memory rootData) { // Reconstruct Origin Merkle Root using the origin proof // Message index in the tree is (nonce - 1), as nonce starts from 1 diff --git a/packages/contracts-core/contracts/hubs/SnapshotHub.sol b/packages/contracts-core/contracts/hubs/SnapshotHub.sol index d3e6cdd0b7..70199d4994 100644 --- a/packages/contracts-core/contracts/hubs/SnapshotHub.sol +++ b/packages/contracts-core/contracts/hubs/SnapshotHub.sol @@ -10,6 +10,7 @@ import {ChainGas, GasData, GasDataLib} from "../libs/stack/GasData.sol"; import {MerkleMath} from "../libs/merkle/MerkleMath.sol"; import {Snapshot, SnapshotLib} from "../libs/memory/Snapshot.sol"; import {State, StateLib} from "../libs/memory/State.sol"; +import {ChainContext} from "../libs/ChainContext.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentSecured} from "../base/AgentSecured.sol"; import {SnapshotHubEvents} from "../events/SnapshotHubEvents.sol"; @@ -168,7 +169,7 @@ abstract contract SnapshotHub is AgentSecured, SnapshotHubEvents, ISnapshotHub { } /// @inheritdoc ISnapshotHub - function getSnapshotProof(uint32 attNonce, uint256 stateIndex) external view returns (bytes32[] memory snapProof) { + function getSnapshotProof(uint32 attNonce, uint8 stateIndex) external view returns (bytes32[] memory snapProof) { if (attNonce == 0 || attNonce >= _notarySnapshots.length) revert NonceOutOfRange(); SummitSnapshot memory snap = _notarySnapshots[attNonce]; uint256 statesAmount = snap.statePtrs.length; @@ -299,11 +300,6 @@ abstract contract SnapshotHub is AgentSecured, SnapshotHubEvents, ISnapshotHub { // ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════ - /// @dev Returns the amount of saved _attestations (created from Notary snapshots) so far. - function _attestationsAmount() internal view returns (uint256) { - return _attestations.length; - } - /// @dev Checks if attestation was previously submitted by a Notary (as a signed snapshot). function _isValidAttestation(Attestation att) internal view returns (bool) { // Check if nonce exists @@ -353,7 +349,7 @@ abstract contract SnapshotHub is AgentSecured, SnapshotHubEvents, ISnapshotHub { } /// @dev Returns indexes of agents who provided state data for the Notary snapshot with the given nonce. - function _stateAgents(uint32 nonce, uint256 stateIndex) + function _stateAgents(uint32 nonce, uint8 stateIndex) internal view returns (uint32 guardIndex, uint32 notaryIndex) @@ -431,8 +427,8 @@ abstract contract SnapshotHub is AgentSecured, SnapshotHubEvents, ISnapshotHub { summitAtt.snapRoot = snapRoot; summitAtt.agentRoot = agentRoot; summitAtt.snapGasHash = snapGasHash; - summitAtt.blockNumber = uint40(block.number); - summitAtt.timestamp = uint40(block.timestamp); + summitAtt.blockNumber = ChainContext.blockNumber(); + summitAtt.timestamp = ChainContext.blockTimestamp(); } /// @dev Checks that an Attestation and its Summit representation are equal. diff --git a/packages/contracts-core/contracts/hubs/StateHub.sol b/packages/contracts-core/contracts/hubs/StateHub.sol index 8a4d9c891b..b5ce9874ff 100644 --- a/packages/contracts-core/contracts/hubs/StateHub.sol +++ b/packages/contracts-core/contracts/hubs/StateHub.sol @@ -6,15 +6,19 @@ import {IncorrectOriginDomain} from "../libs/Errors.sol"; import {GasData, GasDataLib} from "../libs/stack/GasData.sol"; import {HistoricalTree} from "../libs/merkle/MerkleTree.sol"; import {State, StateLib} from "../libs/memory/State.sol"; +import {ChainContext} from "../libs/ChainContext.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentSecured} from "../base/AgentSecured.sol"; import {StateHubEvents} from "../events/StateHubEvents.sol"; import {IStateHub} from "../interfaces/IStateHub.sol"; +// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════ +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice `StateHub` is a parent contract for `Origin`. It is responsible for the following: /// - Keeping track of the historical Origin Merkle Tree containing all the message hashes. /// - Keeping track of the historical Origin States, as well as verifying their validity. abstract contract StateHub is AgentSecured, StateHubEvents, IStateHub { + using SafeCast for uint256; using StateLib for bytes; struct OriginState { @@ -93,7 +97,8 @@ abstract contract StateHub is AgentSecured, StateHubEvents, IStateHub { /// @dev Returns nonce of the next sent message: the amount of saved States so far. /// This always equals to "total amount of sent messages" plus 1. function _nextNonce() internal view returns (uint32) { - return uint32(_originStates.length); + // TODO: consider using more than 32 bits for origin nonces + return _originStates.length.toUint32(); } /// @dev Checks if a state is valid, i.e. if it matches the historical one. @@ -135,8 +140,8 @@ abstract contract StateHub is AgentSecured, StateHubEvents, IStateHub { /// @dev Returns a OriginState struct to save in the contract. function _toOriginState() internal view returns (OriginState memory originState) { - originState.blockNumber = uint40(block.number); - originState.timestamp = uint40(block.timestamp); + originState.blockNumber = ChainContext.blockNumber(); + originState.timestamp = ChainContext.blockTimestamp(); originState.gasData = _fetchGasData(); } diff --git a/packages/contracts-core/contracts/inbox/StatementInbox.sol b/packages/contracts-core/contracts/inbox/StatementInbox.sol index 776c64d567..4a95c940aa 100644 --- a/packages/contracts-core/contracts/inbox/StatementInbox.sol +++ b/packages/contracts-core/contracts/inbox/StatementInbox.sol @@ -79,7 +79,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem /// @inheritdoc IStatementInbox // solhint-disable-next-line ordering function submitStateReportWithSnapshot( - uint256 stateIndex, + uint8 stateIndex, bytes memory srSignature, bytes memory snapPayload, bytes memory snapSignature @@ -107,7 +107,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem /// @inheritdoc IStatementInbox function submitStateReportWithAttestation( - uint256 stateIndex, + uint8 stateIndex, bytes memory srSignature, bytes memory snapPayload, bytes memory attPayload, @@ -138,7 +138,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem /// @inheritdoc IStatementInbox function submitStateReportWithSnapshotProof( - uint256 stateIndex, + uint8 stateIndex, bytes memory statePayload, bytes memory srSignature, bytes32[] memory snapProof, @@ -212,7 +212,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem /// @inheritdoc IStatementInbox function verifyStateWithAttestation( - uint256 stateIndex, + uint8 stateIndex, bytes memory snapPayload, bytes memory attPayload, bytes memory attSignature @@ -237,7 +237,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem /// @inheritdoc IStatementInbox function verifyStateWithSnapshotProof( - uint256 stateIndex, + uint8 stateIndex, bytes memory statePayload, bytes32[] memory snapProof, bytes memory attPayload, @@ -266,7 +266,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem } /// @inheritdoc IStatementInbox - function verifyStateWithSnapshot(uint256 stateIndex, bytes memory snapPayload, bytes memory snapSignature) + function verifyStateWithSnapshot(uint8 stateIndex, bytes memory snapPayload, bytes memory snapSignature) external returns (bool isValidState) { @@ -520,7 +520,7 @@ abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatem * @param state Typed memory view over the provided state payload * @param snapProof Raw payload with snapshot data */ - function _verifySnapshotMerkle(Attestation att, uint256 stateIndex, State state, bytes32[] memory snapProof) + function _verifySnapshotMerkle(Attestation att, uint8 stateIndex, State state, bytes32[] memory snapProof) internal pure { diff --git a/packages/contracts-core/contracts/interfaces/IExecutionHub.sol b/packages/contracts-core/contracts/interfaces/IExecutionHub.sol index 77b89a924c..fa4bb443cc 100644 --- a/packages/contracts-core/contracts/interfaces/IExecutionHub.sol +++ b/packages/contracts-core/contracts/interfaces/IExecutionHub.sol @@ -29,7 +29,7 @@ interface IExecutionHub { bytes memory msgPayload, bytes32[] calldata originProof, bytes32[] calldata snapProof, - uint256 stateIndex, + uint8 stateIndex, uint64 gasLimit ) external; diff --git a/packages/contracts-core/contracts/interfaces/ISnapshotHub.sol b/packages/contracts-core/contracts/interfaces/ISnapshotHub.sol index d28630e405..ff84385876 100644 --- a/packages/contracts-core/contracts/interfaces/ISnapshotHub.sol +++ b/packages/contracts-core/contracts/interfaces/ISnapshotHub.sol @@ -93,5 +93,5 @@ interface ISnapshotHub { * @param stateIndex Index of state in the attestation's snapshot * @return snapProof The snapshot proof */ - function getSnapshotProof(uint32 attNonce, uint256 stateIndex) external view returns (bytes32[] memory snapProof); + function getSnapshotProof(uint32 attNonce, uint8 stateIndex) external view returns (bytes32[] memory snapProof); } diff --git a/packages/contracts-core/contracts/interfaces/IStatementInbox.sol b/packages/contracts-core/contracts/interfaces/IStatementInbox.sol index d223ac5cff..22c1471511 100644 --- a/packages/contracts-core/contracts/interfaces/IStatementInbox.sol +++ b/packages/contracts-core/contracts/interfaces/IStatementInbox.sol @@ -24,7 +24,7 @@ interface IStatementInbox { * @return wasAccepted Whether the Report was accepted (resulting in Dispute between the agents) */ function submitStateReportWithSnapshot( - uint256 stateIndex, + uint8 stateIndex, bytes memory srSignature, bytes memory snapPayload, bytes memory snapSignature @@ -53,7 +53,7 @@ interface IStatementInbox { * @return wasAccepted Whether the Report was accepted (resulting in Dispute between the agents) */ function submitStateReportWithAttestation( - uint256 stateIndex, + uint8 stateIndex, bytes memory srSignature, bytes memory snapPayload, bytes memory attPayload, @@ -86,7 +86,7 @@ interface IStatementInbox { * @return wasAccepted Whether the Report was accepted (resulting in Dispute between the agents) */ function submitStateReportWithSnapshotProof( - uint256 stateIndex, + uint8 stateIndex, bytes memory statePayload, bytes memory srSignature, bytes32[] memory snapProof, @@ -149,7 +149,7 @@ interface IStatementInbox { * Notary is slashed, if return value is FALSE. */ function verifyStateWithAttestation( - uint256 stateIndex, + uint8 stateIndex, bytes memory snapPayload, bytes memory attPayload, bytes memory attSignature @@ -177,7 +177,7 @@ interface IStatementInbox { * Notary is slashed, if return value is FALSE. */ function verifyStateWithSnapshotProof( - uint256 stateIndex, + uint8 stateIndex, bytes memory statePayload, bytes32[] memory snapProof, bytes memory attPayload, @@ -199,7 +199,7 @@ interface IStatementInbox { * @return isValidState Whether the provided state is valid. * Agent is slashed, if return value is FALSE. */ - function verifyStateWithSnapshot(uint256 stateIndex, bytes memory snapPayload, bytes memory snapSignature) + function verifyStateWithSnapshot(uint8 stateIndex, bytes memory snapPayload, bytes memory snapSignature) external returns (bool isValidState); diff --git a/packages/contracts-core/contracts/libs/ChainContext.sol b/packages/contracts-core/contracts/libs/ChainContext.sol new file mode 100644 index 0000000000..290dce4120 --- /dev/null +++ b/packages/contracts-core/contracts/libs/ChainContext.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +/// @notice Library for accessing chain context variables as tightly packed integers. +/// Messaging contracts should rely on this library for accessing chain context variables +/// instead of doing the casting themselves. +library ChainContext { + using SafeCast for uint256; + + /// @notice Returns the current block number as uint40. + /// @dev Reverts if block number is greater than 40 bits, which is not supposed to happen + /// until the block.timestamp overflows (assuming block time is at least 1 second). + function blockNumber() internal view returns (uint40) { + return block.number.toUint40(); + } + + /// @notice Returns the current block timestamp as uint40. + /// @dev Reverts if block timestamp is greater than 40 bits, which is + /// supposed to happen approximately in year 36835. + function blockTimestamp() internal view returns (uint40) { + return block.timestamp.toUint40(); + } + + /// @notice Returns the chain id as uint32. + /// @dev Reverts if chain id is greater than 32 bits, which is not + /// supposed to happen in production. + function chainId() internal view returns (uint32) { + return block.chainid.toUint32(); + } +} diff --git a/packages/contracts-core/contracts/libs/memory/Attestation.sol b/packages/contracts-core/contracts/libs/memory/Attestation.sol index 89f5099aba..1f0f2ec596 100644 --- a/packages/contracts-core/contracts/libs/memory/Attestation.sol +++ b/packages/contracts-core/contracts/libs/memory/Attestation.sol @@ -147,17 +147,20 @@ library AttestationLib { /// @notice Returns nonce of Summit contract at the time, when attestation was created. function nonce(Attestation att) internal pure returns (uint32) { + // Can be safely casted to uint32, since we index 4 bytes return uint32(att.unwrap().indexUint({index_: OFFSET_NONCE, bytes_: 4})); } /// @notice Returns a block number when attestation was created in Summit. function blockNumber(Attestation att) internal pure returns (uint40) { + // Can be safely casted to uint40, since we index 5 bytes return uint40(att.unwrap().indexUint({index_: OFFSET_BLOCK_NUMBER, bytes_: 5})); } /// @notice Returns a block timestamp when attestation was created in Summit. /// @dev This is the timestamp according to the Synapse Chain. function timestamp(Attestation att) internal pure returns (uint40) { + // Can be safely casted to uint40, since we index 5 bytes return uint40(att.unwrap().indexUint({index_: OFFSET_TIMESTAMP, bytes_: 5})); } } diff --git a/packages/contracts-core/contracts/libs/memory/ByteString.sol b/packages/contracts-core/contracts/libs/memory/ByteString.sol index b3c8b84a8d..e8ca65d919 100644 --- a/packages/contracts-core/contracts/libs/memory/ByteString.sol +++ b/packages/contracts-core/contracts/libs/memory/ByteString.sol @@ -96,6 +96,7 @@ library ByteString { MemView memView = unwrap(signature); r = memView.index({index_: OFFSET_R, bytes_: 32}); s = memView.index({index_: OFFSET_S, bytes_: 32}); + // Can be safely casted to uint8, since we index a single byte v = uint8(memView.indexUint({index_: OFFSET_V, bytes_: 1})); } diff --git a/packages/contracts-core/contracts/libs/memory/Receipt.sol b/packages/contracts-core/contracts/libs/memory/Receipt.sol index fbc2471e8f..832503dc35 100644 --- a/packages/contracts-core/contracts/libs/memory/Receipt.sol +++ b/packages/contracts-core/contracts/libs/memory/Receipt.sol @@ -124,11 +124,13 @@ library ReceiptLib { /// @notice Returns receipt's origin field function origin(Receipt receipt) internal pure returns (uint32) { + // Can be safely casted to uint32, since we index 4 bytes return uint32(receipt.unwrap().indexUint({index_: OFFSET_ORIGIN, bytes_: 4})); } /// @notice Returns receipt's destination field function destination(Receipt receipt) internal pure returns (uint32) { + // Can be safely casted to uint32, since we index 4 bytes return uint32(receipt.unwrap().indexUint({index_: OFFSET_DESTINATION, bytes_: 4})); } @@ -144,6 +146,7 @@ library ReceiptLib { /// @notice Returns receipt's "state index" field function stateIndex(Receipt receipt) internal pure returns (uint8) { + // Can be safely casted to uint8, since we index a single byte return uint8(receipt.unwrap().indexUint({index_: OFFSET_STATE_INDEX, bytes_: 1})); } diff --git a/packages/contracts-core/contracts/libs/memory/Snapshot.sol b/packages/contracts-core/contracts/libs/memory/Snapshot.sol index 5dc2905baf..3e34d18907 100644 --- a/packages/contracts-core/contracts/libs/memory/Snapshot.sol +++ b/packages/contracts-core/contracts/libs/memory/Snapshot.sol @@ -161,16 +161,18 @@ library SnapshotLib { /// @param domain Domain of Origin chain /// @param snapProof Proof of inclusion of State Merkle Data into Snapshot Merkle Tree /// @param stateIndex Index of Origin State in the Snapshot - function proofSnapRoot(bytes32 originRoot, uint32 domain, bytes32[] memory snapProof, uint256 stateIndex) + function proofSnapRoot(bytes32 originRoot, uint32 domain, bytes32[] memory snapProof, uint8 stateIndex) internal pure returns (bytes32) { // Index of "leftLeaf" is twice the state position in the snapshot - uint256 leftLeafIndex = stateIndex << 1; + // This is because each state is represented by two leaves in the Snapshot Merkle Tree: + // - leftLeaf is a hash of (originRoot, originDomain) + // - rightLeaf is a hash of (nonce, blockNumber, timestamp, gasData) + uint256 leftLeafIndex = uint256(stateIndex) << 1; // Check that "leftLeaf" index fits into Snapshot Merkle Tree if (leftLeafIndex >= (1 << SNAPSHOT_TREE_HEIGHT)) revert IndexOutOfRange(); - // Reconstruct left sub-leaf of the Origin State: (originRoot, originDomain) bytes32 leftLeaf = StateLib.leftLeaf(originRoot, domain); // Reconstruct snapshot root using proof of inclusion // This will revert if snapshot proof length exceeds Snapshot Tree Height diff --git a/packages/contracts-core/contracts/libs/memory/State.sol b/packages/contracts-core/contracts/libs/memory/State.sol index 78b53b9cb5..d3fcb113c2 100644 --- a/packages/contracts-core/contracts/libs/memory/State.sol +++ b/packages/contracts-core/contracts/libs/memory/State.sol @@ -156,22 +156,26 @@ library StateLib { /// @notice Returns domain of chain where the Origin contract is deployed. function origin(State state) internal pure returns (uint32) { + // Can be safely casted to uint32, since we index 4 bytes return uint32(state.unwrap().indexUint({index_: OFFSET_ORIGIN, bytes_: 4})); } /// @notice Returns nonce of Origin contract at the time, when `root` was the Merkle root. function nonce(State state) internal pure returns (uint32) { + // Can be safely casted to uint32, since we index 4 bytes return uint32(state.unwrap().indexUint({index_: OFFSET_NONCE, bytes_: 4})); } /// @notice Returns a block number when `root` was saved in Origin. function blockNumber(State state) internal pure returns (uint40) { + // Can be safely casted to uint40, since we index 5 bytes return uint40(state.unwrap().indexUint({index_: OFFSET_BLOCK_NUMBER, bytes_: 5})); } /// @notice Returns a block timestamp when `root` was saved in Origin. /// @dev This is the timestamp according to the origin chain. function timestamp(State state) internal pure returns (uint40) { + // Can be safely casted to uint40, since we index 5 bytes return uint40(state.unwrap().indexUint({index_: OFFSET_TIMESTAMP, bytes_: 5})); } diff --git a/packages/contracts-core/contracts/libs/stack/GasData.sol b/packages/contracts-core/contracts/libs/stack/GasData.sol index 3670dd22ae..d4845d96ad 100644 --- a/packages/contracts-core/contracts/libs/stack/GasData.sol +++ b/packages/contracts-core/contracts/libs/stack/GasData.sol @@ -74,6 +74,7 @@ library GasDataLib { Number etherPrice_, Number markup_ ) internal pure returns (GasData) { + // Number type wraps uint16, so could safely be casted to uint96 // forgefmt: disable-next-item return GasData.wrap( uint96(Number.unwrap(gasPrice_)) << SHIFT_GAS_PRICE | @@ -87,6 +88,7 @@ library GasDataLib { /// @notice Wraps padded uint256 value into GasData struct. function wrapGasData(uint256 paddedGasData) internal pure returns (GasData) { + // Casting to uint96 will truncate the highest bits, which is the behavior we want return GasData.wrap(uint96(paddedGasData)); } @@ -132,11 +134,13 @@ library GasDataLib { /// @param gasData_ Chain's gas data /// @param domain_ Chain's domain function encodeChainGas(GasData gasData_, uint32 domain_) internal pure returns (ChainGas) { + // GasData type wraps uint96, so could safely be casted to uint128 return ChainGas.wrap(uint128(GasData.unwrap(gasData_)) << SHIFT_GAS_DATA | uint128(domain_)); } /// @notice Wraps padded uint256 value into ChainGas struct. function wrapChainGas(uint256 paddedChainGas) internal pure returns (ChainGas) { + // Casting to uint128 will truncate the highest bits, which is the behavior we want return ChainGas.wrap(uint128(paddedChainGas)); } diff --git a/packages/contracts-core/contracts/libs/stack/Header.sol b/packages/contracts-core/contracts/libs/stack/Header.sol index 9989acf1de..079fe04666 100644 --- a/packages/contracts-core/contracts/libs/stack/Header.sol +++ b/packages/contracts-core/contracts/libs/stack/Header.sol @@ -53,6 +53,7 @@ library HeaderLib { uint32 destination_, uint32 optimisticPeriod_ ) internal pure returns (Header) { + // All casts below are upcasts, so they are safe // forgefmt: disable-next-item return Header.wrap( uint136(uint8(flag_)) << SHIFT_FLAG | @@ -77,6 +78,7 @@ library HeaderLib { function wrapPadded(uint256 paddedHeader) internal pure returns (Header) { // Check that flag is within range if (!isHeader(paddedHeader)) revert FlagOutOfRange(); + // Casting to uint136 will truncate the highest bits, which is the behavior we want return Header.wrap(uint136(paddedHeader)); } diff --git a/packages/contracts-core/contracts/libs/stack/Number.sol b/packages/contracts-core/contracts/libs/stack/Number.sol index 5ac3d5da59..556b032cbc 100644 --- a/packages/contracts-core/contracts/libs/stack/Number.sol +++ b/packages/contracts-core/contracts/libs/stack/Number.sol @@ -131,6 +131,7 @@ library NumberLib { /// @dev Wraps (mantissa, exponent) pair into Number. function _encode(uint8 mantissa, uint8 exponent) private pure returns (Number) { + // Casts below are upcasts, so they are safe. return Number.wrap(uint16(mantissa) << SHIFT_MANTISSA | uint16(exponent)); } } diff --git a/packages/contracts-core/contracts/libs/stack/Request.sol b/packages/contracts-core/contracts/libs/stack/Request.sol index 7ccbf2010f..419b06d691 100644 --- a/packages/contracts-core/contracts/libs/stack/Request.sol +++ b/packages/contracts-core/contracts/libs/stack/Request.sol @@ -30,6 +30,7 @@ library RequestLib { /// @param gasLimit_ Minimum amount of gas units to supply for execution /// @param version_ Base message version to pass to the recipient function encodeRequest(uint96 gasDrop_, uint64 gasLimit_, uint32 version_) internal pure returns (Request) { + // Casts below are upcasts, so they are safe return Request.wrap(uint192(gasDrop_) << SHIFT_GAS_DROP | uint192(gasLimit_) << SHIFT_GAS_LIMIT | version_); } @@ -39,6 +40,7 @@ library RequestLib { /// The highest bits are discarded, so that the contracts dealing with encoded requests /// don't need to be updated, if a new field is added. function wrapPadded(uint256 paddedRequest) internal pure returns (Request) { + // Casting to uint192 will truncate the highest bits, which is the behavior we want return Request.wrap(uint192(paddedRequest)); } diff --git a/packages/contracts-core/contracts/libs/stack/Tips.sol b/packages/contracts-core/contracts/libs/stack/Tips.sol index d1a46b9212..465051480b 100644 --- a/packages/contracts-core/contracts/libs/stack/Tips.sol +++ b/packages/contracts-core/contracts/libs/stack/Tips.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.17; import {TIPS_GRANULARITY} from "../Constants.sol"; import {TipsOverflow, TipsValueTooLow} from "../Errors.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// Tips is encoded data with "tips paid for sending a base message". /// Note: even though uint256 is also an underlying type for MemView, Tips is stored ON STACK. @@ -48,6 +49,8 @@ using TipsLib for Tips global; /// | (008..000] | deliveryTip | uint64 | 8 | Tip for successful message delivery on destination chain | library TipsLib { + using SafeCast for uint256; + /// @dev Amount of bits to shift to summitTip field uint256 private constant SHIFT_SUMMIT_TIP = 24 * 8; /// @dev Amount of bits to shift to attestationTip field @@ -67,9 +70,12 @@ library TipsLib { pure returns (Tips) { + // forgefmt: disable-next-item return Tips.wrap( - uint256(summitTip_) << SHIFT_SUMMIT_TIP | uint256(attestationTip_) << SHIFT_ATTESTATION_TIP - | uint256(executionTip_) << SHIFT_EXECUTION_TIP | uint256(deliveryTip_) + uint256(summitTip_) << SHIFT_SUMMIT_TIP | + uint256(attestationTip_) << SHIFT_ATTESTATION_TIP | + uint256(executionTip_) << SHIFT_EXECUTION_TIP | + uint256(deliveryTip_) ); } @@ -79,11 +85,14 @@ library TipsLib { pure returns (Tips) { + // In practice, the tips amounts are not supposed to be higher than 2**96, and with 32 bits of granularity + // using uint64 is enough to store the values. However, we still check for overflow just in case. + // TODO: consider using Number type to store the tips values. return encodeTips({ - summitTip_: uint64(summitTip_ >> TIPS_GRANULARITY), - attestationTip_: uint64(attestationTip_ >> TIPS_GRANULARITY), - executionTip_: uint64(executionTip_ >> TIPS_GRANULARITY), - deliveryTip_: uint64(deliveryTip_ >> TIPS_GRANULARITY) + summitTip_: (summitTip_ >> TIPS_GRANULARITY).toUint64(), + attestationTip_: (attestationTip_ >> TIPS_GRANULARITY).toUint64(), + executionTip_: (executionTip_ >> TIPS_GRANULARITY).toUint64(), + deliveryTip_: (deliveryTip_ >> TIPS_GRANULARITY).toUint64() }); } diff --git a/packages/contracts-core/contracts/manager/AgentManager.sol b/packages/contracts-core/contracts/manager/AgentManager.sol index 8eadc20350..d027737170 100644 --- a/packages/contracts-core/contracts/manager/AgentManager.sol +++ b/packages/contracts-core/contracts/manager/AgentManager.sol @@ -98,9 +98,12 @@ abstract contract AgentManager is MessagingBase, AgentManagerEvents, IAgentManag if (_agentDispute[notaryIndex].flag != DisputeFlag.None) revert NotaryInDispute(); _disputes.push(OpenedDispute(guardIndex, notaryIndex, 0)); // Dispute is stored at length - 1, but we store the index + 1 to distinguish from "not in dispute". - uint256 disputePtr = _disputes.length; - _agentDispute[guardIndex] = AgentDispute(DisputeFlag.Pending, uint88(disputePtr), address(0)); - _agentDispute[notaryIndex] = AgentDispute(DisputeFlag.Pending, uint88(disputePtr), address(0)); + // TODO: check if we really need to use 88 bits for dispute indexes. Every dispute ends up in one of + // the agents being slashed, so the number of disputes is limited by the number of agents (currently 2**32). + // Thus we can do the unsafe cast to uint88. + uint88 disputePtr = uint88(_disputes.length); + _agentDispute[guardIndex] = AgentDispute(DisputeFlag.Pending, disputePtr, address(0)); + _agentDispute[notaryIndex] = AgentDispute(DisputeFlag.Pending, disputePtr, address(0)); // Dispute index is length - 1. Note: report that initiated the dispute has the same index in `Inbox`. emit DisputeOpened({disputeIndex: disputePtr - 1, guardIndex: guardIndex, notaryIndex: notaryIndex}); _notifyDisputeOpened(guardIndex, notaryIndex); diff --git a/packages/contracts-core/contracts/manager/BondingManager.sol b/packages/contracts-core/contracts/manager/BondingManager.sol index 86cddeee2c..72805a8579 100644 --- a/packages/contracts-core/contracts/manager/BondingManager.sol +++ b/packages/contracts-core/contracts/manager/BondingManager.sol @@ -10,7 +10,6 @@ import { IncorrectAgentDomain, IncorrectOriginDomain, IndexOutOfRange, - MerkleTreeFull, MustBeSynapseDomain, SlashAgentOptimisticPeriod, SynapseDomainForbidden @@ -24,6 +23,8 @@ import {IAgentSecured} from "../interfaces/IAgentSecured.sol"; import {InterfaceBondingManager} from "../interfaces/InterfaceBondingManager.sol"; import {InterfaceLightManager} from "../interfaces/InterfaceLightManager.sol"; import {InterfaceOrigin} from "../interfaces/InterfaceOrigin.sol"; +// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════ +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice BondingManager keeps track of all existing agents on the Synapse Chain. /// It utilizes a dynamic Merkle Tree to store the agent information. This enables passing only the @@ -39,6 +40,8 @@ import {InterfaceOrigin} from "../interfaces/InterfaceOrigin.sol"; /// - Accepting Manager Message from remote `LightManager` to slash agents on the Synapse Chain, when their fraud /// is proven on the remote chain. contract BondingManager is AgentManager, InterfaceBondingManager { + using SafeCast for uint256; + // ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════ // The address of the Summit contract. @@ -89,8 +92,8 @@ contract BondingManager is AgentManager, InterfaceBondingManager { if (status.flag == AgentFlag.Unknown) { // Unknown address could be added to any domain // New agent will need to be added to `_agents` list: could not have more than 2**32 agents - if (_agents.length >= type(uint32).max) revert MerkleTreeFull(); - index = uint32(_agents.length); + // TODO: consider using more than 32 bits for agent indexes + index = _agents.length.toUint32(); // Current leaf for index is bytes32(0), which is already assigned to `leaf` _agents.push(agent); _domainAgents[domain].push(agent); diff --git a/packages/contracts-core/test/harnesses/client/ReentrantApp.t.sol b/packages/contracts-core/test/harnesses/client/ReentrantApp.t.sol index d480cc66ce..6bbd790fd5 100644 --- a/packages/contracts-core/test/harnesses/client/ReentrantApp.t.sol +++ b/packages/contracts-core/test/harnesses/client/ReentrantApp.t.sol @@ -11,7 +11,7 @@ contract ReentrantApp is IMessageRecipient { bytes internal msgPayload; bytes32[] internal originProof; bytes32[] internal snapProof; - uint256 internal stateIndex; + uint8 internal stateIndex; /// @notice Prevents this contract from being included in the coverage report function testReentrantApp() external {} @@ -20,7 +20,7 @@ contract ReentrantApp is IMessageRecipient { bytes memory msgPayload_, bytes32[] memory originProof_, bytes32[] memory snapProof_, - uint256 stateIndex_ + uint8 stateIndex_ ) external { msgPayload = msgPayload_; originProof = originProof_; diff --git a/packages/contracts-core/test/harnesses/libs/ChainContextHarness.t.sol b/packages/contracts-core/test/harnesses/libs/ChainContextHarness.t.sol new file mode 100644 index 0000000000..336b50b281 --- /dev/null +++ b/packages/contracts-core/test/harnesses/libs/ChainContextHarness.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ChainContext} from "../../../contracts/libs/ChainContext.sol"; + +contract ChainContextHarness { + function blockNumber() public view returns (uint40) { + return ChainContext.blockNumber(); + } + + function blockTimestamp() public view returns (uint40) { + return ChainContext.blockTimestamp(); + } + + function chainId() public view returns (uint32) { + return ChainContext.chainId(); + } +} diff --git a/packages/contracts-core/test/harnesses/libs/memory/SnapshotHarness.t.sol b/packages/contracts-core/test/harnesses/libs/memory/SnapshotHarness.t.sol index cdb90f7e94..8aae782459 100644 --- a/packages/contracts-core/test/harnesses/libs/memory/SnapshotHarness.t.sol +++ b/packages/contracts-core/test/harnesses/libs/memory/SnapshotHarness.t.sol @@ -39,7 +39,7 @@ contract SnapshotHarness { return payload.castToSnapshot().hashValid(); } - function state(bytes memory payload, uint256 stateIndex) public view returns (bytes memory) { + function state(bytes memory payload, uint8 stateIndex) public view returns (bytes memory) { return payload.castToSnapshot().state(stateIndex).unwrap().clone(); } diff --git a/packages/contracts-core/test/mocks/hubs/ExecutionHubMock.t.sol b/packages/contracts-core/test/mocks/hubs/ExecutionHubMock.t.sol index 77079aa3d5..d379d8104c 100644 --- a/packages/contracts-core/test/mocks/hubs/ExecutionHubMock.t.sol +++ b/packages/contracts-core/test/mocks/hubs/ExecutionHubMock.t.sol @@ -13,7 +13,7 @@ contract ExecutionHubMock is BaseMock, IExecutionHub { bytes memory msgPayload, bytes32[] calldata originProof, bytes32[] calldata snapProof, - uint256 stateIndex, + uint8 stateIndex, uint64 gasLimit ) external {} diff --git a/packages/contracts-core/test/mocks/hubs/SnapshotHubMock.t.sol b/packages/contracts-core/test/mocks/hubs/SnapshotHubMock.t.sol index 467072891a..78b69c4a14 100644 --- a/packages/contracts-core/test/mocks/hubs/SnapshotHubMock.t.sol +++ b/packages/contracts-core/test/mocks/hubs/SnapshotHubMock.t.sol @@ -45,5 +45,5 @@ contract SnapshotHubMock is BaseMock, ISnapshotHub { returns (bytes memory snapPayload, bytes memory snapSignature) {} - function getSnapshotProof(uint32 attNonce, uint256 stateIndex) external view returns (bytes32[] memory snapProof) {} + function getSnapshotProof(uint32 attNonce, uint8 stateIndex) external view returns (bytes32[] memory snapProof) {} } diff --git a/packages/contracts-core/test/suite/Destination.t.sol b/packages/contracts-core/test/suite/Destination.t.sol index e3bc89b722..cebaabf2f0 100644 --- a/packages/contracts-core/test/suite/Destination.t.sol +++ b/packages/contracts-core/test/suite/Destination.t.sol @@ -45,6 +45,12 @@ contract DestinationTest is ExecutionHubTest { assertFalse(rootPending); } + function test_constructor_revert_chainIdOverflow() public { + vm.chainId(2 ** 32); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + new Destination({synapseDomain_: 1, agentManager_: address(2), inbox_: address(3)}); + } + function test_cleanSetup(Random memory random) public override { uint32 domain = random.nextUint32(); vm.assume(domain != DOMAIN_SYNAPSE); @@ -166,6 +172,20 @@ contract DestinationTest is ExecutionHubTest { InterfaceDestination(localDestination()).acceptAttestation(0, 0, "", 0, new ChainGas[](0)); } + function test_acceptAttestation_revert_blockTimestampOverflow() public { + address notary = domains[DOMAIN_LOCAL].agent; + + Random memory random = Random("salt"); + RawSnapshot memory rawSnap = random.nextSnapshot(); + RawAttestation memory ra = random.nextAttestation({rawSnap: rawSnap, nonce: 1}); + uint256[] memory snapGas = rawSnap.snapGas(); + (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra); + + vm.warp(2 ** 40); + vm.expectRevert("SafeCast: value doesn't fit in 40 bits"); + lightInbox.submitAttestation(attPayload, attSig, ra._agentRoot, snapGas); + } + function test_acceptAttestation_revert_notaryInDispute(uint256 notaryId) public { address notary = domains[DOMAIN_LOCAL].agents[notaryId % DOMAIN_AGENTS]; openDispute({guard: domains[0].agent, notary: notary}); diff --git a/packages/contracts-core/test/suite/GasOracle.MinimumTips.t.sol b/packages/contracts-core/test/suite/GasOracle.MinimumTips.t.sol index 70c2c00310..5e33057e9e 100644 --- a/packages/contracts-core/test/suite/GasOracle.MinimumTips.t.sol +++ b/packages/contracts-core/test/suite/GasOracle.MinimumTips.t.sol @@ -8,7 +8,7 @@ import {Tips, TipsLib} from "../../contracts/libs/stack/Tips.sol"; import {TIPS_MULTIPLIER} from "../../contracts/libs/Constants.sol"; import {Random, GasOracle, GasOracleTest} from "./GasOracle.t.sol"; -import {RawGasData, RawGasData256} from "../utils/libs/SynapseStructs.t.sol"; +import {RawGasData, RawGasData256, RawRequest} from "../utils/libs/SynapseStructs.t.sol"; // solhint-disable func-name-mixedcase // solhint-disable no-empty-blocks @@ -75,7 +75,7 @@ contract GasOracleMinimumTipsTest is GasOracleTest { function test_getMinimumTips_summitTip(Random memory random) public { uint256 summitTip = 1 << 48; testedGO().setSummitTip(summitTip); - uint256 paddedRequest = random.nextUint192(); + uint256 paddedRequest = rawTestRequest(random).encodeRequest(); uint256 contentLength = random.nextUint16(); Tips tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); // Local chain Ether price is 0.5 ETH, so the summit tip is worth 2x. @@ -91,7 +91,7 @@ contract GasOracleMinimumTipsTest is GasOracleTest { } function test_getMinimumTips_attestationTip(Random memory random) public { - uint256 paddedRequest = random.nextUint192(); + uint256 paddedRequest = rawTestRequest(random).encodeRequest(); uint256 contentLength = random.nextUint16(); Tips tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 0.5 ETH. @@ -110,12 +110,8 @@ contract GasOracleMinimumTipsTest is GasOracleTest { } function test_getMinimumTips_executionTip(Random memory random) public { - Request request = RequestLib.encodeRequest({ - gasDrop_: random.nextUint96(), - gasLimit_: random.nextUint32(), - version_: random.nextUint32() - }); - uint256 paddedRequest = Request.unwrap(request); + RawRequest memory request = rawTestRequest(random); + uint256 paddedRequest = request.encodeRequest(); uint256 contentLength = random.nextUint16(); Tips tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 0.5 ETH. @@ -123,7 +119,7 @@ contract GasOracleMinimumTipsTest is GasOracleTest { // Execution tip is execBuffer + gasPrice * gasLimit + dataPrice * contentLength. assertEq( tips.executionTip() * TIPS_MULTIPLIER, - 4 * (EXEC_BUFFER + GAS_PRICE * request.gasLimit() + DATA_PRICE * contentLength), + 4 * (EXEC_BUFFER + GAS_PRICE * request.gasLimit + DATA_PRICE * contentLength), "!executionTip: 0.5ETH" ); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 2.0 ETH. @@ -132,7 +128,7 @@ contract GasOracleMinimumTipsTest is GasOracleTest { tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); assertEq( tips.executionTip() * TIPS_MULTIPLIER, - EXEC_BUFFER + GAS_PRICE * request.gasLimit() + DATA_PRICE * contentLength, + EXEC_BUFFER + GAS_PRICE * request.gasLimit + DATA_PRICE * contentLength, "!executionTip: 2.0ETH" ); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 4.0 ETH. @@ -141,18 +137,14 @@ contract GasOracleMinimumTipsTest is GasOracleTest { tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); assertEq( tips.executionTip() * TIPS_MULTIPLIER, - (EXEC_BUFFER + GAS_PRICE * request.gasLimit() + DATA_PRICE * contentLength) / 2, + (EXEC_BUFFER + GAS_PRICE * request.gasLimit + DATA_PRICE * contentLength) / 2, "!executionTip: 4.0ETH" ); } function test_getMinimumTips_deliveryTip(Random memory random) public { - Request request = RequestLib.encodeRequest({ - gasDrop_: random.nextUint96(), - gasLimit_: random.nextUint32(), - version_: random.nextUint32() - }); - uint256 paddedRequest = Request.unwrap(request); + RawRequest memory request = rawTestRequest(random); + uint256 paddedRequest = request.encodeRequest(); uint256 contentLength = random.nextUint16(); Tips tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 0.5 ETH. @@ -161,7 +153,7 @@ contract GasOracleMinimumTipsTest is GasOracleTest { // Delivery tip is execution tip * markup (50%), therefore the final value is worth 2x. assertEq( tips.deliveryTip() * TIPS_MULTIPLIER, - 2 * (EXEC_BUFFER + GAS_PRICE * request.gasLimit() + DATA_PRICE * contentLength), + 2 * (EXEC_BUFFER + GAS_PRICE * request.gasLimit + DATA_PRICE * contentLength), "!deliveryTip: 0.5ETH" ); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 2.0 ETH. @@ -171,7 +163,7 @@ contract GasOracleMinimumTipsTest is GasOracleTest { tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); assertEq( tips.deliveryTip() * TIPS_MULTIPLIER, - (EXEC_BUFFER + GAS_PRICE * request.gasLimit() + DATA_PRICE * contentLength) / 2, + (EXEC_BUFFER + GAS_PRICE * request.gasLimit + DATA_PRICE * contentLength) / 2, "!deliveryTip: 2.0ETH" ); // Remote chain Ether price is 2.0 ETH, local chain Ether price is 4.0 ETH. @@ -181,12 +173,18 @@ contract GasOracleMinimumTipsTest is GasOracleTest { tips = TipsLib.wrapPadded(testedGO().getMinimumTips(DOMAIN_REMOTE, paddedRequest, contentLength)); assertEq( tips.deliveryTip() * TIPS_MULTIPLIER, - (EXEC_BUFFER + GAS_PRICE * request.gasLimit() + DATA_PRICE * contentLength) / 4, + (EXEC_BUFFER + GAS_PRICE * request.gasLimit + DATA_PRICE * contentLength) / 4, "!deliveryTip: 4.0ETH" ); // TODO: adjust test when delivery tip will also include the value of gas airdrop. } + function rawTestRequest(Random memory random) internal pure returns (RawRequest memory rr) { + rr = random.nextRequest(); + // Set sensible max values for gasDrop and gasLimit. + rr.boundRequest({maxGasDrop: 10 ** 20, maxGasLimit: 10 ** 8}); + } + function setLocalEtherPrice(uint256 etherPrice) public { testedGO().setGasData({ domain: DOMAIN_LOCAL, diff --git a/packages/contracts-core/test/suite/GasOracle.t.sol b/packages/contracts-core/test/suite/GasOracle.t.sol index 24929df534..c5ad1ccfab 100644 --- a/packages/contracts-core/test/suite/GasOracle.t.sol +++ b/packages/contracts-core/test/suite/GasOracle.t.sol @@ -26,6 +26,12 @@ contract GasOracleTest is MessagingBaseTest { assertEq(cleanContract.destination(), destination_, "!destination"); } + function test_constructor_revert_chainIdOverflow() public { + vm.chainId(2 ** 32); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + new GasOracle({synapseDomain_: 1, destination_: address(2)}); + } + function initializeLocalContract() public override { testedGO().initialize(); } diff --git a/packages/contracts-core/test/suite/Origin.t.sol b/packages/contracts-core/test/suite/Origin.t.sol index 2c18c71265..0ff8797feb 100644 --- a/packages/contracts-core/test/suite/Origin.t.sol +++ b/packages/contracts-core/test/suite/Origin.t.sol @@ -63,6 +63,12 @@ contract OriginTest is AgentSecuredTest { 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(); @@ -85,6 +91,24 @@ contract OriginTest is AgentSecuredTest { 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); diff --git a/packages/contracts-core/test/suite/Summit.t.sol b/packages/contracts-core/test/suite/Summit.t.sol index 2f4e297e29..fe5270a23d 100644 --- a/packages/contracts-core/test/suite/Summit.t.sol +++ b/packages/contracts-core/test/suite/Summit.t.sol @@ -68,6 +68,12 @@ contract SummitTest is AgentSecuredTest { 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); @@ -101,6 +107,20 @@ contract SummitTest is AgentSecuredTest { 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 @@ -290,7 +310,7 @@ contract SummitTest is AgentSecuredTest { assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "!latestAttestation: gas hash"); // Check proofs for every State in the Notary snapshot - for (uint256 j = 0; j < STATES; ++j) { + 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(); diff --git a/packages/contracts-core/test/suite/hubs/ExecutionHub.t.sol b/packages/contracts-core/test/suite/hubs/ExecutionHub.t.sol index 30bf672862..c2152040aa 100644 --- a/packages/contracts-core/test/suite/hubs/ExecutionHub.t.sol +++ b/packages/contracts-core/test/suite/hubs/ExecutionHub.t.sol @@ -522,7 +522,7 @@ abstract contract ExecutionHubTest is AgentSecuredTest { function verify_messageStatus( bytes32 messageHash, bytes32 snapRoot, - uint256 stateIndex, + uint8 stateIndex, MessageStatus flag, address firstExecutor, address finalExecutor @@ -625,7 +625,7 @@ abstract contract ExecutionHubTest is AgentSecuredTest { rbm.tips = RawTips(1, 1, 1, 1); rh.nonce = 1; rh.optimisticPeriod = random.nextUint32(); - sm = SnapshotMock(random.nextState(), RawStateIndex(random.nextUint256(), random.nextUint256())); + sm = SnapshotMock(random.nextState(), RawStateIndex(random.nextUint8(), random.nextUint256())); sm.rsi.boundStateIndex(); } diff --git a/packages/contracts-core/test/suite/inbox/Inbox.t.sol b/packages/contracts-core/test/suite/inbox/Inbox.t.sol index fd4a9ee25d..ba10073daf 100644 --- a/packages/contracts-core/test/suite/inbox/Inbox.t.sol +++ b/packages/contracts-core/test/suite/inbox/Inbox.t.sol @@ -67,6 +67,12 @@ contract InboxTest is StatementInboxTest { new Inbox(DOMAIN_SYNAPSE); } + function test_constructor_revert_chainIdOverflow() public { + vm.chainId(2 ** 32); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + new Inbox({synapseDomain_: DOMAIN_SYNAPSE}); + } + function initializeLocalContract() public override { Inbox(localContract()).initialize(address(0), address(0), address(0), address(0)); } diff --git a/packages/contracts-core/test/suite/inbox/LightInbox.t.sol b/packages/contracts-core/test/suite/inbox/LightInbox.t.sol index 1ce66c4b88..8d0eb63d87 100644 --- a/packages/contracts-core/test/suite/inbox/LightInbox.t.sol +++ b/packages/contracts-core/test/suite/inbox/LightInbox.t.sol @@ -54,7 +54,13 @@ contract LightInboxTest is StatementInboxTest { function test_constructor_revert_onSynapseChain() public { vm.chainId(DOMAIN_SYNAPSE); vm.expectRevert(SynapseDomainForbidden.selector); - LightInbox inbox = new LightInbox(DOMAIN_SYNAPSE); + new LightInbox({synapseDomain_: DOMAIN_SYNAPSE}); + } + + function test_constructor_revert_chainIdOverflow() public { + vm.chainId(2 ** 32); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + new LightInbox({synapseDomain_: 1}); } function initializeLocalContract() public override { diff --git a/packages/contracts-core/test/suite/libs/ChainContext.t.sol b/packages/contracts-core/test/suite/libs/ChainContext.t.sol new file mode 100644 index 0000000000..4ab1f4d1f6 --- /dev/null +++ b/packages/contracts-core/test/suite/libs/ChainContext.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import {ChainContextHarness} from "../../harnesses/libs/ChainContextHarness.t.sol"; + +import {SynapseLibraryTest} from "../../utils/SynapseLibraryTest.t.sol"; + +// solhint-disable func-name-mixedcase +contract ChainContextLibraryTest is SynapseLibraryTest { + ChainContextHarness public libHarness; + + function setUp() public { + libHarness = new ChainContextHarness(); + } + + function test_blockNumber() public { + uint256 expectedBN = 1337; + vm.roll(expectedBN); + assertEq(libHarness.blockNumber(), expectedBN); + } + + function test_blockNumber_maxValue() public { + uint256 expectedBN = 2 ** 40 - 1; + vm.roll(expectedBN); + assertEq(libHarness.blockNumber(), expectedBN); + } + + function test_blockNumber_revert_blockNumberOverflow() public { + uint256 overflowBN = 2 ** 40; + vm.roll(overflowBN); + vm.expectRevert("SafeCast: value doesn't fit in 40 bits"); + libHarness.blockNumber(); + } + + function test_blockTimestamp() public { + uint256 expectedBT = 7331; + vm.warp(expectedBT); + assertEq(libHarness.blockTimestamp(), expectedBT); + } + + function test_blockTimestamp_maxValue() public { + uint256 expectedBT = 2 ** 40 - 1; + vm.warp(expectedBT); + assertEq(libHarness.blockTimestamp(), expectedBT); + } + + function test_blockTimestamp_revert_blockTimestampOverflow() public { + uint256 overflowBT = 2 ** 40; + vm.warp(overflowBT); + vm.expectRevert("SafeCast: value doesn't fit in 40 bits"); + libHarness.blockTimestamp(); + } + + function test_chainId() public { + uint256 expectedChainId = 420; + vm.chainId(expectedChainId); + assertEq(libHarness.chainId(), expectedChainId); + } + + function test_chainId_maxValue() public { + uint256 expectedChainId = 2 ** 32 - 1; + vm.chainId(expectedChainId); + assertEq(libHarness.chainId(), expectedChainId); + } + + function test_chainId_revert_chainIdOverflow() public { + uint256 overflowChainId = 2 ** 32; + vm.chainId(overflowChainId); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + libHarness.chainId(); + } +} diff --git a/packages/contracts-core/test/suite/libs/memory/Snapshot.t.sol b/packages/contracts-core/test/suite/libs/memory/Snapshot.t.sol index 55f8bf4d13..83648e6c1e 100644 --- a/packages/contracts-core/test/suite/libs/memory/Snapshot.t.sol +++ b/packages/contracts-core/test/suite/libs/memory/Snapshot.t.sol @@ -48,7 +48,7 @@ contract SnapshotLibraryTest is SynapseLibraryTest { // Test getters assertEq(libHarness.statesAmount(payload), statesAmount, "!statesAmount"); ChainGas[] memory snapGas = libHarness.snapGas(payload); - for (uint256 i = 0; i < statesAmount; ++i) { + for (uint8 i = 0; i < statesAmount; ++i) { assertEq(libHarness.state(payload, i), statePayloads[i], "!state"); assertEq(snapGas[i].domain(), states[i].origin, "!snapGas.domain"); assertEq(GasData.unwrap(snapGas[i].gasData()), states[i].gasData.encodeGasData(), "!snapGas.gasData"); diff --git a/packages/contracts-core/test/suite/manager/BondingManager.t.sol b/packages/contracts-core/test/suite/manager/BondingManager.t.sol index 4340ae0ee7..ec79e84fe8 100644 --- a/packages/contracts-core/test/suite/manager/BondingManager.t.sol +++ b/packages/contracts-core/test/suite/manager/BondingManager.t.sol @@ -60,6 +60,12 @@ contract BondingManagerTest is AgentManagerTest { new BondingManager(DOMAIN_SYNAPSE); } + function test_constructor_revert_chainIdOverflow() public { + vm.chainId(2 ** 32); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + new BondingManager(DOMAIN_SYNAPSE); + } + function test_setup() public override { super.test_setup(); assertEq(bondingManager.summit(), localSummit(), "!summit"); diff --git a/packages/contracts-core/test/suite/manager/LightManager.t.sol b/packages/contracts-core/test/suite/manager/LightManager.t.sol index 0a90b8cc36..f43f523b52 100644 --- a/packages/contracts-core/test/suite/manager/LightManager.t.sol +++ b/packages/contracts-core/test/suite/manager/LightManager.t.sol @@ -45,6 +45,12 @@ contract LightManagerTest is AgentManagerTest { LightManager(localContract()).initialize(address(0), address(0), address(0)); } + function test_constructor_revert_chainIdOverflow() public { + vm.chainId(2 ** 32); + vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); + new LightManager({synapseDomain_: 1}); + } + // ═══════════════════════════════════════════════ TESTS: SETUP ════════════════════════════════════════════════════ function test_constructor_revert_onSynapseChain() public { diff --git a/packages/contracts-core/test/utils/libs/FakeIt.t.sol b/packages/contracts-core/test/utils/libs/FakeIt.t.sol index 0ea210e0ad..b44ae11fa0 100644 --- a/packages/contracts-core/test/utils/libs/FakeIt.t.sol +++ b/packages/contracts-core/test/utils/libs/FakeIt.t.sol @@ -36,5 +36,5 @@ function fakeSnapshot(RawState memory state, RawStateIndex memory rsi) pure retu /// @notice Returns RawSnapshot struct with fake states. function fakeSnapshot(uint256 statesAmount) pure returns (RawSnapshot memory rawSnap) { RawState memory state; - return fakeSnapshot(state, RawStateIndex(statesAmount, statesAmount)); + return fakeSnapshot(state, RawStateIndex(uint8(statesAmount), statesAmount)); } diff --git a/packages/contracts-core/test/utils/libs/Random.t.sol b/packages/contracts-core/test/utils/libs/Random.t.sol index 13bff11a96..df7436f4bd 100644 --- a/packages/contracts-core/test/utils/libs/Random.t.sol +++ b/packages/contracts-core/test/utils/libs/Random.t.sol @@ -7,6 +7,7 @@ import { RawExecReceipt, RawGasData, RawGasData256, + RawRequest, RawState, RawStateIndex, RawSnapshot @@ -149,8 +150,14 @@ library RandomLib { return r.nextGasData256().compress(); } + function nextRequest(Random memory r) internal pure returns (RawRequest memory rr) { + rr.gasDrop = r.nextUint96(); + rr.gasLimit = r.nextUint64(); + rr.version = r.nextUint32(); + } + function nextStateIndex(Random memory r) internal pure returns (RawStateIndex memory rsi) { - rsi.stateIndex = r.nextUint256(); + rsi.stateIndex = r.nextUint8(); rsi.statesAmount = r.nextUint256(); rsi.boundStateIndex(); } diff --git a/packages/contracts-core/test/utils/libs/SynapseStructs.t.sol b/packages/contracts-core/test/utils/libs/SynapseStructs.t.sol index af2fc75270..f8c791f3ea 100644 --- a/packages/contracts-core/test/utils/libs/SynapseStructs.t.sol +++ b/packages/contracts-core/test/utils/libs/SynapseStructs.t.sol @@ -150,7 +150,7 @@ struct RawState { using CastLib for RawState global; struct RawStateIndex { - uint256 stateIndex; + uint8 stateIndex; uint256 statesAmount; } @@ -227,11 +227,19 @@ library CastLib { request = RequestLib.encodeRequest({gasDrop_: rr.gasDrop, gasLimit_: rr.gasLimit, version_: rr.version}); } + function boundRequest(RawRequest memory rr, uint96 maxGasDrop, uint64 maxGasLimit) internal pure { + require(maxGasDrop != 0, "maxGasDrop can't be 0"); + require(maxGasLimit != 0, "maxGasLimit can't be 0"); + rr.gasDrop = rr.gasDrop % maxGasDrop; + rr.gasLimit = rr.gasLimit % maxGasLimit; + } + function encodeTips(RawTips memory rt) internal pure returns (uint256 encodedTips) { encodedTips = Tips.unwrap(rt.castToTips()); } function boundTips(RawTips memory rt, uint64 maxTipValue) internal pure { + require(maxTipValue != 0, "maxTipValue can't be 0"); rt.summitTip = rt.summitTip % maxTipValue; rt.attestationTip = rt.attestationTip % maxTipValue; rt.executionTip = rt.executionTip % maxTipValue; @@ -436,7 +444,7 @@ library CastLib { // [1 .. SNAPSHOT_MAX_STATES] range rsi.statesAmount = 1 + rsi.statesAmount % SNAPSHOT_MAX_STATES; // [0 .. statesAmount) range - rsi.stateIndex = rsi.stateIndex % rsi.statesAmount; + rsi.stateIndex = uint8(rsi.stateIndex % rsi.statesAmount); } // ═════════════════════════════════════════════════ SNAPSHOT ══════════════════════════════════════════════════════