Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust burn target #6463

Merged
merged 19 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2535f01
Add check for price==0
HenrikJannsen Dec 14, 2022
f799e11
Remove try catch. Will be handled in Future fault handler
HenrikJannsen Dec 14, 2022
4860c11
Change burn target calculation.
HenrikJannsen Dec 15, 2022
d0539e1
In case we have capped burn shares we redistribute the share from the…
HenrikJannsen Dec 15, 2022
7f73ef7
Don't allow the myBurnAmount to be larger than the upperBaseTarget
HenrikJannsen Dec 15, 2022
d585456
Refactor: move out fields used the same way in both if/else branches.
HenrikJannsen Dec 15, 2022
0a941c1
Add custom handling of legacy BM.
HenrikJannsen Dec 15, 2022
6fd68f7
Remove regtest value for DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_…
HenrikJannsen Dec 15, 2022
3ac6921
Remove numIssuance and numBurnOutputs columns to save space
HenrikJannsen Dec 15, 2022
57147a6
Use static fields for opReturnData instead of hardcoded mainnet hashes
HenrikJannsen Dec 15, 2022
decd301
Update BurningManAccountingStore_BTC_MAINNET
HenrikJannsen Dec 15, 2022
2012c9d
Add new average bsq price after historical data
HenrikJannsen Dec 15, 2022
5dd82d7
Increase GENESIS_OUTPUT_AMOUNT_FACTOR and ISSUANCE_BOOST_FACTOR
HenrikJannsen Dec 15, 2022
b5dbce4
Add balance fields for DAO revenue with total burned BSQ and total di…
HenrikJannsen Dec 16, 2022
9efab7e
Exclude legacy BM from DAO balance
HenrikJannsen Dec 18, 2022
7237297
Add sanity check that max share of a non-legacy BM is 20% over MAX_BU…
HenrikJannsen Dec 18, 2022
c36c9b2
Add sanity check for a min. block height for the snapshot height
HenrikJannsen Dec 18, 2022
2a8d1b3
Fix test
HenrikJannsen Dec 18, 2022
21541d6
Add INVALID_SNAPSHOT_HEIGHT to AvailabilityResult.
HenrikJannsen Dec 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions core/src/main/java/bisq/core/dao/burningman/BurnTargetService.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@

import lombok.extern.slf4j.Slf4j;

import static bisq.core.dao.burningman.BurningManPresentationService.OP_RETURN_DATA_LEGACY_BM_DPT;
import static bisq.core.dao.burningman.BurningManPresentationService.OP_RETURN_DATA_LEGACY_BM_FEES;

/**
* Burn target related API. Not touching trade protocol aspects and parameters can be changed here without risking to
* break trade protocol validations.
Expand All @@ -56,10 +59,13 @@ class BurnTargetService {
private static final int NUM_CYCLES_BURN_TARGET = 12;
private static final int NUM_CYCLES_AVERAGE_DISTRIBUTION = 3;

// Estimated block at activation date
private static final int ACTIVATION_BLOCK = Config.baseCurrencyNetwork().isRegtest() ? 111 : 769845;

// Default value for the estimated BTC trade fees per month as BSQ sat value (100 sat = 1 BSQ).
// Default is roughly average of last 12 months at Nov 2022.
// Can be changed with DAO parameter voting.
private static final long DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE = Config.baseCurrencyNetwork().isRegtest() ? 1000000 : 6200000;
private static final long DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE = 6200000;

private final DaoStateService daoStateService;
private final CyclesInDaoStateService cyclesInDaoStateService;
Expand Down Expand Up @@ -102,7 +108,7 @@ Set<ReimbursementModel> getReimbursements(int chainHeight) {
long getBurnTarget(int chainHeight, Collection<BurningManCandidate> burningManCandidates) {
// Reimbursements are taken into account at result vote block
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET);
long accumulatedReimbursements = getAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle);
long accumulatedReimbursements = getAdjustedAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle);

// Param changes are taken into account at first block at next cycle after voting
int heightOfFirstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET - 1);
Expand Down Expand Up @@ -140,7 +146,7 @@ long getBurnTarget(int chainHeight, Collection<BurningManCandidate> burningManCa
long getAverageDistributionPerCycle(int chainHeight) {
// Reimbursements are taken into account at result vote block
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION);
long reimbursements = getAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle);
long reimbursements = getAdjustedAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle);

// Param changes are taken into account at first block at next cycle after voting
int firstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION - 1);
Expand All @@ -162,11 +168,31 @@ private Stream<ReimbursementProposal> getReimbursementProposalsForIssuance(Issua
.map(proposal -> (ReimbursementProposal) proposal);
}

private long getAccumulatedReimbursements(int chainHeight, int fromBlock) {
private long getAdjustedAccumulatedReimbursements(int chainHeight, int fromBlock) {
return getReimbursements(chainHeight).stream()
.filter(reimbursementModel -> reimbursementModel.getHeight() > fromBlock)
.filter(reimbursementModel -> reimbursementModel.getHeight() <= chainHeight)
.mapToLong(ReimbursementModel::getAmount)
.mapToLong(reimbursementModel -> {
long amount = reimbursementModel.getAmount();
if (reimbursementModel.getHeight() > ACTIVATION_BLOCK) {
// As we do not pay out the losing party's security deposit we adjust this here.
// We use 15% as the min. security deposit as we do not have the detail data.
// A trade with 1 BTC has 1.3 BTC in the DPT which goes to BM. The reimbursement is
// only BSQ equivalent to 1.15 BTC. So we map back the 1.15 BTC to 1.3 BTC to account for
// that what the BM received.
// There are multiple unknowns included:
// - Real security deposit can be higher
// - Refund agent can make a custom payout, paying out more or less than expected
// - BSQ/BTC volatility
// - Delay between DPT and reimbursement
long adjusted = Math.round(amount * 1.3 / 1.15);
return adjusted;
} else {
// For old reimbursements we do not apply the adjustment as we had a different policy for
// reimbursing out 100% of the DPT.
return amount;
}
})
.sum();
}

Expand Down Expand Up @@ -202,8 +228,7 @@ private long getBurnedAmountFromLegacyBurningManDPT(Set<Tx> proofOfBurnTxs, int
.filter(tx -> tx.getBlockHeight() <= chainHeight)
.filter(tx -> {
String hash = Hex.encode(tx.getLastTxOutput().getOpReturnData());
return "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(hash) ||
"1701293c488822f98e70e047012f46f5f1647f37deb7".equals(hash);
return OP_RETURN_DATA_LEGACY_BM_DPT.contains(hash);
})
.mapToLong(Tx::getBurntBsq)
.sum();
Expand All @@ -214,7 +239,7 @@ private long getBurnedAmountFromLegacyBurningMansBtcFees(Set<Tx> proofOfBurnTxs,
return proofOfBurnTxs.stream()
.filter(tx -> tx.getBlockHeight() > fromBlock)
.filter(tx -> tx.getBlockHeight() <= chainHeight)
.filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData())))
.filter(tx -> OP_RETURN_DATA_LEGACY_BM_FEES.contains(Hex.encode(tx.getLastTxOutput().getOpReturnData())))
.mapToLong(Tx::getBurntBsq)
.sum();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

import com.google.common.annotations.VisibleForTesting;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
Expand All @@ -61,24 +62,21 @@
public class BurningManPresentationService implements DaoStateListener {
// Burn target gets increased by that amount to give more flexibility.
// Burn target is calculated from reimbursements + estimated BTC fees - burned amounts.
private static final long BURN_TARGET_BOOST_AMOUNT = Config.baseCurrencyNetwork().isRegtest() ? 1000000 : 10000000;
// To avoid that the BM get locked in small total burn amounts we allow to burn up to 1000 BSQ more than the
// calculation to not exceed the cap would suggest.
private static final long MAX_BURN_TARGET_LOWER_FLOOR = 100000;
private static final long BURN_TARGET_BOOST_AMOUNT = 10000000;
public static final String LEGACY_BURNING_MAN_DPT_NAME = "Legacy Burningman (DPT)";
public static final String LEGACY_BURNING_MAN_BTC_FEES_NAME = "Legacy Burningman (BTC fees)";
static final String LEGACY_BURNING_MAN_BTC_FEES_ADDRESS = "38bZBj5peYS3Husdz7AH3gEUiUbYRD951t";
// Those are the opReturn data used by legacy BM for burning BTC received from DPT.
// For regtest testing burn bsq and use the pre-image `dpt` which has the hash 14af04ea7e34bd7378b034ddf90da53b7c27a277.
// The opReturn data gets additionally prefixed with 1701
private static final Set<String> OP_RETURN_DATA_LEGACY_BM_DPT = Config.baseCurrencyNetwork().isRegtest() ?
static final Set<String> OP_RETURN_DATA_LEGACY_BM_DPT = Config.baseCurrencyNetwork().isRegtest() ?
Set.of("170114af04ea7e34bd7378b034ddf90da53b7c27a277") :
Set.of("1701e47e5d8030f444c182b5e243871ebbaeadb5e82f",
"1701293c488822f98e70e047012f46f5f1647f37deb7");
// The opReturn data used by legacy BM for burning BTC received from BTC trade fees.
// For regtest testing burn bsq and use the pre-image `fee` which has the hash b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6.
// The opReturn data gets additionally prefixed with 1701
private static final Set<String> OP_RETURN_DATA_LEGACY_BM_FEES = Config.baseCurrencyNetwork().isRegtest() ?
static final Set<String> OP_RETURN_DATA_LEGACY_BM_FEES = Config.baseCurrencyNetwork().isRegtest() ?
Set.of("1701b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6") :
Set.of("1701721206fe6b40777763de1c741f4fd2706d94775d");

Expand Down Expand Up @@ -173,53 +171,57 @@ public long getExpectedRevenue(BurningManCandidate burningManCandidate) {
return Math.round(burningManCandidate.getCappedBurnAmountShare() * getAverageDistributionPerCycle());
}

// Left side in tuple is the amount to burn to reach the max. burn share based on the total burned amount.
// This value is safe to not burn more than needed and to avoid to get capped.
// The right side is the amount to burn to reach the max. burn share based on the boosted burn target.
// This can lead to burning too much and getting capped.
public Tuple2<Long, Long> getCandidateBurnTarget(BurningManCandidate burningManCandidate) {
long burnTarget = getBurnTarget();
long boostedBurnTarget = burnTarget + BURN_TARGET_BOOST_AMOUNT;
double compensationShare = burningManCandidate.getCompensationShare();
if (burnTarget == 0 || compensationShare == 0) {

if (boostedBurnTarget <= 0 || compensationShare == 0) {
return new Tuple2<>(0L, 0L);
}

double maxCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare);
long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare);
long boostedBurnAmount = burnTarget + BURN_TARGET_BOOST_AMOUNT;
double maxBoostedCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR);
long upperBaseTarget = Math.round(boostedBurnAmount * maxBoostedCompensationShare);
long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(getBurningManCandidatesByName().values(), currentChainHeight);
double maxBoostedCompensationShare = burningManCandidate.getMaxBoostedCompensationShare();
long upperBaseTarget = Math.round(boostedBurnTarget * maxBoostedCompensationShare);
Collection<BurningManCandidate> burningManCandidates = getBurningManCandidatesByName().values();
long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(burningManCandidates, currentChainHeight);

if (totalBurnedAmount == 0) {
// The first BM would reach their max burn share by 5.46 BSQ already. But we suggest the lowerBaseTarget
// as lower target to speed up the bootstrapping.
return new Tuple2<>(lowerBaseTarget, upperBaseTarget);
}

double burnAmountShare = burningManCandidate.getBurnAmountShare();
long candidatesBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount();
if (burnAmountShare < maxBoostedCompensationShare) {
long myBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxCompensationShare);
long myMaxBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxBoostedCompensationShare);

// We limit to base targets
myBurnAmount = Math.min(myBurnAmount, lowerBaseTarget);
myMaxBurnAmount = Math.min(myMaxBurnAmount, upperBaseTarget);
if (burningManCandidate.getAdjustedBurnAmountShare() < maxBoostedCompensationShare) {
long candidatesBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount();

// We allow at least MAX_BURN_TARGET_LOWER_FLOOR (1000 BSQ) to burn, even if that means to hit the cap to give more flexibility
// when low amounts are burned and the 11% cap would lock in BM to small increments per burn iteration.
myMaxBurnAmount = Math.max(myMaxBurnAmount, MAX_BURN_TARGET_LOWER_FLOOR);
// TODO We do not consider adjustedBurnAmountShare. This could lead to slight over burn. Atm we ignore that.
long myBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxBoostedCompensationShare);

// If below dust we set value to 0
myBurnAmount = myBurnAmount < 546 ? 0 : myBurnAmount;
return new Tuple2<>(myBurnAmount, myMaxBurnAmount);

// In case the myBurnAmount would be larger than the upperBaseTarget we use the upperBaseTarget.
myBurnAmount = Math.min(myBurnAmount, upperBaseTarget);

return new Tuple2<>(myBurnAmount, upperBaseTarget);
} else {
// We have reached our cap.
return new Tuple2<>(0L, MAX_BURN_TARGET_LOWER_FLOOR);
return new Tuple2<>(0L, upperBaseTarget);
}
}

@VisibleForTesting
static long getMissingAmountToReachTargetShare(long total, long myAmount, double myTargetShare) {
long others = total - myAmount;
static long getMissingAmountToReachTargetShare(long totalBurnedAmount, long myBurnAmount, double myTargetShare) {
long others = totalBurnedAmount - myBurnAmount;
double shareTargetOthers = 1 - myTargetShare;
double targetAmount = shareTargetOthers > 0 ? myTargetShare / shareTargetOthers * others : 0;
return Math.round(targetAmount) - myAmount;
return Math.round(targetAmount) - myBurnAmount;
}

public Set<ReimbursementModel> getReimbursements() {
Expand Down Expand Up @@ -351,6 +353,12 @@ public Map<String, String> getBurningManNameByAddress() {
return burningManNameByAddress;
}

public long getTotalAmountOfBurnedBsq() {
return getBurningManCandidatesByName().values().stream()
.mapToLong(BurningManCandidate::getAccumulatedBurnAmount)
.sum();
}

public String getGenesisTxId() {
return daoStateService.getGenesisTxId();
}
Expand Down
19 changes: 15 additions & 4 deletions core/src/main/java/bisq/core/dao/burningman/BurningManService.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static boolean isActivated() {
static final String GENESIS_OUTPUT_PREFIX = "Bisq co-founder ";

// Factor for weighting the genesis output amounts.
private static final double GENESIS_OUTPUT_AMOUNT_FACTOR = 0.05;
private static final double GENESIS_OUTPUT_AMOUNT_FACTOR = 0.1;

// The number of cycles we go back for the decay function used for compensation request amounts.
private static final int NUM_CYCLES_COMP_REQUEST_DECAY = 24;
Expand All @@ -91,9 +91,9 @@ public static boolean isActivated() {
// Factor for boosting the issuance share (issuance is compensation requests + genesis output).
// This will be used for increasing the allowed burn amount. The factor gives more flexibility
// and compensates for those who do not burn. The burn share is capped by that factor as well.
// E.g. a contributor with 10% issuance share will be able to receive max 20% of the BTC fees or DPT output
// even if they had burned more and had a higher burn share than 20%.
public static final double ISSUANCE_BOOST_FACTOR = 3;
// E.g. a contributor with 1% issuance share will be able to receive max 10% of the BTC fees or DPT output
// even if they had burned more and had a higher burn share than 10%.
public static final double ISSUANCE_BOOST_FACTOR = 10;

// The max amount the burn share can reach. This value is derived from the min. security deposit in a trade and
// ensures that an attack where a BM would take all sell offers cannot be economically profitable as they would
Expand Down Expand Up @@ -189,6 +189,17 @@ Map<String, BurningManCandidate> getBurningManCandidatesByName(int chainHeight)
.mapToDouble(BurningManCandidate::getAccumulatedDecayedBurnAmount)
.sum();
burningManCandidates.forEach(candidate -> candidate.calculateShares(totalDecayedCompensationAmounts, totalDecayedBurnAmounts));

double sumAllCappedBurnAmountShares = burningManCandidates.stream()
.filter(candidate -> candidate.getBurnAmountShare() >= candidate.getMaxBoostedCompensationShare())
.mapToDouble(BurningManCandidate::getMaxBoostedCompensationShare)
.sum();
double sumAllNonCappedBurnAmountShares = burningManCandidates.stream()
.filter(candidate -> candidate.getBurnAmountShare() < candidate.getMaxBoostedCompensationShare())
.mapToDouble(BurningManCandidate::getBurnAmountShare)
.sum();
burningManCandidates.forEach(candidate -> candidate.calculateCappedAndAdjustedShares(sumAllCappedBurnAmountShares, sumAllNonCappedBurnAmountShares));

return burningManCandidatesByName;
}

Expand Down
Loading