def test_prepare_mediation_fee_config_flat_fee(cli_flat_fee, expected_channel_flat_fee): token_address = factories.make_token_address() fee_config = prepare_mediation_fee_config( cli_token_to_flat_fee=((token_address, cli_flat_fee),), cli_token_to_proportional_fee=((token_address, ProportionalFeeAmount(0)),), cli_token_to_proportional_imbalance_fee=((token_address, ProportionalFeeAmount(0)),), ) assert fee_config.get_flat_fee(token_address) == expected_channel_flat_fee
def test_fee_round_trip(flat_fee, prop_fee, imbalance_fee, amount): """ Tests mediation fee deduction. """ balance = TokenAmount(100_000) prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) imbalance_fee = calculate_imbalance_fees( channel_capacity=balance, proportional_imbalance_fee=ProportionalFeeAmount(imbalance_fee)) payer_channel = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=TokenAmount(0)), partner_state=NettingChannelEndStateProperties(balance=balance), fee_schedule=FeeScheduleState( flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), )) payer_channel_backwards = factories.create( NettingChannelStateProperties( partner_state=NettingChannelEndStateProperties( balance=TokenAmount(0)), our_state=NettingChannelEndStateProperties(balance=balance), fee_schedule=FeeScheduleState( flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), )) payee_channel = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=balance), partner_state=NettingChannelEndStateProperties( balance=TokenAmount(0)), fee_schedule=FeeScheduleState( flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), )) fee_calculation = get_initial_payment_for_final_target_amount( final_amount=PaymentAmount(amount), channels=[payer_channel_backwards, payee_channel]) assert fee_calculation amount_after_fees = get_lock_amount_after_fees( lock=make_hash_time_lock_state(amount=fee_calculation.total_amount), payer_channel=payer_channel, payee_channel=payee_channel, ) assert amount_after_fees assert abs(amount - amount_after_fees) < 100
def test_rebalancing_fee_calculation(): sample = calculate_imbalance_fees(TokenAmount(200), ProportionalFeeAmount(50_000)) # 5% assert sample is not None assert len(sample) == NUM_DISCRETISATION_POINTS assert all(0 <= x <= 200 for x, _ in sample) assert max(x for x, _ in sample) == 200 assert all(0 <= y <= 10 for _, y in sample) assert max(y for _, y in sample) == 10 # 5% of the 200 TokenAmount capacity sample = calculate_imbalance_fees(TokenAmount(100), ProportionalFeeAmount(20_000)) # 2% assert sample is not None assert len(sample) == NUM_DISCRETISATION_POINTS assert all(0 <= x <= 100 for x, _ in sample) assert max(x for x, _ in sample) == 100 assert all(0 <= y <= 2 for _, y in sample) assert max(y for _, y in sample) == 2 # 2% of the 100 TokenAmount capacity sample = calculate_imbalance_fees(TokenAmount(15), ProportionalFeeAmount(50_000)) # 5% assert sample is not None assert len(sample) == 16 assert all(0 <= x <= 16 for x, _ in sample) assert max(x for x, _ in sample) == 15 assert all(0 <= y <= 1 for _, y in sample) assert max(y for _, y in sample) == 1 # 5% of the 5 rounded up # test rounding of the max_balance_fee calculation sample = calculate_imbalance_fees(TokenAmount(1000), ProportionalFeeAmount(5_490)) # 0.549% assert sample is not None assert len(sample) == NUM_DISCRETISATION_POINTS assert all(0 <= x <= 1000 for x, _ in sample) assert max(x for x, _ in sample) == 1000 assert all(0 <= y <= 5 for _, y in sample) assert max(y for _, y in sample) == 5 # 5.49 is rounded to 5 sample = calculate_imbalance_fees(TokenAmount(1000), ProportionalFeeAmount(5_500)) # 0.55% assert sample is not None assert len(sample) == NUM_DISCRETISATION_POINTS assert all(0 <= x <= 1000 for x, _ in sample) assert max(x for x, _ in sample) == 1000 assert all(0 <= y <= 6 for _, y in sample) assert max(y for _, y in sample) == 6 # 5.5 is rounded to 6 # test cases where no imbalance fee is created assert calculate_imbalance_fees(TokenAmount(0), ProportionalFeeAmount(1)) is None assert calculate_imbalance_fees(TokenAmount(10), ProportionalFeeAmount(0)) is None
def test_fee_estimate(flat_fee, prop_fee_cli, max_lin_imbalance_fee, target_amount, expected_fee): """ Tests the backwards fee calculation. """ capacity = TA(10_000) prop_fee = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee_cli)) imbalance_fee = None if max_lin_imbalance_fee > 0: # This created a simple asymmetric imbalance fee imbalance_fee = [(0, 0), (capacity, 0), (2 * capacity, max_lin_imbalance_fee)] tn = TokenNetworkForTests( channels=[ dict(participant1=1, participant2=2), dict(participant1=2, participant2=3) ], default_capacity=capacity, ) tn.set_fee(2, 1, flat=flat_fee, proportional=prop_fee, imbalance_penalty=imbalance_fee) tn.set_fee(2, 3, flat=flat_fee, proportional=prop_fee, imbalance_penalty=imbalance_fee) assert tn.estimate_fee(1, 3, value=PA(target_amount)) == expected_fee
def test_update_fee(order, pathfinding_service_mock, token_network_model): pathfinding_service_mock.database.insert( "token_network", dict(address=token_network_model.address)) if order == "normal": setup_channel(pathfinding_service_mock, token_network_model) fee_schedule = FeeScheduleState( flat=FeeAmount(1), proportional=ProportionalFeeAmount(int(0.1e9)), imbalance_penalty=[(TokenAmount(0), FeeAmount(0)), (TokenAmount(10), FeeAmount(10))], ) fee_update = PFSFeeUpdate( canonical_identifier=CanonicalIdentifier( chain_identifier=ChainID(1), token_network_address=token_network_model.address, channel_identifier=ChannelID(1), ), updating_participant=PARTICIPANT1, fee_schedule=fee_schedule, timestamp=datetime.now(timezone.utc), signature=EMPTY_SIGNATURE, ) fee_update.sign(LocalSigner(PARTICIPANT1_PRIVKEY)) pathfinding_service_mock.handle_message(fee_update) if order == "fee_update_before_channel_open": setup_channel(pathfinding_service_mock, token_network_model) cv = token_network_model.G[PARTICIPANT1][PARTICIPANT2]["view"] for key in ("flat", "proportional", "imbalance_penalty"): assert getattr(cv.fee_schedule_sender, key) == getattr(fee_schedule, key)
def test_prepare_mediation_fee_config_prop_fee(cli_prop_fee): token_address = factories.make_token_address() fee_config = prepare_mediation_fee_config( cli_token_to_flat_fee=(), cli_token_to_proportional_fee=((token_address, ProportionalFeeAmount(cli_prop_fee)),), cli_token_to_proportional_imbalance_fee=((token_address, ProportionalFeeAmount(0)),), ) cli_prop_fee_ratio = cli_prop_fee / 1e6 channel_prop_fee_ratio = fee_config.get_proportional_fee(token_address) / 1e6 assert isclose( 1 + cli_prop_fee_ratio, 1 + channel_prop_fee_ratio + channel_prop_fee_ratio * (1 + cli_prop_fee_ratio), rel_tol=1e-6, )
def get_fee_update_message( # pylint: disable=too-many-arguments updating_participant: Address, chain_id=ChainID(61), channel_identifier=DEFAULT_CHANNEL_ID, token_network_address: TokenNetworkAddress = DEFAULT_TOKEN_NETWORK_ADDRESS, fee_schedule: FeeScheduleState = FeeScheduleState( cap_fees=True, flat=FeeAmount(1), proportional=ProportionalFeeAmount(1)), timestamp: datetime = datetime.utcnow(), privkey_signer: bytes = PRIVATE_KEY_1, ) -> PFSFeeUpdate: fee_message = PFSFeeUpdate( canonical_identifier=CanonicalIdentifier( chain_identifier=chain_id, channel_identifier=channel_identifier, token_network_address=token_network_address, ), updating_participant=updating_participant, fee_schedule=fee_schedule, timestamp=timestamp, signature=EMPTY_SIGNATURE, ) fee_message.sign(LocalSigner(privkey_signer)) return fee_message
def test_fee_add_remove_invariant(flat_fee, prop_fee, imbalance_fee, amount, balance1, balance2): """ First adding and then removing fees must yield the original value """ total_balance = TokenAmount(100_000_000_000_000_000_000) prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) imbalance_fee = calculate_imbalance_fees( channel_capacity=total_balance, proportional_imbalance_fee=ProportionalFeeAmount(imbalance_fee), ) fee_schedule = FeeScheduleState( cap_fees=False, flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ) channel_in = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=total_balance - balance1), partner_state=NettingChannelEndStateProperties(balance=balance1), fee_schedule=fee_schedule, ) ) channel_out = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=balance2), partner_state=NettingChannelEndStateProperties(balance=total_balance - balance2), fee_schedule=fee_schedule, ) ) amount_with_fees = get_amount_with_fees( amount_without_fees=amount, schedule_in=channel_in.fee_schedule, schedule_out=channel_out.fee_schedule, receivable_amount=balance1, balance_in=total_balance - balance1, balance_out=balance2, ) assume(amount_with_fees) assert amount_with_fees amount_without_fees = get_amount_without_fees( amount_with_fees=amount_with_fees, channel_in=channel_in, channel_out=channel_out ) assume(amount_without_fees) assert amount - 1 <= amount_without_fees <= amount + 1
def test_basic_fee(): flat_schedule = FeeScheduleState(flat=FeeAmount(2)) assert flat_schedule.fee_payer(PaymentWithFeeAmount(10), balance=Balance(0)) == FeeAmount(2) prop_schedule = FeeScheduleState( proportional=ProportionalFeeAmount(int(0.01e6))) assert prop_schedule.fee_payer(PaymentWithFeeAmount(40), balance=Balance(0)) == FeeAmount(0) assert prop_schedule.fee_payer(PaymentWithFeeAmount(60), balance=Balance(0)) == FeeAmount(1) assert prop_schedule.fee_payer(PaymentWithFeeAmount(1000), balance=Balance(0)) == FeeAmount(10) combined_schedule = FeeScheduleState(flat=FeeAmount(2), proportional=ProportionalFeeAmount( int(0.01e6))) assert combined_schedule.fee_payer(PaymentWithFeeAmount(60), balance=Balance(0)) == FeeAmount(3)
def ppm_fee_per_channel(per_hop_fee: ProportionalFeeAmount) -> ProportionalFeeAmount: """ Converts proportional-fee-per-mediation into proportional-fee-per-channel Input and output are given in parts-per-million (ppm). See https://raiden-network-specification.readthedocs.io/en/latest/mediation_fees.html #converting-per-hop-proportional-fees-in-per-channel-proportional-fees for how to get to this formula. """ per_hop_ratio = Fraction(per_hop_fee, 10 ** 6) return ProportionalFeeAmount(round(per_hop_ratio / (per_hop_ratio + 2) * 10 ** 6))
def test_get_lock_amount_after_fees_imbalanced_channel( cap_fees, flat_fee, prop_fee, imbalance_fee, initial_amount, expected_amount ): """ Tests mediation fee deduction. """ balance = TokenAmount(100_000) prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) imbalance_fee = calculate_imbalance_fees( channel_capacity=balance, proportional_imbalance_fee=ProportionalFeeAmount(imbalance_fee) ) lock = make_hash_time_lock_state(amount=initial_amount) channel_in = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=TokenAmount(0)), partner_state=NettingChannelEndStateProperties(balance=balance), fee_schedule=FeeScheduleState( cap_fees=cap_fees, flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), ) ) channel_out = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=balance), partner_state=NettingChannelEndStateProperties(balance=TokenAmount(0)), fee_schedule=FeeScheduleState( cap_fees=cap_fees, flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), ) ) locked_after_fees = get_amount_without_fees( amount_with_fees=lock.amount, channel_in=channel_in, channel_out=channel_out ) assert locked_after_fees == expected_amount
def test_update_fee(order, pathfinding_service_mock, token_network_model): metrics_state = save_metrics_state(metrics.REGISTRY) pathfinding_service_mock.database.insert( "token_network", dict(address=token_network_model.address) ) if order == "normal": setup_channel(pathfinding_service_mock, token_network_model) exception_expected = False else: exception_expected = True fee_schedule = FeeScheduleState( flat=FeeAmount(1), proportional=ProportionalFeeAmount(int(0.1e9)), imbalance_penalty=[(TokenAmount(0), FeeAmount(0)), (TokenAmount(10), FeeAmount(10))], ) fee_update = PFSFeeUpdate( canonical_identifier=CanonicalIdentifier( chain_identifier=ChainID(61), token_network_address=token_network_model.address, channel_identifier=ChannelID(1), ), updating_participant=PARTICIPANT1, fee_schedule=fee_schedule, timestamp=datetime.utcnow(), signature=EMPTY_SIGNATURE, ) fee_update.sign(LocalSigner(PARTICIPANT1_PRIVKEY)) pathfinding_service_mock.handle_message(fee_update) # Test for metrics having seen the processing of the message assert ( metrics_state.get_delta( "messages_processing_duration_seconds_sum", labels={"message_type": "PFSFeeUpdate"}, ) > 0.0 ) assert metrics_state.get_delta( "messages_exceptions_total", labels={"message_type": "PFSFeeUpdate"} ) == float(exception_expected) if order == "fee_update_before_channel_open": setup_channel(pathfinding_service_mock, token_network_model) cv = token_network_model.G[PARTICIPANT1][PARTICIPANT2]["view"] for key in ("flat", "proportional", "imbalance_penalty"): assert getattr(cv.fee_schedule_sender, key) == getattr(fee_schedule, key)
def test_get_lock_amount_after_fees(flat_fee, prop_fee, initial_amount, expected_amount): """ Tests mediation fee deduction. """ prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) lock = make_hash_time_lock_state(amount=initial_amount) payer_channel = factories.create( NettingChannelStateProperties(fee_schedule=FeeScheduleState( flat=flat_fee, proportional=prop_fee_per_channel))) payee_channel = factories.create( NettingChannelStateProperties(fee_schedule=FeeScheduleState( flat=flat_fee, proportional=prop_fee_per_channel))) locked_after_fees = get_lock_amount_after_fees(lock=lock, payer_channel=payer_channel, payee_channel=payee_channel) assert locked_after_fees == expected_amount
def test_get_lock_amount_after_fees(flat_fee, prop_fee, initial_amount, expected_amount): """ Tests mediation fee deduction. """ prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) lock = make_hash_time_lock_state(amount=initial_amount) channel_in = factories.create( NettingChannelStateProperties( partner_state=NettingChannelEndStateProperties(balance=TokenAmount(2000)), fee_schedule=FeeScheduleState(flat=flat_fee, proportional=prop_fee_per_channel), ) ) channel_out = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=TokenAmount(2000)), fee_schedule=FeeScheduleState(flat=flat_fee, proportional=prop_fee_per_channel), ) ) locked_after_fees = get_amount_without_fees( amount_with_fees=lock.amount, channel_in=channel_in, channel_out=channel_out ) assert locked_after_fees == expected_amount
def test_compounding_fees(flat_fee_cli, prop_fee_cli, estimated_fee): """ The transferred amount needs to include the fees for all mediators. Earlier mediators will apply the proportional fee not only on the payment amount, but also on the fees for later mediators. """ flat_fee = flat_fee_cli // 2 prop_fee = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee_cli)) tn = TokenNetworkForTests( channels=[ dict(participant1=1, participant2=2), dict(participant1=2, participant2=3), dict(participant1=3, participant2=4), ], default_capacity=TA(10_000), ) tn.set_fee(2, 1, flat=flat_fee, proportional=prop_fee) tn.set_fee(2, 3, flat=flat_fee, proportional=prop_fee) tn.set_fee(3, 2, flat=flat_fee, proportional=prop_fee) tn.set_fee(3, 4, flat=flat_fee, proportional=prop_fee) assert tn.estimate_fee(1, 4, value=PA(1_000)) == estimated_fee
def test_mfee2(): """ Unit test for the fee calculation in the mfee2_proportional_fees scenario """ amount = 10_000 deposit = 100_000 prop_fee = ppm_fee_per_channel(ProportionalFeeAmount(10_000)) fee_schedule = FeeScheduleState(proportional=ProportionalFeeAmount(prop_fee)) channels = make_channel_pair(fee_schedule, deposit) # How much do we need to send so that the target receives `amount`? PFS-like calculation. fee_calculation = get_initial_amount_for_amount_after_fees( amount_after_fees=PaymentAmount(amount), channels=[channels, channels] ) assert fee_calculation amount_with_margin = calculate_safe_amount_with_fee( fee_calculation.amount_without_fees, FeeAmount(sum(fee_calculation.mediation_fees)) ) assert amount_with_margin == 10_213 # print values for scenario print(deposit - amount_with_margin, amount_with_margin) for med_fee in running_sum(fee_calculation.mediation_fees): print(deposit - amount_with_margin + med_fee, amount_with_margin - med_fee)
def test_mediated_transfer_with_fees(raiden_network, number_of_nodes, deposit, token_addresses, network_wait, case_no): """ Test mediation with a variety of fee schedules """ apps = raiden_network token_address = token_addresses[0] chain_state = views.state_from_app(apps[0]) token_network_registry_address = apps[0].raiden.default_registry.address token_network_address = views.get_token_network_address_by_token_address( chain_state, token_network_registry_address, token_address) assert token_network_address def set_fee_schedule(app: App, other_app: App, fee_schedule: FeeScheduleState): channel_state = views.get_channelstate_by_token_network_and_partner( # type: ignore chain_state=views.state_from_raiden(app.raiden), token_network_address=token_network_address, partner_address=other_app.raiden.address, ) assert channel_state channel_state.fee_schedule = fee_schedule def get_best_routes_with_fees(*args, **kwargs): routes = get_best_routes_internal(*args, **kwargs) for r in routes: r.estimated_fee = fee_without_margin return routes def assert_balances(expected_transferred_amounts=List[int]): for i, transferred_amount in enumerate(expected_transferred_amounts): assert_synced_channel_state( # type: ignore token_network_address=token_network_address, app0=apps[i], balance0=deposit - transferred_amount, pending_locks0=[], app1=apps[i + 1], balance1=deposit + transferred_amount, pending_locks1=[], ) amount = PaymentAmount(35) fee_without_margin = FeeAmount(20) fee = FeeAmount(fee_without_margin + calculate_fee_margin(amount, fee_without_margin)) no_fees = FeeScheduleState(flat=FeeAmount(0), proportional=ProportionalFeeAmount(0), imbalance_penalty=None) no_fees_no_cap = FeeScheduleState( flat=FeeAmount(0), proportional=ProportionalFeeAmount(0), imbalance_penalty=None, cap_fees=False, ) cases = [ # The fee is added by the initiator, but no mediator deducts fees. As a # result, the target receives the fee. dict( fee_schedules=[no_fees, no_fees, no_fees], incoming_fee_schedules=[no_fees, no_fees, no_fees], expected_transferred_amounts=[ amount + fee, amount + fee, amount + fee ], ), # The first mediator claims all of the fee. dict( fee_schedules=[no_fees, FeeScheduleState(flat=fee), no_fees], incoming_fee_schedules=[no_fees, no_fees, no_fees], expected_transferred_amounts=[amount + fee, amount, amount], ), # The first mediator has a proportional fee of 20% dict( fee_schedules=[ no_fees, FeeScheduleState( proportional=ProportionalFeeAmount(int(0.20e6))), no_fees, ], incoming_fee_schedules=[no_fees, no_fees, no_fees], expected_transferred_amounts=[ amount + fee, amount + fee - 9, # See test_get_lock_amount_after_fees for where 9 comes from amount + fee - 9, # See test_get_lock_amount_after_fees for where 9 comes from ], ), # Both mediators have a proportional fee of 20% dict( fee_schedules=[ no_fees, FeeScheduleState( proportional=ProportionalFeeAmount(int(0.20e6))), FeeScheduleState( proportional=ProportionalFeeAmount(int(0.20e6))), ], incoming_fee_schedules=[no_fees, no_fees, no_fees], expected_transferred_amounts=[ amount + fee, amount + fee - 9, # See test_get_lock_amount_after_fees for where 9 comes from # See test_get_lock_amount_after_fees for where 9 and 8 come from amount + fee - 9 - 8, ], ), # The first mediator has an imbalance fee that works like a 20% # proportional fee when using the channel in this direction. dict( fee_schedules=[ no_fees, FeeScheduleState( imbalance_penalty=[(0, 200), (1000, 0)]), # type: ignore no_fees, ], incoming_fee_schedules=[no_fees, no_fees, no_fees], expected_transferred_amounts=[ amount + fee, amount + fee - (amount + fee) // 5 + 2, amount + fee - (amount + fee) // 5 + 2, ], ), # Using the same fee_schedules as above on the incoming channel instead # of the outgoing channel of mediator 1 should yield the same result. dict( fee_schedules=[no_fees, no_fees, no_fees], incoming_fee_schedules=[ FeeScheduleState( imbalance_penalty=[(0, 0), (1000, 200)]), # type: ignore None, None, ], expected_transferred_amounts=[ amount + fee, amount + fee - (amount + fee) // 5, amount + fee - (amount + fee) // 5, ], ), # The first mediator has an imbalance fee which will add 1/20 token for # for every token transferred as a reward for moving the channel into a # better state. This causes the target to receive more than the `amount # + fees` which is sent by the initiator. # transferred amount is 55, so 3 token get added from imbalance fee # Here we also need to disable fee capping dict( fee_schedules=[ no_fees, FeeScheduleState( cap_fees=False, imbalance_penalty=[(0, 0), (1000, 50)] # type: ignore ), no_fees, ], incoming_fee_schedules=[no_fees_no_cap, no_fees, no_fees], expected_transferred_amounts=[ amount + fee, amount + fee + 3, amount + fee + 3 ], ), # Same case as above, but with fee capping enabled dict( fee_schedules=[ no_fees, FeeScheduleState( cap_fees=True, imbalance_penalty=[(0, 0), (1000, 50)] # type: ignore ), no_fees, ], incoming_fee_schedules=[no_fees, no_fees, no_fees], expected_transferred_amounts=[ amount + fee, amount + fee, amount + fee ], ), ] case = cases[case_no] for i, fee_schedule in enumerate(case.get("fee_schedules", [])): if fee_schedule: set_fee_schedule(apps[i], apps[i + 1], fee_schedule)
DEFAULT_INITIAL_CHANNEL_TARGET = 3 DEFAULT_WAIT_FOR_SETTLE = True DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS = BlockTimeout(5) DEFAULT_WAIT_BEFORE_LOCK_REMOVAL = BlockTimeout( 2 * DEFAULT_NUMBER_OF_BLOCK_CONFIRMATIONS) DEFAULT_CHANNEL_SYNC_TIMEOUT = 5 DEFAULT_SHUTDOWN_TIMEOUT = 2 DEFAULT_PATHFINDING_MAX_PATHS = 3 DEFAULT_PATHFINDING_MAX_FEE = TokenAmount(5 * 10**16) # about .01$ # PFS has 200 000 blocks (~40days) to cash in DEFAULT_PATHFINDING_IOU_TIMEOUT = BlockTimeout(2 * 10**5) DEFAULT_MEDIATION_FLAT_FEE = FeeAmount(0) DEFAULT_MEDIATION_PROPORTIONAL_FEE = ProportionalFeeAmount( 4000) # 0.4% in parts per million DEFAULT_MEDIATION_PROPORTIONAL_IMBALANCE_FEE = ProportionalFeeAmount( 3000 # 0.3% in parts per million ) DEFAULT_MEDIATION_FEE_MARGIN: float = 0.03 PAYMENT_AMOUNT_BASED_FEE_MARGIN: float = 0.0005 INTERNAL_ROUTING_DEFAULT_FEE_PERC: float = 0.02 MAX_MEDIATION_FEE_PERC: float = 0.2 ORACLE_BLOCKNUMBER_DRIFT_TOLERANCE = 3 ETHERSCAN_API = "https://{network}.etherscan.io/api?module=proxy&action={action}" RAIDEN_CONTRACT_VERSION = raiden_contracts.constants.CONTRACTS_VERSION MIN_REI_THRESHOLD = TokenAmount(55 * 10**17) # about 1.1$
class FeeScheduleState(State): # pylint: disable=not-an-iterable cap_fees: bool = True flat: FeeAmount = FeeAmount(0) proportional: ProportionalFeeAmount = ProportionalFeeAmount( 0) # as micros, e.g. 1% = 0.01e6 imbalance_penalty: Optional[List[Tuple[TokenAmount, FeeAmount]]] = None _penalty_func: Optional[Interpolate] = field(init=False, repr=False, default=None) def __post_init__(self) -> None: self._update_penalty_func() def _update_penalty_func(self) -> None: if self.imbalance_penalty: typecheck(self.imbalance_penalty, list) x_list, y_list = tuple(zip(*self.imbalance_penalty)) self._penalty_func = Interpolate(x_list, y_list) # type: ignore def fee(self, balance: Balance, amount: Fraction) -> Fraction: return (self.flat + Fraction(self.proportional, int(1e6)) * Fraction(abs(amount)) + (self._penalty_func(balance + amount) - self._penalty_func(balance)) if self._penalty_func else Fraction(0)) @staticmethod def mediation_fee_func( schedule_in: "FeeScheduleState", schedule_out: "FeeScheduleState", balance_in: Balance, balance_out: Balance, receivable: TokenAmount, amount_with_fees: PaymentWithFeeAmount, cap_fees: bool, ) -> Interpolate: """ Returns a function which calculates total_mediation_fee(amount_without_fees) """ return _mediation_fee_func( schedule_in=schedule_in, schedule_out=schedule_out, balance_in=balance_in, balance_out=balance_out, receivable=receivable, amount_with_fees=amount_with_fees, amount_without_fees=None, cap_fees=cap_fees, ) @staticmethod def mediation_fee_backwards_func( schedule_in: "FeeScheduleState", schedule_out: "FeeScheduleState", balance_in: Balance, balance_out: Balance, receivable: TokenAmount, amount_without_fees: PaymentWithFeeAmount, cap_fees: bool, ) -> Interpolate: """ Returns a function which calculates total_mediation_fee(amount_with_fees) """ return _mediation_fee_func( schedule_in=schedule_in, schedule_out=schedule_out, balance_in=balance_in, balance_out=balance_out, receivable=receivable, amount_with_fees=None, amount_without_fees=amount_without_fees, cap_fees=cap_fees, )
def test_fee_round_trip(flat_fee, prop_fee, imbalance_fee, amount, balance1, balance2): """Tests mediation fee deduction. First we're doing a PFS-like calculation going backwards from the target amount to get the amount that the initiator has to send. Then we calculate the fees from a mediator's point of view and check if `amount_with_fees - fees = amount`. """ # Find examples where there is a reasonable chance of succeeding amount = int(min(amount, balance1 * 0.95 - 1, balance2 * 0.95 - 1)) assume(amount > 0) total_balance = TokenAmount(100_000_000_000_000_000_000) prop_fee_per_channel = ppm_fee_per_channel(ProportionalFeeAmount(prop_fee)) imbalance_fee = calculate_imbalance_fees( channel_capacity=total_balance, proportional_imbalance_fee=ProportionalFeeAmount(imbalance_fee), ) channel_in = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=total_balance - balance1), partner_state=NettingChannelEndStateProperties(balance=balance1), fee_schedule=FeeScheduleState( cap_fees=False, flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), ) ) channel_out = factories.create( NettingChannelStateProperties( our_state=NettingChannelEndStateProperties(balance=balance2), partner_state=NettingChannelEndStateProperties(balance=total_balance - balance2), fee_schedule=FeeScheduleState( cap_fees=False, flat=FeeAmount(flat_fee), proportional=prop_fee_per_channel, imbalance_penalty=imbalance_fee, ), ) ) # How much do we need to send so that the target receives `amount`? PFS-like calculation. fee_calculation = get_initial_amount_for_amount_after_fees( amount_after_fees=PaymentAmount(amount), channels=[(channel_in, channel_out)] ) assume(fee_calculation) # There is not enough capacity for the payment in all cases assert fee_calculation # How much would a mediator send to the target? Ideally exactly `amount`. amount_without_margin_after_fees = get_amount_without_fees( amount_with_fees=fee_calculation.total_amount, channel_in=channel_in, channel_out=channel_out, ) assume(amount_without_margin_after_fees) # We might lack capacity for the payment assert abs(amount - amount_without_margin_after_fees) <= 1 # Equal except for rounding errors # If we add the fee margin, the mediator must always send at least `amount` to the target! amount_with_fee_and_margin = calculate_safe_amount_with_fee( fee_calculation.amount_without_fees, FeeAmount(sum(fee_calculation.mediation_fees)) ) amount_with_margin_after_fees = get_amount_without_fees( amount_with_fees=amount_with_fee_and_margin, channel_in=channel_in, channel_out=channel_out ) assume(amount_with_margin_after_fees) # We might lack capacity to add margins assert amount_with_margin_after_fees >= amount
class FeeScheduleState(State): # pylint: disable=not-an-iterable flat: FeeAmount = FeeAmount(0) proportional: ProportionalFeeAmount = ProportionalFeeAmount( 0) # as micros, e.g. 1% = 0.01e6 imbalance_penalty: Optional[List[Tuple[TokenAmount, FeeAmount]]] = None _penalty_func: Optional[Interpolate] = field(init=False, repr=False, default=None) def __post_init__(self) -> None: self._update_penalty_func() def _update_penalty_func(self) -> None: if self.imbalance_penalty: assert isinstance(self.imbalance_penalty, list) x_list, y_list = tuple(zip(*self.imbalance_penalty)) self._penalty_func = Interpolate(x_list, y_list) def imbalance_fee(self, amount: PaymentWithFeeAmount, balance: Balance) -> FeeAmount: if self._penalty_func: # Total channel balance - node balance = balance (used as x-axis for the penalty) balance = self._penalty_func.x_list[-1] - balance try: return FeeAmount( round( self._penalty_func(balance + amount) - self._penalty_func(balance))) except ValueError: raise UndefinedMediationFee() return FeeAmount(0) def fee_payer(self, amount: PaymentWithFeeAmount, balance: Balance) -> FeeAmount: imbalance_fee = self.imbalance_fee( amount=PaymentWithFeeAmount(-amount), balance=balance) flat_fee = self.flat prop_fee = int(round(amount * self.proportional / 1e6)) return FeeAmount(flat_fee + prop_fee + imbalance_fee) def fee_payee(self, amount: PaymentWithFeeAmount, balance: Balance, iterations: int = 2) -> FeeAmount: def fee_out(imbalance_fee: FeeAmount) -> FeeAmount: return FeeAmount( round(amount - ((amount - self.flat - imbalance_fee) / (1 + self.proportional / 1e6)))) imbalance_fee = FeeAmount(0) for _ in range(iterations): imbalance_fee = self.imbalance_fee( amount=PaymentWithFeeAmount(amount - fee_out(imbalance_fee)), balance=balance) return fee_out(imbalance_fee) def reversed(self: T) -> T: if not self.imbalance_penalty: return replace(self) max_penalty = max(penalty for x, penalty in self.imbalance_penalty) reversed_instance = replace( self, imbalance_penalty=[(x, FeeAmount(max_penalty - penalty)) for x, penalty in self.imbalance_penalty], ) self._update_penalty_func() return reversed_instance
def test_edge_weight(addresses): # pylint: disable=assigning-non-slot channel_id = ChannelID(1) participant1 = addresses[0] participant2 = addresses[1] capacity = TokenAmount(int(20 * 1e18)) capacity_partner = TokenAmount(int(10 * 1e18)) settle_timeout = BlockTimeout(15) channel = Channel( token_network_address=TokenNetworkAddress(bytes([1])), channel_id=channel_id, participant1=participant1, participant2=participant2, capacity1=capacity, capacity2=capacity_partner, settle_timeout=settle_timeout, ) view, view_partner = channel.views amount = PaymentAmount(int(1e18)) # one RDN # no penalty assert (TokenNetwork.edge_weight(visited=dict(), view=view, view_from_partner=view_partner, amount=amount, fee_penalty=0) == 1) # channel already used in a previous route assert (TokenNetwork.edge_weight( visited={channel_id: 2}, view=view, view_from_partner=view_partner, amount=amount, fee_penalty=0, ) == 3) # absolute fee view.fee_schedule_sender.flat = FeeAmount(int(0.03e18)) assert (TokenNetwork.edge_weight( visited=dict(), view=view, view_from_partner=view_partner, amount=amount, fee_penalty=100, ) == 4) # relative fee view.fee_schedule_sender.flat = FeeAmount(0) view.fee_schedule_sender.proportional = ProportionalFeeAmount(int(0.01e6)) assert (TokenNetwork.edge_weight( visited=dict(), view=view, view_from_partner=view_partner, amount=amount, fee_penalty=100, ) == 2) # partner has not enough capacity for refund (no_refund_weight) -> edge weight +1 view_partner.capacity = TokenAmount(0) assert (TokenNetwork.edge_weight( visited=dict(), view=view, view_from_partner=view_partner, amount=amount, fee_penalty=100, ) == 3)
def test_fees_are_updated_during_startup( raiden_network, token_addresses, deposit, retry_timeout ) -> None: """ Test that the supplied fee settings are correctly forwarded to all channels during node startup. """ app0, app1 = raiden_network token_address = token_addresses[0] def get_channel_state(app) -> NettingChannelState: chain_state = views.state_from_app(app) token_network_registry_address = app.raiden.default_registry.address token_network_address = views.get_token_network_address_by_token_address( chain_state, token_network_registry_address, token_address ) assert token_network_address channel_state = views.get_channelstate_by_token_network_and_partner( chain_state, token_network_address, app1.raiden.address ) assert channel_state return channel_state waiting.wait_both_channel_deposit( app0, app1, app0.raiden.default_registry.address, token_address, deposit, retry_timeout ) # This is the imbalance penalty generated for the deposit # with DEFAULT_MEDIATION_PROPORTIONAL_IMBALANCE_FEE # once both channels have deposited the default (200) deposit default_imbalance_penalty = [ (0, 1), (20, 0), (40, 0), (60, 0), (80, 0), (100, 0), (120, 0), (140, 0), (160, 0), (180, 0), (200, 0), (220, 0), (240, 0), (260, 0), (280, 0), (300, 0), (320, 0), (340, 0), (360, 0), (380, 0), (400, 1), ] # Check that the defaults are used channel_state = get_channel_state(app0) assert channel_state.fee_schedule.flat == DEFAULT_MEDIATION_FLAT_FEE assert channel_state.fee_schedule.proportional == DEFAULT_MEDIATION_PROPORTIONAL_FEE assert channel_state.fee_schedule.imbalance_penalty == default_imbalance_penalty orginal_config = app0.raiden.config.copy() # Now restart app0, and set new flat fee for that token network flat_fee = FeeAmount(100) app0.stop() app0.raiden.config = deepcopy(orginal_config) app0.raiden.config["mediation_fees"].token_to_flat_fee = {token_address: flat_fee} app0.start() channel_state = get_channel_state(app0) assert channel_state.fee_schedule.flat == flat_fee assert channel_state.fee_schedule.proportional == DEFAULT_MEDIATION_PROPORTIONAL_FEE assert channel_state.fee_schedule.imbalance_penalty == default_imbalance_penalty # Now restart app0, and set new proportional fee prop_fee = ProportionalFeeAmount(123) app0.stop() app0.raiden.config = deepcopy(orginal_config) app0.raiden.config["mediation_fees"].token_to_proportional_fee = {token_address: prop_fee} app0.start() channel_state = get_channel_state(app0) assert channel_state.fee_schedule.flat == DEFAULT_MEDIATION_FLAT_FEE assert channel_state.fee_schedule.proportional == prop_fee assert channel_state.fee_schedule.imbalance_penalty == default_imbalance_penalty # Now restart app0, and set new proportional imbalance fee app0.stop() app0.raiden.config = deepcopy(orginal_config) app0.raiden.config["mediation_fees"].token_to_proportional_imbalance_fee = { token_address: 0.05e6 } app0.start() channel_state = get_channel_state(app0) assert channel_state.fee_schedule.flat == DEFAULT_MEDIATION_FLAT_FEE assert channel_state.fee_schedule.proportional == DEFAULT_MEDIATION_PROPORTIONAL_FEE # with 5% imbalance fee full_imbalance_penalty = [ (0, 20), (20, 18), (40, 16), (60, 14), (80, 12), (100, 10), (120, 8), (140, 6), (160, 4), (180, 2), (200, 0), (220, 2), (240, 4), (260, 6), (280, 8), (300, 10), (320, 12), (340, 14), (360, 16), (380, 18), (400, 20), ] assert channel_state.fee_schedule.imbalance_penalty == full_imbalance_penalty