Skip to content

Commit

Permalink
Add decaying vote weight measure
Browse files Browse the repository at this point in the history
  • Loading branch information
0xkorin committed Feb 8, 2024
1 parent f5d031f commit 4d2fe6a
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 2 deletions.
166 changes: 166 additions & 0 deletions contracts/governance/DelegateDecayMeasure.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# @version 0.3.7
"""
@title Vote weight measure with delegation and decay
@author 0xkorin, Yearn Finance
@license GNU AGPLv3
@notice
Base voting weight equal to the measure at launch.
Weight decays linearly to zero in the final 24 hours of the epoch.
Management can delegate voting weight from one account to the other,
which zeroes out the weight for the origin and adds some weight based on the
token balance to the delegate.
"""

interface Measure:
def total_vote_weight() -> uint256: view
def vote_weight(_account: address) -> uint256: view
implements: Measure

interface Staking:
def totalSupply() -> uint256: view
def balanceOf(_account: address) -> uint256: view
def vote_weight(_account: address) -> uint256: view

interface Bootstrap:
def deposited() -> uint256: view
def deposits(_account: address) -> uint256: view

genesis: public(immutable(uint256))
staking: public(immutable(Staking))
bootstrap: public(immutable(address))
delegated_staking: public(immutable(Measure))
management: public(address)
pending_management: public(address)

delegate_multiplier: public(uint256)
delegator: public(HashMap[address, address]) # account => delegate to
delegated: public(HashMap[address, address]) # account => delegated from

event SetDelegateMultiplier:
multiplier: uint256

event Delegate:
account: indexed(address)
receiver: indexed(address)

event PendingManagement:
management: indexed(address)

event SetManagement:
management: indexed(address)

DELEGATE_SCALE: constant(uint256) = 10_000
DAY: constant(uint256) = 24 * 60 * 60
EPOCH_LENGTH: constant(uint256) = 4 * 7 * DAY

@external
def __init__(_genesis: uint256, _staking: address, _bootstrap: address, _delegated_staking: address):
"""
@notice Constructor
@param _genesis Genesis time
@param _staking Staking contract
@param _bootstrap Bootstrap contract
@param _delegated_staking Delegated staking contract
"""
genesis = _genesis
staking = Staking(_staking)
delegated_staking = Measure(_delegated_staking)
bootstrap = _bootstrap
self.management = msg.sender

@external
@view
def total_vote_weight() -> uint256:
"""
@notice Get total vote weight
@return Total vote weight
@dev
Care should be taken to use for quorum purposes, as the sum of actual available
vote weights will be lower than this due to asymptotical vote weight increase.
"""
return staking.totalSupply()

@external
@view
def vote_weight(_account: address) -> uint256:
"""
@notice Get account vote weight
@param _account Account to get vote weight for
@return Account vote weight
"""
weight: uint256 = Bootstrap(bootstrap).deposits(_account)
if weight > 0:
deposited: uint256 = Bootstrap(bootstrap).deposited()
if deposited > 0:
weight = weight * staking.vote_weight(bootstrap) / deposited
else:
weight = 0
weight += staking.vote_weight(_account)

delegated: address = self.delegated[_account]
if delegated != empty(address):
weight += delegated_staking.vote_weight(delegated) * self.delegate_multiplier / DELEGATE_SCALE

left: uint256 = EPOCH_LENGTH - ((block.timestamp - genesis) % EPOCH_LENGTH)
if left <= DAY:
return weight * left / DAY

return weight

@external
def set_delegate_multiplier(_multiplier: uint256):
"""
@notice
Set the delegate multiplier, the value by which delegated
voting weight is multipied by.
@param _multiplier
Delegate multiplier value.
Maximum value is `DELEGATE_SCALE`, which corresponds to one.
"""
assert msg.sender == self.management
assert _multiplier <= DELEGATE_SCALE
self.delegate_multiplier = _multiplier
log SetDelegateMultiplier(_multiplier)

@external
def delegate(_account: address, _receiver: address):
"""
@notice Delegate someones voting weight to someone else
@param _account Account to delegate voting weight from
@param _receiver Account to delegate voting weight to
"""
assert msg.sender == self.management

previous: address = self.delegator[_account]
if previous != empty(address):
self.delegated[previous] = empty(address)

self.delegator[_account] = _receiver
if _receiver != empty(address):
assert self.delegated[_receiver] == empty(address)
self.delegated[_receiver] = _account
log Delegate(_account, _receiver)

@external
def set_management(_management: address):
"""
@notice
Set the pending management address.
Needs to be accepted by that account separately to transfer management over
@param _management New pending management address
"""
assert msg.sender == self.management
self.pending_management = _management
log PendingManagement(_management)

@external
def accept_management():
"""
@notice
Accept management role.
Can only be called by account previously marked as pending management by current management
"""
assert msg.sender == self.pending_management
self.pending_management = empty(address)
self.management = msg.sender
log SetManagement(msg.sender)
48 changes: 46 additions & 2 deletions tests/governance/test_delegate_measure.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
BOOTSTRAP = '0x7cf484D9d16BA26aB3bCdc8EC4a73aC50136d491'
YCHAD = '0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52'
UNIT = 1_000_000_000_000_000_000
MAX = 2**256 - 1
ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
WEEK_LENGTH = 7 * 24 * 60 * 60
DAY_LENGTH = 24 * 60 * 60
WEEK_LENGTH = 7 * DAY_LENGTH
EPOCH_LENGTH = 4 * WEEK_LENGTH

@pytest.fixture
def token():
Expand Down Expand Up @@ -75,7 +78,7 @@ def test_remove_delegate(deployer, alice, measure):
assert measure.delegated(deployer) == ZERO_ADDRESS

def test_delegate_previous(chain, accounts, deployer, alice, token, staking, dstaking, measure):
# delegated voting weight should not be manipulatable, currently fails
# delegated voting weight should not be manipulatable
management = accounts[token.management()]
token.set_minter(deployer, sender=management)
token.mint(deployer, 4 * UNIT, sender=deployer)
Expand All @@ -94,3 +97,44 @@ def test_delegate_previous(chain, accounts, deployer, alice, token, staking, dst
# depositing in same week should not increase weight
dstaking.deposit(UNIT, sender=deployer)
assert measure.vote_weight(alice) == weight

def test_decay(chain, project, accounts, deployer, alice, token, staking, bootstrap, dstaking, measure):
genesis = chain.pending_timestamp // WEEK_LENGTH * WEEK_LENGTH
decay_measure = project.DelegateDecayMeasure.deploy(genesis, staking, bootstrap, dstaking, sender=deployer)

management = accounts[token.management()]
token.set_minter(deployer, sender=management)
token.mint(deployer, 4 * UNIT, sender=deployer)
token.approve(staking, 4 * UNIT, sender=deployer)
staking.mint(2 * UNIT, sender=deployer)
staking.approve(dstaking, 2 * UNIT, sender=deployer)
dstaking.deposit(2 * UNIT, sender=deployer)

token.mint(alice, 4 * UNIT, sender=deployer)
token.approve(staking, 4 * UNIT, sender=alice)
staking.mint(UNIT, sender=alice)

measure.set_delegate_multiplier(5000, sender=deployer)
measure.delegate(deployer, alice, sender=deployer)
decay_measure.set_delegate_multiplier(5000, sender=deployer)
decay_measure.delegate(deployer, alice, sender=deployer)

chain.pending_timestamp = genesis + 3 * WEEK_LENGTH
chain.mine()
weight = decay_measure.vote_weight(alice)
assert weight == measure.vote_weight(alice)

# 24h before end of epoch, voting power is full
chain.pending_timestamp = genesis + EPOCH_LENGTH - DAY_LENGTH
chain.mine()
assert decay_measure.vote_weight(alice) == weight

# 12h before end of epoch, voting power is half
chain.pending_timestamp = genesis + EPOCH_LENGTH - DAY_LENGTH // 2
chain.mine()
assert decay_measure.vote_weight(alice) == weight // 2

# 6h before end of epoch, voting power is a quarter
chain.pending_timestamp = genesis + EPOCH_LENGTH - DAY_LENGTH // 4
chain.mine()
assert decay_measure.vote_weight(alice) == weight // 4

0 comments on commit 4d2fe6a

Please sign in to comment.