Skip to main content

BancorML is a library that builds, optimizes, and evaluates machine learning pipelines in the context of a multi-agent system

Project description

bciAVM

PyPI bancorml-0.1.0-py3-none-any.whl package in bancorml@Release feed in Azure Artifacts PyPI version Documentation Status

BancorML is a simulation library for Bancor v3.

The library demonstrates the following:

Key Functionality

  • BIP15 Demo - Most Bancor v3 features are supported.

    • Withdrawal Algorithm - All BIP15 described examples and features are supported.

    • Trades - All BIP15 described examples and features are supported.

    • Staking - All BIP15 described examples and features are supported.

    • Instant IL protection - All BIP15 described examples and features are supported.

  • Multi-Agent Oriented - High-level architecture designed for multi-agent-oriented programming.

Project setup

If you don't already have one, create a virtualenv using these instructions

Install

BancorML is available for Python 3.6+

To install using pypi, run this command:

$ pip install bancorml

Start

Bancor 3 Product Specifications

The BancorML library is organized into two main classes:

  • Environment(s) - The Bancor v3 protocol is modeled as an environment through which all other agent types act.

  • Agent(s) - Traders, Liquidity Providers, Arbitrageurs, and the Bancor DAO are modeled as agents which interact with their environment.

from bancorml.environments import Bancor3

protocol = Bancor3( block_num=0,
                    alpha=0.2,
                    is_solidity=False,
                    min_liquidity_threshold=1000,
                    exit_fee=.0025,
                    cooldown_period=7,
                    bnt_funding_limit=100000,
                    external_price_feed={
                        "TKN": {'block_num': [0, 1, 2, 3],
                                'price_usd': [1, 2, 3, 4]},
                        "BNT": {'block_num': [0, 1, 2, 3],
                                'price_usd': [2.50, 2.49, 2.51, 2.50]},
                        "LINK": {'block_num': [0, 1, 2, 3],
                                'price_usd': [15.35, 15.20, 15.11, 15.00]},
                        "ETH": {'block_num': [0, 1, 2, 3],
                                'price_usd': [2550.00, 2400.00, 2450.00, 2500.00]},
                        "wBTC": {'block_num': [0, 1, 2, 3],
                                'price_usd': [40123.23, 40312.21, 40111.11, 40000.00]}
                    },
                    pool_fees={
                        "TKN": 0.01,
                        "BNT": 0.01,
                        "LINK": 0.01,
                        "ETH": 0.01,
                        "wBTC": 0.01
                    },
                    whitelisted_tokens=[],
                    bootstrapped_tokens=[],
                    spot_prices={
                        "TKN": {'block_num':[0],
                                'price_usd':[2]},
                        "BNT": {'block_num':[0],
                                'price_usd':[2.50]},
                        "LINK": {'block_num':[0],
                                'price_usd':[15.00]},
                        "ETH": {'block_num':[0],
                                'price_usd':[2500.00]},
                        "wBTC": {'block_num':[0],
                                'price_usd':[40000.00]}
                    },
                    vortex_rates={
                        "TKN": 0.2,
                        "BNT": 0.2,
                        "LINK": 0.2,
                        "ETH": 0.2,
                        "wBTC": 0.2
                    },
                    dao_msig_initialized_pools=[],
                    verbose=True)

A 7-day Cool-Off Period

If an attack vector exists, either discretely or abstractly, its frequency and potential damage can be effectively nullified with the introduction of a mandatory waiting period, prior to the withdrawal of user’s funds. Compared to the current 100 day vesting period, a 7-day cool-off represents an effective drop of 93% in mandatory wait time prior to full protection. In context, the additional prudency still affords users an improved offering, without conceding any vulnerabilities to the system.

protocol.set_param(cooldown_period=7)
protocol.cooldown_period
7

Exit Fees

As a final circumspection against bad behavior, exit fees are introduced into the Bancor ecosystem for the first time. The exit fee is designed to temper the profit motive of the imagined exploit vector. Fortuitously, the severity of the speculative attack is fairly minor, and a 0.25% exit fee is sufficient to render it void. The exit fee is treated as a relief mechanism, and allows the pools from which a withdrawal is processed to retain a small amount of residual value, which alleviates the insurance burden by the same amount. It also serves a financial purpose in defining a geometric surface, beneath which users can withdraw their stake entirely in its own denomination, regardless of the relative surplus or deficit state of the network. This point is discussed in detail while addressing the behavior of the withdrawal algorithm.

protocol.set_param(exit_fee=0.0025)
protocol.exit_fee
0.0025

The protocol is in the bootstrapping phase for all three assets. Users can still interact with the system and deposit liquidity - which is essential for the process. The system needs to attract sufficient quantities of each token to overcome the minimum liquidity threshold prior to the activation of each liquidity pool, respectively. The default minimum_liquidity_threshold is 1,000 BNT, meaning that the pool must mint 1,000 BNT during its activation, and therefore there must be at least a commensurate value of TKN available in the vault. Assume that the prices of each asset are as follows: BNT = $2.50, LINK = $15.00, ETH = $2,500.00, wBTC = $40,000.00.

spot_prices={
    "BNT": {'block_num':[0],
            'price_usd':[2.50]},
    "LINK": {'block_num':[0],
            'price_usd':[15.00]},
    "ETH": {'block_num':[0],
            'price_usd':[2500.00]},
    "wBTC": {'block_num':[0],
            'price_usd':[40000.00]}
}

protocol.set_param(spot_prices=spot_prices)
protocol.spot_prices
{'BNT': {'block_num': [0], 'price_usd': [2.5]},
 'LINK': {'block_num': [0], 'price_usd': [15.0]},
 'ETH': {'block_num': [0], 'price_usd': [2500.0]},
 'wBTC': {'block_num': [0], 'price_usd': [40000.0]}}

Note that when we set the spot_prices, the exchange_rates are automatically updated. In our example, the BancorDAO msig signers will sign transactions with the rate of BNT/TKN for each of the bootstrapping assets. In this case, BNT/ETH = 1,000, BNT/wBTC = 16,000, BNT/LINK = 6.

protocol.exchange_rates
{'BNT': 1.0, 'LINK': 6.0, 'ETH': 1000.0, 'wBTC': 16000.0}

Similarly, the whitelisted_tokens are also automatically updated.

protocol.whitelisted_tokens
['BNT', 'LINK', 'ETH', 'wBTC']

Based on the minimum_liquidity_threshold and the spot_prices mentioned above, in order to bootstrap each pool, at least $25,000 of each asset must be available in the vault. This criterea is automatically tracked on a per token basis and the bootstrapped_tokens are updated accordingly. In our current example, no pools meet the bootsrapping initialization criterea.

protocol.bootstrapped_tokens
[]

This is because the condition to initialize a pool is that there must be at least 10,000 BNT ($25,000 at the quoted price) worth of each token - in this case all three satisfy the criteria.

from bancorml.agents.liquidity_provider_agent import LiquidityProviderAgent

During the bootstrap phase, Alice deposits 10 wBTC, Bob deposits 10 ETH, and Charlie deposits 1,000 LINK.

alice = LiquidityProviderAgent(env=protocol, unique_id="Alice")
bob = LiquidityProviderAgent(env=protocol, unique_id="Bob")
charlie = LiquidityProviderAgent(env=protocol, unique_id="Charlie")
alice.deposit(tkn="wBTC", amount=10, block_num=0)
bob.deposit(tkn="ETH", amount=10, block_num=0)
charlie.deposit(tkn="LINK", amount=1000, block_num=0)
protocol.describe()
spot_rate=16000.0 
 ema=16000.0 
 EMA Test = True, (0.99 * 16000.0) <= 16000.0 <= (1.01 * 16000.0), tkn=wBTC
spot_rate=1000.0 
 ema=1000.0 
 EMA Test = True, (0.99 * 1000.0) <= 1000.0 <= (1.01 * 1000.0), tkn=ETH
spot_rate=6.0 
 ema=6.0 
 EMA Test = True, (0.99 * 6.0) <= 6.0 <= (1.01 * 6.0), tkn=LINK
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:0 - ETH:0 BNT:0 BNT:0 bnBNT:0 Balance:0
1 BNT:0 - wBTC:0 ETH:10 ETH:10 bnETH:10
2 BNT:0 - LINK:0 wBTC:10 wBTC:10 bnwBTC:10
3 LINK:1000 LINK:1000 bnLINK:1000

Note that the trading liquidity available on the pools remains unchanged until the BancorDAO msig signers initialize each pool.

protocol.dao_msig_initialize_pools()
protocol.describe()
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:1000 - ETH:1.0 BNT:3000 BNT:3000 bnBNT:3000 Balance:0
1 BNT:1000 - wBTC:0.0625 ETH:10 ETH:10 bnETH:10
2 BNT:1000 - LINK:166.66666666666666 wBTC:10 wBTC:10 bnwBTC:10
3 LINK:1000 LINK:1000 bnLINK:1000

By default, the initial pool token value is 1:1 with the underlying asset, and only changes after revenue begins accumulating. Therefore, each of these characters receive 10 bnTKN pool tokens for their contribution...

alice.wallet
{'block_num': [0, 0], 'wBTC': [10000, 9990], 'bnwBTC': [0, 10], 'BNT': [0, 0]}
alice.get_wallet("wBTC"), bob.get_wallet("ETH"), charlie.get_wallet("LINK")
(('wBTC=9990', 'bnwBTC=10'),
 ('ETH=9990', 'bnETH=10'),
 ('LINK=999000', 'bnLINK=1000'))

Remember that the condition to initialise new pools is that there must be at least 1,000 BNT ($2,500 at the quoted price) worth of each token - in this case all three satisfy the criteria. We can now double check that the pools have been automatically initialized, as expected.

protocol.bootstrapped_tokens
['LINK', 'ETH', 'wBTC']

Fortunately, the rate at which the pool can grow is independent of the new liquidity additions, and is determined entirely by the token balances available in the vault. To demonstrate, suppose that each user deposits just 1 more TKN each to their respective pools; Alice deposits 1 ETH, Bob deposits 1 wBTC, and Charlie deposits 1 LINK.

alice.deposit(tkn="ETH", amount=1, block_num=0)
bob.deposit(tkn="wBTC", amount=1, block_num=0)
charlie.deposit(tkn="LINK", amount=1, block_num=0)
protocol.describe()
spot_rate=1000.0 
 ema=1000.0 
 EMA Test = True, (0.99 * 1000.0) <= 1000.0 <= (1.01 * 1000.0), tkn=ETH
spot_rate=16000.0 
 ema=16000.0 
 EMA Test = True, (0.99 * 16000.0) <= 16000.0 <= (1.01 * 16000.0), tkn=wBTC
spot_rate=6.0 
 ema=6.0 
 EMA Test = True, (0.99 * 6.0) <= 6.0 <= (1.01 * 6.0), tkn=LINK
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:2000 - ETH:2.0 BNT:6000.0 BNT:6000.0 bnBNT:6000.0 Balance:0
1 BNT:2000 - wBTC:0.125 ETH:11 ETH:11 bnETH:11.0
2 BNT:2000 - LINK:333.3333333333333 wBTC:11 wBTC:11 bnwBTC:11.0
3 LINK:1001 LINK:1001 bnLINK:1001.0

Each of these tokens is accepted directly to the vault. The most important point to note in the change in the system state depicted above is that the additional 1 TKN unit staked was essentially ignored - when the protocol is changing the available trading liquidity, the total vault balance is taken into account, rather than the amount the user has just provided. Each of the pools where a new TKN was staked resulted in a doubling of the available trading liquidity.

Trading and Fees: BNT to TKN

Continuing from the system state described above, the details of how the trading pools are used as a price oracle, the behavior and purpose of the price EMA, and the response of the staking ledger to the accumulation of swap revenue will now be discussed in detail. For the purpose of demonstration, let each of these pools have a 1% swap fee...

pool_fees = {
    "BNT": 0.01,
    "LINK": 0.01,
    "ETH": 0.01,
    "wBTC": 0.01
}

protocol.set_param(pool_fees=pool_fees)
protocol.pool_fees
{'BNT': 0.01, 'LINK': 0.01, 'ETH': 0.01, 'wBTC': 0.01}

...and a Vortex rate of 20%.

vortex_rates = {
    "BNT": 0.2,
    "LINK": 0.2,
    "ETH": 0.2,
    "wBTC": 0.2
}

protocol.set_param(vortex_rates=vortex_rates)
protocol.vortex_rates
{'BNT': 0.2, 'LINK': 0.2, 'ETH': 0.2, 'wBTC': 0.2}

Assume a trader MrT...

from bancorml.agents.trader_agent import TraderAgent
MrT_agent = TraderAgent(env=protocol, unique_id="MrT")

...swaps 200 BNT for LINK; from the perspective of the trader, 200 BNT are sent into the vault, and 30 LINK are removed from the vault and sent back to him.

MrT_agent.swap('BNT', 200, 'LINK', block_num=0)
protocol.describe()
bnt_trading_liquidity=2199.5604395604396, 
tkn_trading_liquidity=303.3333333333333, 
tkn_out=29.999999999999993, 
tkn_fee=0.24242424242424243, 
vortex_fee=0.43956043956043955 
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:2000 - ETH:2.0 BNT:6200.0 BNT:6000.0 bnBNT:6000.0 Balance:0.43956043956043955
1 BNT:2000 - wBTC:0.125 ETH:11 ETH:11 bnETH:11.0
2 BNT:2199.5604395604396 - LINK:303.3333333333333 wBTC:11 wBTC:11 bnwBTC:11.0
3 LINK:971.0 LINK:1001.2424242424242 bnLINK:1001.0

The change to the system state is much more involved. The change in the vault balances agrees with the trader; 200 BNT is received, and 30 LINK was emitted. However, the change in the available trading liquidity presents the first evidence of a consequence of the new design.

The Moving Average

A moving average is utilized as a security measure, where sudden changes in the pool reserves can be detected, and prevent abuse of the protocol’s features. The moving average (ema) is updated with the first trade of the block, for any asset according to the following formula:

where r is the spot rate in units of BNT/TKN as determined by the trading liquidity balances of the pool, and α is an arbitrary constant that determines the responsiveness of the moving average. The α term is a global variable, set at 0.2 (or 20%) at launch of Bancor 3, and is intended to provide a consensus rate for the pool that is resistant to virtual price manipulation attacks. The following chart is an arbitrary depiction of the ema behavior relative to the spot price on a per-block basis. The ema is measured and updated before an action is executed; therefore, the ema response is delayed by a minimum of one action (e.g. a trade or add/remove liquidity event). Further, the ema is only adjusted once per pool, per block.

At genesis, the ema rate is set equal to the spot rate. Therefore, in the above scenario each of the liquidity pools began with the following rates:

We can confirm the history of our EMA matches the genesis state as follows:

protocol.ema
{'TKN': {'block_num': [0], 'ema': [0.8]},
 'BNT': {'block_num': [0], 'ema': [1.0]},
 'wBTC': {'block_num': [0], 'ema': [16000.0]},
 'ETH': {'block_num': [0], 'ema': [1000.0]},
 'LINK': {'block_num': [0], 'ema': [6.0]}}
protocol.alpha
0.2

However, the trading situations do have an effect. To demonstrate, assume that the trades described above happen on consecutive blocks. For the LINK trading pool, both the spot rate and the ema begin at 6, as set at the genesis of the pool. Since the ema is adjusted before the trade is executed, the first trade has no effect on the ema; the spot price changes as expected. The new spot price becomes relevant in the next block, as the ema is updated prior to performing the second trade. First, the ema is updated using the new spot rate, then the second trade is processed. In this example, the lag of the ema means there is a significant gap between it and the spot price after the first block; however, the adjustment in the second block, prior to executing the second trade, results in a close agreement thereafter.

protocol.get_internal_spot_rate('LINK')
7.251298152397054

Trading and Fees: TKN to BNT

Assume now that a trader wants to perform the opposite action, perhaps to close an arbitrage opportunity left open by the previous swap. The trader sends 30.299 LINK into the vault, and the vault sends 197.756685 BNT to the trader. As before, the trader’s intuition agrees with the changing state of the vault balances; however, the changes in the trading liquidity and staked balances require closer examination.

MrT_agent.swap('LINK', 30.299, 'BNT', block_num=1)
protocol.describe()
bnt_trading_liquidity=2001.404207987633, 
tkn_trading_liquidity=333.6323333333333, 
tkn_out=197.75672304140988, 
tkn_fee=1.5980341255871509, 
vortex_fee=0.39950853139678766 
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:2000 - ETH:2.0 BNT:6002.24327695859 BNT:6001.598034125587 bnBNT:6000.0 Balance:0.8390689709572272
1 BNT:2000 - wBTC:0.125 ETH:11 ETH:11 bnETH:11.0
2 BNT:2001.404207987633 - LINK:333.6323333333333 wBTC:11 wBTC:11 bnwBTC:11.0
3 LINK:1001.299 LINK:1001.2424242424242 bnLINK:1001.0

Confirm that the trading liquidity ledger matches:

protocol.get_available_liquidity_ledger('LINK').iloc[-2:]
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
block_num funding_limit BNT LINK
5 0 97000 2199.560440 303.333333
6 0 97000 2001.404208 333.632333

Trading and Fees: TKN to TKN

Assume now that a trader wishes to exchange 0.1 ETH for wBTC. From their perspective the process is similar to that described above. A total of 0.1 ETH tokens were sent into the vault, and the vault sent 0.005571 wBTC tokens back, and the change in the vault balances agree with the trader’s intuition (as before).

However, this situation compounds the effects described in the previous two sections. The changes in the trading liquidity balances, and the accumulation of value to the vortex ledger, can be deduced by considering the two separate legs of the process:

Swap 0.1 ETH for 94.285714 BNT

  • Add 0.76191905 BNT to the staking ledger
  • Add 0.190476 BNT to the vortex ledger

Swap 94.285714 BNT for 0.002271282 wBTC

  • Add 0.0000450205 wBTC to the staking ledger
  • Add 0.197368179 BNT to the vortex ledger
MrT_agent.swap('ETH', 0.1, 'wBTC', block_num=1)
protocol.describe()
spot_rate=1000.0 
 ema=1000.0 
 EMA Test = True, (0.99 * 1000.0) <= 1000.0 <= (1.01 * 1000.0), tkn=ETH
spot_rate=16000.0 
 ema=16000.0 
 EMA Test = True, (0.99 * 16000.0) <= 16000.0 <= (1.01 * 16000.0), tkn=wBTC
bnt_source_trading_liquidity=1905.5238095238094, 
 tkn_source_trading_liquidity=2.1, 
 bnt_destination_trading_liquidity=2094.0883461062235, 
 tkn_destination_trading_liquidity=0.11942871759890858, 
 tkn_out=0.005571282401091405, 
 bnt_fee=0.7619047619047619, 
 tkn_fee=4.5020463847203275e-05, 
 vortex_fee=0.3878443699670836 
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:1905.5238095238094 - ETH:2.1 BNT:6002.24327695859 BNT:6002.359938887492 bnBNT:6000.0 Balance:1.2269133409243107
1 BNT:2094.0883461062235 - wBTC:0.11942871759890858 ETH:11.1 ETH:11 bnETH:11.0
2 BNT:2001.404207987633 - LINK:333.6323333333333 wBTC:10.99442871759891 wBTC:11.000045020463848 bnwBTC:11.0
3 LINK:1001.299 LINK:1001.2424242424242 bnLINK:1001.0

It is important to realize that there is no BNT transfer, despite both pools reporting a change to the BNT trading liquidity. A virtual trade has been performed, where BNT is still used as numeraire, and which allows for the relative value of ETH and wBTC to be known. Since the liquidity pools are virtual, their relative balance of BNT can be adjusted as though the tokens were sent, when in fact the BNT remains where it was in the vault. Thus, a quasi-single hop trade is achieved.

protocol.get_available_liquidity_ledger('wBTC').iloc[-1:]
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
block_num funding_limit BNT wBTC
7 0 97000 2094.088346 0.119429

Staking TKN

The ema serves as a detection mechanism for unusual spikes in the price of an asset against BNT, and prevents changes to the depth of the trading pools to protect the system from harm. To illustrate the effects of the ema, the current state of the system in the present narrative will be carried forward, with each of Alice, Bob, and Charlie providing one additional TKN to the system. Unlike the prior examples where liquidity is added, both the ema, and the pool token valuation need to be taken under consideration.

Before the demonstration continues, recall that each of the pools have a preset 4,000 BNT funding limit at this point in the narrative, and up to this point in time all three have only exhausted half of that amount. Further, recall that the ability to increase the available trading liquidity is determined by the vault balance of the TKN in question, and the current depth of the pool, rather than the amount being contributed by a user during any deposit event.

Assume that the following deposit is occurring on a block wherein no actions have taken place on the ETH pool. Therefore, the ema has not been updated in this block, and it remains where it was.

Alice now deposits 1 ETH. Since no fees have yet accrued to the bnETH pool tokens (i.e. ETH has not yet been the target of any particular trade), the value of the bnETH pool token remains unchanged. Her 1 ETH is added to the vault, the staking ledger is updated, and a new pool token is created for Alice.

alice.deposit('ETH', 1, block_num=1)
protocol.describe()
spot_rate=907.392290249433 
 ema=1000.0 
 EMA Test = False, (0.99 * 1000.0) <= 907.392290249433 <= (1.01 * 1000.0), tkn=ETH
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:1905.5238095238094 - ETH:2.1 BNT:6002.24327695859 BNT:6002.359938887492 bnBNT:6000.0 Balance:1.2269133409243107
1 BNT:2094.0883461062235 - wBTC:0.11942871759890858 ETH:12.1 ETH:12 bnETH:12.0
2 BNT:2001.404207987633 - LINK:333.6323333333333 wBTC:10.99442871759891 wBTC:11.000045020463848 bnwBTC:11.0
3 LINK:1001.299 LINK:1001.2424242424242 bnLINK:1001.0

This allows for the effective price quoted by the pool to be evaluated with a view to detecting artificial price manipulation. The protocol’s confidence is determined by the relative deviation between the ema and spot prices; the protocol will only allow the pool’s trading liquidity to be changed if the difference between these values is small.

In this instance, the addition of Alice’s 1 ETH occurs when the ema on ETH remains at 1,000, whereas its spot rate is 907.26. The deviation between the spot rate and the ema is measured relative to the ema, as follows:

protocol.is_within_ema_tolerance('ETH')
spot_rate=907.392290249433 
 ema=1000.0 
 EMA Test = False, (0.99 * 1000.0) <= 907.392290249433 <= (1.01 * 1000.0), tkn=ETH





False
protocol.get_available_liquidity_ledger('ETH').iloc[-2:]
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
block_num funding_limit BNT ETH
6 0 97000 2000.00000 2.0
7 0 97000 1905.52381 2.1

The staking ledger is reporting a staked amount of wBTC of 11.00004502

protocol.staking_ledger['wBTC'][-1]
11.000045020463848

and the supply of bnwBTC is 11. Therefore, the rate of bnwBTC to wBTC is 0.999996, and this quotient determines the number of bnwBTC pool tokens Bob will receive.

protocol.get_pool_token_supply_ledger('wBTC').iloc[-1:]
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
block_num supply
7 1 11.0

Bob now deposits 1 wBTC. In contrast to Alice’s situation, fees have accumulated on the wBTC pool token. Therefore, the rate of pool token issuance is affected. Assume that this deposit is occurring on the same block as Alice’s deposit, where the trade between ETH and wBTC had been executed on the prior block. Therefore, the wBTC ema has not been updated in this block, and also remains at the genesis rate.

bob.deposit('wBTC', 1, block_num=1)
protocol.describe()
spot_rate=17534.21110271857 
 ema=16000.0 
 EMA Test = False, (0.99 * 16000.0) <= 17534.21110271857 <= (1.01 * 16000.0), tkn=wBTC
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:1905.5238095238094 - ETH:2.1 BNT:6002.24327695859 BNT:6002.359938887492 bnBNT:6000.0 Balance:1.2269133409243107
1 BNT:2094.0883461062235 - wBTC:0.11942871759890858 ETH:12.1 ETH:12 bnETH:12.0
2 BNT:2001.404207987633 - LINK:333.6323333333333 wBTC:11.99442871759891 wBTC:12.000045020463848 bnwBTC:11.99999590724731
3 LINK:1001.299 LINK:1001.2424242424242 bnLINK:1001.0

In this instance, the addition of Bob’s 1 wBTC coccurs when the ema on wBTC remains at 16,000, whereas its spot rate is at 17,534. The deviation (Δ) between the spot rate and the ema is 0.0959, and as before, the protocol will not allow the trading liquidity to be changed on this block. Regardless, the vault still accepts Bob’s wBTC, and issues him bnwBTC as normal.

Charlie now deposits 1 LINK.

protocol.available_liquidity_ledger['LINK']['funding_limit']
[100000, 99000, 99000, 99000, 97000, 97000, 97000, 97000]
protocol.available_liquidity_ledger['LINK']['funding_limit'][-1] = 2000
charlie.deposit('LINK', 1, block_num=2)
protocol.describe()
spot_rate=5.998831671953158 
 ema=5.999766334390633 
 EMA Test = True, (0.99 * 5.999766334390633) <= 5.998831671953158 <= (1.01 * 5.999766334390633), tkn=LINK
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:1905.5238095238094 - ETH:2.1 BNT:8002.945380952407 BNT:8003.062042881308 bnBNT:8000.702103993816 Balance:1.2269133409243107
1 BNT:2094.0883461062235 - wBTC:0.11942871759890858 ETH:12.1 ETH:12 bnETH:12.0
2 BNT:4001.404207987633 - LINK:667.1476265441543 wBTC:11.99442871759891 wBTC:12.000045020463848 bnwBTC:11.99999590724731
3 LINK:1002.299 LINK:1002.2424242424242 bnLINK:1001.9997578765776
protocol.ema
{'TKN': {'block_num': [0], 'ema': [0.8]},
 'BNT': {'block_num': [0], 'ema': [1.0]},
 'wBTC': {'block_num': [0], 'ema': [16000.0]},
 'ETH': {'block_num': [0], 'ema': [1000.0]},
 'LINK': {'block_num': [0, 1], 'ema': [6.0, 5.999766334390633]}}

As was the case for Bob, the fee accrual on bnLINK will affect the issuance of pool tokens. The staking ledger is reporting a staked amount of LINK of 1,001.2424, and the supply of bnLINK is 1,001. Therefore, the rate of bnLINK to LINK is 0.9998, and the issuance of bnLINK to Charlie is determined by this number.

Assume that this deposit occurs on the block following the two LINK trades previously discussed. In this instance, the addition of Charlie’s 1 LINK occurs when the ema on LINK is 5.9998, whereas its spot rate is at 5.9988. The deviation (Δ) between the spot rate and the ema is 0.00017, which is inside the tolerance levels of the protocol, and the protocol will allow the trading liquidity to update.

spot_rate = protocol.get_internal_spot_rate('LINK')
spot_rate
5.99777927520335
ema = protocol.ema['LINK']['ema'][-1]
ema
5.999766334390633

Trading liquidity always updates according to a strict set of criteria. In simple terms, the protocol will always try to increase the liquidity by as much as possible, up to a maximum BNT liquidity increase of 2× relative to the current depth, and without exceeding the preset funding limit. In this example, the current BNT depth on the LINK pool is 2,094.0883. Therefore, with a sufficient funding limit, the pool could double in size, up to a maximum BNT depth of 4,188.1766 BNT. However, the protocol has previously contributed 2,000 BNT of its 4,000 BNT funding limit - it can only increase the trading liquidity by a maximum of 2,000 BNT. The protocol will always default to the smallest of these two values.

By convention, the protocol trusts the ema rate over the spot price. Therefore, in this example, the BNT/LINK quotient is treated as 5.9998. The maximum allowed increase is the BNT funding limit, 2,000 BNT. Therefore, 333.34444 LINK and 2,000 BNT are added to the trading liquidity. As the protocol is minting BNT, it is simultaneously issuing new bnBNT pool tokens for itself. Since the bnBNT pool token has appreciated in value, this will need to be taken into account. The

The staking ledger is reporting a staked amount of BNT of 6,002.3599, and the supply of bnBNT is 6,000. Therefore, the rate of bnBNT to BNT is 0.9996. As the protocol mints 2,000 BNT to increase the trading liquidity, this amount is added to the BNT staking ledger, and the protocol issues itself 1999.2137 bnBNT pool tokens.

protocol.describe()
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:1905.5238095238094 - ETH:2.1 BNT:8002.945380952407 BNT:8003.062042881308 bnBNT:8000.702103993816 Balance:1.2269133409243107
1 BNT:2094.0883461062235 - wBTC:0.11942871759890858 ETH:12.1 ETH:12 bnETH:12.0
2 BNT:4001.404207987633 - LINK:667.1476265441543 wBTC:11.99442871759891 wBTC:12.000045020463848 bnwBTC:11.99999590724731
3 LINK:1002.299 LINK:1002.2424242424242 bnLINK:1001.9997578765776

Unstaking TKN

Withdrawals are predicated on two equally important concepts:

Liquidity providers should be able to withdraw their stake entirely in the token they provided. Any withdrawal should not affect the stakes of other users.

The challenge to meeting these criteria is that the staked amounts will rarely be in agreement with the vault balances. The scale of the problem is also dependent on the proportion of the overall staked amounts being withdrawn by the user, and the relative deficit or surplus for the same TKN. Bancor 3 introduces a novel algorithm for processing withdrawals, which has been constructed specifically for use with the new architecture. This aspect of the protocol’s activities is the most technically challenging to understand. To help structure this part of the discussion, the examples will diverge from the narrative presented up to this point, and will instead use arbitrary cases to demonstrate the logic and consequences of the algorithm.

The state of the system is described using the following variables, and each is a discrete input in the withdrawal algorithm:

a, the BNT balance of the trading liquidity, as judged from the spot rate. b, the TKN balance of the trading liquidity, as judged from the spot rate. c, the difference between the TKN balance of the vault, and the TKN trading liquidity. e, the TKN balance of the staking ledger. m, the associated pool fee for the TKN being unstaked. n, the exit fee of the system. w, the TKN balance of the external protection wallet. x, the precise TKN value of the bnTKN tokens being withdrawn.

The role of the moving average in unstaking actions is slightly different to staking actions. First and foremost, if the deviation (Δ) exceeds the protocol tolerance, the transaction is simply reverted; it is not possible to calculate a ‘default’ withdrawal if there is strong disagreement between the moving average and the spot rate. For the rest of this discussion, it is assumed that this tolerance check has passed.

The withdrawal algorithm has six different outputs:

P, BNT quantity to remove (burn) from the available trading liquidity. Q, BNT ownership renounced by the protocol (i.e. removed from the staking ledger). R, TKN quantity to add to the available trading liquidity from the non-trading liquidity. S, TKN quantity to remove from the vault (and send to the user). T, BNT quantity minted for, and sent to the user as compensation. U, TKN removed from the external protection wallet and sent to the user as compensation.

TKN Surplus Examples

A TKN is in surplus if the total vault balance is greater than the staked amount, after accounting for the exit fee. It is necessary to separate the vault balance into the trading liquidity (b), and non-trading liquidity (c) components; b + c is the vault balance of TKN:

where e is the TKN balance of the staking ledger, and n is the withdrawal fee (e.g. 0.0025, or 0.25%). So long as the above expression evaluates as True, the protocol is in an effective surplus (i.e. the quantity of TKN exceeds that of the combined user stakes). Therefore in all cases, it is guaranteed that any user withdrawing will receive only TKN. However, the protocol will also attempt to reduce the surplus and recover BNT from the secondary markets. The following paragraphs will explore the three different situations that cover the discrete behaviors of the withdrawal algorithm during a surplus state of TKN.

The most important decision the protocol makes is to evaluate the abuse vector described above. To do this, two important thresholds are calculated, hlim and hmax; the former determines whether there is sufficient non-trading TKN in the vault to support the user’s withdrawal without changing the trading pool depth, and the latter determines if an apparent abuse vector exists. These two values are calculated as follows:

These two values, hlim and hmax, define the maximum value for x that is allowable without creating a possible abuse incentive. Therefore, the user’s withdrawal amount (x) must be less than both hlim and hmax:

To examine this further, consider the following case: the staking ledger reports a staked balance of 1,400 TKN (e), the vault balance of TKN is 1,500 TKN (b+c), the TKN trading pool has 1,000 TKN (b) and 1,000 BNT (a) in available liquidity. Then, a user confirms a withdrawal for 100 TKN (x).

tkn = 'TKN'
exit_fee = .0025
pool_fees={"TKN": 0.0020}

protocol_base = Bancor3(
            exit_fee=exit_fee,
            pool_fees=pool_fees
            )

protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 1400
protocol_base.vault_ledger[tkn][-1] = 1500

protocol_base.dao_msig_initialize_pools()

protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1000
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1000

random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
random_agent.get_wallet('TKN')
('TKN=99.7500', 'bnTKN=0')

In this case, hlim evaluates to 466.6667 TKN, and hmax evaluates to 501.486064 TKN. As the user’s withdrawal amount (x) is less than both of these values, there must be sufficient non-trading TKN in the vault to support the withdrawal, and there is no financial incentive to create the withdrawal for the purpose of back running. Therefore the protocol can attempt to recover a small amount of BNT from the outside markets.

protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1000 hlim:466.6666666666666666666666667 p:7.353310513678116682899602871 BNT:992.6466894863218833171003971 TKN:1400.2500 TKN:1300 TKN:0
1 b:1000 hmax:501.4860639152701538342385254 q:0 TKN:1007.392857142857142857142857
2 c:500 is_surplus:True r:7.392857142857142857142857143
3 e:1400 case=arbitrage surplus s:99.7500
4 m:0.002 satisfies_hlim:True t:0
5 n:0.0025 satisfies_hmax:True u:0
6 x:100

Such actions can only be taken if the hlim and hmax tests evaluate to True. To examine a case wherein these tests do not pass, consider a scenario nearly identical to that presented above, but where the non-trading TKN balance (c) is increased to 1,000 TKN. Therefore, the staking ledger reports a staked balance of 1,400 TKN (e), the vault balance of TKN is 2,000 TKN (b+c), the TKN trading pool has 1,000 TKN (b) and 1,000 BNT (a) in available liquidity, and the user confirms a withdrawal for 100 TKN (x).

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 1400
protocol_base.vault_ledger[tkn][-1] = 2000

protocol_base.dao_msig_initialize_pools()

protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1000
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1000

random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1000 hlim:700 p:0 BNT:1000 TKN:1900.2500 TKN:1300 TKN:0
1 b:1000 hmax:18.20819213682819934262338541 q:0 TKN:1000
2 c:1000 is_surplus:True r:0
3 e:1400 case=bootstrap surplus s:99.7500
4 m:0.002 satisfies_hlim:True t:0
5 n:0.0025 satisfies_hmax:False u:0
6 x:100 x(1-n) <= c:True

The last remaining case for withdrawals is when there are insufficient non-trading funds to process the user’s withdrawal - when the inequality presented above is False. Consider the situation where the vault balance of TKN is the same, but the proportion of funds used for trading liquidity is much higher. Instead of a pool only 1,000 TKN deep, let it be 1,995 TKN deep instead, while maintaining the same vault balance (i.e. the non-trading TKN balance (c) is reduced while the trading liquidity balance (a) is increased). As before, the staking ledger reports a staked balance of 1,400 TKN (e), the vault balance of TKN is 2,000 TKN (b+c), the TKN trading pool has 1,995 TKN (b) and 1,995 BNT (a) in available liquidity, and the user confirms a withdrawal for 100 TKN (x).

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 1400
protocol_base.vault_ledger[tkn][-1] = 2000
protocol_base.dao_msig_initialize_pools()
protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1995
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1995

random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1995 hlim:3.5 p:94.7500 BNT:1900.2500 TKN:1900.2500 TKN:1300 TKN:0
1 b:1995 hmax:36.32534331297225768853365389 q:94.7500 TKN:1900.2500
2 c:5 is_surplus:True r:94.7500
3 e:1400 case=default surplus s:99.7500
4 m:0.002 satisfies_hlim:False t:0
5 n:0.0025 satisfies_hmax:False u:0
6 x:100 x(1-n) <= c:False

TKN Deficit Examples A TKN is in deficit if the total vault balance is less than or equal to the staked amount, after accounting for the exit fee. It is necessary to separate the vault balance into the trading liquidity (b), and non-trading liquidity (c) components; b + c is the vault balance of TKN:

where e is the TKN balance of the staking ledger, and n is the withdrawal fee (e.g. 0.0025, or 0.25%). So long as the above expression is True, the protocol is in an effective deficit (i.e. the quantity of TKN is insufficient to reimburse the totality of all user stakes). In this state, it is possible that some users will receive partial reimbursement in BNT. However, the protocol will also attempt to reimburse users entirely in TKN using the reverse of the repricing strategy described for the surplus case - essentially to recapture TKN from the external markets. The following paragraphs will explore the three different situations that cover the discrete behaviors of the withdrawal algorithm during a deficit state of TKN.

As before, the potential for abuse is the most important component of the decision tree. The same thresholds, hlim and hmax, defined previously are used again here; however, the hmax calculation is modified to account for the reversed direction:

The hlim and hmax terms are used in exactly the same fashion as before; they define the maximum value for x that is allowable without creating a possible abuse incentive. Therefore, the user’s withdrawal amount (x) must be less than both hlim and hmax:

The withdrawal algorithm outputs are the same, although the sign is effectively reversed in P and R, relative to the surplus calculations. To maintain unsigned arithmetic, the following descriptions are provided:

P, BNT quantity to add (mint) to the available trading liquidity. Q, BNT ownership renounced by the protocol (i.e. removed from the staking ledger). R, TKN quantity to remove from the available trading liquidity (and send to the user). S, TKN quantity to remove from the vault (and send to the user). T, BNT quantity minted for, and sent to the user as compensation. U, TKN removed from the external protection wallet and sent to the user as compensation.

To examine this further, consider the following case: the staking ledger reports a staked balance of 2,000 TKN (e), the vault balance of TKN is 1,800 TKN (b+c), the TKN trading pool has 1,000 TKN (b) and 1,000 BNT (a) in available liquidity. Then, a user confirms a withdrawal for 100 TKN (x).

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 2000
protocol_base.vault_ledger[tkn][-1] = 1800
protocol_base.dao_msig_initialize_pools()
protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1000
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1000
random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1000 hlim:888.8888888888888888888888889 p:9.826112992473261066810600549 BNT:1009.826112992473261066810601 TKN:1700.2500 TKN:1900 TKN:0
1 b:1000 hmax:276.9641847798160423410924413 q:0 TKN:990.2500
2 c:800 is_surplus:False r:9.7500
3 e:2000 case=arbitrage deficit s:99.7500
4 m:0.002 satisfies_hlim:True t:0
5 n:0.0025 satisfies_hmax:True u:0
6 x:100

In this case, hlim evaluates to 888.888889 TKN, and hmax evaluates to 1138.958507 TKN. As the user’s withdrawal amount (x) is less than both of these values, there must be sufficient non-trading TKN in the vault to support the withdrawal, and there is no financial incentive to create the withdrawal for the purpose of back running. Therefore the protocol can attempt to recover a small amount of TKN from the outside markets.

To examine a case where these abusability thresholds are exceeded, consider a scenario nearly identical to that presented above, but where the TKN balance of the staking ledger (e) is increased to 2,200 TKN. Therefore, the vault balance of TKN remains at 1,800 TKN (b+c), the TKN trading pool has 1,000 TKN (b) and 1,000 BNT (a) in available liquidity, and the user confirms a withdrawal for 100 TKN (x).

For this scenario, hlim evaluates to 977.777778 TKN, and hmax evaluates to 87.855051 TKN. As the user’s withdrawal is still 100 TKN, the hlim test still passes, but the hmax test fails. Therefore, the protocol’s repricing strategy is potentially abusable. Similar to what was discussed in the surplus sections, the withdrawal algorithm defaults to another set of operations, and a bifurcation occurs depending on the sufficiency of the non-trading TKN balance of the vault.

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 2200
protocol_base.vault_ledger[tkn][-1] = 1800
protocol_base.dao_msig_initialize_pools()
protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1000
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1000
random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1000 hlim:977.7777777777777777777777778 p:0 BNT:1000 TKN:1718.386363636363636363636364 TKN:2100 TKN:0
1 b:1000 hmax:87.85505103997602302196281028 q:0 TKN:1000
2 c:800 is_surplus:False r:0
3 e:2200 case=bootstrap deficit s:81.61363636363636363636363636
4 m:0.002 satisfies_hlim:True t:18.13636363636363636363636364
5 n:0.0025 satisfies_hmax:False u:0
6 x:100 x(1-n)(b+c)/e <= c:True

The most intuitive way to interpret the result above is to add the TKN and BNT amounts together. In this case, the BNT/TKN rate is precisely 1:1, which makes it easy. The S and T outputs represent an amount of TKN, and BNT tokens sent to the user, respectively. Since these tokens have the same value, their sum should represent the total value the user was expecting, save for the exit fee. In this case, the sum is precisely 99.75, which is what we expect. These BNT tokens received in reimbursement are minted at withdrawal, and do not originate from inside the protocol. While this example uses a 1:1 valuation of BNT:TKN, the output T handles any BNT valuation.

Lastly, the case wherein the hlim or hmax test fails, and where it is impossible to refund the user without affecting the trading liquidity, should be considered. For this example, let the BNT trading liquidity (a) be 1,800 BNT, let the TKN trading liquidity (b) be 1,800 TKN, let the vault balance of TKN (b+c) be 1,850 TKN, and let the balance of TKN on the staking ledger (e) be 2,200 TKN, and assume the user confirms a withdrawal for 100 TKN (x).

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 2200
protocol_base.vault_ledger[tkn][-1] = 1850
protocol_base.dao_msig_initialize_pools()
protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1800
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1800
random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1800 hlim:59.45945945945945945945945946 p:33.88068181818181818181818182 BNT:1833.880681818181818181818182 TKN:1766.119318181818181818181818 TKN:2100 TKN:0
1 b:1800 hmax:203.6703720524191655132782671 q:33.88068181818181818181818182 TKN:1766.119318181818181818181818
2 c:50 is_surplus:False r:33.88068181818181818181818182
3 e:2200 case=default deficit s:83.88068181818181818181818182
4 m:0.002 satisfies_hlim:False t:15.86931818181818181818181818
5 n:0.0025 satisfies_hmax:True u:0
6 x:100 x(1-n)(b+c)/e <= c:False

External Impermanent Loss Protection

Withdrawals of TKN during a deficit can result in partial reimbursement in BNT, if the thresholds defined by hlim and hmax are exceeded. This is apparent in the algorithm output T, which represents the number of BNT tokens that should be created for the user. In all of the examples examined above, there is an input variable, w, that has been completely neglected, and which gives the protocol access to TKN of last resort.

The external impermanent loss protection feature of Bancor 3 provides token projects with the means to shoulder part of the insurance burden of the network. This is done by providing a fixed quantity of TKN to an external contract - not the vault - which the protocol can use to subsidize users directly in TKN if, and only if they would otherwise receive a partial reimbursement in BNT. The number of TKN provided to this contract is the w variable.

To explore this behavior thoroughly, at least two examples are required. Since the behavior of the external protection wallet is only relevant in cases where a user has received BNT from the protocol while withdrawing TKN, it seems appropriate to revisit the two examples above where this phenomenon has already played out (i.e. the previous two scenarios where either of the hlim or hmax tests failed. Consider the situation where the TKN trading pool has 1,000 BNT (a) and 1,000 TKN (b) in available liquidity, the vault balance of TKN is 1,800 TKN (b+c), the balance of TKN on the staking ledger is 2,200 TKN, and the user confirms a withdrawal for 100 TKN (x). However, this time, let the external protection balance be 1,000 TKN.

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 2200
protocol_base.vault_ledger[tkn][-1] = 1800
protocol_base.dao_msig_initialize_pools()
protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1000
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1000

# Newly introduced in this example
protocol_base.external_protection_wallet_ledger[tkn][-1] = 1000

random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1000 hlim:977.7777777777777777777777778 p:0 BNT:1000 TKN:1718.386363636363636363636364 TKN:2100 TKN:981.8636363636363636363636364
1 b:1000 hmax:87.85505103997602302196281028 q:0 TKN:1000
2 c:800 is_surplus:False r:0
3 e:2200 case=bootstrap deficit ep1 s:81.61363636363636363636363636
4 m:0.002 satisfies_hlim:True t:0
5 n:0.0025 satisfies_hmax:False u:18.13636363636363636363636364
6 x:100 x(1-n)(b+c)/e <= c:True

Lastly, let the BNT trading liquidity (a) be 1,800 BNT, let the TKN trading liquidity (b) be 1,800 TKN, let the vault balance of TKN (b+c) be 1,850 TKN, and let the balance of TKN on the staking ledger (e) be 2,200 TKN, and assume the user confirms a withdrawal for 100 TKN (x). However, this time, let the external protection balance be 10 TKN.

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_whitelisted_tokens([tkn, 'BNT'])
protocol_base.staking_ledger[tkn][-1] = 2200
protocol_base.vault_ledger[tkn][-1] = 1850
protocol_base.dao_msig_initialize_pools()
protocol_base.available_liquidity_ledger[tkn][tkn][-1] = 1800
protocol_base.available_liquidity_ledger[tkn]['BNT'][-1] = 1800
protocol_base.external_protection_wallet_ledger[tkn][-1] = 10
random_agent = LiquidityProviderAgent(unique_id='Random User', env=protocol_base)
random_agent.withdraw('TKN', 100)
protocol_base.describe(withdraw=True)
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
inputs tests outputs trading_liquidity vault_ledger staking_ledger external_protection_ledger
0 a:1800 hlim:59.45945945945945945945945946 p:33.88068181818181818181818182 BNT:1766.119318181818181818181818 TKN:1766.119318181818181818181818 TKN:2100 TKN:0
1 b:1800 hmax:203.6703720524191655132782671 q:33.88068181818181818181818182 TKN:1766.119318181818181818181818
2 c:50 is_surplus:False r:33.88068181818181818181818182
3 e:2200 case=default deficit ep2 s:83.88068181818181818181818182
4 m:0.002 satisfies_hlim:False t:5.869318181818181818181818178
5 n:0.0025 satisfies_hmax:True u:10
6 x:100 x(1-n)(b+c)/e <= c:False

Staking BNT

In Bancor 3, assume that BNT liquidity providers are receiving protocol bnBNT tokens in return for destroying BNT. The calculation is essentially identical to the standard pool token method, and its outcome is relatively easy to understand. However, there are some nuances to the process that should be highlighted.

For this section, we can consider the state of the system after the initial bootstrapping and first trading activity described earlier. The system snapshot is as follows:

To demonstrate, assume a fourth participant, David, wishes to provide 1,000 BNT liquidity to the protocol. The only calculations the protocol must perform are to value the BNT David is providing. The staking ledger is reporting a total of 6,002.3599 BNT, and the bnBNT pool token supply is 6,000 bnBNT. Therefore, the bnBNT/BNT exchange rate is 0.9996068, and David’s 1,000 BNT is worth 999.60683797 bnBNT. When David confirms this transaction, the protocol simply transfers this amount of bnBNT to him, from its own balance; the BNT that David provided is burned immediately.

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_param(spot_prices=spot_prices)

# vault
protocol_base.vault_ledger['BNT'][-1] = 6002.2433
protocol_base.vault_ledger['ETH'][-1] = 12.1
protocol_base.vault_ledger['wBTC'][-1] = 10.99
protocol_base.vault_ledger['LINK'][-1] = 1001.299

# staking_ledger
protocol_base.staking_ledger['BNT'][-1] = 6002.3599
protocol_base.staking_ledger['ETH'][-1] = 12
protocol_base.staking_ledger['wBTC'][-1] = 11.00004502
protocol_base.staking_ledger['LINK'][-1] = 1001.2424

# staking_ledger
protocol_base.pool_token_supply_ledger['bnBNT_ERC20_contract']['supply'][-1] = 6000
protocol_base.pool_token_supply_ledger['bnETH_ERC20_contract']['supply'][-1] = 12
protocol_base.pool_token_supply_ledger['bnwBTC_ERC20_contract']['supply'][-1] = 11
protocol_base.pool_token_supply_ledger['bnLINK_ERC20_contract']['supply'][-1] = 1001

protocol_base.vortex_ledger['BNT'][-1] = 1.22691

david = LiquidityProviderAgent(unique_id='David', env=protocol_base)
david.deposit('BNT', 1000)
protocol_base.describe()
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:0 - ETH:0 BNT:6002.2433 BNT:6002.3599 bnBNT:6000 Balance:1.22691
1 BNT:0 - wBTC:0 ETH:12.1 ETH:12 bnETH:12
2 BNT:0 - LINK:0 wBTC:10.99 wBTC:11.00004502 bnwBTC:11
3 LINK:1001.299 LINK:1001.2424 bnLINK:1001
david.get_wallet('BNT')
('BNT=999000', 'bnBNT=999.6068379705122')

Unstaking BNT

The BNT unstaking process is the reverse of the staking process. Users return their bnBNT tokens, and vBNT at a 1:1 rate; the vBNT is destroyed, and the bnBNT becomes the property of the Bancor Protocol. The protocol mints new BNT, equal in value to the bnBNT tokens the user is relinquishing back to the protocol (save for the withdrawal fee).

To demonstrate the process, imagine an arbitrary period of time has elapsed, where the future system snapshot is as follows. In this hypothetical, lots of trading activity has occurred, and the network participants have added new liquidity; however, David has maintained his bnBNT position, and for the sake of demonstration remains the sole BNT liquidity provider for the system.

As David withdraws, the protocol calculates the BNT value of his bnBNT pool tokens (BNT/bnBNT = 1.0877723) and mints BNT for David at this rate. Therefore, after David has completed the 7-day cooldown, the protocol destroys his vBNT and repossesses his bnBNT. David then receives 1,084.6263 BNT (after accounting for the exit fee). Nothing else in the protocol is changed.

protocol_base = Bancor3(pool_fees=pool_fees, exit_fee=exit_fee)
protocol_base.set_param(spot_prices=spot_prices, exit_fee=exit_fee)

# vault
protocol_base.vault_ledger['BNT'][-1] = 10156.354
protocol_base.vault_ledger['ETH'][-1] = 20.2131241
protocol_base.vault_ledger['wBTC'][-1] = 20.2323152
protocol_base.vault_ledger['LINK'][-1] = 10997.778

# staking_ledger
protocol_base.staking_ledger['BNT'][-1] = 9789.951
protocol_base.staking_ledger['ETH'][-1] = 19.789951
protocol_base.staking_ledger['wBTC'][-1] = 20.569877
protocol_base.staking_ledger['LINK'][-1] = 11357.65

# staking_ledger
protocol_base.pool_token_supply_ledger['bnBNT_ERC20_contract']['supply'][-1] = 9000
protocol_base.pool_token_supply_ledger['bnETH_ERC20_contract']['supply'][-1] = 19
protocol_base.pool_token_supply_ledger['bnwBTC_ERC20_contract']['supply'][-1] = 18
protocol_base.pool_token_supply_ledger['bnLINK_ERC20_contract']['supply'][-1] = 9000

protocol_base.vortex_ledger['BNT'][-1] = 123.456

david = LiquidityProviderAgent(unique_id='David', env=protocol_base)
david.withdraw('BNT', 1000)
protocol_base.describe()
<style scoped> .dataframe tbody tr th:only-of-type { vertical-align: middle; }
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
</style>
trading_liquidity vault_ledger staking_ledger pool_token_supply_ledger vortex_ledger
0 BNT:0 - ETH:0 BNT:10156.354 BNT:9789.951 bnBNT:9000 Balance:123.456
1 BNT:0 - wBTC:0 ETH:20.2131241 ETH:19.789951 bnETH:19
2 BNT:0 - LINK:0 wBTC:20.2323152 wBTC:20.569877 bnwBTC:18
3 LINK:10997.778 LINK:11357.65 bnLINK:9000

Support

Project support can be found in four places depending on the type of question:

  1. For bugs, issues, or feature requests start a Github issue.
  2. For everything else, the core developers can be reached by email at mike@bancor.network

Built by Bancor Research Team

bancorml is a project built by bancor.network.

bancorml

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bancorml-0.2.38.tar.gz (255.3 kB view hashes)

Uploaded source

Built Distribution

bancorml-0.2.38-py3-none-any.whl (421.4 kB view hashes)

Uploaded py3

Supported by

AWS AWS Cloud computing Datadog Datadog Monitoring Facebook / Instagram Facebook / Instagram PSF Sponsor Fastly Fastly CDN Google Google Object Storage and Download Analytics Huawei Huawei PSF Sponsor Microsoft Microsoft PSF Sponsor NVIDIA NVIDIA PSF Sponsor Pingdom Pingdom Monitoring Salesforce Salesforce PSF Sponsor Sentry Sentry Error logging StatusPage StatusPage Status page