# @version 0.3.7
# (c) Curve.Fi, 2023

"""
@title Curve Tricrypto Factory
@license MIT
@author Curve.Fi
@notice Permissionless 3-coin cryptoswap pool deployer and registry
"""

interface TricryptoPool:
    def balances(i: uint256) -> uint256: view

interface ERC20:
    def decimals() -> uint256: view

interface LiquidityGauge:
    def initialize(_lp_token: address): nonpayable


event TricryptoPoolDeployed:
    pool: address
    coins: address[N_COINS]
    A: uint256
    gamma: uint256
    mid_fee: uint256
    out_fee: uint256
    allowed_extra_profit: uint256
    fee_gamma: uint256
    adjustment_step: uint256
    ma_exp_time: uint256
    initial_prices: uint256[N_COINS-1]
    deployer: address

event LiquidityGaugeDeployed:
    pool: address
    gauge: address

event UpdateFeeReceiver:
    _old_fee_receiver: address
    _new_fee_receiver: address

event UpdatePoolImplementation:
    _implemention_id: uint256
    _old_pool_implementation: address
    _new_pool_implementation: address

event UpdateGaugeImplementation:
    _old_gauge_implementation: address
    _new_gauge_implementation: address

event UpdateMathImplementation:
    _old_math_implementation: address
    _new_math_implementation: address

event UpdateViewsImplementation:
    _old_views_implementation: address
    _new_views_implementation: address

event TransferOwnership:
    _old_owner: address
    _new_owner: address


struct PoolArray:
    liquidity_gauge: address
    coins: address[N_COINS]
    decimals: uint256[N_COINS]


N_COINS: constant(uint256) = 3
A_MULTIPLIER: constant(uint256) = 10000

# Limits
MAX_FEE: constant(uint256) = 10 * 10 ** 9

MIN_GAMMA: constant(uint256) = 10 ** 10
MAX_GAMMA: constant(uint256) = 5 * 10**16

MIN_A: constant(uint256) = N_COINS ** N_COINS * A_MULTIPLIER / 100
MAX_A: constant(uint256) = 1000 * A_MULTIPLIER * N_COINS**N_COINS

PRICE_SIZE: constant(int128) = 256 / (N_COINS - 1)
PRICE_MASK: constant(uint256) = 2**PRICE_SIZE - 1

WETH: public(immutable(address))

admin: public(address)
future_admin: public(address)

# fee receiver for all pools:
fee_receiver: public(address)

pool_implementations: public(HashMap[uint256, address])
gauge_implementation: public(address)
views_implementation: public(address)
math_implementation: public(address)

# mapping of coins -> pools for trading
# a mapping key is generated for each pair of addresses via
# `bitwise_xor(convert(a, uint256), convert(b, uint256))`
markets: HashMap[uint256, address[4294967296]]
market_counts: HashMap[uint256, uint256]

pool_count: public(uint256)              # actual length of pool_list
pool_data: HashMap[address, PoolArray]
pool_list: public(address[4294967296])   # master list of pools


@external
def __init__(_fee_receiver: address, _admin: address, _weth: address):

    WETH = _weth

    self.fee_receiver = _fee_receiver
    self.admin = _admin

    log UpdateFeeReceiver(empty(address), _fee_receiver)
    log TransferOwnership(empty(address), _admin)


@internal
@view
def _pack(x: uint256[3]) -> uint256:
    """
    @notice Packs 3 integers with values <= 10**18 into a uint256
    @param x The uint256[3] to pack
    @return The packed uint256
    """
    return shift(x[0], 128) | shift(x[1], 64) | x[2]



# <--- Pool Deployers --->

@external
def deploy_pool(
    _name: String[32],
    _symbol: String[30],
    _coins: address[N_COINS],
    implementation_id: uint256,
    A: uint256,
    gamma: uint256,
    mid_fee: uint256,
    out_fee: uint256,
    fee_gamma: uint256,
    allowed_extra_profit: uint256,
    adjustment_step: uint256,
    ma_exp_time: uint256,
    initial_prices: uint256[N_COINS-1],
) -> address:
    """
    @notice Deploy a new pool
    @param _name Name of the new plain pool
    @param _symbol Symbol for the new plain pool - will be concatenated with factory symbol

    @return Address of the deployed pool
    """
    pool_implementation: address = self.pool_implementations[implementation_id]
    assert pool_implementation != empty(address), "Pool implementation not set"

    # Validate parameters
    assert A > MIN_A-1
    assert A < MAX_A+1

    assert gamma > MIN_GAMMA-1
    assert gamma < MAX_GAMMA+1

    assert mid_fee < MAX_FEE-1  # mid_fee can be zero
    assert out_fee >= mid_fee
    assert out_fee < MAX_FEE-1
    assert fee_gamma < 10**18+1
    assert fee_gamma > 0

    assert allowed_extra_profit < 10**18+1

    assert adjustment_step < 10**18+1
    assert adjustment_step > 0

    assert ma_exp_time < 872542  # 7 * 24 * 60 * 60 / ln(2)
    assert ma_exp_time > 86  # 60 / ln(2)

    assert min(initial_prices[0], initial_prices[1]) > 10**6
    assert max(initial_prices[0], initial_prices[1]) < 10**30

    assert _coins[0] != _coins[1] and _coins[1] != _coins[2] and _coins[0] != _coins[2], "Duplicate coins"

    name: String[64] = concat("Curve.fi Factory 3crypto Pool: ", _name)
    symbol: String[32] = concat(_symbol, "-f")

    decimals: uint256[N_COINS] = empty(uint256[N_COINS])
    precisions: uint256[N_COINS] = empty(uint256[N_COINS])
    for i in range(N_COINS):
        d: uint256 = ERC20(_coins[i]).decimals()
        assert d < 19, "Max 18 decimals for coins"
        decimals[i] = d
        precisions[i] = 10** (18 - d)

    # pack precisions
    packed_precisions: uint256 = self._pack(precisions)

    # pack fees
    packed_fee_params: uint256 = self._pack(
        [mid_fee, out_fee, fee_gamma]
    )

    # pack liquidity rebalancing params
    packed_rebalancing_params: uint256 = self._pack(
        [allowed_extra_profit, adjustment_step, ma_exp_time]
    )

    # pack A_gamma
    packed_A_gamma: uint256 = shift(A, 128)
    packed_A_gamma = packed_A_gamma | gamma

    # pack initial prices
    packed_prices: uint256 = 0
    for k in range(N_COINS - 1):
        packed_prices = shift(packed_prices, PRICE_SIZE)
        p: uint256 = initial_prices[N_COINS - 2 - k]
        assert p < PRICE_MASK
        packed_prices = p | packed_prices

    # pool is an ERC20 implementation
    pool: address = create_from_blueprint(
        pool_implementation,
        _name,
        _symbol,
        _coins,
        self.math_implementation,
        WETH,
        packed_precisions,
        packed_A_gamma,
        packed_fee_params,
        packed_rebalancing_params,
        packed_prices,
        code_offset=3
    )

    # populate pool data
    length: uint256 = self.pool_count
    self.pool_list[length] = pool
    self.pool_count = length + 1
    self.pool_data[pool].decimals = decimals
    self.pool_data[pool].coins = _coins

    # add coins to market:
    self._add_coins_to_market(_coins[0], _coins[1], pool)
    self._add_coins_to_market(_coins[0], _coins[2], pool)
    self._add_coins_to_market(_coins[1], _coins[2], pool)

    log TricryptoPoolDeployed(
        pool,
        _coins,
        A,
        gamma,
        mid_fee,
        out_fee,
        allowed_extra_profit,
        fee_gamma,
        adjustment_step,
        ma_exp_time,
        initial_prices,
        msg.sender,
    )

    return pool


@internal
def _add_coins_to_market(coin_a: address, coin_b: address, pool: address):

    key: uint256 = (
        convert(coin_a, uint256) ^ convert(coin_b, uint256)
    )

    length: uint256 = self.market_counts[key]
    self.markets[key][length] = pool
    self.market_counts[key] = length + 1


@external
def deploy_gauge(_pool: address) -> address:
    """
    @notice Deploy a liquidity gauge for a factory pool
    @param _pool Factory pool address to deploy a gauge for
    @return Address of the deployed gauge
    """
    assert self.pool_data[_pool].coins[0] != empty(address), "Unknown pool"
    assert self.pool_data[_pool].liquidity_gauge == empty(address), "Gauge already deployed"
    assert self.gauge_implementation != empty(address), "Gauge implementation not set"

    gauge: address = create_from_blueprint(self.gauge_implementation, _pool, code_offset=3)
    self.pool_data[_pool].liquidity_gauge = gauge

    log LiquidityGaugeDeployed(_pool, gauge)
    return gauge


# <--- Admin / Guarded Functionality --->


@external
def set_fee_receiver(_fee_receiver: address):
    """
    @notice Set fee receiver
    @param _fee_receiver Address that fees are sent to
    """
    assert msg.sender == self.admin, "dev: admin only"

    log UpdateFeeReceiver(self.fee_receiver, _fee_receiver)
    self.fee_receiver = _fee_receiver


@external
def set_pool_implementation(
    _pool_implementation: address, _implementation_index: uint256
):
    """
    @notice Set pool implementation
    @dev Set to empty(address) to prevent deployment of new pools
    @param _pool_implementation Address of the new pool implementation
    @param _implementation_index Index of the pool implementation
    """
    assert msg.sender == self.admin, "dev: admin only"

    log UpdatePoolImplementation(
        _implementation_index,
        self.pool_implementations[_implementation_index],
        _pool_implementation
    )

    self.pool_implementations[_implementation_index] = _pool_implementation


@external
def set_gauge_implementation(_gauge_implementation: address):
    """
    @notice Set gauge implementation
    @dev Set to empty(address) to prevent deployment of new gauges
    @param _gauge_implementation Address of the new token implementation
    """
    assert msg.sender == self.admin, "dev: admin only"

    log UpdateGaugeImplementation(self.gauge_implementation, _gauge_implementation)
    self.gauge_implementation = _gauge_implementation


@external
def set_views_implementation(_views_implementation: address):
    """
    @notice Set views contract implementation
    @param _views_implementation Address of the new views contract
    """
    assert msg.sender == self.admin,  "dev: admin only"

    log UpdateViewsImplementation(self.views_implementation, _views_implementation)
    self.views_implementation = _views_implementation


@external
def set_math_implementation(_math_implementation: address):
    """
    @notice Set math implementation
    @param _math_implementation Address of the new math contract
    """
    assert msg.sender == self.admin, "dev: admin only"

    log UpdateMathImplementation(self.math_implementation, _math_implementation)
    self.math_implementation = _math_implementation


@external
def commit_transfer_ownership(_addr: address):
    """
    @notice Transfer ownership of this contract to `addr`
    @param _addr Address of the new owner
    """
    assert msg.sender == self.admin, "dev: admin only"

    self.future_admin = _addr


@external
def accept_transfer_ownership():
    """
    @notice Accept a pending ownership transfer
    @dev Only callable by the new owner
    """
    assert msg.sender == self.future_admin, "dev: future admin only"

    log TransferOwnership(self.admin, msg.sender)
    self.admin = msg.sender


# <--- Factory Getters --->


@view
@external
def find_pool_for_coins(_from: address, _to: address, i: uint256 = 0) -> address:
    """
    @notice Find an available pool for exchanging two coins
    @param _from Address of coin to be sent
    @param _to Address of coin to be received
    @param i Index value. When multiple pools are available
            this value is used to return the n'th address.
    @return Pool address
    """
    key: uint256 = convert(_from, uint256) ^ convert(_to, uint256)
    return self.markets[key][i]


# <--- Pool Getters --->


@view
@external
def get_coins(_pool: address) -> address[N_COINS]:
    """
    @notice Get the coins within a pool
    @param _pool Pool address
    @return List of coin addresses
    """
    return self.pool_data[_pool].coins


@view
@external
def get_decimals(_pool: address) -> uint256[N_COINS]:
    """
    @notice Get decimal places for each coin within a pool
    @param _pool Pool address
    @return uint256 list of decimals
    """
    return self.pool_data[_pool].decimals


@view
@external
def get_balances(_pool: address) -> uint256[N_COINS]:
    """
    @notice Get balances for each coin within a pool
    @dev For pools using lending, these are the wrapped coin balances
    @param _pool Pool address
    @return uint256 list of balances
    """
    return [
        TricryptoPool(_pool).balances(0),
        TricryptoPool(_pool).balances(1),
        TricryptoPool(_pool).balances(2),
    ]


@view
@external
def get_coin_indices(
    _pool: address,
    _from: address,
    _to: address
) -> (uint256, uint256):
    """
    @notice Convert coin addresses to indices for use with pool methods
    @param _pool Pool address
    @param _from Coin address to be used as `i` within a pool
    @param _to Coin address to be used as `j` within a pool
    @return uint256 `i`, uint256 `j`
    """
    coins: address[N_COINS] = self.pool_data[_pool].coins

    for i in range(N_COINS):
        for j in range(N_COINS):
            if i == j:
                continue

            if coins[i] == _from and coins[j] == _to:
                return i, j

    raise "Coins not found"


@view
@external
def get_gauge(_pool: address) -> address:
    """
    @notice Get the address of the liquidity gauge contract for a factory pool
    @dev Returns `empty(address)` if a gauge has not been deployed
    @param _pool Pool address
    @return Implementation contract address
    """
    return self.pool_data[_pool].liquidity_gauge


@view
@external
def get_eth_index(_pool: address) -> uint256:
    """
    @notice Get the index of WETH for a pool
    @dev Returns max_value(uint256) if WETH is not a coin in the pool
    """
    for i in range(N_COINS):
        if self.pool_data[_pool].coins[i] == WETH:
            return i
    a: uint256 = max_value(uint256)
    return a


@view
@external
def get_market_counts(coin_a: address, coin_b: address) -> uint256:
    """
    @notice Gets the number of markets with the specified coins.
    @return Number of pools with the input coins
    """

    key: uint256 = (
        convert(coin_a, uint256) ^ convert(coin_b, uint256)
    )

    return self.market_counts[key]