# @version 0.3.7

# @notice The Llamas auction house
# @author The Llamas
# @license MIT
#
# ___________.__                 .____     .__
# \__    ___/|  |__    ____      |    |    |  |  _____     _____  _____     ______
#   |    |   |  |  \ _/ __ \     |    |    |  |  \__  \   /     \ \__  \   /  ___/
#   |    |   |   Y  \\  ___/     |    |___ |  |__ / __ \_|  Y Y  \ / __ \_ \___ \
#   |____|   |___|  / \___  >    |_______ \|____/(____  /|__|_|  /(____  //____  >
#                 \/      \/             \/           \/       \/      \/      \/


interface Llama:
    def mint() -> uint256: nonpayable
    def burn(token_id: uint256): nonpayable
    def transferFrom(
        from_addr: address, to_addr: address, token_id: uint256
    ): nonpayable


struct Auction:
        llama_id: uint256
        amount: uint256
        start_time: uint256
        end_time: uint256
        bidder: address
        settled: bool


event AuctionBid:
    _llama_id: indexed(uint256)
    _sender: address
    _value: uint256
    _extended: bool


event AuctionExtended:
    _llama_id: indexed(uint256)
    _end_time: uint256


event AuctionTimeBufferUpdated:
    _time_buffer: uint256


event AuctionReservePriceUpdated:
    _reserve_price: uint256


event AuctionMinBidIncrementPercentageUpdated:
    _min_bid_increment_percentage: uint256


event AuctionDurationUpdated:
    _duration: uint256


event AuctionCreated:
    _llama_id: indexed(uint256)
    _start_time: uint256
    _end_time: uint256


event AuctionSettled:
    _llama_id: indexed(uint256)
    _winner: address
    _amount: uint256


event Withdraw:
    _withdrawer: indexed(address)
    _amount: uint256


# Technically vyper doesn't need this as it is automatic
# in all recent vyper versions, but Etherscan verification
# will bork without it.
IDENTITY_PRECOMPILE: constant(
    address
) = 0x0000000000000000000000000000000000000004

ADMIN_MAX_WITHDRAWALS: constant(uint256) = 100

# Auction
llamas: public(Llama)
time_buffer: public(uint256)
reserve_price: public(uint256)
min_bid_increment_percentage: public(uint256)
duration: public(uint256)
auction: public(Auction)
pending_returns: public(HashMap[address, uint256])

# WL Auction
wl_enabled: public(bool)
wl_signer: public(address)
wl_auctions_won: public(HashMap[address, uint256])

# Permissions
owner: public(address)

# Pause
paused: public(bool)


@external
def __init__(
    _llamas: Llama,
    _time_buffer: uint256,
    _reserve_price: uint256,
    _min_bid_increment_percentage: uint256,
    _duration: uint256,
):
    self.llamas = _llamas
    self.time_buffer = _time_buffer
    self.reserve_price = _reserve_price
    self.min_bid_increment_percentage = _min_bid_increment_percentage
    self.duration = _duration
    self.owner = msg.sender
    self.paused = True
    self.wl_enabled = True
    self.wl_signer = msg.sender


### AUCTION CREATION/SETTLEMENT ###


@external
@nonreentrant("lock")
def settle_current_and_create_new_auction():
    """
    @dev Settle the current auction and start a new one.
      Throws if the auction house is paused.
    """

    assert self.paused == False, "Auction house is paused"

    self._settle_auction()
    self._create_auction()


@external
@nonreentrant("lock")
def settle_auction():
    """
    @dev Settle the current auction.
      Throws if the auction house is not paused.
    """

    assert self.paused == True, "Auction house is not paused"

    self._settle_auction()


### BIDDING ###


@external
@payable
@nonreentrant("lock")
def create_friend_bid(llama_id: uint256, bid_amount: uint256, sig: Bytes[65]):
    """
    @dev Create a bid.
      Throws if the whitelist is not enabled.
      Throws if the `sig` is invalid.
      Throws if the `msg.sender` has already won one whitelist auctions.
    """

    assert self.wl_enabled == True, "WL auction is not enabled"
    assert self._check_friend_signature(sig, msg.sender), "Signature is invalid"
    assert self.wl_auctions_won[msg.sender] < 1, "Already won 1 WL auction"

    self._create_bid(llama_id, bid_amount)


@external
@payable
@nonreentrant("lock")
def create_wl_bid(llama_id: uint256, bid_amount: uint256, sig: Bytes[65]):
    """
    @dev Create a bid.
      Throws if the whitelist is not enabled.
      Throws if the `sig` is invalid.
      Throws if the `msg.sender` has already won two whitelist auctions.
    """

    assert self.wl_enabled == True, "WL auction is not enabled"
    assert self._check_wl_signature(sig, msg.sender), "Signature is invalid"
    assert self.wl_auctions_won[msg.sender] < 2, "Already won 2 WL auctions"

    self._create_bid(llama_id, bid_amount)


@external
@payable
@nonreentrant("lock")
def create_bid(llama_id: uint256, bid_amount: uint256):
    """
    @dev Create a bid.
      Throws if the whitelist is enabled.
    """

    assert self.wl_enabled == False, "Public auction is not enabled"

    self._create_bid(llama_id, bid_amount)


### WITHDRAW ###


@external
@nonreentrant("lock")
def withdraw():
    """
    @dev Withdraw ETH after losing auction.
    """

    pending_amount: uint256 = self.pending_returns[msg.sender]
    self.pending_returns[msg.sender] = 0
    send(msg.sender, pending_amount)

    log Withdraw(msg.sender, pending_amount)


### ADMIN FUNCTIONS


@external
def withdraw_stale(addresses: DynArray[address, ADMIN_MAX_WITHDRAWALS]):
    """
    @dev Admin function to withdraw pending returns that have not been claimed.
    """

    assert msg.sender == self.owner, "Caller is not the owner"

    total_fee: uint256 = 0
    for _address in addresses:
        pending_amount: uint256 = self.pending_returns[_address]
        if pending_amount == 0:
            continue
        # Take a 5% fee
        fee: uint256 = (pending_amount * 5) / 100
        withdrawer_return: uint256 = pending_amount - fee
        self.pending_returns[_address] = 0
        send(_address, withdrawer_return)
        total_fee += fee

    send(self.owner, total_fee)


@external
def pause():
    """
    @notice Admin function to pause to auction house.
    """

    assert msg.sender == self.owner, "Caller is not the owner"
    self._pause()


@external
def unpause():
    """
    @notice Admin function to unpause to auction house.
    """

    assert msg.sender == self.owner, "Caller is not the owner"
    self._unpause()

    if self.auction.start_time == 0 or self.auction.settled:
        self._create_auction()


@external
def set_time_buffer(_time_buffer: uint256):
    """
    @notice Admin function to set the time buffer.
    """

    assert msg.sender == self.owner, "Caller is not the owner"

    self.time_buffer = _time_buffer

    log AuctionTimeBufferUpdated(_time_buffer)


@external
def set_reserve_price(_reserve_price: uint256):
    """
    @notice Admin function to set the reserve price.
    """

    assert msg.sender == self.owner, "Caller is not the owner"

    self.reserve_price = _reserve_price

    log AuctionReservePriceUpdated(_reserve_price)


@external
def set_min_bid_increment_percentage(_min_bid_increment_percentage: uint256):
    """
    @notice Admin function to set the min bid increment percentage.
    """

    assert msg.sender == self.owner, "Caller is not the owner"
    assert (
        _min_bid_increment_percentage >= 2
        and _min_bid_increment_percentage <= 15
    ), "_min_bid_increment_percentage out of range"

    self.min_bid_increment_percentage = _min_bid_increment_percentage

    log AuctionMinBidIncrementPercentageUpdated(_min_bid_increment_percentage)


@external
def set_duration(_duration: uint256):
    """
    @notice Admin function to set the duration.
    """

    assert msg.sender == self.owner, "Caller is not the owner"
    assert _duration >= 3600 and _duration <= 259200, "_duration out of range"

    self.duration = _duration

    log AuctionDurationUpdated(_duration)


@external
def set_owner(_owner: address):
    """
    @notice Admin function to set the owner
    """

    assert msg.sender == self.owner, "Caller is not the owner"
    assert _owner != empty(address), "Cannot set owner to zero address"

    self.owner = _owner


@external
def enable_wl():
    """
    @notice Admin function to enable the whitelist.
    """

    assert msg.sender == self.owner, "Caller is not the owner"

    self.wl_enabled = True


@external
def disable_wl():
    """
    @notice Admin function to disable the whitelist.
    """

    assert msg.sender == self.owner, "Caller is not the owner"

    self.wl_enabled = False


@external
def set_wl_signer(_wl_signer: address):
    """
    @notice Admin function to set the whitelist signer.
    """

    assert msg.sender == self.owner, "Caller is not the owner"

    self.wl_signer = _wl_signer


@internal
def _create_auction():
    _llama_id: uint256 = self.llamas.mint()
    _start_time: uint256 = block.timestamp
    _end_time: uint256 = _start_time + self.duration

    self.auction = Auction(
        {
            llama_id: _llama_id,
            amount: 0,
            start_time: _start_time,
            end_time: _end_time,
            bidder: empty(address),
            settled: False,
        }
    )

    log AuctionCreated(_llama_id, _start_time, _end_time)


@internal
def _settle_auction():
    assert self.auction.start_time != 0, "Auction hasn't begun"
    assert self.auction.settled == False, "Auction has already been settled"
    assert block.timestamp > self.auction.end_time, "Auction hasn't completed"

    self.auction.settled = True

    if self.auction.bidder == empty(address):
        self.llamas.transferFrom(self, self.owner, self.auction.llama_id)
    else:
        self.llamas.transferFrom(
            self, self.auction.bidder, self.auction.llama_id
        )
        if self.wl_enabled:
            self.wl_auctions_won[self.auction.bidder] += 1
    if self.auction.amount > 0:
        send(self.owner, self.auction.amount)

    log AuctionSettled(
        self.auction.llama_id, self.auction.bidder, self.auction.amount
    )


@internal
@payable
def _create_bid(llama_id: uint256, amount: uint256):
    if msg.value < amount:
        missing_amount: uint256 = amount - msg.value
        # Try to use the users pending returns
        assert (
            self.pending_returns[msg.sender] >= missing_amount
        ), "Does not have enough pending returns to cover remainder"
        self.pending_returns[msg.sender] -= missing_amount
    assert self.auction.llama_id == llama_id, "Llama not up for auction"
    assert block.timestamp < self.auction.end_time, "Auction expired"
    assert amount >= self.reserve_price, "Must send at least reservePrice"
    assert amount >= self.auction.amount + (
        (self.auction.amount * self.min_bid_increment_percentage) / 100
    ), "Must send more than last bid by min_bid_increment_percentage amount"

    last_bidder: address = self.auction.bidder

    if last_bidder != empty(address):
        self.pending_returns[last_bidder] += self.auction.amount

    self.auction.amount = amount
    self.auction.bidder = msg.sender

    extended: bool = self.auction.end_time - block.timestamp < self.time_buffer

    if extended:
        self.auction.end_time = block.timestamp + self.time_buffer

    log AuctionBid(self.auction.llama_id, msg.sender, amount, extended)

    if extended:
        log AuctionExtended(self.auction.llama_id, self.auction.end_time)


@internal
def _pause():
    self.paused = True


@internal
def _unpause():
    self.paused = False


@internal
@view
def _check_wl_signature(sig: Bytes[65], sender: address) -> bool:
    r: uint256 = convert(slice(sig, 0, 32), uint256)
    s: uint256 = convert(slice(sig, 32, 32), uint256)
    v: uint256 = convert(slice(sig, 64, 1), uint256)
    ethSignedHash: bytes32 = keccak256(
        concat(
            b"\x19Ethereum Signed Message:\n32",
            keccak256(_abi_encode("whitelist:", sender)),
        )
    )

    return self.wl_signer == ecrecover(ethSignedHash, v, r, s)


@internal
@view
def _check_friend_signature(sig: Bytes[65], sender: address) -> bool:
    r: uint256 = convert(slice(sig, 0, 32), uint256)
    s: uint256 = convert(slice(sig, 32, 32), uint256)
    v: uint256 = convert(slice(sig, 64, 1), uint256)
    ethSignedHash: bytes32 = keccak256(
        concat(
            b"\x19Ethereum Signed Message:\n32",
            keccak256(_abi_encode("friend:", sender)),
        )
    )

    return self.wl_signer == ecrecover(ethSignedHash, v, r, s)