Accounts
0xa2...ce00
0xa2...CE00

0xa2...CE00

$500
This contract's source code is verified!
Contract Metadata
Compiler
0.3.10+commit.91361694
Language
Vyper
Contract Source Code
File 1 of 1: FeeCollector.vy
# pragma version 0.3.10
"""
@title FeeCollector
@license MIT
@author Curve Finance
@notice Collects fees and delegates to burner for exchange
"""


interface ERC20:
    def approve(_to: address, _value: uint256): nonpayable
    def transfer(_to: address, _value: uint256) -> bool: nonpayable
    def transferFrom(_from: address, _to: address, _value: uint256) -> bool: nonpayable
    def balanceOf(_owner: address) -> uint256: view

interface wETH:
    def balanceOf(_owner: address) -> uint256: view
    def transferFrom(_sender: address, _receiver: address, _amount: uint256): nonpayable
    def transfer(_receiver: address, _amount: uint256): nonpayable
    def withdraw(_amount: uint256): nonpayable
    def deposit(): payable

interface Curve:
    def withdraw_admin_fees(): nonpayable

interface Burner:
    def burn(_coins: DynArray[ERC20, MAX_LEN], _receiver: address): nonpayable
    def push_target() -> uint256: nonpayable
    def supportsInterface(_interface_id: bytes4) -> bool: view

interface Hooker:
    def duty_act(_hook_inputs: DynArray[HookInput, MAX_HOOK_LEN], _receiver: address=msg.sender) -> uint256: payable
    def buffer_amount() -> uint256: view
    def supportsInterface(_interface_id: bytes4) -> bool: view


event SetMaxFee:
    epoch: indexed(Epoch)
    max_fee: uint256

event SetBurner:
    burner: indexed(Burner)

event SetHooker:
    hooker: indexed(Hooker)

event SetTarget:
    target: indexed(ERC20)

event SetKilled:
    coin: indexed(ERC20)
    epoch_mask: Epoch

event SetOwner:
    owner: indexed(address)

event SetEmergencyOwner:
    emergency_owner: indexed(address)


enum Epoch:
    SLEEP  # 1
    COLLECT  # 2
    EXCHANGE  # 4
    FORWARD  # 8


struct Transfer:
    coin: ERC20
    to: address
    amount: uint256  # 2^256-1 for the whole balance


struct HookInput:
    hook_id: uint8
    value: uint256
    data: Bytes[8192]


struct RecoverInput:
    coin: ERC20
    amount: uint256


struct KilledInput:
    coin: ERC20
    killed: Epoch  # True where killed


ETH_ADDRESS: constant(address) = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
WETH: immutable(wETH)

MAX_LEN: constant(uint256) = 64
MAX_HOOK_LEN: constant(uint256) = 32
ONE: constant(uint256) = 10 ** 18  # Precision

START_TIME: constant(uint256) = 1600300800  # ts of distribution start
WEEK: constant(uint256) = 7 * 24 * 3600
EPOCH_TIMESTAMPS: constant(uint256[17]) = [
    0, 0,  # 1
    4 * 24 * 3600,  # 2
    0, 5 * 24 * 3600,   # 4
    0, 0, 0, 6 * 24 * 3600,  # 8
    0, 0, 0, 0, 0, 0, 0, WEEK,  # 16, next period
]

target: public(ERC20)  # coin swapped into
max_fee: public(uint256[9])  # max_fee[Epoch]

BURNER_INTERFACE_ID: constant(bytes4) = 0xa3b5e311
HOOKER_INTERFACE_ID: constant(bytes4) = 0xe569b44d
burner: public(Burner)
hooker: public(Hooker)

last_hooker_approve: uint256

is_killed: public(HashMap[ERC20, Epoch])
ALL_COINS: constant(ERC20) = empty(ERC20)  # Auxiliary indicator for all coins (=ZERO_ADDRESS)

owner: public(address)
emergency_owner: public(address)


@external
def __init__(_target_coin: ERC20, _weth: wETH, _owner: address, _emergency_owner: address):
    """
    @notice Contract constructor
    @param _target_coin Coin to swap to
    @param _weth Wrapped ETH(native coin) address
    @param _owner Owner address
    @param _emergency_owner Emergency owner address. Can kill the contract
    """
    self.target = _target_coin
    WETH = _weth
    self.owner = _owner
    self.emergency_owner = _emergency_owner

    self.max_fee[convert(Epoch.COLLECT, uint256)] = ONE / 100  # 1%
    self.max_fee[convert(Epoch.FORWARD, uint256)] = ONE / 100  # 1%

    self.is_killed[ALL_COINS] = Epoch.COLLECT | Epoch.FORWARD  # Set burner first
    self.is_killed[_target_coin] = Epoch.COLLECT | Epoch.EXCHANGE  # Keep target coin in contract

    log SetTarget(_target_coin)
    log SetOwner(_owner)
    log SetEmergencyOwner(_emergency_owner)
    log SetMaxFee(Epoch.COLLECT, ONE / 100)
    log SetMaxFee(Epoch.FORWARD, ONE / 100)
    log SetKilled(ALL_COINS, Epoch.COLLECT | Epoch.FORWARD)
    log SetKilled(_target_coin, Epoch.COLLECT | Epoch.FORWARD)


@external
@payable
def __default__():
    # Deposited ETH can be converted using `burn(ETH_ADDRESS)`
    pass


@external
def withdraw_many(_pools: DynArray[address, MAX_LEN]):
    """
    @notice Withdraw admin fees from multiple pools
    @param _pools List of pool address to withdraw admin fees from
    """
    for pool in _pools:
        Curve(pool).withdraw_admin_fees()


@external
@payable
def burn(_coin: address) -> bool:
    """
    @notice Transfer coin from contract with approval
    @dev Needed for back compatability along with dealing raw ETH
    @param _coin Coin to transfer
    @return True if did not fail, back compatability
    """
    if _coin == ETH_ADDRESS:  # Deposit
        WETH.deposit(value=self.balance)
    else:
        amount: uint256 = ERC20(_coin).balanceOf(msg.sender)
        ERC20(_coin).transferFrom(msg.sender, self, amount)
    return True


@internal
@pure
def _epoch_ts(ts: uint256) -> Epoch:
    ts = (ts - START_TIME) % WEEK
    for epoch in [Epoch.SLEEP, Epoch.COLLECT, Epoch.EXCHANGE, Epoch.FORWARD]:
        if ts < EPOCH_TIMESTAMPS[2 * convert(epoch, uint256)]:
            return epoch
    raise UNREACHABLE


@external
@view
def epoch(ts: uint256=block.timestamp) -> Epoch:
    """
    @notice Get epoch at certain timestamp
    @param ts Timestamp. Current by default
    @return Epoch
    """
    return self._epoch_ts(ts)


@internal
@pure
def _epoch_time_frame(epoch: Epoch, ts: uint256) -> (uint256, uint256):
    subset: uint256 = convert(epoch, uint256)
    assert subset & (subset - 1) == 0, "Bad Epoch"

    ts = ts - (ts - START_TIME) % WEEK
    return (ts + EPOCH_TIMESTAMPS[convert(epoch, uint256)], ts + EPOCH_TIMESTAMPS[2 * convert(epoch, uint256)])


@external
@view
def epoch_time_frame(_epoch: Epoch, _ts: uint256=block.timestamp) -> (uint256, uint256):
    """
    @notice Get time frame of certain epoch
    @param _epoch Epoch
    @param _ts Timestamp to anchor to. Current by default
    @return [start, end) time frame boundaries
    """
    return self._epoch_time_frame(_epoch, _ts)


@internal
@view
def _fee(epoch: Epoch, ts: uint256) -> uint256:
    start: uint256 = 0
    end: uint256 = 0
    start, end = self._epoch_time_frame(epoch, ts)
    if ts >= end:
        return 0
    return self.max_fee[convert(epoch, uint256)] * (ts + 1 - start) / (end - start)


@external
@view
def fee(_epoch: Epoch=empty(Epoch), _ts: uint256=block.timestamp) -> uint256:
    """
    @notice Calculate keeper's fee
    @param _epoch Epoch to count fee for
    @param _ts Timestamp of collection
    @return Fee with base 10^18
    """
    if _epoch == empty(Epoch):
        return self._fee(self._epoch_ts(_ts), _ts)
    return self._fee(_epoch, _ts)


@external
@nonreentrant("transfer")
def transfer(_transfers: DynArray[Transfer, MAX_LEN]):
    """
    @dev No approvals so can change burner easily
    @param _transfers Transfers to apply
    """
    assert msg.sender == self.burner.address, "Only Burner"
    epoch: Epoch = self._epoch_ts(block.timestamp)
    assert epoch in Epoch.COLLECT | Epoch.EXCHANGE, "Wrong Epoch"

    for transfer in _transfers:
        assert not self.is_killed[transfer.coin] in epoch, "Killed coin"

        amount: uint256 = transfer.amount
        if amount == max_value(uint256):
            amount = transfer.coin.balanceOf(self)
        assert transfer.coin.transfer(transfer.to, amount, default_return_value=True)


@external
@nonreentrant("collect")
def collect(_coins: DynArray[ERC20, MAX_LEN], _receiver: address=msg.sender):
    """
    @notice Collect earned fees. Collection should happen under callback to earn caller fees.
    @param _coins Coins to collect sorted in ascending order
    @param _receiver Receiver of caller `collect_fee`s
    """
    assert self._epoch_ts(block.timestamp) == Epoch.COLLECT, "Wrong epoch"
    assert not self.is_killed[ALL_COINS] in Epoch.COLLECT, "Killed epoch"

    for i in range(len(_coins), bound=MAX_LEN):
        assert not self.is_killed[_coins[i]] in Epoch.COLLECT, "Killed coin"
        # Eliminate case of repeated coins
        if i > 0:
            assert convert(_coins[i].address, uint160) > convert(_coins[i - 1].address, uint160), "Coins not sorted"

    self.burner.burn(_coins, _receiver)


@external
@view
def can_exchange(_coins: DynArray[ERC20, MAX_LEN]) -> bool:
    """
    @notice Check whether coins are allowed to be exchanged
    @param _coins Coins to exchange
    @return Boolean value if coins are allowed to be exchanged
    """
    if self._epoch_ts(block.timestamp) != Epoch.EXCHANGE or\
        self.is_killed[ALL_COINS] in Epoch.EXCHANGE:
        return False
    for coin in _coins:
        if self.is_killed[coin] in Epoch.EXCHANGE:
            return False
    return True


@external
@payable
@nonreentrant("forward")
def forward(_hook_inputs: DynArray[HookInput, MAX_HOOK_LEN], _receiver: address=msg.sender) -> uint256:
    """
    @notice Transfer target coin forward
    @param _hook_inputs Input parameters for forward hooks
    @param _receiver Receiver of caller `forward_fee`
    @return Amount of received fee
    """
    assert self._epoch_ts(block.timestamp) == Epoch.FORWARD, "Wrong epoch"
    target: ERC20 = self.target
    assert not (self.is_killed[ALL_COINS] | self.is_killed[target]) in Epoch.FORWARD, "Killed"

    self.burner.push_target()
    amount: uint256 = target.balanceOf(self)

    # Account buffer
    hooker: Hooker = self.hooker
    hooker_buffer: uint256 = hooker.buffer_amount()
    amount -= min(hooker_buffer, amount)

    fee: uint256 = self._fee(Epoch.FORWARD, block.timestamp) * amount / ONE
    target.transfer(_receiver, fee)

    target.transfer(hooker.address, amount - fee)
    if self.last_hooker_approve < (block.timestamp - START_TIME) / WEEK:  # First time this week
        target.approve(hooker.address, hooker_buffer)
        self.last_hooker_approve = (block.timestamp - START_TIME) / WEEK
    fee += hooker.duty_act(_hook_inputs, _receiver, value=msg.value)

    return fee


@external
def recover(_recovers: DynArray[RecoverInput, MAX_LEN], _receiver: address):
    """
    @notice Recover ERC20 tokens or Ether from this contract
    @dev Callable only by owner and emergency owner
    @param _recovers (Token, amount) to recover
    @param _receiver Receiver of coins
    """
    assert msg.sender in [self.owner, self.emergency_owner], "Only owner"

    for input in _recovers:
        amount: uint256 = input.amount
        if input.coin.address == ETH_ADDRESS:
            if amount == max_value(uint256):
                amount = self.balance
            raw_call(_receiver, b"", value=amount)
        else:
            if amount == max_value(uint256):
                amount = input.coin.balanceOf(self)
            input.coin.transfer(_receiver, amount)  # do not need safe transfer


@external
def set_max_fee(_epoch: Epoch, _max_fee: uint256):
    """
    @notice Set keeper's max fee
    @dev Callable only by owner
    @param _epoch Epoch to set fee for
    @param _max_fee Maximum fee to set
    """
    assert msg.sender == self.owner, "Only owner"
    subset: uint256 = convert(_epoch, uint256)
    assert subset & (subset - 1) == 0, "Bad Epoch"
    assert _max_fee <= ONE, "Bad max_fee"
    self.max_fee[convert(_epoch, uint256)] = _max_fee

    log SetMaxFee(_epoch, _max_fee)


@external
def set_burner(_new_burner: Burner):
    """
    @notice Set burner for exchanging coins, must implement BURNER_INTERFACE
    @dev Callable only by owner
    @param _new_burner Address of the new contract
    """
    assert msg.sender == self.owner, "Only owner"
    assert _new_burner.supportsInterface(BURNER_INTERFACE_ID)
    self.burner = _new_burner

    log SetBurner(_new_burner)


@external
def set_hooker(_new_hooker: Hooker):
    """
    @notice Set contract for hooks, must implement HOOKER_INTERFACE
    @dev Callable only by owner
    @param _new_hooker Address of the new contract
    """
    assert msg.sender == self.owner, "Only owner"
    assert _new_hooker.supportsInterface(HOOKER_INTERFACE_ID)

    if self.hooker != empty(Hooker):
        self.target.approve(self.hooker.address, 0)
    self.hooker = _new_hooker

    log SetHooker(_new_hooker)


@external
def set_target(_new_target: ERC20):
    """
    @notice Set new coin for fees accumulation
    @dev Callable only by owner
    @param _new_target Address of the new target coin
    """
    assert msg.sender == self.owner, "Only owner"

    target: ERC20 = self.target
    self.is_killed[target] = empty(Epoch)  # allow to collect and exchange
    log SetKilled(target, empty(Epoch))

    self.target = _new_target
    self.is_killed[_new_target] = Epoch.COLLECT | Epoch.EXCHANGE  # Keep target coin in contract
    log SetTarget(_new_target)
    log SetKilled(_new_target, Epoch.COLLECT | Epoch.EXCHANGE)


@external
def set_killed(_input: DynArray[KilledInput, MAX_LEN]):
    """
    @notice Stop a contract or specific coin to be burnt
    @dev Callable only by owner or emergency owner
    @param _input Array of (coin address, killed phases enum)
    """
    assert msg.sender in [self.owner, self.emergency_owner], "Only owner"

    for input in _input:
        self.is_killed[input.coin] = input.killed
        log SetKilled(input.coin, input.killed)


@external
def set_owner(_new_owner: address):
    """
    @notice Set owner of the contract
    @dev Callable only by current owner
    @param _new_owner Address of the new owner
    """
    assert msg.sender == self.owner, "Only owner"
    assert _new_owner != empty(address)
    self.owner = _new_owner
    log SetOwner(_new_owner)


@external
def set_emergency_owner(_new_owner: address):
    """
    @notice Set emergency owner of the contract
    @dev Callable only by current owner
    @param _new_owner Address of the new emergency owner
    """
    assert msg.sender == self.owner, "Only owner"
    assert _new_owner != empty(address)
    self.emergency_owner = _new_owner
    log SetEmergencyOwner(_new_owner)
Settings
{
  "compilationTarget": {
    "FeeCollector.vy": "FeeCollector"
  },
  "outputSelection": {
    "FeeCollector.vy": [
      "abi",
      "ast",
      "interface",
      "ir",
      "userdoc",
      "devdoc",
      "evm.bytecode.object",
      "evm.bytecode.opcodes",
      "evm.deployedBytecode.object",
      "evm.deployedBytecode.opcodes",
      "evm.deployedBytecode.sourceMap",
      "evm.methodIdentifiers"
    ]
  },
  "search_paths": [
    "."
  ]
}
ABI
[{"anonymous":false,"inputs":[{"indexed":true,"name":"epoch","type":"uint256"},{"indexed":false,"name":"max_fee","type":"uint256"}],"name":"SetMaxFee","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"burner","type":"address"}],"name":"SetBurner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"hooker","type":"address"}],"name":"SetHooker","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"target","type":"address"}],"name":"SetTarget","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"coin","type":"address"},{"indexed":false,"name":"epoch_mask","type":"uint256"}],"name":"SetKilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"}],"name":"SetOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"emergency_owner","type":"address"}],"name":"SetEmergencyOwner","type":"event"},{"inputs":[{"name":"_target_coin","type":"address"},{"name":"_weth","type":"address"},{"name":"_owner","type":"address"},{"name":"_emergency_owner","type":"address"}],"outputs":[],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"name":"_pools","type":"address[]"}],"name":"withdraw_many","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_coin","type":"address"}],"name":"burn","outputs":[{"name":"","type":"bool"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"epoch","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"ts","type":"uint256"}],"name":"epoch","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_epoch","type":"uint256"}],"name":"epoch_time_frame","outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_epoch","type":"uint256"},{"name":"_ts","type":"uint256"}],"name":"epoch_time_frame","outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_epoch","type":"uint256"}],"name":"fee","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"_epoch","type":"uint256"},{"name":"_ts","type":"uint256"}],"name":"fee","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"name":"coin","type":"address"},{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"name":"_transfers","type":"tuple[]"}],"name":"transfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_coins","type":"address[]"}],"name":"collect","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_coins","type":"address[]"},{"name":"_receiver","type":"address"}],"name":"collect","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_coins","type":"address[]"}],"name":"can_exchange","outputs":[{"name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}],"name":"_hook_inputs","type":"tuple[]"}],"name":"forward","outputs":[{"name":"","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"name":"hook_id","type":"uint8"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"}],"name":"_hook_inputs","type":"tuple[]"},{"name":"_receiver","type":"address"}],"name":"forward","outputs":[{"name":"","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"name":"coin","type":"address"},{"name":"amount","type":"uint256"}],"name":"_recovers","type":"tuple[]"},{"name":"_receiver","type":"address"}],"name":"recover","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_epoch","type":"uint256"},{"name":"_max_fee","type":"uint256"}],"name":"set_max_fee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_burner","type":"address"}],"name":"set_burner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_hooker","type":"address"}],"name":"set_hooker","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_target","type":"address"}],"name":"set_target","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"name":"coin","type":"address"},{"name":"killed","type":"uint256"}],"name":"_input","type":"tuple[]"}],"name":"set_killed","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_owner","type":"address"}],"name":"set_owner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_new_owner","type":"address"}],"name":"set_emergency_owner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"target","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"uint256"}],"name":"max_fee","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"burner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"hooker","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"name":"arg0","type":"address"}],"name":"is_killed","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"emergency_owner","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"}]