ETH Price: $1,975.80 (+0.69%)

Transaction Decoder

Block:
24507989 at Feb-21-2026 09:40:23 PM +UTC
Transaction Fee:
0.00002576402890348 ETH $0.05
Gas Used:
320,152 Gas / 0.080474365 Gwei

Emitted Events:

614 Vyper_contract.Transfer( _from=[Sender] 0x641dac75bcc5711f212419483930be85c6a65fe0, _to=[Receiver] Vyper_contract, _value=2100000000000000000000 )
615 Vyper_contract.Deposit( provider=[Sender] 0x641dac75bcc5711f212419483930be85c6a65fe0, value=2100000000000000000000, locktime=1897257600, type=1, ts=1771710023 )
616 Vyper_contract.Supply( prevSupply=857805047259951452476675696, supply=857807147259951452476675696 )

Account State Difference:

  Address   Before After State Difference Code
(Titan Builder)
11.593419922993513936 Eth11.593435930593513936 Eth0.0000160076
0x5f3b5DfE...2a494e2A2
0x641daC75...5C6a65Fe0
0.004746602132046344 Eth
Nonce: 9
0.004720838103142864 Eth
Nonce: 10
0.00002576402890348
0xD533a949...bA034cd52

Execution Trace

Vyper_contract.create_lock( _value=2100000000000000000000, _unlock_time=1897853985 )
  • Vyper_contract.transferFrom( _from=0x641daC75Bcc5711f212419483930bE85C6a65Fe0, _to=0x5f3b5DfEb7B28CDbD7FAba78963EE202a494e2A2, _value=2100000000000000000000 ) => ( True )
    File 1 of 2: Vyper_contract
    # @version 0.2.4
    """
    @title Voting Escrow
    @author Curve Finance
    @license MIT
    @notice Votes have a weight depending on time, so that users are
            committed to the future of (whatever they are voting for)
    @dev Vote weight decays linearly over time. Lock time cannot be
         more than `MAXTIME` (4 years).
    """
    
    # Voting escrow to have time-weighted votes
    # Votes have a weight depending on time, so that users are committed
    # to the future of (whatever they are voting for).
    # The weight in this implementation is linear, and lock cannot be more than maxtime:
    # w ^
    # 1 +        /
    #   |      /
    #   |    /
    #   |  /
    #   |/
    # 0 +--------+------> time
    #       maxtime (4 years?)
    
    struct Point:
        bias: int128
        slope: int128  # - dweight / dt
        ts: uint256
        blk: uint256  # block
    # We cannot really do block numbers per se b/c slope is per time, not per block
    # and per block could be fairly bad b/c Ethereum changes blocktimes.
    # What we can do is to extrapolate ***At functions
    
    struct LockedBalance:
        amount: int128
        end: uint256
    
    
    interface ERC20:
        def decimals() -> uint256: view
        def name() -> String[64]: view
        def symbol() -> String[32]: view
        def transfer(to: address, amount: uint256) -> bool: nonpayable
        def transferFrom(spender: address, to: address, amount: uint256) -> bool: nonpayable
    
    
    # Interface for checking whether address belongs to a whitelisted
    # type of a smart wallet.
    # When new types are added - the whole contract is changed
    # The check() method is modifying to be able to use caching
    # for individual wallet addresses
    interface SmartWalletChecker:
        def check(addr: address) -> bool: nonpayable
    
    DEPOSIT_FOR_TYPE: constant(int128) = 0
    CREATE_LOCK_TYPE: constant(int128) = 1
    INCREASE_LOCK_AMOUNT: constant(int128) = 2
    INCREASE_UNLOCK_TIME: constant(int128) = 3
    
    
    event CommitOwnership:
        admin: address
    
    event ApplyOwnership:
        admin: address
    
    event Deposit:
        provider: indexed(address)
        value: uint256
        locktime: indexed(uint256)
        type: int128
        ts: uint256
    
    event Withdraw:
        provider: indexed(address)
        value: uint256
        ts: uint256
    
    event Supply:
        prevSupply: uint256
        supply: uint256
    
    
    WEEK: constant(uint256) = 7 * 86400  # all future times are rounded by week
    MAXTIME: constant(uint256) = 4 * 365 * 86400  # 4 years
    MULTIPLIER: constant(uint256) = 10 ** 18
    
    token: public(address)
    supply: public(uint256)
    
    locked: public(HashMap[address, LockedBalance])
    
    epoch: public(uint256)
    point_history: public(Point[100000000000000000000000000000])  # epoch -> unsigned point
    user_point_history: public(HashMap[address, Point[1000000000]])  # user -> Point[user_epoch]
    user_point_epoch: public(HashMap[address, uint256])
    slope_changes: public(HashMap[uint256, int128])  # time -> signed slope change
    
    # Aragon's view methods for compatibility
    controller: public(address)
    transfersEnabled: public(bool)
    
    name: public(String[64])
    symbol: public(String[32])
    version: public(String[32])
    decimals: public(uint256)
    
    # Checker for whitelisted (smart contract) wallets which are allowed to deposit
    # The goal is to prevent tokenizing the escrow
    future_smart_wallet_checker: public(address)
    smart_wallet_checker: public(address)
    
    admin: public(address)  # Can and will be a smart contract
    future_admin: public(address)
    
    
    @external
    def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]):
        """
        @notice Contract constructor
        @param token_addr `ERC20CRV` token address
        @param _name Token name
        @param _symbol Token symbol
        @param _version Contract version - required for Aragon compatibility
        """
        self.admin = msg.sender
        self.token = token_addr
        self.point_history[0].blk = block.number
        self.point_history[0].ts = block.timestamp
        self.controller = msg.sender
        self.transfersEnabled = True
    
        _decimals: uint256 = ERC20(token_addr).decimals()
        assert _decimals <= 255
        self.decimals = _decimals
    
        self.name = _name
        self.symbol = _symbol
        self.version = _version
    
    
    @external
    def commit_transfer_ownership(addr: address):
        """
        @notice Transfer ownership of VotingEscrow contract to `addr`
        @param addr Address to have ownership transferred to
        """
        assert msg.sender == self.admin  # dev: admin only
        self.future_admin = addr
        log CommitOwnership(addr)
    
    
    @external
    def apply_transfer_ownership():
        """
        @notice Apply ownership transfer
        """
        assert msg.sender == self.admin  # dev: admin only
        _admin: address = self.future_admin
        assert _admin != ZERO_ADDRESS  # dev: admin not set
        self.admin = _admin
        log ApplyOwnership(_admin)
    
    
    @external
    def commit_smart_wallet_checker(addr: address):
        """
        @notice Set an external contract to check for approved smart contract wallets
        @param addr Address of Smart contract checker
        """
        assert msg.sender == self.admin
        self.future_smart_wallet_checker = addr
    
    
    @external
    def apply_smart_wallet_checker():
        """
        @notice Apply setting external contract to check approved smart contract wallets
        """
        assert msg.sender == self.admin
        self.smart_wallet_checker = self.future_smart_wallet_checker
    
    
    @internal
    def assert_not_contract(addr: address):
        """
        @notice Check if the call is from a whitelisted smart contract, revert if not
        @param addr Address to be checked
        """
        if addr != tx.origin:
            checker: address = self.smart_wallet_checker
            if checker != ZERO_ADDRESS:
                if SmartWalletChecker(checker).check(addr):
                    return
            raise "Smart contract depositors not allowed"
    
    
    @external
    @view
    def get_last_user_slope(addr: address) -> int128:
        """
        @notice Get the most recently recorded rate of voting power decrease for `addr`
        @param addr Address of the user wallet
        @return Value of the slope
        """
        uepoch: uint256 = self.user_point_epoch[addr]
        return self.user_point_history[addr][uepoch].slope
    
    
    @external
    @view
    def user_point_history__ts(_addr: address, _idx: uint256) -> uint256:
        """
        @notice Get the timestamp for checkpoint `_idx` for `_addr`
        @param _addr User wallet address
        @param _idx User epoch number
        @return Epoch time of the checkpoint
        """
        return self.user_point_history[_addr][_idx].ts
    
    
    @external
    @view
    def locked__end(_addr: address) -> uint256:
        """
        @notice Get timestamp when `_addr`'s lock finishes
        @param _addr User wallet
        @return Epoch time of the lock end
        """
        return self.locked[_addr].end
    
    
    @internal
    def _checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance):
        """
        @notice Record global and per-user data to checkpoint
        @param addr User's wallet address. No user checkpoint if 0x0
        @param old_locked Pevious locked amount / end lock time for the user
        @param new_locked New locked amount / end lock time for the user
        """
        u_old: Point = empty(Point)
        u_new: Point = empty(Point)
        old_dslope: int128 = 0
        new_dslope: int128 = 0
        _epoch: uint256 = self.epoch
    
        if addr != ZERO_ADDRESS:
            # Calculate slopes and biases
            # Kept at zero when they have to
            if old_locked.end > block.timestamp and old_locked.amount > 0:
                u_old.slope = old_locked.amount / MAXTIME
                u_old.bias = u_old.slope * convert(old_locked.end - block.timestamp, int128)
            if new_locked.end > block.timestamp and new_locked.amount > 0:
                u_new.slope = new_locked.amount / MAXTIME
                u_new.bias = u_new.slope * convert(new_locked.end - block.timestamp, int128)
    
            # Read values of scheduled changes in the slope
            # old_locked.end can be in the past and in the future
            # new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros
            old_dslope = self.slope_changes[old_locked.end]
            if new_locked.end != 0:
                if new_locked.end == old_locked.end:
                    new_dslope = old_dslope
                else:
                    new_dslope = self.slope_changes[new_locked.end]
    
        last_point: Point = Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number})
        if _epoch > 0:
            last_point = self.point_history[_epoch]
        last_checkpoint: uint256 = last_point.ts
        # initial_last_point is used for extrapolation to calculate block number
        # (approximately, for *At methods) and save them
        # as we cannot figure that out exactly from inside the contract
        initial_last_point: Point = last_point
        block_slope: uint256 = 0  # dblock/dt
        if block.timestamp > last_point.ts:
            block_slope = MULTIPLIER * (block.number - last_point.blk) / (block.timestamp - last_point.ts)
        # If last point is already recorded in this block, slope=0
        # But that's ok b/c we know the block in such case
    
        # Go over weeks to fill history and calculate what the current point is
        t_i: uint256 = (last_checkpoint / WEEK) * WEEK
        for i in range(255):
            # Hopefully it won't happen that this won't get used in 5 years!
            # If it does, users will be able to withdraw but vote weight will be broken
            t_i += WEEK
            d_slope: int128 = 0
            if t_i > block.timestamp:
                t_i = block.timestamp
            else:
                d_slope = self.slope_changes[t_i]
            last_point.bias -= last_point.slope * convert(t_i - last_checkpoint, int128)
            last_point.slope += d_slope
            if last_point.bias < 0:  # This can happen
                last_point.bias = 0
            if last_point.slope < 0:  # This cannot happen - just in case
                last_point.slope = 0
            last_checkpoint = t_i
            last_point.ts = t_i
            last_point.blk = initial_last_point.blk + block_slope * (t_i - initial_last_point.ts) / MULTIPLIER
            _epoch += 1
            if t_i == block.timestamp:
                last_point.blk = block.number
                break
            else:
                self.point_history[_epoch] = last_point
    
        self.epoch = _epoch
        # Now point_history is filled until t=now
    
        if addr != ZERO_ADDRESS:
            # If last point was in this block, the slope change has been applied already
            # But in such case we have 0 slope(s)
            last_point.slope += (u_new.slope - u_old.slope)
            last_point.bias += (u_new.bias - u_old.bias)
            if last_point.slope < 0:
                last_point.slope = 0
            if last_point.bias < 0:
                last_point.bias = 0
    
        # Record the changed point into history
        self.point_history[_epoch] = last_point
    
        if addr != ZERO_ADDRESS:
            # Schedule the slope changes (slope is going down)
            # We subtract new_user_slope from [new_locked.end]
            # and add old_user_slope to [old_locked.end]
            if old_locked.end > block.timestamp:
                # old_dslope was <something> - u_old.slope, so we cancel that
                old_dslope += u_old.slope
                if new_locked.end == old_locked.end:
                    old_dslope -= u_new.slope  # It was a new deposit, not extension
                self.slope_changes[old_locked.end] = old_dslope
    
            if new_locked.end > block.timestamp:
                if new_locked.end > old_locked.end:
                    new_dslope -= u_new.slope  # old slope disappeared at this point
                    self.slope_changes[new_locked.end] = new_dslope
                # else: we recorded it already in old_dslope
    
            # Now handle user history
            user_epoch: uint256 = self.user_point_epoch[addr] + 1
    
            self.user_point_epoch[addr] = user_epoch
            u_new.ts = block.timestamp
            u_new.blk = block.number
            self.user_point_history[addr][user_epoch] = u_new
    
    
    @internal
    def _deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128):
        """
        @notice Deposit and lock tokens for a user
        @param _addr User's wallet address
        @param _value Amount to deposit
        @param unlock_time New time when to unlock the tokens, or 0 if unchanged
        @param locked_balance Previous locked amount / timestamp
        """
        _locked: LockedBalance = locked_balance
        supply_before: uint256 = self.supply
    
        self.supply = supply_before + _value
        old_locked: LockedBalance = _locked
        # Adding to existing lock, or if a lock is expired - creating a new one
        _locked.amount += convert(_value, int128)
        if unlock_time != 0:
            _locked.end = unlock_time
        self.locked[_addr] = _locked
    
        # Possibilities:
        # Both old_locked.end could be current or expired (>/< block.timestamp)
        # value == 0 (extend lock) or value > 0 (add to lock or extend lock)
        # _locked.end > block.timestamp (always)
        self._checkpoint(_addr, old_locked, _locked)
    
        if _value != 0:
            assert ERC20(self.token).transferFrom(_addr, self, _value)
    
        log Deposit(_addr, _value, _locked.end, type, block.timestamp)
        log Supply(supply_before, supply_before + _value)
    
    
    @external
    def checkpoint():
        """
        @notice Record global data to checkpoint
        """
        self._checkpoint(ZERO_ADDRESS, empty(LockedBalance), empty(LockedBalance))
    
    
    @external
    @nonreentrant('lock')
    def deposit_for(_addr: address, _value: uint256):
        """
        @notice Deposit `_value` tokens for `_addr` and add to the lock
        @dev Anyone (even a smart contract) can deposit for someone else, but
             cannot extend their locktime and deposit for a brand new user
        @param _addr User's wallet address
        @param _value Amount to add to user's lock
        """
        _locked: LockedBalance = self.locked[_addr]
    
        assert _value > 0  # dev: need non-zero value
        assert _locked.amount > 0, "No existing lock found"
        assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"
    
        self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE)
    
    
    @external
    @nonreentrant('lock')
    def create_lock(_value: uint256, _unlock_time: uint256):
        """
        @notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time`
        @param _value Amount to deposit
        @param _unlock_time Epoch time when tokens unlock, rounded down to whole weeks
        """
        self.assert_not_contract(msg.sender)
        unlock_time: uint256 = (_unlock_time / WEEK) * WEEK  # Locktime is rounded down to weeks
        _locked: LockedBalance = self.locked[msg.sender]
    
        assert _value > 0  # dev: need non-zero value
        assert _locked.amount == 0, "Withdraw old tokens first"
        assert unlock_time > block.timestamp, "Can only lock until time in the future"
        assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"
    
        self._deposit_for(msg.sender, _value, unlock_time, _locked, CREATE_LOCK_TYPE)
    
    
    @external
    @nonreentrant('lock')
    def increase_amount(_value: uint256):
        """
        @notice Deposit `_value` additional tokens for `msg.sender`
                without modifying the unlock time
        @param _value Amount of tokens to deposit and add to the lock
        """
        self.assert_not_contract(msg.sender)
        _locked: LockedBalance = self.locked[msg.sender]
    
        assert _value > 0  # dev: need non-zero value
        assert _locked.amount > 0, "No existing lock found"
        assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"
    
        self._deposit_for(msg.sender, _value, 0, _locked, INCREASE_LOCK_AMOUNT)
    
    
    @external
    @nonreentrant('lock')
    def increase_unlock_time(_unlock_time: uint256):
        """
        @notice Extend the unlock time for `msg.sender` to `_unlock_time`
        @param _unlock_time New epoch time for unlocking
        """
        self.assert_not_contract(msg.sender)
        _locked: LockedBalance = self.locked[msg.sender]
        unlock_time: uint256 = (_unlock_time / WEEK) * WEEK  # Locktime is rounded down to weeks
    
        assert _locked.end > block.timestamp, "Lock expired"
        assert _locked.amount > 0, "Nothing is locked"
        assert unlock_time > _locked.end, "Can only increase lock duration"
        assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"
    
        self._deposit_for(msg.sender, 0, unlock_time, _locked, INCREASE_UNLOCK_TIME)
    
    
    @external
    @nonreentrant('lock')
    def withdraw():
        """
        @notice Withdraw all tokens for `msg.sender`
        @dev Only possible if the lock has expired
        """
        _locked: LockedBalance = self.locked[msg.sender]
        assert block.timestamp >= _locked.end, "The lock didn't expire"
        value: uint256 = convert(_locked.amount, uint256)
    
        old_locked: LockedBalance = _locked
        _locked.end = 0
        _locked.amount = 0
        self.locked[msg.sender] = _locked
        supply_before: uint256 = self.supply
        self.supply = supply_before - value
    
        # old_locked can have either expired <= timestamp or zero end
        # _locked has only 0 end
        # Both can have >= 0 amount
        self._checkpoint(msg.sender, old_locked, _locked)
    
        assert ERC20(self.token).transfer(msg.sender, value)
    
        log Withdraw(msg.sender, value, block.timestamp)
        log Supply(supply_before, supply_before - value)
    
    
    # The following ERC20/minime-compatible methods are not real balanceOf and supply!
    # They measure the weights for the purpose of voting, so they don't represent
    # real coins.
    
    @internal
    @view
    def find_block_epoch(_block: uint256, max_epoch: uint256) -> uint256:
        """
        @notice Binary search to estimate timestamp for block number
        @param _block Block to find
        @param max_epoch Don't go beyond this epoch
        @return Approximate timestamp for block
        """
        # Binary search
        _min: uint256 = 0
        _max: uint256 = max_epoch
        for i in range(128):  # Will be always enough for 128-bit numbers
            if _min >= _max:
                break
            _mid: uint256 = (_min + _max + 1) / 2
            if self.point_history[_mid].blk <= _block:
                _min = _mid
            else:
                _max = _mid - 1
        return _min
    
    
    @external
    @view
    def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256:
        """
        @notice Get the current voting power for `msg.sender`
        @dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility
        @param addr User wallet address
        @param _t Epoch time to return voting power at
        @return User voting power
        """
        _epoch: uint256 = self.user_point_epoch[addr]
        if _epoch == 0:
            return 0
        else:
            last_point: Point = self.user_point_history[addr][_epoch]
            last_point.bias -= last_point.slope * convert(_t - last_point.ts, int128)
            if last_point.bias < 0:
                last_point.bias = 0
            return convert(last_point.bias, uint256)
    
    
    @external
    @view
    def balanceOfAt(addr: address, _block: uint256) -> uint256:
        """
        @notice Measure voting power of `addr` at block height `_block`
        @dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime
        @param addr User's wallet address
        @param _block Block to calculate the voting power at
        @return Voting power
        """
        # Copying and pasting totalSupply code because Vyper cannot pass by
        # reference yet
        assert _block <= block.number
    
        # Binary search
        _min: uint256 = 0
        _max: uint256 = self.user_point_epoch[addr]
        for i in range(128):  # Will be always enough for 128-bit numbers
            if _min >= _max:
                break
            _mid: uint256 = (_min + _max + 1) / 2
            if self.user_point_history[addr][_mid].blk <= _block:
                _min = _mid
            else:
                _max = _mid - 1
    
        upoint: Point = self.user_point_history[addr][_min]
    
        max_epoch: uint256 = self.epoch
        _epoch: uint256 = self.find_block_epoch(_block, max_epoch)
        point_0: Point = self.point_history[_epoch]
        d_block: uint256 = 0
        d_t: uint256 = 0
        if _epoch < max_epoch:
            point_1: Point = self.point_history[_epoch + 1]
            d_block = point_1.blk - point_0.blk
            d_t = point_1.ts - point_0.ts
        else:
            d_block = block.number - point_0.blk
            d_t = block.timestamp - point_0.ts
        block_time: uint256 = point_0.ts
        if d_block != 0:
            block_time += d_t * (_block - point_0.blk) / d_block
    
        upoint.bias -= upoint.slope * convert(block_time - upoint.ts, int128)
        if upoint.bias >= 0:
            return convert(upoint.bias, uint256)
        else:
            return 0
    
    
    @internal
    @view
    def supply_at(point: Point, t: uint256) -> uint256:
        """
        @notice Calculate total voting power at some point in the past
        @param point The point (bias/slope) to start search from
        @param t Time to calculate the total voting power at
        @return Total voting power at that time
        """
        last_point: Point = point
        t_i: uint256 = (last_point.ts / WEEK) * WEEK
        for i in range(255):
            t_i += WEEK
            d_slope: int128 = 0
            if t_i > t:
                t_i = t
            else:
                d_slope = self.slope_changes[t_i]
            last_point.bias -= last_point.slope * convert(t_i - last_point.ts, int128)
            if t_i == t:
                break
            last_point.slope += d_slope
            last_point.ts = t_i
    
        if last_point.bias < 0:
            last_point.bias = 0
        return convert(last_point.bias, uint256)
    
    
    @external
    @view
    def totalSupply(t: uint256 = block.timestamp) -> uint256:
        """
        @notice Calculate total voting power
        @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility
        @return Total voting power
        """
        _epoch: uint256 = self.epoch
        last_point: Point = self.point_history[_epoch]
        return self.supply_at(last_point, t)
    
    
    @external
    @view
    def totalSupplyAt(_block: uint256) -> uint256:
        """
        @notice Calculate total voting power at some point in the past
        @param _block Block to calculate the total voting power at
        @return Total voting power at `_block`
        """
        assert _block <= block.number
        _epoch: uint256 = self.epoch
        target_epoch: uint256 = self.find_block_epoch(_block, _epoch)
    
        point: Point = self.point_history[target_epoch]
        dt: uint256 = 0
        if target_epoch < _epoch:
            point_next: Point = self.point_history[target_epoch + 1]
            if point.blk != point_next.blk:
                dt = (_block - point.blk) * (point_next.ts - point.ts) / (point_next.blk - point.blk)
        else:
            if point.blk != block.number:
                dt = (_block - point.blk) * (block.timestamp - point.ts) / (block.number - point.blk)
        # Now dt contains info on how far are we beyond point
    
        return self.supply_at(point, point.ts + dt)
    
    
    # Dummy methods for compatibility with Aragon
    
    @external
    def changeController(_newController: address):
        """
        @dev Dummy method required for Aragon compatibility
        """
        assert msg.sender == self.controller
        self.controller = _newController

    File 2 of 2: Vyper_contract
    # @version 0.2.4
    """
    @title Curve DAO Token
    @author Curve Finance
    @license MIT
    @notice ERC20 with piecewise-linear mining supply.
    @dev Based on the ERC-20 token standard as defined at
         https://eips.ethereum.org/EIPS/eip-20
    """
    
    from vyper.interfaces import ERC20
    
    implements: ERC20
    
    
    event Transfer:
        _from: indexed(address)
        _to: indexed(address)
        _value: uint256
    
    event Approval:
        _owner: indexed(address)
        _spender: indexed(address)
        _value: uint256
    
    event UpdateMiningParameters:
        time: uint256
        rate: uint256
        supply: uint256
    
    event SetMinter:
        minter: address
    
    event SetAdmin:
        admin: address
    
    
    name: public(String[64])
    symbol: public(String[32])
    decimals: public(uint256)
    
    balanceOf: public(HashMap[address, uint256])
    allowances: HashMap[address, HashMap[address, uint256]]
    total_supply: uint256
    
    minter: public(address)
    admin: public(address)
    
    # General constants
    YEAR: constant(uint256) = 86400 * 365
    
    # Allocation:
    # =========
    # * shareholders - 30%
    # * emplyees - 3%
    # * DAO-controlled reserve - 5%
    # * Early users - 5%
    # == 43% ==
    # left for inflation: 57%
    
    # Supply parameters
    INITIAL_SUPPLY: constant(uint256) = 1_303_030_303
    INITIAL_RATE: constant(uint256) = 274_815_283 * 10 ** 18 / YEAR  # leading to 43% premine
    RATE_REDUCTION_TIME: constant(uint256) = YEAR
    RATE_REDUCTION_COEFFICIENT: constant(uint256) = 1189207115002721024  # 2 ** (1/4) * 1e18
    RATE_DENOMINATOR: constant(uint256) = 10 ** 18
    INFLATION_DELAY: constant(uint256) = 86400
    
    # Supply variables
    mining_epoch: public(int128)
    start_epoch_time: public(uint256)
    rate: public(uint256)
    
    start_epoch_supply: uint256
    
    
    @external
    def __init__(_name: String[64], _symbol: String[32], _decimals: uint256):
        """
        @notice Contract constructor
        @param _name Token full name
        @param _symbol Token symbol
        @param _decimals Number of decimals for token
        """
        init_supply: uint256 = INITIAL_SUPPLY * 10 ** _decimals
        self.name = _name
        self.symbol = _symbol
        self.decimals = _decimals
        self.balanceOf[msg.sender] = init_supply
        self.total_supply = init_supply
        self.admin = msg.sender
        log Transfer(ZERO_ADDRESS, msg.sender, init_supply)
    
        self.start_epoch_time = block.timestamp + INFLATION_DELAY - RATE_REDUCTION_TIME
        self.mining_epoch = -1
        self.rate = 0
        self.start_epoch_supply = init_supply
    
    
    @internal
    def _update_mining_parameters():
        """
        @dev Update mining rate and supply at the start of the epoch
             Any modifying mining call must also call this
        """
        _rate: uint256 = self.rate
        _start_epoch_supply: uint256 = self.start_epoch_supply
    
        self.start_epoch_time += RATE_REDUCTION_TIME
        self.mining_epoch += 1
    
        if _rate == 0:
            _rate = INITIAL_RATE
        else:
            _start_epoch_supply += _rate * RATE_REDUCTION_TIME
            self.start_epoch_supply = _start_epoch_supply
            _rate = _rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
    
        self.rate = _rate
    
        log UpdateMiningParameters(block.timestamp, _rate, _start_epoch_supply)
    
    
    @external
    def update_mining_parameters():
        """
        @notice Update mining rate and supply at the start of the epoch
        @dev Callable by any address, but only once per epoch
             Total supply becomes slightly larger if this function is called late
        """
        assert block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME  # dev: too soon!
        self._update_mining_parameters()
    
    
    @external
    def start_epoch_time_write() -> uint256:
        """
        @notice Get timestamp of the current mining epoch start
                while simultaneously updating mining parameters
        @return Timestamp of the epoch
        """
        _start_epoch_time: uint256 = self.start_epoch_time
        if block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME:
            self._update_mining_parameters()
            return self.start_epoch_time
        else:
            return _start_epoch_time
    
    
    @external
    def future_epoch_time_write() -> uint256:
        """
        @notice Get timestamp of the next mining epoch start
                while simultaneously updating mining parameters
        @return Timestamp of the next epoch
        """
        _start_epoch_time: uint256 = self.start_epoch_time
        if block.timestamp >= _start_epoch_time + RATE_REDUCTION_TIME:
            self._update_mining_parameters()
            return self.start_epoch_time + RATE_REDUCTION_TIME
        else:
            return _start_epoch_time + RATE_REDUCTION_TIME
    
    
    @internal
    @view
    def _available_supply() -> uint256:
        return self.start_epoch_supply + (block.timestamp - self.start_epoch_time) * self.rate
    
    
    @external
    @view
    def available_supply() -> uint256:
        """
        @notice Current number of tokens in existence (claimed or unclaimed)
        """
        return self._available_supply()
    
    
    @external
    @view
    def mintable_in_timeframe(start: uint256, end: uint256) -> uint256:
        """
        @notice How much supply is mintable from start timestamp till end timestamp
        @param start Start of the time interval (timestamp)
        @param end End of the time interval (timestamp)
        @return Tokens mintable from `start` till `end`
        """
        assert start <= end  # dev: start > end
        to_mint: uint256 = 0
        current_epoch_time: uint256 = self.start_epoch_time
        current_rate: uint256 = self.rate
    
        # Special case if end is in future (not yet minted) epoch
        if end > current_epoch_time + RATE_REDUCTION_TIME:
            current_epoch_time += RATE_REDUCTION_TIME
            current_rate = current_rate * RATE_DENOMINATOR / RATE_REDUCTION_COEFFICIENT
    
        assert end <= current_epoch_time + RATE_REDUCTION_TIME  # dev: too far in future
    
        for i in range(999):  # Curve will not work in 1000 years. Darn!
            if end >= current_epoch_time:
                current_end: uint256 = end
                if current_end > current_epoch_time + RATE_REDUCTION_TIME:
                    current_end = current_epoch_time + RATE_REDUCTION_TIME
    
                current_start: uint256 = start
                if current_start >= current_epoch_time + RATE_REDUCTION_TIME:
                    break  # We should never get here but what if...
                elif current_start < current_epoch_time:
                    current_start = current_epoch_time
    
                to_mint += current_rate * (current_end - current_start)
    
                if start >= current_epoch_time:
                    break
    
            current_epoch_time -= RATE_REDUCTION_TIME
            current_rate = current_rate * RATE_REDUCTION_COEFFICIENT / RATE_DENOMINATOR  # double-division with rounding made rate a bit less => good
            assert current_rate <= INITIAL_RATE  # This should never happen
    
        return to_mint
    
    
    @external
    def set_minter(_minter: address):
        """
        @notice Set the minter address
        @dev Only callable once, when minter has not yet been set
        @param _minter Address of the minter
        """
        assert msg.sender == self.admin  # dev: admin only
        assert self.minter == ZERO_ADDRESS  # dev: can set the minter only once, at creation
        self.minter = _minter
        log SetMinter(_minter)
    
    
    @external
    def set_admin(_admin: address):
        """
        @notice Set the new admin.
        @dev After all is set up, admin only can change the token name
        @param _admin New admin address
        """
        assert msg.sender == self.admin  # dev: admin only
        self.admin = _admin
        log SetAdmin(_admin)
    
    
    @external
    @view
    def totalSupply() -> uint256:
        """
        @notice Total number of tokens in existence.
        """
        return self.total_supply
    
    
    @external
    @view
    def allowance(_owner : address, _spender : address) -> uint256:
        """
        @notice Check the amount of tokens that an owner allowed to a spender
        @param _owner The address which owns the funds
        @param _spender The address which will spend the funds
        @return uint256 specifying the amount of tokens still available for the spender
        """
        return self.allowances[_owner][_spender]
    
    
    @external
    def transfer(_to : address, _value : uint256) -> bool:
        """
        @notice Transfer `_value` tokens from `msg.sender` to `_to`
        @dev Vyper does not allow underflows, so the subtraction in
             this function will revert on an insufficient balance
        @param _to The address to transfer to
        @param _value The amount to be transferred
        @return bool success
        """
        assert _to != ZERO_ADDRESS  # dev: transfers to 0x0 are not allowed
        self.balanceOf[msg.sender] -= _value
        self.balanceOf[_to] += _value
        log Transfer(msg.sender, _to, _value)
        return True
    
    
    @external
    def transferFrom(_from : address, _to : address, _value : uint256) -> bool:
        """
         @notice Transfer `_value` tokens from `_from` to `_to`
         @param _from address The address which you want to send tokens from
         @param _to address The address which you want to transfer to
         @param _value uint256 the amount of tokens to be transferred
         @return bool success
        """
        assert _to != ZERO_ADDRESS  # dev: transfers to 0x0 are not allowed
        # NOTE: vyper does not allow underflows
        #       so the following subtraction would revert on insufficient balance
        self.balanceOf[_from] -= _value
        self.balanceOf[_to] += _value
        self.allowances[_from][msg.sender] -= _value
        log Transfer(_from, _to, _value)
        return True
    
    
    @external
    def approve(_spender : address, _value : uint256) -> bool:
        """
        @notice Approve `_spender` to transfer `_value` tokens on behalf of `msg.sender`
        @dev Approval may only be from zero -> nonzero or from nonzero -> zero in order
            to mitigate the potential race condition described here:
            https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
        @param _spender The address which will spend the funds
        @param _value The amount of tokens to be spent
        @return bool success
        """
        assert _value == 0 or self.allowances[msg.sender][_spender] == 0
        self.allowances[msg.sender][_spender] = _value
        log Approval(msg.sender, _spender, _value)
        return True
    
    
    @external
    def mint(_to: address, _value: uint256) -> bool:
        """
        @notice Mint `_value` tokens and assign them to `_to`
        @dev Emits a Transfer event originating from 0x00
        @param _to The account that will receive the created tokens
        @param _value The amount that will be created
        @return bool success
        """
        assert msg.sender == self.minter  # dev: minter only
        assert _to != ZERO_ADDRESS  # dev: zero address
    
        if block.timestamp >= self.start_epoch_time + RATE_REDUCTION_TIME:
            self._update_mining_parameters()
    
        _total_supply: uint256 = self.total_supply + _value
        assert _total_supply <= self._available_supply()  # dev: exceeds allowable mint amount
        self.total_supply = _total_supply
    
        self.balanceOf[_to] += _value
        log Transfer(ZERO_ADDRESS, _to, _value)
    
        return True
    
    
    @external
    def burn(_value: uint256) -> bool:
        """
        @notice Burn `_value` tokens belonging to `msg.sender`
        @dev Emits a Transfer event with a destination of 0x00
        @param _value The amount that will be burned
        @return bool success
        """
        self.balanceOf[msg.sender] -= _value
        self.total_supply -= _value
    
        log Transfer(msg.sender, ZERO_ADDRESS, _value)
        return True
    
    
    @external
    def set_name(_name: String[64], _symbol: String[32]):
        """
        @notice Change the token name and symbol to `_name` and `_symbol`
        @dev Only callable by the admin account
        @param _name New token name
        @param _symbol New token symbol
        """
        assert msg.sender == self.admin, "Only admin is allowed to change name"
        self.name = _name
        self.symbol = _symbol