Intro | bApp Onboarding Guide
This guide outlines the steps for based applications developers looking to build on the bApps platform.
- Define core attributes:
-
bApp
: a unique 20-byte EVM address that uniquely identifies the bApp. -
tokens
: A list of ERC-20 tokens to be used in the bApp's security mechanism. For the native ETH token, use the special address0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
. -
sharedRiskLevels
: a list of$\beta$ values, one for each token, representing the bApp's tolerance for risk (token over-usage). Each$\beta$ value ranges from 0 to 4,294.967295. Since it's encoded in as auint32
, its first six digits represent decimal places. For example, a stored value of 1_000_000 corresponds to a real value of 1.0.
- Optional Non-Slashable Validator Balance: If the bApp uses non-slashable validator balance, it should be configured off-chain, in the bApp's network.
- Register the bApp: Use the
registerBApp
function of the smart contract:
function registerBApp(
address bApp,
address[] calldata tokens,
uint32[] calldata sharedRiskLevels,
string calldata metadataURI
)
metadataURI
: A link to a JSON file containing additional details about your bApp, such as its name, description, logo, and website.
- Update Configuration: After registering, the bApp configuration can be updated only by the
owner
account. Namely, more tokens can be added withaddTokensToBApp
, the tokens' shared risk levels updated withupdateBAppTokens
, and the metadata updated withupdateMetadataURI
.
Once the bApp is registered, strategies can join it and allocate capital to secure it.
The strategy opts-in to the bApp by using the optInToBApp
function of the smart contract:
function optInToBApp(
uint256 strategyId,
address bApp,
address[] calldata tokens,
uint32[] calldata obligationPercentages,
bytes calldata data
)
tokens
: List of tokens to obligate to the bApp.obligationPercentages
: The proportion of each token's balance to commit to the bApp. Though it's encoded as a uint32, its first two digits represent decimal places of a percentage value. For example, a stored value of 5000 corresponds to 50.00%.data
: An extra optional field for off-chain information required by the bApp for participation.
For example, if tokens = [SSV]
and obligationPercentages = [50%]
, then 50% of the strategy's SSV
balance will be obligated to the bApp.
The strategy’s owner can later update its obligations by modifying existing ones or adding a new token obligation. Obligations can be increased instantly (fastUpdateObligation
), but decreasing obligations requires a timelock (proposeUpdateObligation
→ finalizeUpdateObligation
) to ensure slashable capital can’t be pulled out instantly.
To compose their balances, strategies:
- receive ERC20 (or ETH) via deposits from accounts.
- inherit the non-slashable validator balance from its owner account. Accounts delegate validator balances between themselves, and the strategy inherits all balances delegated to its owner.
If a token is allocated to a bApp (usedTokens[strategyId][token] != 0
), accounts need to propose a withdrawal and wait a timelock before finalizing it, ensuring the slashable collateral cannot be removed instantly.
bApp clients track the weight of each participant in the bApp. For that, clients will:
- Gather Obligated Balances: First, for each token used by the bApp, it should get the obligated balance from each strategy.
ObligatedBalance mapping(Token -> Strategy -> Amount)
- Sum Obligations: From
ObligatedBalance
, it can sum all obligations and compute the total amount obligated to the bApp by all strategies.
TotalBAppBalance mapping(Token -> Amount)
- Calculate Risk: For each token, it should get the risk (token-over usage) of each strategy.
Risk mapping(Token -> Strategy -> Float)
- Compute Risk-Aware Weights: With this information, it can compute the weight of a participant for a certain token by
where
Note
If the bApp uses validator balance, the client should also read a map[Strategy]ValidatorBalance
with the amount from each strategy. As this capital doesn't involve any type of risk, all risk values can be set to 0. Thus, for this capital, this is equivalent to
- Combine into the Final Weight: With the per-token weights, the final step is to compute a final weight for the participant using a combination function. Such function is defined by the bApp and can be tailored to its specific needs. Traditional examples include the arithmetic mean, geometric mean, and harmonic mean.
Example: Let's consider a bApp that uses tokens
where
In this subsection, we detail how the data for computing the participants' weights can be read from the chain state.
Map of obligation balances
function ObligatedBalances(bApp)
obligatedBalances = New(Map<Token, Map<Strategy, Amount>>)
# Get bApp tokens
bAppTokens = api.GetbAppTokens(bApp)
# Loop through every strategy
strategies = api.GetStrategies()
for strategy in strategies do
# Check if strategy participates in the bApp
ownerAccount := api.GetStrategyOwnerAccount(strategy)
if api.GetStrategyOptedInToBApp(ownerAccount, bApp) != strategy then
# If not, continue
continue
# Get strategy balance
balance = api.GetStrategyBalance(strategy)
# Add obligated balance for each bApp token
for token in bAppTokens do
obligationPercentage = api.GetObligation(strategy, bApp, token)
obligatedBalances[token][strategy] = obligationPercentage * balance[token]
return obligatedBalances
Map of validator balances
function ValidatorBalances(bApp)
validatorBalances = New(Map<Strategy, Amount>)
# Loop through every strategy
strategies = api.GetStrategies()
for strategy in strategies do
# Get account that owns the strategy
ownerAccount = api.GetStrategyOwnerAccount(strategy)
# Check if strategy participates in the bApp
if api.GetStrategyOptedInToBApp(ownerAccount, bApp) != strategy then
# If not, continue
continue
# Store validator balance
validatorBalances[strategy] = ComputeEffectiveValidatorBalance(ownerAccount)
return obligatedBalances
function ComputeEffectiveValidatorBalance(account)
total = 0
# Get all other accounts that delegated to it along with the percentages
delegatorsToAccount = New(Map<Account, Percentage>)
delegatorsToAccount = api.GetDelegatorsToAccount(account)
# Add the delegated balances
for delegator, percentage in delegatorsToAccount
total += GetOriginalValidatorBalance(delegator) * percentage
return total
function GetOriginalValidatorBalance(account)
total = 0
# Get SSV validators from account
validatorsPubKeys = SSVNode.GetValidatorsPubKeys(account)
for PubKey in validatorsPubKeys
# Get validator balance and active status
balance, isActive = ETHNode.GetValidatorBalance(PubKey)
if isActive
total += balance
return total
Map of risks
function Risks(bApp)
risks = New(Map<Token, Map<Strategy, Percentage>>)
# Get bApp tokens
bAppTokens = api.GetbAppTokens(bApp)
# Loop through every strategy
strategies = api.GetStrategies()
for strategy in strategies do
# Check if strategy participates in the bApp
ownerAccount := api.GetStrategyOwnerAccount(strategy)
if api.GetStrategyOptedInToBApp(ownerAccount, bApp) != strategy then
# If not, continue
continue
# Store risk (i.e. sum of all obligation percentages)
risks[token][strategy] = api.AddAllObligationsForToken(strategy, token)
return risks
API Calls
For reference, we list the API calls used in the above snippets along with the chain state variables that should be read for each call:
GetbAppTokens(bApp)
:bAppTokens
GetStrategies()
:strategies
GetStrategyOptedInToBApp(account, bApp)
:accountBAppStrategy
GetStrategyBalance(strategy)
:strategyTokenBalances
GetObligation(strategy, bApp, token)
:obligations
GetStrategyOwnerAccount(strategy)
:strategies
GetTotalDelegation(account)
:totalDelegatedPercentage
GetDelegatorsToAccount(account)
:delegations
Consider a bApp with the following configuration:
Configuration | Value |
---|---|
Tokens |
[SSV] |
SharedRiskLevels ( |
[2] |
Uses validator balance | True |
Final weight combination function |
This setup means:
- The only slashable token in use is SSV, with
$\beta = 2$ . - Validator balance is included in the model.
- The combination function is a harmonic mean, where SSV carries twice the weight of validator balance.
The following strategies have opted-in to the bApp:
Strategy | SSV Balance | SSV Obligation | Risk for SSV token | Validator Balance |
---|---|---|---|---|
1 | 100 | 50% | 1.5 (150%) | 32 |
2 | 200 | 10% | 1 (100%) | 96 |
The obligated balances are:
- Strategy 1:
$100 * 50% = 50$ SSV - Strategy 2:
$200 * 10% = 20$ SSV
Thus, in total, the bApp has:
-
$50 + 20 = 70$ SSV -
$32 + 96 = 128$ validator balance
First, compute the normalization constant for the SSV token:
Using this coefficient, we can compute the weights:
Thus, the weights for the SSV token are:
- Strategy 1: 47.9%
- Strategy 2: 52.1%
Note that, despite Strategy 1 obligating
For validator balance:
Thus, the validator balance weights are:
- Strategy 1: 25%
- Strategy 2: 75%
Since validator balance carries no risk, it remains proportional to the amount contributed.
Using the harmonic mean combination, we have:
Thus, the final weights are:
- Strategy 1: 38.7%
- Strategy 2: 61.3%