contract UnionZap is Ownable, UnionBase {
    using SafeERC20 for IERC20;

    address public votiumDistributor =
        0x378Ba9B73309bE80BF4C2c027aAD799766a7ED5A;
    address public unionDistributor;

    address private constant SUSHI_ROUTER =
        0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F;
    address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address private constant CVXCRV_DEPOSIT =
        0x8014595F2AB54cD7c604B00E9fb932176fDc86Ae;
    address public constant VOTIUM_REGISTRY =
        0x92e6E43f99809dF84ed2D533e1FD8017eb966ee2;

    uint256 private constant BASE_TX_GAS = 21000;
    uint256 private constant FINAL_TRANSFER_GAS = 50000;

    uint256 public unionDues = 200;
    uint256 public constant FEE_DENOMINATOR = 10000;
    uint256 public constant MAX_DUES = 400;

    ISushiRouter router = ISushiRouter(SUSHI_ROUTER);

    struct claimParam {
        address token;
        uint256 index;
        uint256 amount;
        bytes32[] merkleProof;
    }

    event Received(address sender, uint256 amount);
    event Distributed(uint256 amount, uint256 fees, bool locked);
    event DistributorUpdated(address distributor);
    event VotiumDistributorUpdated(address distributor);
    event DuesUpdated(uint256 dues);
    event FundsRetrieved(address token, uint256 amount);

    constructor(address distributor_) {
        unionDistributor = distributor_;
    }

    /// @notice Update union fees
    /// @param dues - Fees taken from the collected bribes in bips
    function setUnionDues(uint256 dues) external onlyOwner {
        require(dues <= MAX_DUES, "Dues too high");
        unionDues = dues;
        emit DuesUpdated(dues);
    }

    /// @notice Update the contract used to distribute funds
    /// @param distributor_ - Address of the new contract
    function updateDistributor(address distributor_) external onlyOwner {
        require(distributor_ != address(0));
        unionDistributor = distributor_;
        emit DistributorUpdated(distributor_);
    }

    /// @notice Update the votium contract address to claim for
    /// @param distributor_ - Address of the new contract
    function updateVotiumDistributor(address distributor_) external onlyOwner {
        require(distributor_ != address(0));
        votiumDistributor = distributor_;
        emit VotiumDistributorUpdated(distributor_);
    }

    /// @notice Withdraws specified ERC20 tokens to the multisig
    /// @param tokens - the tokens to retrieve
    /// @dev This is needed to handle tokens that don't have ETH pairs on sushi
    /// or need to be swapped on other chains (NBST, WormholeLUNA...)
    function retrieveTokens(address[] calldata tokens) external onlyOwner {
        for (uint256 i; i < tokens.length; ++i) {
            address token = tokens[i];
            uint256 tokenBalance = IERC20(token).balanceOf(address(this));
            IERC20(token).safeTransfer(msg.sender, tokenBalance);
            emit FundsRetrieved(token, tokenBalance);
        }
    }

    /// @notice Execute calls on behalf of contract in case of emergency
    function execute(
        address _to,
        uint256 _value,
        bytes calldata _data
    ) external onlyOwner returns (bool, bytes memory) {
        (bool success, bytes memory result) = _to.call{value: _value}(_data);
        return (success, result);
    }

    /// @notice Change forwarding address in Votium registry
    /// @param _to - address that will be forwarded to
    /// @dev To be used in case of migration, rewards can be forwarded to
    /// new contracts
    function setForwarding(address _to) external onlyOwner {
        IVotiumRegistry(VOTIUM_REGISTRY).setRegistry(_to);
    }

    /// @notice Set approvals for the tokens used when swapping
    function setApprovals() external onlyOwner {
        IERC20(CRV_TOKEN).safeApprove(CURVE_CVXCRV_CRV_POOL, 0);
        IERC20(CRV_TOKEN).safeApprove(CURVE_CVXCRV_CRV_POOL, 2**256 - 1);

        IERC20(CRV_TOKEN).safeApprove(CVXCRV_DEPOSIT, 0);
        IERC20(CRV_TOKEN).safeApprove(CVXCRV_DEPOSIT, 2**256 - 1);

        IERC20(CVXCRV_TOKEN).safeApprove(CVXCRV_STAKING_CONTRACT, 0);
        IERC20(CVXCRV_TOKEN).safeApprove(CVXCRV_STAKING_CONTRACT, 2**256 - 1);
    }

    /// @notice Swap a token for ETH
    /// @param token - address of the token to swap
    /// @param amount - amount of the token to swap
    /// @dev Swaps are executed via Sushi router, will revert if pair
    /// does not exist. Tokens must have a WETH pair.
    function _swapToETH(address token, uint256 amount) internal onlyOwner {
        address[] memory path = new address[](2);
        path[0] = token;
        path[1] = WETH;

        IERC20(token).safeApprove(SUSHI_ROUTER, 0);
        IERC20(token).safeApprove(SUSHI_ROUTER, amount);

        router.swapExactTokensForETH(
            amount,
            0,
            path,
            address(this),
            block.timestamp + 60
        );
    }

    /// @notice Claims all specified rewards from Votium
    /// @param claimParams - an array containing the info necessary to claim for
    /// each available token
    /// @dev Used to retrieve tokens that need to be transferred
    function claim(IMultiMerkleStash.claimParam[] calldata claimParams)
        public
        onlyOwner
    {
        require(claimParams.length > 0, "No claims");
        // claim all from votium
        IMultiMerkleStash(votiumDistributor).claimMulti(
            address(this),
            claimParams
        );
    }

    /// @notice Claims all specified rewards and swaps them to ETH
    /// @param claimParams - an array containing the info necessary to claim for
    /// each available token
    function claimAndSwap(IMultiMerkleStash.claimParam[] calldata claimParams)
        external
        onlyOwner
    {
        // initialize gas counting
        uint256 _startGas = gasleft();
        bool _locked = false;
        claim(claimParams);
        // swap all claims to ETH
        for (uint256 i; i < claimParams.length; ++i) {
            address _token = claimParams[i].token;
            uint256 _balance = IERC20(_token).balanceOf(address(this));
            // unwrap WETH
            if (_token == WETH) {
                IWETH(WETH).withdraw(_balance);
            }
            // no need to swap bribes paid out in cvxCRV or CRV
            else if ((_token == CRV_TOKEN) || (_token == CVXCRV_TOKEN)) {
                continue;
            } else {
                _swapToETH(_token, _balance);
            }
        }
        uint256 _ethBalance = address(this).balance;

        // swap from ETH to CRV
        uint256 _swappedCrv = _swapEthToCrv(_ethBalance);

        uint256 _crvBalance = IERC20(CRV_TOKEN).balanceOf(address(this));

        uint256 _quote = crvCvxCrvSwap.get_dy(
            CVXCRV_CRV_INDEX,
            CVXCRV_CVXCRV_INDEX,
            _crvBalance
        );
        // swap on Curve if there is a premium for doing so
        if (_quote > _crvBalance) {
            _swapCrvToCvxCrv(_crvBalance, address(this));
        }
        // otherwise deposit & lock
        else {
            ICvxCrvDeposit(CVXCRV_DEPOSIT).deposit(_crvBalance, true);
            _locked = true;
        }

        uint256 _cvxCrvBalance = IERC20(CVXCRV_TOKEN).balanceOf(address(this));
        // estimate gas cost
        uint256 _gasUsed = _startGas -
            gasleft() +
            BASE_TX_GAS +
            16 *
            msg.data.length +
            FINAL_TRANSFER_GAS;
        // compute the ETH/CRV exchange rate based on previous curve swap
        uint256 _gasCostInCrv = (_gasUsed * tx.gasprice * _swappedCrv) /
            _ethBalance;
        uint256 _fees = (_cvxCrvBalance * unionDues) / FEE_DENOMINATOR;
        uint256 _netDeposit = _cvxCrvBalance - _fees - _gasCostInCrv;
        // freeze distributor and transfer funds
        IMerkleDistributor(unionDistributor).freeze();
        IERC20(CVXCRV_TOKEN).safeTransfer(unionDistributor, _netDeposit);
        require(
            cvxCrvStaking.stakeFor(
                owner(),
                IERC20(CVXCRV_TOKEN).balanceOf(address(this))
            ),
            "Staking failed"
        );
        emit Distributed(_netDeposit, _cvxCrvBalance - _netDeposit, _locked);
    }

    receive() external payable {
        emit Received(msg.sender, msg.value);
    }
}
function setForwarding(address _to) external onlyOwner {
    IVotiumRegistry(VOTIUM_REGISTRY).setRegistry(_to);
}

/// @notice Change forwarding address in Votium registry
/// @param _to - address that will be forwarded to
/// @dev To be used in case of migration, rewards can be forwarded to
/// new contracts