Transaction Hash:
Block:
16378424 at Jan-10-2023 07:13:11 PM +UTC
Transaction Fee:
0.0072306660408822 ETH
$14.68
Gas Used:
270,200 Gas / 26.760422061 Gwei
Emitted Events:
| 259 |
Vyper_contract.VoteForGauge( time=1673377991, user=[Sender] 0xed75dad20bdd2f916a329e0a2b88c8de0dd00a28, gauge_addr=0x1cEBdB08...360B8a3a6, weight=0 )
|
Account State Difference:
| Address | Before | After | State Difference | ||
|---|---|---|---|---|---|
| 0x2F50D538...C9D5846bB | (Curve: Gauge Controller) | ||||
|
0x95222290...5CC4BAfe5
Miner
| (beaverbuild) | 11.632343131888117472 Eth | 11.632445710201900872 Eth | 0.0001025783137834 | |
| 0xed75DAd2...E0DD00a28 |
1.264859255754268275 Eth
Nonce: 294
|
1.257628589713386075 Eth
Nonce: 295
| 0.0072306660408822 |
Execution Trace
Vyper_contract.vote_for_gauge_weights( _gauge_addr=0x1cEBdB0856dd985fAe9b8fEa2262469360B8a3a6, _user_weight=0 )
-
Vyper_contract.get_last_user_slope( addr=0xed75DAd20bDd2f916A329E0A2b88C8DE0DD00a28 ) => ( 63231882791322 )
-
Vyper_contract.locked__end( _addr=0xed75DAd20bDd2f916A329E0A2b88C8DE0DD00a28 ) => ( 1737590400 )
File 1 of 2: Vyper_contract
File 2 of 2: Vyper_contract
# @version 0.2.4
"""
@title Gauge Controller
@author Curve Finance
@license MIT
@notice Controls liquidity gauges and the issuance of coins through the gauges
"""
# 7 * 86400 seconds - all future times are rounded by week
WEEK: constant(uint256) = 604800
# Cannot change weight votes more often than once in 10 days
WEIGHT_VOTE_DELAY: constant(uint256) = 10 * 86400
struct Point:
bias: uint256
slope: uint256
struct VotedSlope:
slope: uint256
power: uint256
end: uint256
interface VotingEscrow:
def get_last_user_slope(addr: address) -> int128: view
def locked__end(addr: address) -> uint256: view
event CommitOwnership:
admin: address
event ApplyOwnership:
admin: address
event AddType:
name: String[64]
type_id: int128
event NewTypeWeight:
type_id: int128
time: uint256
weight: uint256
total_weight: uint256
event NewGaugeWeight:
gauge_address: address
time: uint256
weight: uint256
total_weight: uint256
event VoteForGauge:
time: uint256
user: address
gauge_addr: address
weight: uint256
event NewGauge:
addr: address
gauge_type: int128
weight: uint256
MULTIPLIER: constant(uint256) = 10 ** 18
admin: public(address) # Can and will be a smart contract
future_admin: public(address) # Can and will be a smart contract
token: public(address) # CRV token
voting_escrow: public(address) # Voting escrow
# Gauge parameters
# All numbers are "fixed point" on the basis of 1e18
n_gauge_types: public(int128)
n_gauges: public(int128)
gauge_type_names: public(HashMap[int128, String[64]])
# Needed for enumeration
gauges: public(address[1000000000])
# we increment values by 1 prior to storing them here so we can rely on a value
# of zero as meaning the gauge has not been set
gauge_types_: HashMap[address, int128]
vote_user_slopes: public(HashMap[address, HashMap[address, VotedSlope]]) # user -> gauge_addr -> VotedSlope
vote_user_power: public(HashMap[address, uint256]) # Total vote power used by user
last_user_vote: public(HashMap[address, HashMap[address, uint256]]) # Last user vote's timestamp for each gauge address
# Past and scheduled points for gauge weight, sum of weights per type, total weight
# Point is for bias+slope
# changes_* are for changes in slope
# time_* are for the last change timestamp
# timestamps are rounded to whole weeks
points_weight: public(HashMap[address, HashMap[uint256, Point]]) # gauge_addr -> time -> Point
changes_weight: HashMap[address, HashMap[uint256, uint256]] # gauge_addr -> time -> slope
time_weight: public(HashMap[address, uint256]) # gauge_addr -> last scheduled time (next week)
points_sum: public(HashMap[int128, HashMap[uint256, Point]]) # type_id -> time -> Point
changes_sum: HashMap[int128, HashMap[uint256, uint256]] # type_id -> time -> slope
time_sum: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
points_total: public(HashMap[uint256, uint256]) # time -> total weight
time_total: public(uint256) # last scheduled time
points_type_weight: public(HashMap[int128, HashMap[uint256, uint256]]) # type_id -> time -> type weight
time_type_weight: public(uint256[1000000000]) # type_id -> last scheduled time (next week)
@external
def __init__(_token: address, _voting_escrow: address):
"""
@notice Contract constructor
@param _token `ERC20CRV` contract address
@param _voting_escrow `VotingEscrow` contract address
"""
assert _token != ZERO_ADDRESS
assert _voting_escrow != ZERO_ADDRESS
self.admin = msg.sender
self.token = _token
self.voting_escrow = _voting_escrow
self.time_total = block.timestamp / WEEK * WEEK
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of GaugeController 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 pending ownership transfer
"""
assert msg.sender == self.admin # dev: admin only
_admin: address = self.future_admin
assert _admin != ZERO_ADDRESS # dev: admin not set
self.admin = _admin
log ApplyOwnership(_admin)
@external
@view
def gauge_types(_addr: address) -> int128:
"""
@notice Get gauge type for address
@param _addr Gauge address
@return Gauge type id
"""
gauge_type: int128 = self.gauge_types_[_addr]
assert gauge_type != 0
return gauge_type - 1
@internal
def _get_type_weight(gauge_type: int128) -> uint256:
"""
@notice Fill historic type weights week-over-week for missed checkins
and return the type weight for the future week
@param gauge_type Gauge type id
@return Type weight
"""
t: uint256 = self.time_type_weight[gauge_type]
if t > 0:
w: uint256 = self.points_type_weight[gauge_type][t]
for i in range(500):
if t > block.timestamp:
break
t += WEEK
self.points_type_weight[gauge_type][t] = w
if t > block.timestamp:
self.time_type_weight[gauge_type] = t
return w
else:
return 0
@internal
def _get_sum(gauge_type: int128) -> uint256:
"""
@notice Fill sum of gauge weights for the same type week-over-week for
missed checkins and return the sum for the future week
@param gauge_type Gauge type id
@return Sum of weights
"""
t: uint256 = self.time_sum[gauge_type]
if t > 0:
pt: Point = self.points_sum[gauge_type][t]
for i in range(500):
if t > block.timestamp:
break
t += WEEK
d_bias: uint256 = pt.slope * WEEK
if pt.bias > d_bias:
pt.bias -= d_bias
d_slope: uint256 = self.changes_sum[gauge_type][t]
pt.slope -= d_slope
else:
pt.bias = 0
pt.slope = 0
self.points_sum[gauge_type][t] = pt
if t > block.timestamp:
self.time_sum[gauge_type] = t
return pt.bias
else:
return 0
@internal
def _get_total() -> uint256:
"""
@notice Fill historic total weights week-over-week for missed checkins
and return the total for the future week
@return Total weight
"""
t: uint256 = self.time_total
_n_gauge_types: int128 = self.n_gauge_types
if t > block.timestamp:
# If we have already checkpointed - still need to change the value
t -= WEEK
pt: uint256 = self.points_total[t]
for gauge_type in range(100):
if gauge_type == _n_gauge_types:
break
self._get_sum(gauge_type)
self._get_type_weight(gauge_type)
for i in range(500):
if t > block.timestamp:
break
t += WEEK
pt = 0
# Scales as n_types * n_unchecked_weeks (hopefully 1 at most)
for gauge_type in range(100):
if gauge_type == _n_gauge_types:
break
type_sum: uint256 = self.points_sum[gauge_type][t].bias
type_weight: uint256 = self.points_type_weight[gauge_type][t]
pt += type_sum * type_weight
self.points_total[t] = pt
if t > block.timestamp:
self.time_total = t
return pt
@internal
def _get_weight(gauge_addr: address) -> uint256:
"""
@notice Fill historic gauge weights week-over-week for missed checkins
and return the total for the future week
@param gauge_addr Address of the gauge
@return Gauge weight
"""
t: uint256 = self.time_weight[gauge_addr]
if t > 0:
pt: Point = self.points_weight[gauge_addr][t]
for i in range(500):
if t > block.timestamp:
break
t += WEEK
d_bias: uint256 = pt.slope * WEEK
if pt.bias > d_bias:
pt.bias -= d_bias
d_slope: uint256 = self.changes_weight[gauge_addr][t]
pt.slope -= d_slope
else:
pt.bias = 0
pt.slope = 0
self.points_weight[gauge_addr][t] = pt
if t > block.timestamp:
self.time_weight[gauge_addr] = t
return pt.bias
else:
return 0
@external
def add_gauge(addr: address, gauge_type: int128, weight: uint256 = 0):
"""
@notice Add gauge `addr` of type `gauge_type` with weight `weight`
@param addr Gauge address
@param gauge_type Gauge type
@param weight Gauge weight
"""
assert msg.sender == self.admin
assert (gauge_type >= 0) and (gauge_type < self.n_gauge_types)
assert self.gauge_types_[addr] == 0 # dev: cannot add the same gauge twice
n: int128 = self.n_gauges
self.n_gauges = n + 1
self.gauges[n] = addr
self.gauge_types_[addr] = gauge_type + 1
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
if weight > 0:
_type_weight: uint256 = self._get_type_weight(gauge_type)
_old_sum: uint256 = self._get_sum(gauge_type)
_old_total: uint256 = self._get_total()
self.points_sum[gauge_type][next_time].bias = weight + _old_sum
self.time_sum[gauge_type] = next_time
self.points_total[next_time] = _old_total + _type_weight * weight
self.time_total = next_time
self.points_weight[addr][next_time].bias = weight
if self.time_sum[gauge_type] == 0:
self.time_sum[gauge_type] = next_time
self.time_weight[addr] = next_time
log NewGauge(addr, gauge_type, weight)
@external
def checkpoint():
"""
@notice Checkpoint to fill data common for all gauges
"""
self._get_total()
@external
def checkpoint_gauge(addr: address):
"""
@notice Checkpoint to fill data for both a specific gauge and common for all gauges
@param addr Gauge address
"""
self._get_weight(addr)
self._get_total()
@internal
@view
def _gauge_relative_weight(addr: address, time: uint256) -> uint256:
"""
@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18
(e.g. 1.0 == 1e18). Inflation which will be received by it is
inflation_rate * relative_weight / 1e18
@param addr Gauge address
@param time Relative weight at the specified timestamp in the past or present
@return Value of relative weight normalized to 1e18
"""
t: uint256 = time / WEEK * WEEK
_total_weight: uint256 = self.points_total[t]
if _total_weight > 0:
gauge_type: int128 = self.gauge_types_[addr] - 1
_type_weight: uint256 = self.points_type_weight[gauge_type][t]
_gauge_weight: uint256 = self.points_weight[addr][t].bias
return MULTIPLIER * _type_weight * _gauge_weight / _total_weight
else:
return 0
@external
@view
def gauge_relative_weight(addr: address, time: uint256 = block.timestamp) -> uint256:
"""
@notice Get Gauge relative weight (not more than 1.0) normalized to 1e18
(e.g. 1.0 == 1e18). Inflation which will be received by it is
inflation_rate * relative_weight / 1e18
@param addr Gauge address
@param time Relative weight at the specified timestamp in the past or present
@return Value of relative weight normalized to 1e18
"""
return self._gauge_relative_weight(addr, time)
@external
def gauge_relative_weight_write(addr: address, time: uint256 = block.timestamp) -> uint256:
"""
@notice Get gauge weight normalized to 1e18 and also fill all the unfilled
values for type and gauge records
@dev Any address can call, however nothing is recorded if the values are filled already
@param addr Gauge address
@param time Relative weight at the specified timestamp in the past or present
@return Value of relative weight normalized to 1e18
"""
self._get_weight(addr)
self._get_total() # Also calculates get_sum
return self._gauge_relative_weight(addr, time)
@internal
def _change_type_weight(type_id: int128, weight: uint256):
"""
@notice Change type weight
@param type_id Type id
@param weight New type weight
"""
old_weight: uint256 = self._get_type_weight(type_id)
old_sum: uint256 = self._get_sum(type_id)
_total_weight: uint256 = self._get_total()
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
_total_weight = _total_weight + old_sum * weight - old_sum * old_weight
self.points_total[next_time] = _total_weight
self.points_type_weight[type_id][next_time] = weight
self.time_total = next_time
self.time_type_weight[type_id] = next_time
log NewTypeWeight(type_id, next_time, weight, _total_weight)
@external
def add_type(_name: String[64], weight: uint256 = 0):
"""
@notice Add gauge type with name `_name` and weight `weight`
@param _name Name of gauge type
@param weight Weight of gauge type
"""
assert msg.sender == self.admin
type_id: int128 = self.n_gauge_types
self.gauge_type_names[type_id] = _name
self.n_gauge_types = type_id + 1
if weight != 0:
self._change_type_weight(type_id, weight)
log AddType(_name, type_id)
@external
def change_type_weight(type_id: int128, weight: uint256):
"""
@notice Change gauge type `type_id` weight to `weight`
@param type_id Gauge type id
@param weight New Gauge weight
"""
assert msg.sender == self.admin
self._change_type_weight(type_id, weight)
@internal
def _change_gauge_weight(addr: address, weight: uint256):
# Change gauge weight
# Only needed when testing in reality
gauge_type: int128 = self.gauge_types_[addr] - 1
old_gauge_weight: uint256 = self._get_weight(addr)
type_weight: uint256 = self._get_type_weight(gauge_type)
old_sum: uint256 = self._get_sum(gauge_type)
_total_weight: uint256 = self._get_total()
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
self.points_weight[addr][next_time].bias = weight
self.time_weight[addr] = next_time
new_sum: uint256 = old_sum + weight - old_gauge_weight
self.points_sum[gauge_type][next_time].bias = new_sum
self.time_sum[gauge_type] = next_time
_total_weight = _total_weight + new_sum * type_weight - old_sum * type_weight
self.points_total[next_time] = _total_weight
self.time_total = next_time
log NewGaugeWeight(addr, block.timestamp, weight, _total_weight)
@external
def change_gauge_weight(addr: address, weight: uint256):
"""
@notice Change weight of gauge `addr` to `weight`
@param addr `GaugeController` contract address
@param weight New Gauge weight
"""
assert msg.sender == self.admin
self._change_gauge_weight(addr, weight)
@external
def vote_for_gauge_weights(_gauge_addr: address, _user_weight: uint256):
"""
@notice Allocate voting power for changing pool weights
@param _gauge_addr Gauge which `msg.sender` votes for
@param _user_weight Weight for a gauge in bps (units of 0.01%). Minimal is 0.01%. Ignored if 0
"""
escrow: address = self.voting_escrow
slope: uint256 = convert(VotingEscrow(escrow).get_last_user_slope(msg.sender), uint256)
lock_end: uint256 = VotingEscrow(escrow).locked__end(msg.sender)
_n_gauges: int128 = self.n_gauges
next_time: uint256 = (block.timestamp + WEEK) / WEEK * WEEK
assert lock_end > next_time, "Your token lock expires too soon"
assert (_user_weight >= 0) and (_user_weight <= 10000), "You used all your voting power"
assert block.timestamp >= self.last_user_vote[msg.sender][_gauge_addr] + WEIGHT_VOTE_DELAY, "Cannot vote so often"
gauge_type: int128 = self.gauge_types_[_gauge_addr] - 1
assert gauge_type >= 0, "Gauge not added"
# Prepare slopes and biases in memory
old_slope: VotedSlope = self.vote_user_slopes[msg.sender][_gauge_addr]
old_dt: uint256 = 0
if old_slope.end > next_time:
old_dt = old_slope.end - next_time
old_bias: uint256 = old_slope.slope * old_dt
new_slope: VotedSlope = VotedSlope({
slope: slope * _user_weight / 10000,
end: lock_end,
power: _user_weight
})
new_dt: uint256 = lock_end - next_time # dev: raises when expired
new_bias: uint256 = new_slope.slope * new_dt
# Check and update powers (weights) used
power_used: uint256 = self.vote_user_power[msg.sender]
power_used = power_used + new_slope.power - old_slope.power
self.vote_user_power[msg.sender] = power_used
assert (power_used >= 0) and (power_used <= 10000), 'Used too much power'
## Remove old and schedule new slope changes
# Remove slope changes for old slopes
# Schedule recording of initial slope for next_time
old_weight_bias: uint256 = self._get_weight(_gauge_addr)
old_weight_slope: uint256 = self.points_weight[_gauge_addr][next_time].slope
old_sum_bias: uint256 = self._get_sum(gauge_type)
old_sum_slope: uint256 = self.points_sum[gauge_type][next_time].slope
self.points_weight[_gauge_addr][next_time].bias = max(old_weight_bias + new_bias, old_bias) - old_bias
self.points_sum[gauge_type][next_time].bias = max(old_sum_bias + new_bias, old_bias) - old_bias
if old_slope.end > next_time:
self.points_weight[_gauge_addr][next_time].slope = max(old_weight_slope + new_slope.slope, old_slope.slope) - old_slope.slope
self.points_sum[gauge_type][next_time].slope = max(old_sum_slope + new_slope.slope, old_slope.slope) - old_slope.slope
else:
self.points_weight[_gauge_addr][next_time].slope += new_slope.slope
self.points_sum[gauge_type][next_time].slope += new_slope.slope
if old_slope.end > block.timestamp:
# Cancel old slope changes if they still didn't happen
self.changes_weight[_gauge_addr][old_slope.end] -= old_slope.slope
self.changes_sum[gauge_type][old_slope.end] -= old_slope.slope
# Add slope changes for new slopes
self.changes_weight[_gauge_addr][new_slope.end] += new_slope.slope
self.changes_sum[gauge_type][new_slope.end] += new_slope.slope
self._get_total()
self.vote_user_slopes[msg.sender][_gauge_addr] = new_slope
# Record last action time
self.last_user_vote[msg.sender][_gauge_addr] = block.timestamp
log VoteForGauge(block.timestamp, msg.sender, _gauge_addr, _user_weight)
@external
@view
def get_gauge_weight(addr: address) -> uint256:
"""
@notice Get current gauge weight
@param addr Gauge address
@return Gauge weight
"""
return self.points_weight[addr][self.time_weight[addr]].bias
@external
@view
def get_type_weight(type_id: int128) -> uint256:
"""
@notice Get current type weight
@param type_id Type id
@return Type weight
"""
return self.points_type_weight[type_id][self.time_type_weight[type_id]]
@external
@view
def get_total_weight() -> uint256:
"""
@notice Get current total (type-weighted) weight
@return Total weight
"""
return self.points_total[self.time_total]
@external
@view
def get_weights_sum_per_type(type_id: int128) -> uint256:
"""
@notice Get sum of gauge weights per type
@param type_id Type id
@return Sum of gauge weights
"""
return self.points_sum[type_id][self.time_sum[type_id]].biasFile 2 of 2: Vyper_contract
# @version 0.2.4
"""
@title Voting Escrow
@author Curve Finance
@license MIT
@notice Votes have a weight depending on time, so that users are
committed to the future of (whatever they are voting for)
@dev Vote weight decays linearly over time. Lock time cannot be
more than `MAXTIME` (4 years).
"""
# Voting escrow to have time-weighted votes
# Votes have a weight depending on time, so that users are committed
# to the future of (whatever they are voting for).
# The weight in this implementation is linear, and lock cannot be more than maxtime:
# w ^
# 1 + /
# | /
# | /
# | /
# |/
# 0 +--------+------> time
# maxtime (4 years?)
struct Point:
bias: int128
slope: int128 # - dweight / dt
ts: uint256
blk: uint256 # block
# We cannot really do block numbers per se b/c slope is per time, not per block
# and per block could be fairly bad b/c Ethereum changes blocktimes.
# What we can do is to extrapolate ***At functions
struct LockedBalance:
amount: int128
end: uint256
interface ERC20:
def decimals() -> uint256: view
def name() -> String[64]: view
def symbol() -> String[32]: view
def transfer(to: address, amount: uint256) -> bool: nonpayable
def transferFrom(spender: address, to: address, amount: uint256) -> bool: nonpayable
# Interface for checking whether address belongs to a whitelisted
# type of a smart wallet.
# When new types are added - the whole contract is changed
# The check() method is modifying to be able to use caching
# for individual wallet addresses
interface SmartWalletChecker:
def check(addr: address) -> bool: nonpayable
DEPOSIT_FOR_TYPE: constant(int128) = 0
CREATE_LOCK_TYPE: constant(int128) = 1
INCREASE_LOCK_AMOUNT: constant(int128) = 2
INCREASE_UNLOCK_TIME: constant(int128) = 3
event CommitOwnership:
admin: address
event ApplyOwnership:
admin: address
event Deposit:
provider: indexed(address)
value: uint256
locktime: indexed(uint256)
type: int128
ts: uint256
event Withdraw:
provider: indexed(address)
value: uint256
ts: uint256
event Supply:
prevSupply: uint256
supply: uint256
WEEK: constant(uint256) = 7 * 86400 # all future times are rounded by week
MAXTIME: constant(uint256) = 4 * 365 * 86400 # 4 years
MULTIPLIER: constant(uint256) = 10 ** 18
token: public(address)
supply: public(uint256)
locked: public(HashMap[address, LockedBalance])
epoch: public(uint256)
point_history: public(Point[100000000000000000000000000000]) # epoch -> unsigned point
user_point_history: public(HashMap[address, Point[1000000000]]) # user -> Point[user_epoch]
user_point_epoch: public(HashMap[address, uint256])
slope_changes: public(HashMap[uint256, int128]) # time -> signed slope change
# Aragon's view methods for compatibility
controller: public(address)
transfersEnabled: public(bool)
name: public(String[64])
symbol: public(String[32])
version: public(String[32])
decimals: public(uint256)
# Checker for whitelisted (smart contract) wallets which are allowed to deposit
# The goal is to prevent tokenizing the escrow
future_smart_wallet_checker: public(address)
smart_wallet_checker: public(address)
admin: public(address) # Can and will be a smart contract
future_admin: public(address)
@external
def __init__(token_addr: address, _name: String[64], _symbol: String[32], _version: String[32]):
"""
@notice Contract constructor
@param token_addr `ERC20CRV` token address
@param _name Token name
@param _symbol Token symbol
@param _version Contract version - required for Aragon compatibility
"""
self.admin = msg.sender
self.token = token_addr
self.point_history[0].blk = block.number
self.point_history[0].ts = block.timestamp
self.controller = msg.sender
self.transfersEnabled = True
_decimals: uint256 = ERC20(token_addr).decimals()
assert _decimals <= 255
self.decimals = _decimals
self.name = _name
self.symbol = _symbol
self.version = _version
@external
def commit_transfer_ownership(addr: address):
"""
@notice Transfer ownership of VotingEscrow contract to `addr`
@param addr Address to have ownership transferred to
"""
assert msg.sender == self.admin # dev: admin only
self.future_admin = addr
log CommitOwnership(addr)
@external
def apply_transfer_ownership():
"""
@notice Apply ownership transfer
"""
assert msg.sender == self.admin # dev: admin only
_admin: address = self.future_admin
assert _admin != ZERO_ADDRESS # dev: admin not set
self.admin = _admin
log ApplyOwnership(_admin)
@external
def commit_smart_wallet_checker(addr: address):
"""
@notice Set an external contract to check for approved smart contract wallets
@param addr Address of Smart contract checker
"""
assert msg.sender == self.admin
self.future_smart_wallet_checker = addr
@external
def apply_smart_wallet_checker():
"""
@notice Apply setting external contract to check approved smart contract wallets
"""
assert msg.sender == self.admin
self.smart_wallet_checker = self.future_smart_wallet_checker
@internal
def assert_not_contract(addr: address):
"""
@notice Check if the call is from a whitelisted smart contract, revert if not
@param addr Address to be checked
"""
if addr != tx.origin:
checker: address = self.smart_wallet_checker
if checker != ZERO_ADDRESS:
if SmartWalletChecker(checker).check(addr):
return
raise "Smart contract depositors not allowed"
@external
@view
def get_last_user_slope(addr: address) -> int128:
"""
@notice Get the most recently recorded rate of voting power decrease for `addr`
@param addr Address of the user wallet
@return Value of the slope
"""
uepoch: uint256 = self.user_point_epoch[addr]
return self.user_point_history[addr][uepoch].slope
@external
@view
def user_point_history__ts(_addr: address, _idx: uint256) -> uint256:
"""
@notice Get the timestamp for checkpoint `_idx` for `_addr`
@param _addr User wallet address
@param _idx User epoch number
@return Epoch time of the checkpoint
"""
return self.user_point_history[_addr][_idx].ts
@external
@view
def locked__end(_addr: address) -> uint256:
"""
@notice Get timestamp when `_addr`'s lock finishes
@param _addr User wallet
@return Epoch time of the lock end
"""
return self.locked[_addr].end
@internal
def _checkpoint(addr: address, old_locked: LockedBalance, new_locked: LockedBalance):
"""
@notice Record global and per-user data to checkpoint
@param addr User's wallet address. No user checkpoint if 0x0
@param old_locked Pevious locked amount / end lock time for the user
@param new_locked New locked amount / end lock time for the user
"""
u_old: Point = empty(Point)
u_new: Point = empty(Point)
old_dslope: int128 = 0
new_dslope: int128 = 0
_epoch: uint256 = self.epoch
if addr != ZERO_ADDRESS:
# Calculate slopes and biases
# Kept at zero when they have to
if old_locked.end > block.timestamp and old_locked.amount > 0:
u_old.slope = old_locked.amount / MAXTIME
u_old.bias = u_old.slope * convert(old_locked.end - block.timestamp, int128)
if new_locked.end > block.timestamp and new_locked.amount > 0:
u_new.slope = new_locked.amount / MAXTIME
u_new.bias = u_new.slope * convert(new_locked.end - block.timestamp, int128)
# Read values of scheduled changes in the slope
# old_locked.end can be in the past and in the future
# new_locked.end can ONLY by in the FUTURE unless everything expired: than zeros
old_dslope = self.slope_changes[old_locked.end]
if new_locked.end != 0:
if new_locked.end == old_locked.end:
new_dslope = old_dslope
else:
new_dslope = self.slope_changes[new_locked.end]
last_point: Point = Point({bias: 0, slope: 0, ts: block.timestamp, blk: block.number})
if _epoch > 0:
last_point = self.point_history[_epoch]
last_checkpoint: uint256 = last_point.ts
# initial_last_point is used for extrapolation to calculate block number
# (approximately, for *At methods) and save them
# as we cannot figure that out exactly from inside the contract
initial_last_point: Point = last_point
block_slope: uint256 = 0 # dblock/dt
if block.timestamp > last_point.ts:
block_slope = MULTIPLIER * (block.number - last_point.blk) / (block.timestamp - last_point.ts)
# If last point is already recorded in this block, slope=0
# But that's ok b/c we know the block in such case
# Go over weeks to fill history and calculate what the current point is
t_i: uint256 = (last_checkpoint / WEEK) * WEEK
for i in range(255):
# Hopefully it won't happen that this won't get used in 5 years!
# If it does, users will be able to withdraw but vote weight will be broken
t_i += WEEK
d_slope: int128 = 0
if t_i > block.timestamp:
t_i = block.timestamp
else:
d_slope = self.slope_changes[t_i]
last_point.bias -= last_point.slope * convert(t_i - last_checkpoint, int128)
last_point.slope += d_slope
if last_point.bias < 0: # This can happen
last_point.bias = 0
if last_point.slope < 0: # This cannot happen - just in case
last_point.slope = 0
last_checkpoint = t_i
last_point.ts = t_i
last_point.blk = initial_last_point.blk + block_slope * (t_i - initial_last_point.ts) / MULTIPLIER
_epoch += 1
if t_i == block.timestamp:
last_point.blk = block.number
break
else:
self.point_history[_epoch] = last_point
self.epoch = _epoch
# Now point_history is filled until t=now
if addr != ZERO_ADDRESS:
# If last point was in this block, the slope change has been applied already
# But in such case we have 0 slope(s)
last_point.slope += (u_new.slope - u_old.slope)
last_point.bias += (u_new.bias - u_old.bias)
if last_point.slope < 0:
last_point.slope = 0
if last_point.bias < 0:
last_point.bias = 0
# Record the changed point into history
self.point_history[_epoch] = last_point
if addr != ZERO_ADDRESS:
# Schedule the slope changes (slope is going down)
# We subtract new_user_slope from [new_locked.end]
# and add old_user_slope to [old_locked.end]
if old_locked.end > block.timestamp:
# old_dslope was <something> - u_old.slope, so we cancel that
old_dslope += u_old.slope
if new_locked.end == old_locked.end:
old_dslope -= u_new.slope # It was a new deposit, not extension
self.slope_changes[old_locked.end] = old_dslope
if new_locked.end > block.timestamp:
if new_locked.end > old_locked.end:
new_dslope -= u_new.slope # old slope disappeared at this point
self.slope_changes[new_locked.end] = new_dslope
# else: we recorded it already in old_dslope
# Now handle user history
user_epoch: uint256 = self.user_point_epoch[addr] + 1
self.user_point_epoch[addr] = user_epoch
u_new.ts = block.timestamp
u_new.blk = block.number
self.user_point_history[addr][user_epoch] = u_new
@internal
def _deposit_for(_addr: address, _value: uint256, unlock_time: uint256, locked_balance: LockedBalance, type: int128):
"""
@notice Deposit and lock tokens for a user
@param _addr User's wallet address
@param _value Amount to deposit
@param unlock_time New time when to unlock the tokens, or 0 if unchanged
@param locked_balance Previous locked amount / timestamp
"""
_locked: LockedBalance = locked_balance
supply_before: uint256 = self.supply
self.supply = supply_before + _value
old_locked: LockedBalance = _locked
# Adding to existing lock, or if a lock is expired - creating a new one
_locked.amount += convert(_value, int128)
if unlock_time != 0:
_locked.end = unlock_time
self.locked[_addr] = _locked
# Possibilities:
# Both old_locked.end could be current or expired (>/< block.timestamp)
# value == 0 (extend lock) or value > 0 (add to lock or extend lock)
# _locked.end > block.timestamp (always)
self._checkpoint(_addr, old_locked, _locked)
if _value != 0:
assert ERC20(self.token).transferFrom(_addr, self, _value)
log Deposit(_addr, _value, _locked.end, type, block.timestamp)
log Supply(supply_before, supply_before + _value)
@external
def checkpoint():
"""
@notice Record global data to checkpoint
"""
self._checkpoint(ZERO_ADDRESS, empty(LockedBalance), empty(LockedBalance))
@external
@nonreentrant('lock')
def deposit_for(_addr: address, _value: uint256):
"""
@notice Deposit `_value` tokens for `_addr` and add to the lock
@dev Anyone (even a smart contract) can deposit for someone else, but
cannot extend their locktime and deposit for a brand new user
@param _addr User's wallet address
@param _value Amount to add to user's lock
"""
_locked: LockedBalance = self.locked[_addr]
assert _value > 0 # dev: need non-zero value
assert _locked.amount > 0, "No existing lock found"
assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"
self._deposit_for(_addr, _value, 0, self.locked[_addr], DEPOSIT_FOR_TYPE)
@external
@nonreentrant('lock')
def create_lock(_value: uint256, _unlock_time: uint256):
"""
@notice Deposit `_value` tokens for `msg.sender` and lock until `_unlock_time`
@param _value Amount to deposit
@param _unlock_time Epoch time when tokens unlock, rounded down to whole weeks
"""
self.assert_not_contract(msg.sender)
unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks
_locked: LockedBalance = self.locked[msg.sender]
assert _value > 0 # dev: need non-zero value
assert _locked.amount == 0, "Withdraw old tokens first"
assert unlock_time > block.timestamp, "Can only lock until time in the future"
assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"
self._deposit_for(msg.sender, _value, unlock_time, _locked, CREATE_LOCK_TYPE)
@external
@nonreentrant('lock')
def increase_amount(_value: uint256):
"""
@notice Deposit `_value` additional tokens for `msg.sender`
without modifying the unlock time
@param _value Amount of tokens to deposit and add to the lock
"""
self.assert_not_contract(msg.sender)
_locked: LockedBalance = self.locked[msg.sender]
assert _value > 0 # dev: need non-zero value
assert _locked.amount > 0, "No existing lock found"
assert _locked.end > block.timestamp, "Cannot add to expired lock. Withdraw"
self._deposit_for(msg.sender, _value, 0, _locked, INCREASE_LOCK_AMOUNT)
@external
@nonreentrant('lock')
def increase_unlock_time(_unlock_time: uint256):
"""
@notice Extend the unlock time for `msg.sender` to `_unlock_time`
@param _unlock_time New epoch time for unlocking
"""
self.assert_not_contract(msg.sender)
_locked: LockedBalance = self.locked[msg.sender]
unlock_time: uint256 = (_unlock_time / WEEK) * WEEK # Locktime is rounded down to weeks
assert _locked.end > block.timestamp, "Lock expired"
assert _locked.amount > 0, "Nothing is locked"
assert unlock_time > _locked.end, "Can only increase lock duration"
assert unlock_time <= block.timestamp + MAXTIME, "Voting lock can be 4 years max"
self._deposit_for(msg.sender, 0, unlock_time, _locked, INCREASE_UNLOCK_TIME)
@external
@nonreentrant('lock')
def withdraw():
"""
@notice Withdraw all tokens for `msg.sender`
@dev Only possible if the lock has expired
"""
_locked: LockedBalance = self.locked[msg.sender]
assert block.timestamp >= _locked.end, "The lock didn't expire"
value: uint256 = convert(_locked.amount, uint256)
old_locked: LockedBalance = _locked
_locked.end = 0
_locked.amount = 0
self.locked[msg.sender] = _locked
supply_before: uint256 = self.supply
self.supply = supply_before - value
# old_locked can have either expired <= timestamp or zero end
# _locked has only 0 end
# Both can have >= 0 amount
self._checkpoint(msg.sender, old_locked, _locked)
assert ERC20(self.token).transfer(msg.sender, value)
log Withdraw(msg.sender, value, block.timestamp)
log Supply(supply_before, supply_before - value)
# The following ERC20/minime-compatible methods are not real balanceOf and supply!
# They measure the weights for the purpose of voting, so they don't represent
# real coins.
@internal
@view
def find_block_epoch(_block: uint256, max_epoch: uint256) -> uint256:
"""
@notice Binary search to estimate timestamp for block number
@param _block Block to find
@param max_epoch Don't go beyond this epoch
@return Approximate timestamp for block
"""
# Binary search
_min: uint256 = 0
_max: uint256 = max_epoch
for i in range(128): # Will be always enough for 128-bit numbers
if _min >= _max:
break
_mid: uint256 = (_min + _max + 1) / 2
if self.point_history[_mid].blk <= _block:
_min = _mid
else:
_max = _mid - 1
return _min
@external
@view
def balanceOf(addr: address, _t: uint256 = block.timestamp) -> uint256:
"""
@notice Get the current voting power for `msg.sender`
@dev Adheres to the ERC20 `balanceOf` interface for Aragon compatibility
@param addr User wallet address
@param _t Epoch time to return voting power at
@return User voting power
"""
_epoch: uint256 = self.user_point_epoch[addr]
if _epoch == 0:
return 0
else:
last_point: Point = self.user_point_history[addr][_epoch]
last_point.bias -= last_point.slope * convert(_t - last_point.ts, int128)
if last_point.bias < 0:
last_point.bias = 0
return convert(last_point.bias, uint256)
@external
@view
def balanceOfAt(addr: address, _block: uint256) -> uint256:
"""
@notice Measure voting power of `addr` at block height `_block`
@dev Adheres to MiniMe `balanceOfAt` interface: https://github.com/Giveth/minime
@param addr User's wallet address
@param _block Block to calculate the voting power at
@return Voting power
"""
# Copying and pasting totalSupply code because Vyper cannot pass by
# reference yet
assert _block <= block.number
# Binary search
_min: uint256 = 0
_max: uint256 = self.user_point_epoch[addr]
for i in range(128): # Will be always enough for 128-bit numbers
if _min >= _max:
break
_mid: uint256 = (_min + _max + 1) / 2
if self.user_point_history[addr][_mid].blk <= _block:
_min = _mid
else:
_max = _mid - 1
upoint: Point = self.user_point_history[addr][_min]
max_epoch: uint256 = self.epoch
_epoch: uint256 = self.find_block_epoch(_block, max_epoch)
point_0: Point = self.point_history[_epoch]
d_block: uint256 = 0
d_t: uint256 = 0
if _epoch < max_epoch:
point_1: Point = self.point_history[_epoch + 1]
d_block = point_1.blk - point_0.blk
d_t = point_1.ts - point_0.ts
else:
d_block = block.number - point_0.blk
d_t = block.timestamp - point_0.ts
block_time: uint256 = point_0.ts
if d_block != 0:
block_time += d_t * (_block - point_0.blk) / d_block
upoint.bias -= upoint.slope * convert(block_time - upoint.ts, int128)
if upoint.bias >= 0:
return convert(upoint.bias, uint256)
else:
return 0
@internal
@view
def supply_at(point: Point, t: uint256) -> uint256:
"""
@notice Calculate total voting power at some point in the past
@param point The point (bias/slope) to start search from
@param t Time to calculate the total voting power at
@return Total voting power at that time
"""
last_point: Point = point
t_i: uint256 = (last_point.ts / WEEK) * WEEK
for i in range(255):
t_i += WEEK
d_slope: int128 = 0
if t_i > t:
t_i = t
else:
d_slope = self.slope_changes[t_i]
last_point.bias -= last_point.slope * convert(t_i - last_point.ts, int128)
if t_i == t:
break
last_point.slope += d_slope
last_point.ts = t_i
if last_point.bias < 0:
last_point.bias = 0
return convert(last_point.bias, uint256)
@external
@view
def totalSupply(t: uint256 = block.timestamp) -> uint256:
"""
@notice Calculate total voting power
@dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility
@return Total voting power
"""
_epoch: uint256 = self.epoch
last_point: Point = self.point_history[_epoch]
return self.supply_at(last_point, t)
@external
@view
def totalSupplyAt(_block: uint256) -> uint256:
"""
@notice Calculate total voting power at some point in the past
@param _block Block to calculate the total voting power at
@return Total voting power at `_block`
"""
assert _block <= block.number
_epoch: uint256 = self.epoch
target_epoch: uint256 = self.find_block_epoch(_block, _epoch)
point: Point = self.point_history[target_epoch]
dt: uint256 = 0
if target_epoch < _epoch:
point_next: Point = self.point_history[target_epoch + 1]
if point.blk != point_next.blk:
dt = (_block - point.blk) * (point_next.ts - point.ts) / (point_next.blk - point.blk)
else:
if point.blk != block.number:
dt = (_block - point.blk) * (block.timestamp - point.ts) / (block.number - point.blk)
# Now dt contains info on how far are we beyond point
return self.supply_at(point, point.ts + dt)
# Dummy methods for compatibility with Aragon
@external
def changeController(_newController: address):
"""
@dev Dummy method required for Aragon compatibility
"""
assert msg.sender == self.controller
self.controller = _newController