Skip to main content
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.29;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { IMessageRecipient } from "./interfaces/IMessageRecipient.sol";
import { IOracleTrigger } from "./interfaces/oracle/IOracleTrigger.sol";
import { TypeCasts } from "./libs/TypeCasts.sol";

import { IInterchainSecurityModule, ISpecifiesInterchainSecurityModule } from "./interfaces/IInterchainSecurityModule.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

using TypeCasts for address;


contract OracleRequestRecipient is
    Ownable,
    IMessageRecipient,
    ISpecifiesInterchainSecurityModule,
    ReentrancyGuard
{

    IInterchainSecurityModule public interchainSecurityModule;

    mapping(uint32 => mapping(bytes32 => bool)) public whitelistedSenders;

    address private oracleTriggerAddress;

    event ReceivedCall(address indexed caller, string key);

    event WhitelistUpdated(
        uint32 indexed origin,
        bytes32 indexed sender,
        bool status
    );

    event OracleTriggerUpdated(
        address indexed oldAddress,
        address indexed newAddress
    );

    event InterchainSecurityModuleUpdated(
        address indexed previousISM,
        address indexed newISM
    );


    event TokensRecovered(address indexed recipient, uint256 amount);

    error EmptyOracleRequestData();
    error OracleTriggerNotSet();
    error SenderNotWhitelisted(bytes32 sender, uint32 origin);
    error UnauthorizedCaller(address caller);
    error InvalidSenderAddress();
    error AlreadyWhitelisted(bytes32 sender, uint32 origin);
    error InvalidISMAddress();
    error InvalidOracleTriggerAddress();
    error InvalidReceiver();
    error NoBalanceToWithdraw();
    error TransferFailed();

    function handle(
        uint32 _origin,
        bytes32 _sender,
        bytes calldata _data
    ) external payable virtual override nonReentrant {
        if (_data.length == 0) revert EmptyOracleRequestData();
        if (oracleTriggerAddress == address(0)) revert OracleTriggerNotSet();

        if (!whitelistedSenders[_origin][_sender])
            revert SenderNotWhitelisted(_sender, _origin);

        address sender = address(uint160(uint256(_sender)));

        if (msg.sender != IOracleTrigger(oracleTriggerAddress).getMailBox()) {
            revert UnauthorizedCaller(msg.sender);
        }

        string memory key = abi.decode(_data, (string));

        emit ReceivedCall(sender, key);

        IOracleTrigger(oracleTriggerAddress).dispatch{ value: msg.value }(
            _origin,
            sender,
            key
        );
    }

    function addToWhitelist(
        uint32 _origin,
        bytes32 _sender
    ) external onlyOwner {
        if (_sender == bytes32(0)) {
            revert InvalidSenderAddress();
        }

        if (whitelistedSenders[_origin][_sender]) {
            revert AlreadyWhitelisted(_sender, _origin);
        }

        whitelistedSenders[_origin][_sender] = true;
        emit WhitelistUpdated(_origin, _sender, true);
    }

    function removeFromWhitelist(
        uint32 _origin,
        bytes32 _sender
    ) external onlyOwner {
        if (_sender == bytes32(0)) {
            revert InvalidSenderAddress();
        }
        whitelistedSenders[_origin][_sender] = false;
        emit WhitelistUpdated(_origin, _sender, false);
    }

    function setInterchainSecurityModule(address _ism) external onlyOwner {
        if (_ism == address(0)) {
            revert InvalidISMAddress();
        }
        emit InterchainSecurityModuleUpdated(
            address(interchainSecurityModule),
            _ism
        );

        interchainSecurityModule = IInterchainSecurityModule(_ism);
    }

    function setOracleTriggerAddress(
        address _oracleTrigger
    ) external onlyOwner {
        if (_oracleTrigger == address(0)) {
            revert InvalidOracleTriggerAddress();
        }
        emit OracleTriggerUpdated(oracleTriggerAddress, _oracleTrigger);

        oracleTriggerAddress = _oracleTrigger;
    }

    receive() external payable {}

    function retrieveLostTokens(address receiver) external onlyOwner {
        if (receiver == address(0)) {
            revert InvalidReceiver();
        }
        uint256 balance = address(this).balance;
        if (balance == 0) {
            revert NoBalanceToWithdraw();
        }
        (bool success, ) = payable(receiver).call{ value: balance }("");
        if (!success) {
            revert TransferFailed();
        }
        emit TokensRecovered(receiver, balance);
    }

    function getOracleTriggerAddress() external view returns (address) {
        return oracleTriggerAddress;
    }
}
I