BancorML is a library that builds, optimizes, and evaluates machine learning pipelines in the context of a multi-agent system
Project description
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
.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()
.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
.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
.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
.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:]
.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
.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:]
.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
.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:]
.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:]
.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
.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
.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()
.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)
.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)
.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)
.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)
.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)
.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)
.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)
.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)
.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()
.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()
.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:
- For bugs, issues, or feature requests start a Github issue.
- 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file bancorml-0.2.38.tar.gz
.
File metadata
- Download URL: bancorml-0.2.38.tar.gz
- Upload date:
- Size: 255.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.64.0 importlib-metadata/4.11.3 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.8.12
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 4510e2ffbac7f65471fc579090e90f776cb7d60882b40cd0087fdb63bcbefadb |
|
MD5 | 54fd1bfb4c748711a4a30b9eddfea380 |
|
BLAKE2b-256 | 17ed83c4bdab6ea03c0ebac996cfb9312288279433f9f7bebcf1f66cfc5a7643 |
File details
Details for the file bancorml-0.2.38-py3-none-any.whl
.
File metadata
- Download URL: bancorml-0.2.38-py3-none-any.whl
- Upload date:
- Size: 421.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.8.0 pkginfo/1.8.2 readme-renderer/34.0 requests/2.27.1 requests-toolbelt/0.9.1 urllib3/1.26.8 tqdm/4.64.0 importlib-metadata/4.11.3 keyring/23.5.0 rfc3986/2.0.0 colorama/0.4.4 CPython/3.8.12
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 33994f2cc989a96e1c47f0242c0046b6a07d50a6a291788e939fc4caea9ce994 |
|
MD5 | 7f69fe75c4e2bcc6ad2d340a7c1e360b |
|
BLAKE2b-256 | b5036d7f4d522acceccea80b586ca244f576d7783fab28783eb96a75cca509a4 |