// File: AdminControl.sol



pragma solidity ^0.8.10;

abstract contract AdminControl {
    address public admin;

    event ChangeAdmin(address indexed _old, address indexed _new);
    event ApplyAdmin(address indexed _old, address indexed _new);

    function initAdminControl(address _admin) internal {
        require(_admin != address(0), "AdminControl: address(0)");
        admin = _admin;
        emit ChangeAdmin(address(0), _admin);
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "AdminControl: not admin");
        _;
    }

    function changeAdmin(address _admin) external onlyAdmin {
        require(_admin != address(0), "AdminControl: address(0)");
        admin = _admin;
        emit ChangeAdmin(admin, _admin);
    }
}

// File: AnyCallApp.sol



// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.10;

import "./AdminControl.sol";
import "./IAnycallProxy.sol";
import "./IAnycallExecutor.sol";
import "./IFeePool.sol";

abstract contract AnyCallApp is AdminControl {
    address public callProxy;

    // associated client app on each chain
    mapping(uint256 => address) public clientPeers; // key is chainId

    modifier onlyExecutor() {
        require(
            msg.sender == IAnycallProxy(callProxy).executor(),
            "AppBase: onlyExecutor"
        );
        _;
    }

    function initAnyCallApp(address _callProxy, address _admin) public {
        require(_callProxy != address(0));
        callProxy = _callProxy;
        initAdminControl(_admin);
    }

    receive() external payable {
        address _pool = IAnycallProxy(callProxy).config();
        IFeePool(_pool).deposit{value: msg.value}(address(this));
    }

    function withdraw(address _to, uint256 _amount) external onlyAdmin {
        (bool success, ) = _to.call{value: _amount}("");
        require(success);
    }

    function setCallProxy(address _callProxy) external onlyAdmin {
        require(_callProxy != address(0));
        callProxy = _callProxy;
    }

    function setClientPeers(
        uint256[] calldata _chainIds,
        address[] calldata _peers
    ) external onlyAdmin {
        require(_chainIds.length == _peers.length);
        for (uint256 i = 0; i < _chainIds.length; i++) {
            clientPeers[_chainIds[i]] = _peers[i];
        }
    }

    function depositFee() external payable {
        address _pool = IAnycallProxy(callProxy).config();
        IFeePool(_pool).deposit{value: msg.value}(address(this));
    }

    function withdrawFee(address _to, uint256 _amount) external onlyAdmin {
        address _pool = IAnycallProxy(callProxy).config();
        IFeePool(_pool).withdraw(_amount);

        (bool success, ) = _to.call{value: _amount}("");
        require(success);
    }

    function withdrawAllFee(address _pool, address _to) external onlyAdmin {
        uint256 _amount = IFeePool(_pool).executionBudget(address(this));
        IFeePool(_pool).withdraw(_amount);

        (bool success, ) = _to.call{value: _amount}("");
        require(success);
    }

    function executionBudget() external view returns (uint256) {
        address _pool = IAnycallProxy(callProxy).config();
        return IFeePool(_pool).executionBudget(address(this));
    }

    /// @dev Customized logic for processing incoming messages
    function _anyExecute(
        uint256 fromChainID,
        bytes memory data
    ) internal virtual returns (bool success, bytes memory result);

    /// @dev Customized logic for processing fallback messages
    function _anyFallback(
        uint256 fromChainID,
        bytes memory data
    ) internal virtual returns (bool success, bytes memory result);

    /// @dev Send anyCall
    function _anyCall(
        address _to,
        bytes memory _data,
        uint256 _toChainID,
        uint256 fee
    ) internal {
        // reserve 10 percent for fallback
        uint256 fee1 = fee / 10;
        uint256 fee2 = fee - fee1;
        address _pool = IAnycallProxy(callProxy).config();
        IFeePool(_pool).deposit{value: fee1}(address(this));
        IAnycallProxy(callProxy).anyCall{value: fee2}(
            _to,
            _data,
            _toChainID,
            4,
            ""
        );
    }

    function anyExecute(
        bytes memory data
    ) external onlyExecutor returns (bool success, bytes memory result) {
        (address from, uint256 fromChainID, ) = IAnycallExecutor(
            IAnycallProxy(callProxy).executor()
        ).context();
        require(clientPeers[fromChainID] == from, "AppBase: wrong context");
        return _anyExecute(fromChainID, data);
    }

    function anyFallback(
        bytes calldata data
    ) external onlyExecutor returns (bool success, bytes memory result) {
        (address from, uint256 fromChainID, ) = IAnycallExecutor(
            IAnycallProxy(callProxy).executor()
        ).context();
        require(clientPeers[fromChainID] == from, "AppBase: wrong context");
        return _anyFallback(fromChainID, data);
    }
}

// File: DFaxFee.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "./IFeeScheme.sol";

contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status = _NOT_ENTERED;

    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        _status = _NOT_ENTERED;
    }

    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

contract DFaxFee is ReentrancyGuard {
    address public dfaxFeeAdmin;

    address[] public feeOwners;
    uint256[] public feeOwnerAccrued;
    uint256[] public feeOwnerWeights;

    IFeeScheme public feeScheme;

    uint256 internal dFeeCharged;

    modifier onlyDFaxFeeAdmin() {
        require(msg.sender == dfaxFeeAdmin);
        _;
    }

    constructor() {}

    function initDFaxFee(
        address _dFaxFeeAdmin,
        address defaultFeeScheme
    ) internal {
        _setDFaxFeeAdmin(_dFaxFeeAdmin);
        _setFeeScheme(defaultFeeScheme);
    }

    function setDFaxFeeAdmin(address dfaxFeeAdmin) public onlyDFaxFeeAdmin {
        _setDFaxFeeAdmin(dfaxFeeAdmin);
    }

    function _setDFaxFeeAdmin(address _dfaxFeeAdmin) internal {
        dfaxFeeAdmin = _dfaxFeeAdmin;
    }

    function addFeeOwner(
        address feeOwner,
        uint256 weight
    ) public onlyDFaxFeeAdmin {
        for (uint i = 0; i < feeOwners.length; i++) {
            require(feeOwners[i] != feeOwner, "duplicated fee owner address");
        }
        feeOwners.push(feeOwner);
        feeOwnerWeights.push(weight);
    }

    function getFeeOwnerWeight(address feeOwner) public view returns (uint256) {
        for (uint i = 0; i < feeOwners.length; i++) {
            if (feeOwners[i] == feeOwner) {
                return feeOwnerWeights[i];
            }
        }
        return 0;
    }

    function getTotalWeight() public view returns (uint256) {
        uint256 totalWeight;
        for (uint i = 0; i < feeOwners.length; i++) {
            totalWeight += feeOwnerWeights[i];
        }
        return totalWeight;
    }

    function updateFeeOwner(
        address feeOwner,
        uint256 weight
    ) public onlyDFaxFeeAdmin {
        _updateFeeOwner(feeOwner, weight);
    }

    function _updateFeeOwner(
        address feeOwner,
        uint256 weight
    ) public onlyDFaxFeeAdmin {
        for (uint i = 0; i < feeOwners.length; i++) {
            if (feeOwners[i] == feeOwner) {
                feeOwnerWeights[i] = weight;
            }
        }
    }

    function removeFeeOwner(address feeOwner) public onlyDFaxFeeAdmin {
        _updateFeeOwner(feeOwner, 0);
    }

    function setFeeScheme(address feeScheme) public onlyDFaxFeeAdmin {
        _setFeeScheme(feeScheme);
    }

    function _setFeeScheme(address _feeScheme) internal {
        feeScheme = IFeeScheme(_feeScheme);
    }

    function chargeFee(
        address sender,
        uint256 toChainID,
        uint256 amount
    ) internal returns (uint256) {
        uint256 fee = calcFee(sender, toChainID, amount);
        (bool succ, ) = address(this).call{value: fee}("");
        require(succ, "charge fee failed");

        uint256 totalWeight;
        for (uint i = 0; i < feeOwners.length; i++) {
            totalWeight += feeOwnerWeights[i];
        }
        for (uint i = 0; i < feeOwners.length; i++) {
            feeOwnerAccrued[i] += (fee * feeOwnerWeights[i]) / totalWeight;
        }

        return fee;
    }

    function withdrawDFaxFee(address to, uint256 amount) public nonReentrant {
        uint index = 0;
        for (uint i = 0; i < feeOwners.length; i++) {
            if (feeOwners[i] == msg.sender) {
                index = i;
            }
        }
        require(feeOwnerAccrued[index] >= amount, "amount exceeds fee accrued");
        feeOwnerAccrued[index] -= amount;
        (bool succ, ) = to.call{value: amount}("");
        require(succ);
    }

    function calcFee(
        address sender,
        uint256 toChainID,
        uint256 amount
    ) public view returns (uint256) {
        if (address(feeScheme) == address(0)) {
            return 0;
        }
        return feeScheme.calcFee(sender, toChainID, amount);
    }
}

// File: ERC20Gateway.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "./AnyCallApp.sol";
import "./ISwapInSafetyControl.sol";
import "./DFaxFee.sol";

interface IDecimal {
    function decimals() external view returns (uint8);
}

interface IERC20Gateway {
    function token() external view returns (address);

    function Swapout(
        uint256 amount,
        address receiver,
        uint256 toChainID
    ) external payable returns (uint256 swapoutSeq);
}

abstract contract ERC20Gateway is
    IERC20Gateway,
    AnyCallApp,
    DFaxFee
{
    address public token;
    mapping(uint256 => uint8) public decimals;
    uint256 public swapoutSeq;

    address private _initiator;
    bool public initialized = false;

    ISwapInSafetyControl public safetyControl;

    constructor() DFaxFee() {
        _initiator = msg.sender;
    }

    function initERC20Gateway(
        address anyCallProxy,
        address token_,
        address admin,
        address _safetyControl,
        address dFaxFeeAdmin,
        address defaultFeeScheme
    ) public {
        require(_initiator == msg.sender && !initialized);
        initialized = true;
        token = token_;
        initAnyCallApp(anyCallProxy, admin);
        safetyControl = ISwapInSafetyControl(_safetyControl);
        initDFaxFee(dFaxFeeAdmin, defaultFeeScheme);
    }

    function _swapout(
        uint256 amount,
        address sender
    ) internal virtual returns (bool);

    function _swapin(
        uint256 amount,
        address receiver
    ) internal virtual returns (bool);

    event LogAnySwapOut(
        uint256 amount,
        address sender,
        address receiver,
        uint256 toChainID,
        uint256 swapoutSeq
    );

    function setDecimals(
        uint256[] memory chainIDs,
        uint8[] memory decimals_
    ) external onlyAdmin {
        for (uint256 i = 0; i < chainIDs.length; i++) {
            decimals[chainIDs[i]] = decimals_[i];
        }
    }

    function decimal(uint256 chainID) external view returns (uint8) {
        return (
            decimals[chainID] > 0
                ? decimals[chainID]
                : IDecimal(token).decimals()
        );
    }

    function convertDecimal(
        uint256 amount,
        uint8 d_0
    ) public view returns (uint256) {
        uint8 d_1 = IDecimal(token).decimals();
        if (d_0 > d_1) {
            for (uint8 i = 0; i < (d_0 - d_1); i++) {
                amount = amount / 10;
            }
        } else {
            for (uint8 i = 0; i < (d_1 - d_0); i++) {
                amount = amount * 10;
            }
        }
        return amount;
    }

    function setSatetyControl(address newSafetyControl) external onlyAdmin {
        safetyControl = ISwapInSafetyControl(newSafetyControl);
    }

    function Swapout(
        uint256 amount,
        address receiver,
        uint256 destChainID
    ) external payable returns (uint256) {
        require(_swapout(amount, msg.sender));
        swapoutSeq++;
        bytes memory data = abi.encode(
            amount,
            IDecimal(token).decimals(),
            receiver,
            swapoutSeq
        );
        uint256 dFeeCharged = chargeFee(msg.sender, destChainID, amount);
        uint256 anyCallFee = msg.value - dFeeCharged;
        _anyCall(clientPeers[destChainID], data, destChainID, anyCallFee);
        emit LogAnySwapOut(
            amount,
            msg.sender,
            receiver,
            destChainID,
            swapoutSeq
        );
        return swapoutSeq;
    }

    function _anyExecute(
        uint256 fromChainID,
        bytes memory data
    )
        internal
        override
        nonReentrant
        returns (bool success, bytes memory result)
    {
        (uint256 amount, uint8 _decimals, address receiver, ) = abi.decode(
            data,
            (uint256, uint8, address, uint256)
        );
        amount = convertDecimal(amount, _decimals);
        if (address(safetyControl) != address(0)) {
            require(
                safetyControl.checkSwapIn(fromChainID, amount, receiver),
                "swapin restricted"
            );
        }
        success = _swapin(amount, receiver);
        safetyControl.update(fromChainID, amount, receiver);
    }

    function _anyFallback(
        uint256 fromChainID,
        bytes memory data
    )
        internal
        override
        nonReentrant
        returns (bool success, bytes memory result)
    {
        (uint256 amount, , address originSender, , ) = abi.decode(
            data,
            (uint256, uint8, address, address, uint256)
        );
        success = (_swapin(amount, originSender));
    }
}

// File: ERC20Gateway_Pool.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

import "./ERC20Gateway.sol";

interface ITransfer {
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    function approve(address spender, uint256 amount) external returns (bool);
}

interface IWNative {
    function deposit() external payable;

    function withdraw(uint wad) external;
}

contract ERC20Gateway_Pool is ERC20Gateway {
    bool public isWNative;

    function description() external pure returns (string memory) {
        return "ERC20Gateway_Pool";
    }

    function initIsWNative(bool _isWNative) public {
        require(!initialized);
        isWNative = _isWNative;
    }

    function _swapout(
        uint256 amount,
        address sender
    ) internal override returns (bool) {
        if (isWNative && msg.value > 0) {
            if (msg.value < amount) {
                return false;
            }
            IWNative(token).deposit{value: msg.value}();
            return true;
        } else {
            return ITransfer(token).transferFrom(sender, address(this), amount);
        }
    }

    function _swapin(
        uint256 amount,
        address receiver
    ) internal override returns (bool) {
        ITransfer(token).approve(address(this), amount);
        if (isWNative) {
            try IWNative(token).withdraw(amount) {} catch {
                return false;
            }
            (bool succ, ) = receiver.call{value: amount}("");
            return succ;
        } else {
            return
                ITransfer(token).transferFrom(address(this), receiver, amount);
        }
    }

    mapping(address => uint256) public balanceOf;

    event Deposit(address, uint256);
    event Withdraw(address, uint256);

    function deposit(uint256 amount) external {
        bool succ = ITransfer(token).transferFrom(
            msg.sender,
            address(this),
            amount
        );
        require(succ);
        balanceOf[msg.sender] += amount;
        emit Deposit(msg.sender, amount);
    }

    function withdraw(uint256 amount) external {
        balanceOf[msg.sender] -= amount;
        bool succ = ITransfer(token).transferFrom(
            address(this),
            msg.sender,
            amount
        );
        require(succ);
        emit Withdraw(msg.sender, amount);
    }
}

// File: IAnycallExecutor.sol



pragma solidity ^0.8.10;

/// IAnycallExecutor interface of the anycall executor
interface IAnycallExecutor {
    function context()
        external
        view
        returns (
            address from,
            uint256 fromChainID,
            uint256 nonce
        );

    function execute(
        address _to,
        bytes calldata _data,
        address _from,
        uint256 _fromChainID,
        uint256 _nonce,
        uint256 _flags,
        bytes calldata _extdata
    ) external returns (bool success, bytes memory result);
}
// File: IAnycallProxy.sol



pragma solidity ^0.8.10;

/// IAnycallProxy interface of the anycall proxy
interface IAnycallProxy {
    function executor() external view returns (address);

    function config() external view returns (address);

    function anyCall(
        address _to,
        bytes calldata _data,
        uint256 _toChainID,
        uint256 _flags,
        bytes calldata _extdata
    ) external payable;
}

// File: IFeePool.sol



pragma solidity ^0.8.10;

interface IFeePool {
    function deposit(address _account) external payable;

    function withdraw(uint256 _amount) external;

    function executionBudget(address _account) external view returns (uint256);
}
// File: IFeeScheme.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

interface IFeeScheme {
    function calcFee(
        address sender,
        uint256 toChainID,
        uint256 amount
    ) external view virtual returns (uint256);
}

// File: ISwapInSafetyControl.sol



// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.0;

abstract contract ISwapInSafetyControl {
    address safetyAdmin;
    address gateway;

    function initSafetyControl(
        address _safetyAdmin,
        address _gateway
    ) internal {
        setSafetyAdmin(_safetyAdmin);
        gateway = _gateway;
    }

    function checkSwapIn(
        uint256 fromChainID,
        uint256 amount,
        address receiver
    ) public view virtual returns (bool);

    function update(
        uint256 fromChainID,
        uint256 amount,
        address receiver
    ) public {
        require(msg.sender == gateway);
        _update(fromChainID, amount, receiver);
    }

    function _update(
        uint256 fromChainID,
        uint256 amount,
        address receiver
    ) internal virtual;

    function setSafetyAdmin(address _admin) internal virtual {
        safetyAdmin = _admin;
    }

    function changeAdmin(address to) public {
        require(msg.sender == safetyAdmin);
        safetyAdmin = to;
    }
}