// File: contracts/core/Core.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "./managers/FallbackManager.sol";
import "./ICoreStorage.sol";

import "../helpers/Utils.sol";

/// @title Cyan Wallet Core - A Cyan wallet's core features.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
contract Core is ICoreStorage, IFallbackManager {
    struct Call {
        address to;
        uint256 value;
        bytes data;
    }

    constructor() ICoreStorage(msg.sender) {}

    /// @notice Initiates new wallet.
    /// @param owner Address of the wallet owner.
    function initiate(address owner) external {
        require(_owner == address(0x0), "Wallet already initialized.");
        require(owner != address(0x0), "Invalid owner address.");

        _owner = owner;
        emit SetOwner(owner);
    }

    /// @notice Main transaction handling method of the wallet.
    ///      Note: All the non-core transactions go through this method.
    /// @param to Destination contract address.
    /// @param value Native token value of the transaction.
    /// @param data Data payload of the transaction.
    /// @return Result of the transaction.
    function execute(
        address to,
        uint256 value,
        bytes calldata data
    ) public payable onlyDelegateCall onlyOwner returns (bytes memory) {
        require(address(this).balance >= value, "Not enough balance.");
        if (data.length == 0) {
            return Utils._execute(to, value, data);
        }

        bytes4 funcHash = Utils.parseFunctionSelector(data);
        address module = Core(_this).getModule(to, funcHash);
        require(module != address(0x0), "Not supported method.");

        (bool success, bytes memory result) = module.delegatecall(
            abi.encodeWithSignature("handleTransaction(address,uint256,bytes)", to, value, data)
        );
        if (!success) {
            assembly {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
        }
        return result;
    }

    function executeBatch(Call[] calldata data) external payable onlyDelegateCall onlyOwner {
        for (uint8 i = 0; i < data.length; ++i) {
            execute(data[i].to, data[i].value, data[i].data);
        }
    }

    /// @inheritdoc IModuleManager
    function executeModule(bytes calldata data) external override onlyDelegateCall onlyOperator returns (bytes memory) {
        bytes4 funcHash = Utils.parseFunctionSelector(data);
        address module = Core(_this).getInternalModule(funcHash);
        require(module != address(0x0), "Not supported method.");

        (bool success, bytes memory result) = module.delegatecall(data);
        if (!success) {
            assembly {
                let ptr := mload(0x40)
                returndatacopy(ptr, 0, returndatasize())
                revert(ptr, returndatasize())
            }
        }
        return result;
    }

    /// @inheritdoc IFallbackManager
    function setFallbackHandler(address handler) external override noDelegateCall onlyAdmin {
        require(handler != address(0x0), "Invalid handler address.");
        _setFallbackHandler(handler);
    }

    fallback() external payable onlyDelegateCall {
        address handler = Core(_this).getFallbackHandler();
        assembly {
            if iszero(handler) {
                return(0, 0)
            }

            calldatacopy(0, 0, calldatasize())
            let success := delegatecall(gas(), handler, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            if gt(success, 0) {
                return(0, returndatasize())
            }

            revert(0, returndatasize())
        }
    }
}

// File: contracts/helpers/Utils.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

library Utils {
    /// @notice Executes a transaction to the given address.
    /// @param to Target address.
    /// @param value Native token value to be sent to the address.
    /// @param data Data to be sent to the address.
    /// @return result Result of the transaciton.
    function _execute(
        address to,
        uint256 value,
        bytes memory data
    ) internal returns (bytes memory result) {
        assembly {
            let success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)

            mstore(result, returndatasize())
            returndatacopy(add(result, 0x20), 0, returndatasize())

            if eq(success, 0) {
                revert(add(result, 0x20), returndatasize())
            }
        }
    }

    /// @notice Recover signer address from signature.
    /// @param signedHash Arbitrary length data signed on the behalf of the wallet.
    /// @param signature Signature byte array associated with signedHash.
    /// @return Recovered signer address.
    function recoverSigner(bytes32 signedHash, bytes memory signature) internal pure returns (address) {
        uint8 v;
        bytes32 r;
        bytes32 s;
        // we jump 32 (0x20) as the first slot of bytes contains the length
        // we jump 65 (0x41) per signature
        // for v we load 32 bytes ending with v (the first 31 come from s) then apply a mask
        assembly {
            r := mload(add(signature, 0x20))
            s := mload(add(signature, 0x40))
            v := byte(0, mload(add(signature, 0x60)))
        }
        require(v == 27 || v == 28, "Bad v value in signature.");

        address recoveredAddress = ecrecover(signedHash, v, r, s);
        require(recoveredAddress != address(0), "ecrecover returned 0.");
        return recoveredAddress;
    }

    /// @notice Helper method to parse the function selector from data.
    /// @param data Any data to be parsed, mostly calldata of transaction.
    /// @return result Parsed function sighash.
    function parseFunctionSelector(bytes memory data) internal pure returns (bytes4 result) {
        require(data.length >= 4, "Invalid data.");
        assembly {
            result := mload(add(data, 0x20))
        }
    }

    /// @notice Parse uint256 from given data.
    /// @param data Any data to be parsed, mostly calldata of transaction.
    /// @param position Position in the data.
    /// @return result Uint256 parsed from given data.
    function getUint256At(bytes memory data, uint8 position) internal pure returns (uint256 result) {
        assembly {
            result := mload(add(data, add(position, 0x20)))
        }
    }
}

// File: contracts/core/ICoreStorage.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "./managers/DelegateCallManager.sol";
import "./managers/RoleManager.sol";
import "./managers/ModuleManager.sol";

/// @title Cyan Wallet Core Storage - A Cyan wallet's core storage features.
/// @dev This contract must be the very first parent of the Core contract and Module contracts.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
abstract contract ICoreStorage is DelegateCallManager, IRoleManager, IModuleManager {
    constructor(address admin) IRoleManager(admin) {
        require(admin != address(0x0), "Invalid admin address.");
    }

    /// @inheritdoc IModuleManager
    function setModule(
        address target,
        bytes4 funcHash,
        address module
    ) external override noDelegateCall onlyAdmin {
        emit SetModule(target, funcHash, _modules[target][funcHash], module);
        _modules[target][funcHash] = module;
    }

    /// @inheritdoc IModuleManager
    function setInternalModule(bytes4 funcHash, address module) external override noDelegateCall onlyAdmin {
        emit SetInternalModule(funcHash, _internalModules[funcHash], module);
        _internalModules[funcHash] = module;
    }

    /// @inheritdoc IRoleManager
    function getOwner() external view override onlyDelegateCall returns (address) {
        return _owner;
    }

    /// @inheritdoc IRoleManager
    function setAdmin(address admin) external override noDelegateCall onlyAdmin {
        require(admin != address(0x0), "Invalid admin address.");
        _admin = admin;
        emit SetAdmin(admin);
    }

    /// @inheritdoc IRoleManager
    function getAdmin() external view override noDelegateCall returns (address) {
        return _admin;
    }

    /// @inheritdoc IRoleManager
    function setOperator(address operator, bool isActive) external override noDelegateCall onlyAdmin {
        require(operator != address(0x0), "Invalid operator address.");
        _operators[operator] = isActive;
        emit SetOperator(operator, isActive);
    }

    /// @inheritdoc IRoleManager
    function _checkOnlyAdmin() internal view override {
        if (address(this) != _this) {
            require(ICoreStorage(_this).getAdmin() == msg.sender, "Caller is not an admin.");
        } else {
            require(_admin == msg.sender, "Caller is not an admin.");
        }
    }

    /// @inheritdoc IRoleManager
    function isOperator(address operator) external view override noDelegateCall returns (bool result) {
        return _operators[operator];
    }

    /// @inheritdoc IRoleManager
    function _checkOnlyOperator() internal view override {
        require(ICoreStorage(_this).isOperator(msg.sender), "Caller is not an operator.");
    }

    /// @inheritdoc IRoleManager
    function _checkOnlyOwner() internal view override {
        require(_owner == msg.sender, "Caller is not an owner.");
    }
}

// File: contracts/core/managers/FallbackManager.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

/// @title Cyan Wallet Fallback Manager - A Cyan wallet's fallback manager.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
abstract contract IFallbackManager {
    // keccak256("core.fallbackHandler.address")
    bytes32 internal constant FALLBACK_HANDLER_STORAGE_SLOT =
        0x7734d301adfb6b9d8ff43068373ec4ffef29a42d1456fb5e0ba2ebb9f4793edb;

    event ChangedFallbackHandler(address handler);

    /// @notice Sets the fallback handler.
    /// @param handler Address of the fallback handler.
    function _setFallbackHandler(address handler) internal {
        bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT;
        assembly {
            sstore(slot, handler)
        }
        emit ChangedFallbackHandler(handler);
    }

    /// @notice Sets the fallback handler.
    /// @param handler Address of the fallback handler.
    function setFallbackHandler(address handler) external virtual;

    /// @notice Returns the fallback handler.
    /// @return handler Address of the fallback handler.
    function getFallbackHandler() external view returns (address handler) {
        bytes32 slot = FALLBACK_HANDLER_STORAGE_SLOT;
        assembly {
            handler := sload(slot)
        }
    }

    /// @notice Returns an native token balance of the wallet.
    /// return native token balance of the wallet.
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }

    /// @notice Allows the wallet to receive native token.
    receive() external payable {}
}

// File: contracts/core/managers/DelegateCallManager.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

/// @title Manage the delegatecall to a contract
/// @notice Base contract that provides a modifier for managing delegatecall to methods in a child contract
abstract contract DelegateCallManager {
    /// @dev The address of this contract
    address payable internal immutable _this;

    constructor() {
        // Immutables are computed in the init code of the contract, and then inlined into the deployed bytecode.
        // In other words, this variable won't change when it's checked at runtime.
        _this = payable(address(this));
    }

    /// @dev Private method is used instead of inlining into modifier because modifiers are copied into each method,
    ///     and the use of immutable means the address bytes are copied in every place the modifier is used.
    function _checkNotDelegateCall() private view {
        require(address(this) == _this, "Only direct calls allowed.");
    }

    /// @dev Private method is used instead of inlining into modifier because modifiers are copied into each method,
    ///     and the use of immutable means the address bytes are copied in every place the modifier is used.
    function _checkOnlyDelegateCall() private view {
        require(address(this) != _this, "Cannot be called directly.");
    }

    /// @notice Prevents delegatecall into the modified method
    modifier noDelegateCall() {
        _checkNotDelegateCall();
        _;
    }

    /// @notice Prevents non delegatecall into the modified method
    modifier onlyDelegateCall() {
        _checkOnlyDelegateCall();
        _;
    }
}

// File: contracts/core/managers/ModuleManager.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

/// @title Cyan Wallet Module Manager Storage - A Cyan wallet's module manager's storage.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
abstract contract ModuleManagerStorage {
    /// @notice Storing allowed contract methods.
    ///     Note: Target Contract Address => Sighash of method => Module address
    mapping(address => mapping(bytes4 => address)) internal _modules;

    /// @notice Storing internally allowed module methods.
    ///     Note: Sighash of module method => Module address
    mapping(bytes4 => address) internal _internalModules;
}

/// @title Cyan Wallet Module Manager - A Cyan wallet's module manager's functionalities.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
abstract contract IModuleManager is ModuleManagerStorage {
    event SetModule(address target, bytes4 funcHash, address oldModule, address newModule);
    event SetInternalModule(bytes4 funcHash, address oldModule, address newModule);

    /// @notice Sets the handler module of the target's function.
    /// @param target Address of the target contract.
    /// @param funcHash Sighash of the target contract's method.
    /// @param module Address of the handler module.
    function setModule(
        address target,
        bytes4 funcHash,
        address module
    ) external virtual;

    /// @notice Returns a handling module of the target function.
    /// @param target Address of the target contract.
    /// @param funcHash Sighash of the target contract's method.
    /// @return module Handler module.
    function getModule(address target, bytes4 funcHash) external view returns (address) {
        return _modules[target][funcHash];
    }

    /// @notice Sets the internal handler module of the function.
    /// @param funcHash Sighash of the module method.
    /// @param module Address of the handler module.
    function setInternalModule(bytes4 funcHash, address module) external virtual;

    /// @notice Returns an internal handling module of the given function.
    /// @param funcHash Sighash of the module's method.
    /// @return module Handler module.
    function getInternalModule(bytes4 funcHash) external view returns (address) {
        return _internalModules[funcHash];
    }

    /// @notice Used to call module functions on the wallet.
    ///     Usually used to call locking function of the module on the wallet.
    /// @param data Data payload of the transaction.
    /// @return Result of the execution.
    function executeModule(bytes memory data) external virtual returns (bytes memory);
}

// File: contracts/core/managers/RoleManager.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

/// @title Cyan Wallet Role Manager - A Cyan wallet's role manager's storage.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
abstract contract RoleManagerStorage {
    address[3] internal _deprecatedOperators; // Deprecated
    address internal _admin;
    address internal _owner;
    mapping(address => bool) internal _operators;
}

/// @title Cyan Wallet Role Manager - A Cyan wallet's role manager's functionalities.
/// @author Bulgantamir Gankhuyag - <bulgaa@usecyan.com>
/// @author Naranbayar Uuganbayar - <naba@usecyan.com>
abstract contract IRoleManager is RoleManagerStorage {
    event SetOwner(address owner);
    event SetAdmin(address admin);
    event SetOperator(address operator, bool isActive);

    modifier onlyOperator() {
        _checkOnlyOperator();
        _;
    }

    modifier onlyAdmin() {
        _checkOnlyAdmin();
        _;
    }

    modifier onlyOwner() {
        _checkOnlyOwner();
        _;
    }

    constructor(address admin) {
        require(admin != address(0x0), "Invalid admin address.");
        _admin = admin;
    }

    /// @notice Returns current owner of the wallet.
    /// @return Address of the current owner.
    function getOwner() external view virtual returns (address);

    /// @notice Changes the current admin.
    /// @param admin New admin address.
    function setAdmin(address admin) external virtual;

    /// @notice Returns current admin of the core contract.
    /// @return Address of the current admin.
    function getAdmin() external view virtual returns (address);

    /// @notice Sets the operator status.
    /// @param operator Operator address.
    /// @param isActive Is active or not.
    function setOperator(address operator, bool isActive) external virtual;

    /// @notice Checks whether the given address is an operator.
    /// @param operator Address that will be checked.
    /// @return result Boolean result.
    function isOperator(address operator) external view virtual returns (bool result);

    /// @notice Checks whether the message sender is an operator.
    function _checkOnlyOperator() internal view virtual;

    /// @notice Checks whether the message sender is an admin.
    function _checkOnlyAdmin() internal view virtual;

    /// @notice Checks whether the message sender is an owner.
    function _checkOnlyOwner() internal view virtual;
}