synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/client/PingPongClient.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {TypeCasts} from "../libs/TypeCasts.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {MessageRecipient} from "./MessageRecipient.sol";

contract PingPongClient is MessageRecipient {
    using TypeCasts for address;

    struct PingPongMessage {
        uint256 pingId;
        bool isPing;
        uint16 counter;
    }

    // ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════

    uint256 public random;

    /// @notice Amount of "Ping" messages sent.
    uint256 public pingsSent;

    /// @notice Amount of "Ping" messages received.
    /// Every received Ping message leads to sending a Pong message back to initial sender.
    uint256 public pingsReceived;

    /// @notice Amount of "Pong" messages received.
    /// When all messages are delivered, should be equal to `pingsSent`
    uint256 public pongsReceived;

    // ══════════════════════════════════════════════════ EVENTS ═══════════════════════════════════════════════════════

    /// @notice Emitted when a Ping message is sent.
    /// Triggered externally, or by receveing a Pong message with instructions to do more pings.
    event PingSent(uint256 pingId);

    /// @notice Emitted when a Ping message is received.
    /// Will always send a Pong message back.
    event PingReceived(uint256 pingId);

    /// @notice Emitted when a Pong message is sent.
    /// Triggered whenever a Ping message is received.
    event PongSent(uint256 pingId);

    /// @notice Emitted when a Pong message is received.
    /// Will initiate a new Ping, if the counter in the message is non-zero.
    event PongReceived(uint256 pingId);

    // ════════════════════════════════════════════════ CONSTRUCTOR ════════════════════════════════════════════════════

    constructor(address origin_, address destination_) MessageRecipient(origin_, destination_) {
        // Initiate "random" value
        random = uint256(keccak256(abi.encode(block.number)));
    }

    // ═══════════════════════════════════════════════ MESSAGE LOGIC ═══════════════════════════════════════════════════

    function doPings(uint16 pingCount, uint32 destination_, address recipient, uint16 counter) external {
        for (uint256 i = 0; i < pingCount; ++i) {
            _ping(destination_, recipient.addressToBytes32(), counter);
        }
    }

    /// @notice Send a Ping message to destination chain.
    /// Upon receiving a Ping, a Pong message will be sent back.
    /// If `counter > 0`, this process will be repeated when the Pong message is received.
    /// @param destination_ Chain to send Ping message to
    /// @param recipient    Recipient of Ping message
    /// @param counter      Additional amount of Ping-Pong rounds to conclude
    function doPing(uint32 destination_, address recipient, uint16 counter) external {
        _ping(destination_, recipient.addressToBytes32(), counter);
    }

    // ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════

    function nextOptimisticPeriod() public view returns (uint32 period) {
        // Use random optimistic period up to one minute
        return uint32(random % 1 minutes);
    }

    // ═════════════════════════════════════ INTERNAL LOGIC: RECEIVE MESSAGES ══════════════════════════════════════════

    /// @inheritdoc MessageRecipient
    function _receiveBaseMessageUnsafe(uint32 origin_, uint32, bytes32 sender, uint256, uint32, bytes memory content)
        internal
        override
    {
        PingPongMessage memory message = abi.decode(content, (PingPongMessage));
        if (message.isPing) {
            // Ping is received
            ++pingsReceived;
            emit PingReceived(message.pingId);
            // Send Pong back
            _pong(origin_, sender, message);
        } else {
            // Pong is received
            ++pongsReceived;
            emit PongReceived(message.pingId);
            // Send extra ping, if initially requested
            if (message.counter != 0) {
                _ping(origin_, sender, message.counter - 1);
            }
        }
    }

    // ═══════════════════════════════════════ INTERNAL LOGIC: SEND MESSAGES ═══════════════════════════════════════════

    /// @dev Returns a random optimistic period value from 0 to 59 seconds.
    function _optimisticPeriod() internal returns (uint32 period) {
        // Use random optimistic period up to one minute
        period = nextOptimisticPeriod();
        // Adjust "random" value
        random = uint256(keccak256(abi.encode(random)));
    }

    /**
     * @dev Send a "Ping" or "Pong" message.
     * @param destination_  Domain of destination chain
     * @param recipient     Message recipient on destination chain
     * @param message   Ping-pong message
     */
    function _sendMessage(uint32 destination_, bytes32 recipient, PingPongMessage memory message) internal {
        // TODO: this probably shouldn't be hardcoded
        MessageRequest memory request = MessageRequest({gasDrop: 0, gasLimit: 500_000, version: 0});
        bytes memory content = abi.encode(message);
        _sendBaseMessage({
            destination_: destination_,
            recipient: recipient,
            optimisticPeriod: _optimisticPeriod(),
            tipsValue: 0,
            request: request,
            content: content
        });
    }

    /// @dev Initiate a new Ping-Pong round.
    function _ping(uint32 destination_, bytes32 recipient, uint16 counter) internal {
        uint256 pingId = pingsSent++;
        _sendMessage(destination_, recipient, PingPongMessage({pingId: pingId, isPing: true, counter: counter}));
        emit PingSent(pingId);
    }

    /// @dev Send a Pong message back.
    function _pong(uint32 destination_, bytes32 recipient, PingPongMessage memory message) internal {
        _sendMessage(
            destination_, recipient, PingPongMessage({pingId: message.pingId, isPing: false, counter: message.counter})
        );
        emit PongSent(message.pingId);
    }
}