Skip to content

Commit

Permalink
Merge pull request #9 from patriacaelum/process-for-whitelisting-brokers
Browse files Browse the repository at this point in the history
Process for whitelisting brokers
  • Loading branch information
patriacaelum authored Nov 1, 2021
2 parents 63c5c8e + 51047a6 commit 39c1a14
Show file tree
Hide file tree
Showing 5 changed files with 373 additions and 49 deletions.
47 changes: 37 additions & 10 deletions parameterized/proposal_inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from collections import defaultdict
from eth_account import Account

from .whitelist_mechanism import WhitelistMechanism, NoVote, OwnerVote, PayerVote, EqualVote, WeightedVote, UnanimousVote

pn.extension()


Expand Down Expand Up @@ -103,6 +105,8 @@ class ProposalInverter(Wallet):
current_epoch = pm.Number(0, doc="number of epochs that have passed")
cancel_epoch = pm.Number(0, doc="last epoch where minimum conditions were been met")
payer_contributions = pm.Dict(defaultdict(int), doc="maps each payer's public key to their accumulated contribution")
broker_whitelist = pm.ClassSelector(WhitelistMechanism, default=OwnerVote())
payer_whitelist = pm.ClassSelector(WhitelistMechanism, default=NoVote())

# Parameters
min_stake = pm.Number(5, doc="minimum funds that a broker must stake to join")
Expand All @@ -126,7 +130,7 @@ def __init__(self, owner: Wallet, initial_funds: float, **params):
self.committed_brokers = set()

self.started = self._minimum_start_conditions_met()

def add_broker(self, broker: Wallet, stake: float):
"""
A broker can join the agreement (and must also join the stream associated with that agreement) by staking the
Expand All @@ -147,7 +151,7 @@ def add_broker(self, broker: Wallet, stake: float):
print("Failed to add broker, minimum stake not met")
elif self.cancelled:
print("Failed to add broker, proposal has been cancelled")
else:
elif self.broker_whitelist.in_whitelist(broker):
broker.funds -= stake
self.funds += stake
self.broker_agreements[broker.public] = BrokerAgreement(
Expand All @@ -157,6 +161,9 @@ def add_broker(self, broker: Wallet, stake: float):
total_claimed=0
)
self.committed_brokers.add(broker)
else:
self.broker_whitelist.add_waitlist(broker)
print("Warning: broker not yet whitelisted, added to waitlist")

return broker

Expand Down Expand Up @@ -223,10 +230,11 @@ def iter_epoch(self, n_epochs: int=1):
"""
for epoch in range(n_epochs):
if not self.cancelled:
if not self.started:
self.started = self._minimum_start_conditions_met()

if self.started:
self._allocate_funds()
else:
self.started = self._minimum_start_conditions_met()

self.current_epoch += 1

Expand All @@ -239,7 +247,7 @@ def _allocate_funds(self):
broker_agreement.allocated_funds += self.get_broker_claimable_funds()

# Use cancel_epoch to record when the cancellation condition was triggered
if self.number_of_brokers() >= self.min_brokers or self.get_horizon() >= self.min_horizon:
if self._minimum_start_conditions_met():
self.cancel_epoch = self.current_epoch

# If the forced cancellation conditions are met for a period longer than the buffer period, trigger the cancel function
Expand Down Expand Up @@ -273,10 +281,14 @@ def pay(self, payer: Wallet, tokens: float):
Furthermore, the Horizon H is increased
H+ = (R + ΔF)/ ΔA = H + (ΔF/ΔA)
"""
payer.funds -= tokens
self.funds += tokens
if self.payer_whitelist.in_whitelist(payer):
payer.funds -= tokens
self.funds += tokens

self.payer_contributions[payer.public] += tokens
self.payer_contributions[payer.public] += tokens
else:
self.payer_whitelist.add_waitlist(payer)
print("Warning: payer not yet whitelisted, added to waitlist")

return payer

Expand Down Expand Up @@ -322,9 +334,24 @@ def _minimum_start_conditions_met(self):
- If the specified minimum number of payers has been met
- If the specified minimum horizon has been met
"""
min_brokers_met = self.number_of_brokers() >= self.min_brokers
min_payers_met = len(self.payer_contributions.keys()) >= self.min_payers
min_horizon_met = self.get_horizon() >= self.min_horizon

return min_payers_met and min_horizon_met
return min_brokers_met and min_payers_met and min_horizon_met


def vote_broker(self, voter: Wallet, broker: Wallet, vote: bool):
"""
This is the outward facing interface to directly affect which brokers
are whitelisted. The actual mechanism is dependent on which whitelisting
mechanism is used.
"""
self.broker_whitelist.vote(self, voter, broker, vote)

def vote_payer(self, voter: Wallet, payer: Wallet, vote: bool):
"""
This is the outward facing interface to directly affect which payers are
whitelisted. The actual mechanism is dependent on which whitelisting
mechanism is used.
"""
self.payer_whitelist.vote(self, voter, payer, vote)
64 changes: 27 additions & 37 deletions parameterized/test_proposal_inverter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from .proposal_inverter import Wallet, ProposalInverter
from .whitelist_mechanism import NoVote, OwnerVote


@pytest.fixture
Expand All @@ -13,7 +14,7 @@ def owner():

@pytest.fixture
def inverter(owner):
inverter = owner.deploy(500)
inverter = owner.deploy(500, broker_whitelist=NoVote())

return inverter

Expand Down Expand Up @@ -175,39 +176,17 @@ def test_cancel(owner, inverter, broker1, broker2):
assert inverter.funds == 0
assert inverter.get_allocated_funds() == 0


def test_forced_cancel_case1(broker1):
"""
First test case involves using an inverter where the minimum number of brokers is 2. If only one broker joins and
the minimum horizon is reached, then the forced cancel should be triggered and all remaining funds should be
allocated to the single broker in the inverter.
"""
# Deploy proposal inverter
owner = Wallet()
owner.funds = 1000
inverter = owner.deploy(100, min_brokers=2)

# Add broker
broker1 = inverter.add_broker(broker1, 10)

# Iterate past the buffer period to trigger the forced cancel
inverter.iter_epoch(10)

assert inverter.number_of_brokers() < inverter.min_brokers
assert inverter.get_horizon() < inverter.min_horizon
assert inverter.get_allocated_funds() == inverter.funds


def test_forced_cancel_case2(broker1):
def test_forced_cancel_case1(broker1):
"""
Second test case occurs when the inverter is below the minimum horizon and all brokers leave. In this case, there
First test case occurs when the inverter is below the minimum horizon and all brokers leave. In this case, there
are no brokers to allocate the funds to, so when the forced cancel is triggered, all funds should be returned to the
owner.
"""
# Deploy proposal inverter
owner = Wallet()
owner.funds = 1000
inverter = owner.deploy(100)
inverter = owner.deploy(100, broker_whitelist=NoVote())

# Add broker
broker1 = inverter.add_broker(broker1, 9)
Expand All @@ -225,44 +204,55 @@ def test_forced_cancel_case2(broker1):
assert inverter.get_allocated_funds() == inverter.funds


def test_forced_cancel_case3(broker1, broker2):
def test_forced_cancel_case2(broker1, broker2, payer):
"""
Third test case is to ensure the forced cancel counter resets if the inverter is no longer under the minimum
Second test case is to ensure the forced cancel counter resets if the inverter is no longer under the minimum
conditions. The inverter dips below the minimum conditions for a few epochs less than the specified buffer period,
and the goes back up. The counter should reset, and then the inverter should dip back down and trigger the forced
cancel.
"""
# Deploy proposal inverter
owner = Wallet()
owner.funds = 1000
inverter = owner.deploy(100, min_brokers=2)
inverter = owner.deploy(100, min_brokers=2, broker_whitelist=NoVote())

# Add brokers
broker1 = inverter.add_broker(broker1, 10)
broker2 = inverter.add_broker(broker2, 10)

assert inverter.funds == 120
assert inverter.get_horizon() >= inverter.min_horizon

# Dip below minimum conditions but before the forced cancel triggers
inverter.iter_epoch(6)

assert inverter.number_of_brokers() < inverter.min_brokers
assert inverter.get_horizon() < inverter.min_horizon
assert inverter.get_allocated_funds() < inverter.funds

# Add a second broker and funds to meet the minimum conditions again
broker2 = inverter.add_broker(broker2, 60)
payer = inverter.pay(payer, 60)

assert inverter.number_of_brokers() >= inverter.min_brokers
assert inverter.get_horizon() >= inverter.min_horizon

# Dip below minimum conditions and trigger the forced cancel
broker1 = inverter.remove_broker(broker1)

inverter.iter_epoch(6)

assert inverter.number_of_brokers() < inverter.min_brokers
assert inverter.get_horizon() < inverter.min_horizon
assert inverter.get_allocated_funds() < inverter.funds

inverter.iter_epoch(4)

assert inverter.get_allocated_funds() == inverter.funds
def test_owner_vote(owner, broker1, payer):
inverter = owner.deploy(500, broker_whitelist=OwnerVote())

# Broker applies to proposal, but not yet whitelisted
broker1 = inverter.add_broker(broker1, 10)

assert broker1.funds == 100
assert inverter.number_of_brokers() == 0

# Owner whitelists broker
inverter.vote_broker(owner, broker1, True)
broker1 = inverter.add_broker(broker1, 10)

assert broker1.funds == 90
assert inverter.number_of_brokers() == 1
5 changes: 3 additions & 2 deletions parameterized/test_wallet.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from .proposal_inverter import Wallet
from .whitelist_mechanism import NoVote


def test_deploy_proposal_inverter():
owner = Wallet()
owner.funds = 1000
inverter = owner.deploy(500)
inverter = owner.deploy(500, broker_whitelist=NoVote())

assert inverter.funds == 500
assert inverter.current_epoch == 0
Expand All @@ -15,7 +16,7 @@ def test_remove_proposal_inverter():
# Deploy proposal inverter
owner = Wallet()
owner.funds = 1000
inverter = owner.deploy(500)
inverter = owner.deploy(500, broker_whitelist=NoVote())

# Add brokers (each with a different initial stake)
broker1 = Wallet()
Expand Down
134 changes: 134 additions & 0 deletions parameterized/test_whitelist_mechanism.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import pytest

from .proposal_inverter import Wallet, ProposalInverter
from .whitelist_mechanism import NoVote, OwnerVote, PayerVote, EqualVote, WeightedVote, UnanimousVote


@pytest.fixture
def owner():
owner = Wallet()
owner.funds = 500

return owner


@pytest.fixture
def payer1():
payer1 = Wallet()
payer1.funds = 500

return payer1


@pytest.fixture
def payer2():
payer2 = Wallet()
payer2.funds = 500

return payer2


@pytest.fixture
def inverter(owner, payer1, payer2):
inverter = owner.deploy(300)
payer1 = inverter.pay(payer1, 200)
payer2 = inverter.pay(payer2, 100)

return inverter


@pytest.fixture
def broker():
broker = Wallet()
broker.funds = 100

return broker


def test_no_vote(owner, payer1, inverter, broker):
mechanism = NoVote()

# Votes should not impact whitelisting, and should whitelist all brokers
mechanism.vote(inverter, payer1, broker, False)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == True


def test_owner_vote(owner, payer1, inverter, broker):
mechanism = OwnerVote()

# Case where payer cannot whitelist a broker
mechanism.vote(inverter, payer1, broker, True)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == False

# Case where only the owner can whitelist a broker
mechanism.vote(inverter, owner, broker, True)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == True


def test_payer_vote(payer1, payer2, inverter, broker):
mechanism = PayerVote()

# Case where any payer can whitelist a broker and override a blacklist vote
mechanism.vote(inverter, payer1, broker, False)

assert mechanism.in_waitlist(broker) == True
assert mechanism.in_whitelist(broker) == False

mechanism.vote(inverter, payer2, broker, True)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == True


def test_equal_vote(payer1, payer2, inverter, broker):
mechanism = EqualVote(min_vote=0.5)

mechanism.vote(inverter, payer1, broker, True)

assert mechanism.in_waitlist(broker) == True
assert mechanism.in_whitelist(broker) == False

mechanism.vote(inverter, payer2, broker, True)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == True


def test_weighted_vote(payer1, payer2, inverter, broker):
mechanism = WeightedVote(min_vote=0.6)

# Case where two voters do not have enough combined funds
mechanism.vote(inverter, payer1, broker, True)
mechanism.vote(inverter, payer2, broker, True)

assert mechanism.in_waitlist(broker) == True
assert mechanism.in_whitelist(broker) == False

# Case where a payer increases their funds to increase their weight
payer1 = inverter.pay(payer1, 200)

mechanism.vote(inverter, payer1, broker, True)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == True


def test_unanimous_vote(owner, payer1, payer2, inverter, broker):
mechanism = UnanimousVote()

mechanism.vote(inverter, payer1, broker, True)
mechanism.vote(inverter, payer2, broker, True)

assert mechanism.in_waitlist(broker) == True
assert mechanism.in_whitelist(broker) == False

mechanism.vote(inverter, owner, broker, True)

assert mechanism.in_waitlist(broker) == False
assert mechanism.in_whitelist(broker) == True
Loading

0 comments on commit 39c1a14

Please sign in to comment.