Transaction Hash:
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 | ||
|---|---|---|---|---|---|
| 0x1B49F587...80e1B7490 | |||||
|
0x4838B106...B0BAD5f97
Miner
| (Titan Builder) | 17.443258479301897961 Eth | 17.443790289301897961 Eth | 0.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
File 2 of 3: VeAethir
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)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)