ETH Price: $1,975.49 (+0.29%)

Transaction Decoder

Block:
24012126 at Dec-14-2025 04:46:11 PM +UTC
Transaction Fee:
0.000545552171158275 ETH $1.08
Gas Used:
265,905 Gas / 2.051680755 Gwei

Emitted Events:

274 VeAethir.Transfer( from=[Sender] 0xa50ee886c6c904437c8179250c1661691298a89e, to=[Receiver] Voting Escrow, value=298939552663226620156 )
275 Voting Escrow.Deposit( provider=[Sender] 0xa50ee886c6c904437c8179250c1661691298a89e, value=298939552663226620156, locktime=1846454400, type=2, ts=1765730771 )
276 Voting Escrow.Supply( prevSupply=400005202970637134226717438, supply=400005501910189797453337594 )

Account State Difference:

  Address   Before After State Difference Code
0x1B49F587...80e1B7490
(Titan Builder)
17.443258479301897961 Eth17.443790289301897961 Eth0.00053181
0x784BC33B...747D60bc4
0xa50eE886...91298A89E
0.007010754150638321 Eth
Nonce: 54
0.006465201979480046 Eth
Nonce: 55
0.000545552171158275

Execution Trace

Voting.increase_amount( _value=298939552663226620156 )
  • Escrow.increase_amount( _value=298939552663226620156 )
    • VeAethir.transferFrom( from=0xa50eE886C6C904437c8179250c1661691298A89E, to=0x784BC33B9f8fC8e8dE76Dbd3c7b393D747D60bc4, value=298939552663226620156 ) => ( True )
      File 1 of 3: Voting Escrow
      # @version 0.3.7
      
      """
      @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` (set by creator).
      """
      
      # 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
      
      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 balanceOf(account: address) -> uint256: view
          def transfer(to: address, amount: uint256) -> bool: nonpayable
          def approve(spender: 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
      
      interface BalancerMinter:
          def mint(gauge: address) -> uint256: nonpayable
      
      interface RewardDistributor:
          def depositToken(token: address, amount: uint256): 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 EarlyUnlock:
          status: bool
      
      event PenaltySpeed:
          penalty_k: uint256
      
      event PenaltyTreasury:
          penalty_treasury: address
      
      event TotalUnlock:
          status: bool
      
      event RewardReceiver:
          newReceiver: 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 WithdrawEarly:
          provider: indexed(address)
          penalty: uint256
          time_left: uint256
      
      event Supply:
          prevSupply: uint256
          supply: uint256
      
      
      WEEK: constant(uint256) = 7 * 86400  # all future times are rounded by week
      MAXTIME: public(uint256)
      MULTIPLIER: constant(uint256) = 10**18
      
      TOKEN: public(address)
      
      NAME: String[64]
      SYMBOL: String[32]
      DECIMALS: uint256
      
      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
      
      # 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)
      
      # unlock admins can be set only once. Zero-address means unlock is disabled
      admin_unlock_all: public(address)
      admin_early_unlock: public(address)
      
      future_admin: public(address)
      
      is_initialized: public(bool)
      
      early_unlock: public(bool)
      penalty_k: public(uint256)
      prev_penalty_k: public(uint256)
      penalty_upd_ts: public(uint256)
      PENALTY_COOLDOWN: constant(uint256) = 60 # cooldown to prevent font-run on penalty change
      PENALTY_MULTIPLIER: constant(uint256) = 10
      
      penalty_treasury: public(address)
      
      balMinter: public(address)
      balToken: public(address)
      rewardReceiver: public(address)
      rewardReceiverChangeable: public(bool)
      
      rewardDistributor: public(address)
      
      all_unlock: public(bool)
      
      
      @external
      def initialize(
          _token_addr: address,
          _name: String[64],
          _symbol: String[32],
          _admin_addr: address,
          _admin_unlock_all: address,
          _admin_early_unlock: address,
          _max_time: uint256,
          _balToken: address,
          _balMinter: address,
          _rewardReceiver: address,
          _rewardReceiverChangeable: bool,
          _rewardDistributor: address
      ):
          """
          @notice Contract constructor
          @param _token_addr 80/20 Token-WETH BPT token address
          @param _name Token name
          @param _symbol Token symbol
          @param _admin_addr Contract admin address
          @param _admin_unlock_all Admin to enable Unlock-All feature (zero-address to disable forever)
          @param _admin_early_unlock Admin to enable Eraly-Unlock feature (zero-address to disable forever)
          @param _max_time Locking max time
          @param _balToken Address of the Balancer token
          @param _balMinter Address of the Balancer minter
          @param _rewardReceiver Address of the reward receiver
          @param _rewardReceiverChangeable Boolean indicating whether the reward receiver is changeable
          @param _rewardDistributor The RewardDistributor contract address
          """
      
          assert(not self.is_initialized), 'only once'
          self.is_initialized = True
      
          assert(_admin_addr != empty(address)), '!empty'
          self.admin = _admin_addr
      
          self.penalty_k = 10
          self.prev_penalty_k = 10
          self.penalty_upd_ts = block.timestamp
          self.penalty_treasury = _admin_addr
      
          self.TOKEN = _token_addr
          self.point_history[0].blk = block.number
          self.point_history[0].ts = block.timestamp
      
          _decimals: uint256 = ERC20(_token_addr).decimals()  # also validates token for non-zero
          assert (_decimals >= 6 and _decimals <= 255), '!decimals'
      
          self.NAME = _name
          self.SYMBOL = _symbol
          self.DECIMALS = _decimals
      
          assert(_max_time >= WEEK and _max_time <= WEEK * 52 * 5), '!maxlock'
          self.MAXTIME = _max_time
      
          self.admin_unlock_all = _admin_unlock_all
          self.admin_early_unlock = _admin_early_unlock
      
          self.balToken = _balToken
          self.balMinter = _balMinter
          self.rewardReceiver = _rewardReceiver
          self.rewardReceiverChangeable = _rewardReceiverChangeable
          self.rewardDistributor = _rewardDistributor
      
      
      @external
      @view
      def token() -> address:
          return self.TOKEN
      
      @external
      @view
      def name() -> String[64]:
          return self.NAME
      
      @external
      @view
      def symbol() -> String[32]:
          return self.SYMBOL
      
      @external
      @view
      def decimals() -> uint256:
          return self.DECIMALS
      
      @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 != empty(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 != empty(address):
                  if SmartWalletChecker(checker).check(addr):
                      return
              raise "Smart contract depositors not allowed"
      
      
      @external
      def set_early_unlock(_early_unlock: bool):
          """
          @notice Sets the availability for users to unlock their locks before lock-end with penalty
          @dev Only the admin_early_unlock can execute this function.
          @param _early_unlock A boolean indicating whether early unlock is allowed or not.
          """
          assert msg.sender == self.admin_early_unlock, '!admin'  # dev: admin_early_unlock only
          assert _early_unlock != self.early_unlock, 'already'
          
          self.early_unlock = _early_unlock
          log EarlyUnlock(_early_unlock)
      
      
      @external
      def set_early_unlock_penalty_speed(_penalty_k: uint256):
          """
          @notice Sets penalty speed for early unlocking
          @dev Only the admin can execute this function. To prevent frontrunning we use PENALTY_COOLDOWN period
          @param _penalty_k Coefficient indicating the penalty speed for early unlock.
                            Must be between 0 and 50, inclusive. Default 10 - means linear speed.
          """
          assert msg.sender == self.admin_early_unlock, '!admin'  # dev: admin_early_unlock only
          assert _penalty_k <= 50, '!k'
          assert block.timestamp > self.penalty_upd_ts + PENALTY_COOLDOWN, 'early' # to avoid frontrun
      
          self.prev_penalty_k = self.penalty_k
          self.penalty_k = _penalty_k
          self.penalty_upd_ts = block.timestamp
      
          log PenaltySpeed(_penalty_k)
      
      
      @external
      def set_penalty_treasury(_penalty_treasury: address):
          """
          @notice Sets penalty treasury address
          @dev Only the admin_early_unlock can execute this function.
          @param _penalty_treasury The address to collect early penalty (default admin address)
          """
          assert msg.sender == self.admin_early_unlock, '!admin'  # dev: admin_early_unlock only
          assert _penalty_treasury != empty(address), '!zero'
         
          self.penalty_treasury = _penalty_treasury
          log PenaltyTreasury(_penalty_treasury)
      
      
      @external
      def set_all_unlock():
          """
          @notice Deactivates VotingEscrow and allows users to unlock their locks before lock-end. 
                  New deposits will no longer be accepted.
          @dev Only the admin_unlock_all can execute this function. Make sure there are no rewards for distribution in other contracts.
          """
          assert msg.sender == self.admin_unlock_all, '!admin'  # dev: admin_unlock_all only
          self.all_unlock = True
          log TotalUnlock(True)
      
      
      @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 != empty(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 / convert(self.MAXTIME, int128)
                  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 / convert(self.MAXTIME, int128)
                  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 != empty(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 != empty(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
          """
          # block all new deposits (and extensions) in case of unlocked contract
          assert (not self.all_unlock), "all unlocked,no sense"
      
          _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, default_return_value=True)
      
          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(empty(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 + self.MAXTIME), "Voting lock too long"
      
          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 + self.MAXTIME), "Voting lock too long"
      
          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 or self.all_unlock, "lock !expire or !unlock"
          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, default_return_value=True)
      
          log Withdraw(msg.sender, value, block.timestamp)
          log Supply(supply_before, supply_before - value)
      
      
      @external
      @nonreentrant("lock")
      def withdraw_early():
          """
          @notice Withdraws locked tokens for `msg.sender` before lock-end with penalty
          @dev Only possible if `early_unlock` is enabled (true)
          By defualt there is linear formula for calculating penalty. 
          In some cases an admin can configure penalty speed using `set_early_unlock_penalty_speed()`
          
          L - lock amount
          k - penalty coefficient, defined by admin (default 1)
          Tleft - left time to unlock
          Tmax - MAXLOCK time
          Penalty amount = L * k * (Tlast / Tmax)
          """
          assert(self.early_unlock == True), "!early unlock"
      
          _locked: LockedBalance = self.locked[msg.sender]
          assert block.timestamp < _locked.end, "lock expired"
      
          value: uint256 = convert(_locked.amount, uint256)
      
          time_left: uint256 = _locked.end - block.timestamp
          
          # to avoid front-run with penalty_k
          penalty_k_: uint256 = 0
          if block.timestamp > self.penalty_upd_ts + PENALTY_COOLDOWN:
              penalty_k_ = self.penalty_k
          else:
              penalty_k_ = self.prev_penalty_k
      
          penalty_ratio: uint256 = (time_left * MULTIPLIER / self.MAXTIME) * penalty_k_
          penalty: uint256 = (value * penalty_ratio / MULTIPLIER) / PENALTY_MULTIPLIER    
          if penalty > value:
              penalty = value
          user_amount: uint256 = value - penalty
      
          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)
      
          if penalty > 0:
              assert ERC20(self.TOKEN).transfer(self.penalty_treasury, penalty, default_return_value=True)
          if user_amount > 0:
              assert ERC20(self.TOKEN).transfer(msg.sender, user_amount, default_return_value=True)
      
          log Withdraw(msg.sender, value, block.timestamp)
          log Supply(supply_before, supply_before - value)
          log WithdrawEarly(msg.sender, penalty, time_left)
      
      
      # 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 find epoch containing block number
          @param _block Block to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _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
      
      @internal
      @view
      def find_timestamp_epoch(_timestamp: uint256, max_epoch: uint256) -> uint256:
          """
          @notice Binary search to find epoch for timestamp
          @param _timestamp timestamp to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _timestamp
          """
          # 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].ts <= _timestamp:
                  _min = _mid
              else:
                  _max = _mid - 1
          return _min
      
      
      @internal
      @view
      def find_block_user_epoch(_addr: address, _block: uint256, max_epoch: uint256) -> uint256:
          """
          @notice Binary search to find epoch for block number
          @param _addr User for which to find user epoch for
          @param _block Block to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _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.user_point_history[_addr][_mid].blk <= _block:
                  _min = _mid
              else:
                  _max = _mid - 1
          return _min
      
      
      @internal
      @view
      def find_timestamp_user_epoch(_addr: address, _timestamp: uint256, max_epoch: uint256) -> uint256:
          """
          @notice Binary search to find user epoch for timestamp
          @param _addr User for which to find user epoch for
          @param _timestamp timestamp to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _timestamp
          """
          # 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.user_point_history[_addr][_mid].ts <= _timestamp:
                  _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 = 0
          if _t == block.timestamp:
              # No need to do binary search, will always live in current epoch
              _epoch = self.user_point_epoch[addr]
          else:
              _epoch = self.find_timestamp_user_epoch(addr, _t, 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
      
          _user_epoch: uint256 = self.find_block_user_epoch(addr, _block, self.user_point_epoch[addr])
          upoint: Point = self.user_point_history[addr][_user_epoch]
      
          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 = 0
          if t == block.timestamp:
              # No need to do binary search, will always live in current epoch
              _epoch = self.epoch
          else:
              _epoch = self.find_timestamp_epoch(t, self.epoch)
      
          if _epoch == 0:
              return 0
          else:
              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)
      
      @external
      @nonreentrant("lock")
      def claimExternalRewards():
          """
          @notice Claims BAL rewards
          @dev Only possible if the TOKEN is Guage contract
          """
          BalancerMinter(self.balMinter).mint(self.TOKEN)
          balBalance: uint256 = ERC20(self.balToken).balanceOf(self)
          if balBalance > 0:
              # distributes rewards using rewardDistributor into current week
              if self.rewardReceiver == self.rewardDistributor:
                  assert ERC20(self.balToken).approve(self.rewardDistributor, balBalance, default_return_value=True)
                  RewardDistributor(self.rewardDistributor).depositToken(self.balToken, balBalance)
              else:
                  assert ERC20(self.balToken).transfer(self.rewardReceiver, balBalance, default_return_value=True)
      
      
      @external
      def changeRewardReceiver(newReceiver: address):
          """
          @notice Changes the reward receiver address
          @param newReceiver New address to set as the reward receiver
          """
          assert msg.sender == self.admin, '!admin'
          assert (self.rewardReceiverChangeable), '!available'
          assert newReceiver != empty(address), '!empty'
      
          self.rewardReceiver = newReceiver
          log RewardReceiver(newReceiver)

      File 2 of 3: VeAethir
      // SPDX-License-Identifier: MIT
      // OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
      pragma solidity ^0.8.20;
      import {Context} from "../utils/Context.sol";
      /**
       * @dev Contract module which provides a basic access control mechanism, where
       * there is an account (an owner) that can be granted exclusive access to
       * specific functions.
       *
       * The initial owner is set to the address provided by the deployer. This can
       * later be changed with {transferOwnership}.
       *
       * This module is used through inheritance. It will make available the modifier
       * `onlyOwner`, which can be applied to your functions to restrict their use to
       * the owner.
       */
      abstract contract Ownable is Context {
          address private _owner;
          /**
           * @dev The caller account is not authorized to perform an operation.
           */
          error OwnableUnauthorizedAccount(address account);
          /**
           * @dev The owner is not a valid owner account. (eg. `address(0)`)
           */
          error OwnableInvalidOwner(address owner);
          event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
          /**
           * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
           */
          constructor(address initialOwner) {
              if (initialOwner == address(0)) {
                  revert OwnableInvalidOwner(address(0));
              }
              _transferOwnership(initialOwner);
          }
          /**
           * @dev Throws if called by any account other than the owner.
           */
          modifier onlyOwner() {
              _checkOwner();
              _;
          }
          /**
           * @dev Returns the address of the current owner.
           */
          function owner() public view virtual returns (address) {
              return _owner;
          }
          /**
           * @dev Throws if the sender is not the owner.
           */
          function _checkOwner() internal view virtual {
              if (owner() != _msgSender()) {
                  revert OwnableUnauthorizedAccount(_msgSender());
              }
          }
          /**
           * @dev Leaves the contract without owner. It will not be possible to call
           * `onlyOwner` functions. Can only be called by the current owner.
           *
           * NOTE: Renouncing ownership will leave the contract without an owner,
           * thereby disabling any functionality that is only available to the owner.
           */
          function renounceOwnership() public virtual onlyOwner {
              _transferOwnership(address(0));
          }
          /**
           * @dev Transfers ownership of the contract to a new account (`newOwner`).
           * Can only be called by the current owner.
           */
          function transferOwnership(address newOwner) public virtual onlyOwner {
              if (newOwner == address(0)) {
                  revert OwnableInvalidOwner(address(0));
              }
              _transferOwnership(newOwner);
          }
          /**
           * @dev Transfers ownership of the contract to a new account (`newOwner`).
           * Internal function without access restriction.
           */
          function _transferOwnership(address newOwner) internal virtual {
              address oldOwner = _owner;
              _owner = newOwner;
              emit OwnershipTransferred(oldOwner, newOwner);
          }
      }
      // SPDX-License-Identifier: MIT
      // OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol)
      pragma solidity ^0.8.20;
      /**
       * @dev Standard ERC20 Errors
       * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC20 tokens.
       */
      interface IERC20Errors {
          /**
           * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
           * @param sender Address whose tokens are being transferred.
           * @param balance Current balance for the interacting account.
           * @param needed Minimum amount required to perform a transfer.
           */
          error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
          /**
           * @dev Indicates a failure with the token `sender`. Used in transfers.
           * @param sender Address whose tokens are being transferred.
           */
          error ERC20InvalidSender(address sender);
          /**
           * @dev Indicates a failure with the token `receiver`. Used in transfers.
           * @param receiver Address to which tokens are being transferred.
           */
          error ERC20InvalidReceiver(address receiver);
          /**
           * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers.
           * @param spender Address that may be allowed to operate on tokens without being their owner.
           * @param allowance Amount of tokens a `spender` is allowed to operate with.
           * @param needed Minimum amount required to perform a transfer.
           */
          error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
          /**
           * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
           * @param approver Address initiating an approval operation.
           */
          error ERC20InvalidApprover(address approver);
          /**
           * @dev Indicates a failure with the `spender` to be approved. Used in approvals.
           * @param spender Address that may be allowed to operate on tokens without being their owner.
           */
          error ERC20InvalidSpender(address spender);
      }
      /**
       * @dev Standard ERC721 Errors
       * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC721 tokens.
       */
      interface IERC721Errors {
          /**
           * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20.
           * Used in balance queries.
           * @param owner Address of the current owner of a token.
           */
          error ERC721InvalidOwner(address owner);
          /**
           * @dev Indicates a `tokenId` whose `owner` is the zero address.
           * @param tokenId Identifier number of a token.
           */
          error ERC721NonexistentToken(uint256 tokenId);
          /**
           * @dev Indicates an error related to the ownership over a particular token. Used in transfers.
           * @param sender Address whose tokens are being transferred.
           * @param tokenId Identifier number of a token.
           * @param owner Address of the current owner of a token.
           */
          error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);
          /**
           * @dev Indicates a failure with the token `sender`. Used in transfers.
           * @param sender Address whose tokens are being transferred.
           */
          error ERC721InvalidSender(address sender);
          /**
           * @dev Indicates a failure with the token `receiver`. Used in transfers.
           * @param receiver Address to which tokens are being transferred.
           */
          error ERC721InvalidReceiver(address receiver);
          /**
           * @dev Indicates a failure with the `operator`’s approval. Used in transfers.
           * @param operator Address that may be allowed to operate on tokens without being their owner.
           * @param tokenId Identifier number of a token.
           */
          error ERC721InsufficientApproval(address operator, uint256 tokenId);
          /**
           * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
           * @param approver Address initiating an approval operation.
           */
          error ERC721InvalidApprover(address approver);
          /**
           * @dev Indicates a failure with the `operator` to be approved. Used in approvals.
           * @param operator Address that may be allowed to operate on tokens without being their owner.
           */
          error ERC721InvalidOperator(address operator);
      }
      /**
       * @dev Standard ERC1155 Errors
       * Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC1155 tokens.
       */
      interface IERC1155Errors {
          /**
           * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers.
           * @param sender Address whose tokens are being transferred.
           * @param balance Current balance for the interacting account.
           * @param needed Minimum amount required to perform a transfer.
           * @param tokenId Identifier number of a token.
           */
          error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);
          /**
           * @dev Indicates a failure with the token `sender`. Used in transfers.
           * @param sender Address whose tokens are being transferred.
           */
          error ERC1155InvalidSender(address sender);
          /**
           * @dev Indicates a failure with the token `receiver`. Used in transfers.
           * @param receiver Address to which tokens are being transferred.
           */
          error ERC1155InvalidReceiver(address receiver);
          /**
           * @dev Indicates a failure with the `operator`’s approval. Used in transfers.
           * @param operator Address that may be allowed to operate on tokens without being their owner.
           * @param owner Address of the current owner of a token.
           */
          error ERC1155MissingApprovalForAll(address operator, address owner);
          /**
           * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals.
           * @param approver Address initiating an approval operation.
           */
          error ERC1155InvalidApprover(address approver);
          /**
           * @dev Indicates a failure with the `operator` to be approved. Used in approvals.
           * @param operator Address that may be allowed to operate on tokens without being their owner.
           */
          error ERC1155InvalidOperator(address operator);
          /**
           * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation.
           * Used in batch transfers.
           * @param idsLength Length of the array of token identifiers
           * @param valuesLength Length of the array of token amounts
           */
          error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);
      }
      // SPDX-License-Identifier: MIT
      // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/ERC20.sol)
      pragma solidity ^0.8.20;
      import {IERC20} from "./IERC20.sol";
      import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
      import {Context} from "../../utils/Context.sol";
      import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
      /**
       * @dev Implementation of the {IERC20} interface.
       *
       * This implementation is agnostic to the way tokens are created. This means
       * that a supply mechanism has to be added in a derived contract using {_mint}.
       *
       * TIP: For a detailed writeup see our guide
       * https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How
       * to implement supply mechanisms].
       *
       * The default value of {decimals} is 18. To change this, you should override
       * this function so it returns a different value.
       *
       * We have followed general OpenZeppelin Contracts guidelines: functions revert
       * instead returning `false` on failure. This behavior is nonetheless
       * conventional and does not conflict with the expectations of ERC20
       * applications.
       *
       * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
       * This allows applications to reconstruct the allowance for all accounts just
       * by listening to said events. Other implementations of the EIP may not emit
       * these events, as it isn't required by the specification.
       */
      abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
          mapping(address account => uint256) private _balances;
          mapping(address account => mapping(address spender => uint256)) private _allowances;
          uint256 private _totalSupply;
          string private _name;
          string private _symbol;
          /**
           * @dev Sets the values for {name} and {symbol}.
           *
           * All two of these values are immutable: they can only be set once during
           * construction.
           */
          constructor(string memory name_, string memory symbol_) {
              _name = name_;
              _symbol = symbol_;
          }
          /**
           * @dev Returns the name of the token.
           */
          function name() public view virtual returns (string memory) {
              return _name;
          }
          /**
           * @dev Returns the symbol of the token, usually a shorter version of the
           * name.
           */
          function symbol() public view virtual returns (string memory) {
              return _symbol;
          }
          /**
           * @dev Returns the number of decimals used to get its user representation.
           * For example, if `decimals` equals `2`, a balance of `505` tokens should
           * be displayed to a user as `5.05` (`505 / 10 ** 2`).
           *
           * Tokens usually opt for a value of 18, imitating the relationship between
           * Ether and Wei. This is the default value returned by this function, unless
           * it's overridden.
           *
           * NOTE: This information is only used for _display_ purposes: it in
           * no way affects any of the arithmetic of the contract, including
           * {IERC20-balanceOf} and {IERC20-transfer}.
           */
          function decimals() public view virtual returns (uint8) {
              return 18;
          }
          /**
           * @dev See {IERC20-totalSupply}.
           */
          function totalSupply() public view virtual returns (uint256) {
              return _totalSupply;
          }
          /**
           * @dev See {IERC20-balanceOf}.
           */
          function balanceOf(address account) public view virtual returns (uint256) {
              return _balances[account];
          }
          /**
           * @dev See {IERC20-transfer}.
           *
           * Requirements:
           *
           * - `to` cannot be the zero address.
           * - the caller must have a balance of at least `value`.
           */
          function transfer(address to, uint256 value) public virtual returns (bool) {
              address owner = _msgSender();
              _transfer(owner, to, value);
              return true;
          }
          /**
           * @dev See {IERC20-allowance}.
           */
          function allowance(address owner, address spender) public view virtual returns (uint256) {
              return _allowances[owner][spender];
          }
          /**
           * @dev See {IERC20-approve}.
           *
           * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on
           * `transferFrom`. This is semantically equivalent to an infinite approval.
           *
           * Requirements:
           *
           * - `spender` cannot be the zero address.
           */
          function approve(address spender, uint256 value) public virtual returns (bool) {
              address owner = _msgSender();
              _approve(owner, spender, value);
              return true;
          }
          /**
           * @dev See {IERC20-transferFrom}.
           *
           * Emits an {Approval} event indicating the updated allowance. This is not
           * required by the EIP. See the note at the beginning of {ERC20}.
           *
           * NOTE: Does not update the allowance if the current allowance
           * is the maximum `uint256`.
           *
           * Requirements:
           *
           * - `from` and `to` cannot be the zero address.
           * - `from` must have a balance of at least `value`.
           * - the caller must have allowance for ``from``'s tokens of at least
           * `value`.
           */
          function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
              address spender = _msgSender();
              _spendAllowance(from, spender, value);
              _transfer(from, to, value);
              return true;
          }
          /**
           * @dev Moves a `value` amount of tokens from `from` to `to`.
           *
           * This internal function is equivalent to {transfer}, and can be used to
           * e.g. implement automatic token fees, slashing mechanisms, etc.
           *
           * Emits a {Transfer} event.
           *
           * NOTE: This function is not virtual, {_update} should be overridden instead.
           */
          function _transfer(address from, address to, uint256 value) internal {
              if (from == address(0)) {
                  revert ERC20InvalidSender(address(0));
              }
              if (to == address(0)) {
                  revert ERC20InvalidReceiver(address(0));
              }
              _update(from, to, value);
          }
          /**
           * @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
           * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
           * this function.
           *
           * Emits a {Transfer} event.
           */
          function _update(address from, address to, uint256 value) internal virtual {
              if (from == address(0)) {
                  // Overflow check required: The rest of the code assumes that totalSupply never overflows
                  _totalSupply += value;
              } else {
                  uint256 fromBalance = _balances[from];
                  if (fromBalance < value) {
                      revert ERC20InsufficientBalance(from, fromBalance, value);
                  }
                  unchecked {
                      // Overflow not possible: value <= fromBalance <= totalSupply.
                      _balances[from] = fromBalance - value;
                  }
              }
              if (to == address(0)) {
                  unchecked {
                      // Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
                      _totalSupply -= value;
                  }
              } else {
                  unchecked {
                      // Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
                      _balances[to] += value;
                  }
              }
              emit Transfer(from, to, value);
          }
          /**
           * @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
           * Relies on the `_update` mechanism
           *
           * Emits a {Transfer} event with `from` set to the zero address.
           *
           * NOTE: This function is not virtual, {_update} should be overridden instead.
           */
          function _mint(address account, uint256 value) internal {
              if (account == address(0)) {
                  revert ERC20InvalidReceiver(address(0));
              }
              _update(address(0), account, value);
          }
          /**
           * @dev Destroys a `value` amount of tokens from `account`, lowering the total supply.
           * Relies on the `_update` mechanism.
           *
           * Emits a {Transfer} event with `to` set to the zero address.
           *
           * NOTE: This function is not virtual, {_update} should be overridden instead
           */
          function _burn(address account, uint256 value) internal {
              if (account == address(0)) {
                  revert ERC20InvalidSender(address(0));
              }
              _update(account, address(0), value);
          }
          /**
           * @dev Sets `value` as the allowance of `spender` over the `owner` s tokens.
           *
           * This internal function is equivalent to `approve`, and can be used to
           * e.g. set automatic allowances for certain subsystems, etc.
           *
           * Emits an {Approval} event.
           *
           * Requirements:
           *
           * - `owner` cannot be the zero address.
           * - `spender` cannot be the zero address.
           *
           * Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument.
           */
          function _approve(address owner, address spender, uint256 value) internal {
              _approve(owner, spender, value, true);
          }
          /**
           * @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
           *
           * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
           * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any
           * `Approval` event during `transferFrom` operations.
           *
           * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to
           * true using the following override:
           * ```
           * function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
           *     super._approve(owner, spender, value, true);
           * }
           * ```
           *
           * Requirements are the same as {_approve}.
           */
          function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
              if (owner == address(0)) {
                  revert ERC20InvalidApprover(address(0));
              }
              if (spender == address(0)) {
                  revert ERC20InvalidSpender(address(0));
              }
              _allowances[owner][spender] = value;
              if (emitEvent) {
                  emit Approval(owner, spender, value);
              }
          }
          /**
           * @dev Updates `owner` s allowance for `spender` based on spent `value`.
           *
           * Does not update the allowance value in case of infinite allowance.
           * Revert if not enough allowance is available.
           *
           * Does not emit an {Approval} event.
           */
          function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
              uint256 currentAllowance = allowance(owner, spender);
              if (currentAllowance != type(uint256).max) {
                  if (currentAllowance < value) {
                      revert ERC20InsufficientAllowance(spender, currentAllowance, value);
                  }
                  unchecked {
                      _approve(owner, spender, currentAllowance - value, false);
                  }
              }
          }
      }
      // SPDX-License-Identifier: MIT
      // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol)
      pragma solidity ^0.8.20;
      import {IERC20} from "../IERC20.sol";
      /**
       * @dev Interface for the optional metadata functions from the ERC20 standard.
       */
      interface IERC20Metadata is IERC20 {
          /**
           * @dev Returns the name of the token.
           */
          function name() external view returns (string memory);
          /**
           * @dev Returns the symbol of the token.
           */
          function symbol() external view returns (string memory);
          /**
           * @dev Returns the decimals places of the token.
           */
          function decimals() external view returns (uint8);
      }
      // SPDX-License-Identifier: MIT
      // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)
      pragma solidity ^0.8.20;
      /**
       * @dev Interface of the ERC20 standard as defined in the EIP.
       */
      interface IERC20 {
          /**
           * @dev Emitted when `value` tokens are moved from one account (`from`) to
           * another (`to`).
           *
           * Note that `value` may be zero.
           */
          event Transfer(address indexed from, address indexed to, uint256 value);
          /**
           * @dev Emitted when the allowance of a `spender` for an `owner` is set by
           * a call to {approve}. `value` is the new allowance.
           */
          event Approval(address indexed owner, address indexed spender, uint256 value);
          /**
           * @dev Returns the value of tokens in existence.
           */
          function totalSupply() external view returns (uint256);
          /**
           * @dev Returns the value of tokens owned by `account`.
           */
          function balanceOf(address account) external view returns (uint256);
          /**
           * @dev Moves a `value` amount of tokens from the caller's account to `to`.
           *
           * Returns a boolean value indicating whether the operation succeeded.
           *
           * Emits a {Transfer} event.
           */
          function transfer(address to, uint256 value) external returns (bool);
          /**
           * @dev Returns the remaining number of tokens that `spender` will be
           * allowed to spend on behalf of `owner` through {transferFrom}. This is
           * zero by default.
           *
           * This value changes when {approve} or {transferFrom} are called.
           */
          function allowance(address owner, address spender) external view returns (uint256);
          /**
           * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
           * caller's tokens.
           *
           * Returns a boolean value indicating whether the operation succeeded.
           *
           * IMPORTANT: Beware that changing an allowance with this method brings the risk
           * that someone may use both the old and the new allowance by unfortunate
           * transaction ordering. One possible solution to mitigate this race
           * condition is to first reduce the spender's allowance to 0 and set the
           * desired value afterwards:
           * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
           *
           * Emits an {Approval} event.
           */
          function approve(address spender, uint256 value) external returns (bool);
          /**
           * @dev Moves a `value` amount of tokens from `from` to `to` using the
           * allowance mechanism. `value` is then deducted from the caller's
           * allowance.
           *
           * Returns a boolean value indicating whether the operation succeeded.
           *
           * Emits a {Transfer} event.
           */
          function transferFrom(address from, address to, uint256 value) external returns (bool);
      }
      // SPDX-License-Identifier: MIT
      // OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
      pragma solidity ^0.8.20;
      /**
       * @dev Provides information about the current execution context, including the
       * sender of the transaction and its data. While these are generally available
       * via msg.sender and msg.data, they should not be accessed in such a direct
       * manner, since when dealing with meta-transactions the account sending and
       * paying for execution may not be the actual sender (as far as an application
       * is concerned).
       *
       * This contract is only required for intermediate, library-like contracts.
       */
      abstract contract Context {
          function _msgSender() internal view virtual returns (address) {
              return msg.sender;
          }
          function _msgData() internal view virtual returns (bytes calldata) {
              return msg.data;
          }
          function _contextSuffixLength() internal view virtual returns (uint256) {
              return 0;
          }
      }
      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.24;
      import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
      import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
      /**
       * @title VeAethir
       * @notice ERC20 token representing Aethir token that will be used in VotingEscrow.
       * @dev Only the owner can mint and burn tokens.
       */
      contract VeAethir is ERC20, Ownable {
          /**
           * @dev Constructor that initializes the ERC20 token with a name and symbol.
           */
          constructor() ERC20("VotingEscrowed-Aethir", "veAethir") Ownable(msg.sender) {}
          /**
           * @notice Mints `value` tokens to the `account`.
           * @dev Requirements: only the owner can call this function.
           * @param account The address to mint tokens to.
           * @param value The amount of tokens to mint.
           */
          function mint(address account, uint256 value) external onlyOwner {
              _mint(account, value);
          }
          /**
           * @notice Burns `value` tokens from the `account`.
           * @dev Requirements: only the owner can call this function.
           * @param account The address to burn tokens from.
           * @param value The amount of tokens to burn.
           */
          function burn(address account, uint256 value) external onlyOwner {
              _burn(account, value);
          }
      }
      

      File 3 of 3: Voting Escrow
      # @version 0.3.7
      
      """
      @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` (set by creator).
      """
      
      # 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
      
      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 balanceOf(account: address) -> uint256: view
          def transfer(to: address, amount: uint256) -> bool: nonpayable
          def approve(spender: 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
      
      interface BalancerMinter:
          def mint(gauge: address) -> uint256: nonpayable
      
      interface RewardDistributor:
          def depositToken(token: address, amount: uint256): 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 EarlyUnlock:
          status: bool
      
      event PenaltySpeed:
          penalty_k: uint256
      
      event PenaltyTreasury:
          penalty_treasury: address
      
      event TotalUnlock:
          status: bool
      
      event RewardReceiver:
          newReceiver: 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 WithdrawEarly:
          provider: indexed(address)
          penalty: uint256
          time_left: uint256
      
      event Supply:
          prevSupply: uint256
          supply: uint256
      
      
      WEEK: constant(uint256) = 7 * 86400  # all future times are rounded by week
      MAXTIME: public(uint256)
      MULTIPLIER: constant(uint256) = 10**18
      
      TOKEN: public(address)
      
      NAME: String[64]
      SYMBOL: String[32]
      DECIMALS: uint256
      
      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
      
      # 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)
      
      # unlock admins can be set only once. Zero-address means unlock is disabled
      admin_unlock_all: public(address)
      admin_early_unlock: public(address)
      
      future_admin: public(address)
      
      is_initialized: public(bool)
      
      early_unlock: public(bool)
      penalty_k: public(uint256)
      prev_penalty_k: public(uint256)
      penalty_upd_ts: public(uint256)
      PENALTY_COOLDOWN: constant(uint256) = 60 # cooldown to prevent font-run on penalty change
      PENALTY_MULTIPLIER: constant(uint256) = 10
      
      penalty_treasury: public(address)
      
      balMinter: public(address)
      balToken: public(address)
      rewardReceiver: public(address)
      rewardReceiverChangeable: public(bool)
      
      rewardDistributor: public(address)
      
      all_unlock: public(bool)
      
      
      @external
      def initialize(
          _token_addr: address,
          _name: String[64],
          _symbol: String[32],
          _admin_addr: address,
          _admin_unlock_all: address,
          _admin_early_unlock: address,
          _max_time: uint256,
          _balToken: address,
          _balMinter: address,
          _rewardReceiver: address,
          _rewardReceiverChangeable: bool,
          _rewardDistributor: address
      ):
          """
          @notice Contract constructor
          @param _token_addr 80/20 Token-WETH BPT token address
          @param _name Token name
          @param _symbol Token symbol
          @param _admin_addr Contract admin address
          @param _admin_unlock_all Admin to enable Unlock-All feature (zero-address to disable forever)
          @param _admin_early_unlock Admin to enable Eraly-Unlock feature (zero-address to disable forever)
          @param _max_time Locking max time
          @param _balToken Address of the Balancer token
          @param _balMinter Address of the Balancer minter
          @param _rewardReceiver Address of the reward receiver
          @param _rewardReceiverChangeable Boolean indicating whether the reward receiver is changeable
          @param _rewardDistributor The RewardDistributor contract address
          """
      
          assert(not self.is_initialized), 'only once'
          self.is_initialized = True
      
          assert(_admin_addr != empty(address)), '!empty'
          self.admin = _admin_addr
      
          self.penalty_k = 10
          self.prev_penalty_k = 10
          self.penalty_upd_ts = block.timestamp
          self.penalty_treasury = _admin_addr
      
          self.TOKEN = _token_addr
          self.point_history[0].blk = block.number
          self.point_history[0].ts = block.timestamp
      
          _decimals: uint256 = ERC20(_token_addr).decimals()  # also validates token for non-zero
          assert (_decimals >= 6 and _decimals <= 255), '!decimals'
      
          self.NAME = _name
          self.SYMBOL = _symbol
          self.DECIMALS = _decimals
      
          assert(_max_time >= WEEK and _max_time <= WEEK * 52 * 5), '!maxlock'
          self.MAXTIME = _max_time
      
          self.admin_unlock_all = _admin_unlock_all
          self.admin_early_unlock = _admin_early_unlock
      
          self.balToken = _balToken
          self.balMinter = _balMinter
          self.rewardReceiver = _rewardReceiver
          self.rewardReceiverChangeable = _rewardReceiverChangeable
          self.rewardDistributor = _rewardDistributor
      
      
      @external
      @view
      def token() -> address:
          return self.TOKEN
      
      @external
      @view
      def name() -> String[64]:
          return self.NAME
      
      @external
      @view
      def symbol() -> String[32]:
          return self.SYMBOL
      
      @external
      @view
      def decimals() -> uint256:
          return self.DECIMALS
      
      @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 != empty(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 != empty(address):
                  if SmartWalletChecker(checker).check(addr):
                      return
              raise "Smart contract depositors not allowed"
      
      
      @external
      def set_early_unlock(_early_unlock: bool):
          """
          @notice Sets the availability for users to unlock their locks before lock-end with penalty
          @dev Only the admin_early_unlock can execute this function.
          @param _early_unlock A boolean indicating whether early unlock is allowed or not.
          """
          assert msg.sender == self.admin_early_unlock, '!admin'  # dev: admin_early_unlock only
          assert _early_unlock != self.early_unlock, 'already'
          
          self.early_unlock = _early_unlock
          log EarlyUnlock(_early_unlock)
      
      
      @external
      def set_early_unlock_penalty_speed(_penalty_k: uint256):
          """
          @notice Sets penalty speed for early unlocking
          @dev Only the admin can execute this function. To prevent frontrunning we use PENALTY_COOLDOWN period
          @param _penalty_k Coefficient indicating the penalty speed for early unlock.
                            Must be between 0 and 50, inclusive. Default 10 - means linear speed.
          """
          assert msg.sender == self.admin_early_unlock, '!admin'  # dev: admin_early_unlock only
          assert _penalty_k <= 50, '!k'
          assert block.timestamp > self.penalty_upd_ts + PENALTY_COOLDOWN, 'early' # to avoid frontrun
      
          self.prev_penalty_k = self.penalty_k
          self.penalty_k = _penalty_k
          self.penalty_upd_ts = block.timestamp
      
          log PenaltySpeed(_penalty_k)
      
      
      @external
      def set_penalty_treasury(_penalty_treasury: address):
          """
          @notice Sets penalty treasury address
          @dev Only the admin_early_unlock can execute this function.
          @param _penalty_treasury The address to collect early penalty (default admin address)
          """
          assert msg.sender == self.admin_early_unlock, '!admin'  # dev: admin_early_unlock only
          assert _penalty_treasury != empty(address), '!zero'
         
          self.penalty_treasury = _penalty_treasury
          log PenaltyTreasury(_penalty_treasury)
      
      
      @external
      def set_all_unlock():
          """
          @notice Deactivates VotingEscrow and allows users to unlock their locks before lock-end. 
                  New deposits will no longer be accepted.
          @dev Only the admin_unlock_all can execute this function. Make sure there are no rewards for distribution in other contracts.
          """
          assert msg.sender == self.admin_unlock_all, '!admin'  # dev: admin_unlock_all only
          self.all_unlock = True
          log TotalUnlock(True)
      
      
      @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 != empty(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 / convert(self.MAXTIME, int128)
                  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 / convert(self.MAXTIME, int128)
                  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 != empty(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 != empty(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
          """
          # block all new deposits (and extensions) in case of unlocked contract
          assert (not self.all_unlock), "all unlocked,no sense"
      
          _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, default_return_value=True)
      
          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(empty(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 + self.MAXTIME), "Voting lock too long"
      
          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 + self.MAXTIME), "Voting lock too long"
      
          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 or self.all_unlock, "lock !expire or !unlock"
          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, default_return_value=True)
      
          log Withdraw(msg.sender, value, block.timestamp)
          log Supply(supply_before, supply_before - value)
      
      
      @external
      @nonreentrant("lock")
      def withdraw_early():
          """
          @notice Withdraws locked tokens for `msg.sender` before lock-end with penalty
          @dev Only possible if `early_unlock` is enabled (true)
          By defualt there is linear formula for calculating penalty. 
          In some cases an admin can configure penalty speed using `set_early_unlock_penalty_speed()`
          
          L - lock amount
          k - penalty coefficient, defined by admin (default 1)
          Tleft - left time to unlock
          Tmax - MAXLOCK time
          Penalty amount = L * k * (Tlast / Tmax)
          """
          assert(self.early_unlock == True), "!early unlock"
      
          _locked: LockedBalance = self.locked[msg.sender]
          assert block.timestamp < _locked.end, "lock expired"
      
          value: uint256 = convert(_locked.amount, uint256)
      
          time_left: uint256 = _locked.end - block.timestamp
          
          # to avoid front-run with penalty_k
          penalty_k_: uint256 = 0
          if block.timestamp > self.penalty_upd_ts + PENALTY_COOLDOWN:
              penalty_k_ = self.penalty_k
          else:
              penalty_k_ = self.prev_penalty_k
      
          penalty_ratio: uint256 = (time_left * MULTIPLIER / self.MAXTIME) * penalty_k_
          penalty: uint256 = (value * penalty_ratio / MULTIPLIER) / PENALTY_MULTIPLIER    
          if penalty > value:
              penalty = value
          user_amount: uint256 = value - penalty
      
          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)
      
          if penalty > 0:
              assert ERC20(self.TOKEN).transfer(self.penalty_treasury, penalty, default_return_value=True)
          if user_amount > 0:
              assert ERC20(self.TOKEN).transfer(msg.sender, user_amount, default_return_value=True)
      
          log Withdraw(msg.sender, value, block.timestamp)
          log Supply(supply_before, supply_before - value)
          log WithdrawEarly(msg.sender, penalty, time_left)
      
      
      # 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 find epoch containing block number
          @param _block Block to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _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
      
      @internal
      @view
      def find_timestamp_epoch(_timestamp: uint256, max_epoch: uint256) -> uint256:
          """
          @notice Binary search to find epoch for timestamp
          @param _timestamp timestamp to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _timestamp
          """
          # 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].ts <= _timestamp:
                  _min = _mid
              else:
                  _max = _mid - 1
          return _min
      
      
      @internal
      @view
      def find_block_user_epoch(_addr: address, _block: uint256, max_epoch: uint256) -> uint256:
          """
          @notice Binary search to find epoch for block number
          @param _addr User for which to find user epoch for
          @param _block Block to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _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.user_point_history[_addr][_mid].blk <= _block:
                  _min = _mid
              else:
                  _max = _mid - 1
          return _min
      
      
      @internal
      @view
      def find_timestamp_user_epoch(_addr: address, _timestamp: uint256, max_epoch: uint256) -> uint256:
          """
          @notice Binary search to find user epoch for timestamp
          @param _addr User for which to find user epoch for
          @param _timestamp timestamp to find
          @param max_epoch Don't go beyond this epoch
          @return Epoch which contains _timestamp
          """
          # 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.user_point_history[_addr][_mid].ts <= _timestamp:
                  _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 = 0
          if _t == block.timestamp:
              # No need to do binary search, will always live in current epoch
              _epoch = self.user_point_epoch[addr]
          else:
              _epoch = self.find_timestamp_user_epoch(addr, _t, 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
      
          _user_epoch: uint256 = self.find_block_user_epoch(addr, _block, self.user_point_epoch[addr])
          upoint: Point = self.user_point_history[addr][_user_epoch]
      
          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 = 0
          if t == block.timestamp:
              # No need to do binary search, will always live in current epoch
              _epoch = self.epoch
          else:
              _epoch = self.find_timestamp_epoch(t, self.epoch)
      
          if _epoch == 0:
              return 0
          else:
              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)
      
      @external
      @nonreentrant("lock")
      def claimExternalRewards():
          """
          @notice Claims BAL rewards
          @dev Only possible if the TOKEN is Guage contract
          """
          BalancerMinter(self.balMinter).mint(self.TOKEN)
          balBalance: uint256 = ERC20(self.balToken).balanceOf(self)
          if balBalance > 0:
              # distributes rewards using rewardDistributor into current week
              if self.rewardReceiver == self.rewardDistributor:
                  assert ERC20(self.balToken).approve(self.rewardDistributor, balBalance, default_return_value=True)
                  RewardDistributor(self.rewardDistributor).depositToken(self.balToken, balBalance)
              else:
                  assert ERC20(self.balToken).transfer(self.rewardReceiver, balBalance, default_return_value=True)
      
      
      @external
      def changeRewardReceiver(newReceiver: address):
          """
          @notice Changes the reward receiver address
          @param newReceiver New address to set as the reward receiver
          """
          assert msg.sender == self.admin, '!admin'
          assert (self.rewardReceiverChangeable), '!available'
          assert newReceiver != empty(address), '!empty'
      
          self.rewardReceiver = newReceiver
          log RewardReceiver(newReceiver)