Commit 3bc7e659 authored by Son of Odin's avatar Son of Odin 💬

Merge branch '319-issue' into 'master'

319-issue add logic to subsidize pool with slashed bond (RUNE)

Closes #319

See merge request !500
parents 6b7334c3 48dfef21
Pipeline #111969438 passed with stages
in 78 minutes and 50 seconds
......@@ -66,20 +66,67 @@ func refundTx(ctx sdk.Context, tx ObservedTx, store TxOutStore, keeper Keeper, r
return nil
}
func refundBond(ctx sdk.Context, tx common.Tx, nodeAcc NodeAccount, keeper Keeper, txOut TxOutStore) error {
if nodeAcc.Status == NodeActive {
ctx.Logger().Info("node still active , cannot refund bond", "node address", nodeAcc.NodeAddress, "node pub key", nodeAcc.PubKeySet.Secp256k1)
func subsidizePoolWithSlashBond(ctx sdk.Context, keeper Keeper, ygg Vault, yggTotalStolen, slashRuneAmt sdk.Uint) error {
// Thorchain did not slash the node account
if slashRuneAmt.IsZero() {
return nil
}
ygg, err := keeper.GetVault(ctx, nodeAcc.PubKeySet.Secp256k1)
if err != nil {
return err
stolenRUNE := ygg.GetCoin(common.RuneAsset()).Amount
slashRuneAmt = common.SafeSub(slashRuneAmt, stolenRUNE)
yggTotalStolen = common.SafeSub(yggTotalStolen, stolenRUNE)
type fund struct {
stolenAsset sdk.Uint
subsidiseRune sdk.Uint
}
if !ygg.IsYggdrasil() {
return fmt.Errorf("this is not a Yggdrasil vault")
// here need to use a map to hold on to the amount of RUNE need to be subsidized to each pool
// reason being , if ygg pool has both RUNE and BNB coin left, these two coin share the same pool
// which is BNB pool , if add the RUNE directly back to pool , it will affect BNB price , which will affect the result
subsidizeAmounts := make(map[common.Asset]fund)
for _, coin := range ygg.Coins {
asset := coin.Asset
if coin.Asset.IsRune() {
// when the asset is RUNE, thorchain don't need to update the RUNE balance on pool
continue
}
f, ok := subsidizeAmounts[asset]
if !ok {
f = fund{
stolenAsset: sdk.ZeroUint(),
subsidiseRune: sdk.ZeroUint(),
}
}
pool, err := keeper.GetPool(ctx, asset)
if err != nil {
return err
}
f.stolenAsset = f.stolenAsset.Add(coin.Amount)
runeValue := pool.AssetValueInRune(coin.Amount)
// the amount of RUNE thorchain used to subsidize the pool is calculate by ratio
// slashRune * (stealAssetRuneValue /totalStealAssetRuneValue)
subsidizeAmt := slashRuneAmt.Mul(runeValue).Quo(yggTotalStolen)
f.subsidiseRune = f.subsidiseRune.Add(subsidizeAmt)
subsidizeAmounts[asset] = f
}
// Calculate total value (in rune) the Yggdrasil pool has
for asset, f := range subsidizeAmounts {
pool, err := keeper.GetPool(ctx, asset)
if err != nil {
return err
}
pool.BalanceRune = pool.BalanceRune.Add(f.subsidiseRune)
pool.BalanceAsset = common.SafeSub(pool.BalanceAsset, f.stolenAsset)
if err := keeper.SetPool(ctx, pool); nil != err {
return fmt.Errorf("fail to save pool: %w", err)
}
}
return nil
}
// getTotalYggValueInRune will go through all the coins in ygg , and calculate the total value in RUNE
// return value will be totalValueInRune,error
func getTotalYggValueInRune(ctx sdk.Context, keeper Keeper, ygg Vault) (sdk.Uint, error) {
yggRune := sdk.ZeroUint()
for _, coin := range ygg.Coins {
if coin.Asset.IsRune() {
......@@ -87,21 +134,42 @@ func refundBond(ctx sdk.Context, tx common.Tx, nodeAcc NodeAccount, keeper Keepe
} else {
pool, err := keeper.GetPool(ctx, coin.Asset)
if err != nil {
return err
return sdk.ZeroUint(), err
}
yggRune = yggRune.Add(pool.AssetValueInRune(coin.Amount))
}
}
return yggRune, nil
}
func refundBond(ctx sdk.Context, tx common.Tx, nodeAcc NodeAccount, keeper Keeper, txOut TxOutStore) error {
if nodeAcc.Status == NodeActive {
ctx.Logger().Info("node still active , cannot refund bond", "node address", nodeAcc.NodeAddress, "node pub key", nodeAcc.PubKeySet.Secp256k1)
return nil
}
ygg, err := keeper.GetVault(ctx, nodeAcc.PubKeySet.Secp256k1)
if err != nil {
return err
}
if !ygg.IsYggdrasil() {
return errors.New("this is not a Yggdrasil vault")
}
// Calculate total value (in rune) the Yggdrasil pool has
yggRune, err := getTotalYggValueInRune(ctx, keeper, ygg)
if nil != err {
return fmt.Errorf("fail to get total ygg value in RUNE: %w", err)
}
if nodeAcc.Bond.LT(yggRune) {
ctx.Logger().Error(fmt.Sprintf("Node Account (%s) left with more funds in their Yggdrasil vault than their bond's value (%s / %s)", nodeAcc.NodeAddress, yggRune, nodeAcc.Bond))
}
// slashing 1.5 * yggdrasil remains
yggRune = yggRune.MulUint64(15).QuoUint64(10)
nodeAcc.Bond = common.SafeSub(nodeAcc.Bond, yggRune)
if nodeAcc.Bond.GT(sdk.ZeroUint()) {
slashRune := yggRune.MulUint64(3).QuoUint64(2)
bondBeforeSlash := nodeAcc.Bond
nodeAcc.Bond = common.SafeSub(nodeAcc.Bond, slashRune)
if !nodeAcc.Bond.IsZero() {
active, err := keeper.GetAsgardVaultsByStatus(ctx, ActiveVault)
if err != nil {
ctx.Logger().Error("fail to get active vaults", "error", err)
......@@ -134,6 +202,10 @@ func refundBond(ctx sdk.Context, tx common.Tx, nodeAcc NodeAccount, keeper Keepe
if err := keeper.UpsertEvent(ctx, e); nil != err {
return fmt.Errorf("fail to save bond return event: %w", err)
}
} else {
// if it get into here that means the node account doesn't have any bond left after slash.
// which means the real slashed RUNE could be the bond they have before slash
slashRune = bondBeforeSlash
}
nodeAcc.Bond = sdk.ZeroUint()
......@@ -143,8 +215,13 @@ func refundBond(ctx sdk.Context, tx common.Tx, nodeAcc NodeAccount, keeper Keepe
ctx.Logger().Error(fmt.Sprintf("fail to save node account(%s)", nodeAcc), "error", err)
return err
}
if err := subsidizePoolWithSlashBond(ctx, keeper, ygg, yggRune, slashRune); nil != err {
ctx.Logger().Error("fail to subsidize pool with slashed bond", "error", err)
return err
}
// delete the ygg vault, there is nothing left in the ygg vault
return keeper.DeleteVault(ctx, ygg.PubKey)
return nil
}
// Checks if the observed vault pubkey is a valid asgard or ygg vault
......
......@@ -23,12 +23,19 @@ func (k *TestRefundBondKeeper) GetAsgardVaultsByStatus(_ sdk.Context, _ VaultSta
return k.vaults, nil
}
func (k *TestRefundBondKeeper) GetVault(_ sdk.Context, _ common.PubKey) (Vault, error) {
return k.ygg, nil
func (k *TestRefundBondKeeper) GetVault(_ sdk.Context, pk common.PubKey) (Vault, error) {
if k.ygg.PubKey.Equals(pk) {
return k.ygg, nil
}
return Vault{}, kaboom
}
func (k *TestRefundBondKeeper) GetPool(_ sdk.Context, _ common.Asset) (Pool, error) {
return k.pool, nil
func (k *TestRefundBondKeeper) GetPool(_ sdk.Context, asset common.Asset) (Pool, error) {
if k.pool.Asset.Equals(asset) {
return k.pool, nil
}
return NewPool(), kaboom
}
func (k *TestRefundBondKeeper) SetNodeAccount(_ sdk.Context, na NodeAccount) error {
......@@ -38,15 +45,161 @@ func (k *TestRefundBondKeeper) SetNodeAccount(_ sdk.Context, na NodeAccount) err
func (k *TestRefundBondKeeper) UpsertEvent(_ sdk.Context, e Event) error {
return nil
}
func (k *TestRefundBondKeeper) SetPool(_ sdk.Context, p Pool) error {
if k.pool.Asset.Equals(p.Asset) {
k.pool = p
return nil
}
return kaboom
}
func (k *TestRefundBondKeeper) DeleteVault(_ sdk.Context, key common.PubKey) error {
if k.ygg.PubKey.Equals(key) {
k.ygg = NewVault(1, InactiveVault, AsgardVault, GetRandomPubKey())
}
return nil
}
func (s *HelperSuite) TestSubsidizePoolWithSlashBond(c *C) {
ctx, k := setupKeeperForTest(c)
ygg := GetRandomVault()
c.Assert(subsidizePoolWithSlashBond(ctx, k, ygg, sdk.NewUint(100*common.One), sdk.ZeroUint()), IsNil)
poolBNB := NewPool()
poolBNB.Asset = common.BNBAsset
poolBNB.BalanceRune = sdk.NewUint(100 * common.One)
poolBNB.BalanceAsset = sdk.NewUint(100 * common.One)
poolBNB.Status = PoolEnabled
c.Assert(k.SetPool(ctx, poolBNB), IsNil)
poolTCAN := NewPool()
tCanAsset, err := common.NewAsset("BNB.TCAN-014")
c.Assert(err, IsNil)
poolTCAN.Asset = tCanAsset
poolTCAN.BalanceRune = sdk.NewUint(200 * common.One)
poolTCAN.BalanceAsset = sdk.NewUint(200 * common.One)
poolTCAN.Status = PoolEnabled
c.Assert(k.SetPool(ctx, poolTCAN), IsNil)
func (s *HelperSuite) TestRefundBond(c *C) {
poolBTC := NewPool()
poolBTC.Asset = common.BTCAsset
poolBTC.BalanceAsset = sdk.NewUint(300 * common.One)
poolBTC.BalanceRune = sdk.NewUint(300 * common.One)
poolBTC.Status = PoolEnabled
c.Assert(k.SetPool(ctx, poolBTC), IsNil)
ygg.Type = YggdrasilVault
ygg.Coins = common.Coins{
common.NewCoin(common.RuneAsset(), sdk.NewUint(1*common.One)),
common.NewCoin(common.BNBAsset, sdk.NewUint(1*common.One)), // 1
common.NewCoin(tCanAsset, sdk.NewUint(common.One).QuoUint64(2)), // 0.5 TCAN
common.NewCoin(common.BTCAsset, sdk.NewUint(common.One).QuoUint64(4)), // 0.25 BTC
}
totalRuneLeft, err := getTotalYggValueInRune(ctx, k, ygg)
c.Assert(err, IsNil)
totalRuneStolen := ygg.GetCoin(common.RuneAsset()).Amount
slashAmt := totalRuneLeft.MulUint64(3).QuoUint64(2)
c.Assert(subsidizePoolWithSlashBond(ctx, k, ygg, totalRuneLeft, slashAmt), IsNil)
slashAmt = common.SafeSub(slashAmt, totalRuneStolen)
totalRuneLeft = common.SafeSub(totalRuneLeft, totalRuneStolen)
amountBNBForBNBPool := slashAmt.Mul(poolBNB.AssetValueInRune(sdk.NewUint(common.One))).Quo(totalRuneLeft)
runeBNB := poolBNB.BalanceRune.Add(amountBNBForBNBPool)
bnbPoolAsset := poolBNB.BalanceAsset.Sub(sdk.NewUint(common.One))
poolBNB, err = k.GetPool(ctx, common.BNBAsset)
c.Assert(err, IsNil)
c.Assert(poolBNB.BalanceRune.Equal(runeBNB), Equals, true)
c.Assert(poolBNB.BalanceAsset.Equal(bnbPoolAsset), Equals, true)
amountRuneForTCANPool := slashAmt.Mul(poolTCAN.AssetValueInRune(sdk.NewUint(common.One).QuoUint64(2))).Quo(totalRuneLeft)
runeTCAN := poolTCAN.BalanceRune.Add(amountRuneForTCANPool)
tcanPoolAsset := poolTCAN.BalanceAsset.Sub(sdk.NewUint(common.One).QuoUint64(2))
poolTCAN, err = k.GetPool(ctx, tCanAsset)
c.Assert(err, IsNil)
c.Assert(poolTCAN.BalanceRune.Equal(runeTCAN), Equals, true)
c.Assert(poolTCAN.BalanceAsset.Equal(tcanPoolAsset), Equals, true)
amountRuneForBTCPool := slashAmt.Mul(poolBTC.AssetValueInRune(sdk.NewUint(common.One).QuoUint64(4))).Quo(totalRuneLeft)
runeBTC := poolBTC.BalanceRune.Add(amountRuneForBTCPool)
btcPoolAsset := poolBTC.BalanceAsset.Sub(sdk.NewUint(common.One).QuoUint64(4))
poolBTC, err = k.GetPool(ctx, common.BTCAsset)
c.Assert(err, IsNil)
c.Assert(poolBTC.BalanceRune.Equal(runeBTC), Equals, true)
c.Assert(poolBTC.BalanceAsset.Equal(btcPoolAsset), Equals, true)
ygg1 := GetRandomVault()
ygg1.Type = YggdrasilVault
ygg1.Coins = common.Coins{
common.NewCoin(tCanAsset, sdk.NewUint(common.One*2)), // 2 TCAN
common.NewCoin(common.BTCAsset, sdk.NewUint(common.One*4)), // 4 BTC
}
totalRuneLeft, err = getTotalYggValueInRune(ctx, k, ygg1)
c.Assert(err, IsNil)
slashAmt = sdk.NewUint(100 * common.One)
c.Assert(subsidizePoolWithSlashBond(ctx, k, ygg1, totalRuneLeft, slashAmt), IsNil)
amountRuneForTCANPool = slashAmt.Mul(poolTCAN.AssetValueInRune(sdk.NewUint(common.One * 2))).Quo(totalRuneLeft)
runeTCAN = poolTCAN.BalanceRune.Add(amountRuneForTCANPool)
poolTCAN, err = k.GetPool(ctx, tCanAsset)
c.Assert(err, IsNil)
c.Assert(poolTCAN.BalanceRune.Equal(runeTCAN), Equals, true)
amountRuneForBTCPool = slashAmt.Mul(poolBTC.AssetValueInRune(sdk.NewUint(common.One * 4))).Quo(totalRuneLeft)
runeBTC = poolBTC.BalanceRune.Add(amountRuneForBTCPool)
poolBTC, err = k.GetPool(ctx, common.BTCAsset)
c.Assert(err, IsNil)
c.Assert(poolBTC.BalanceRune.Equal(runeBTC), Equals, true)
}
func (s *HelperSuite) TestRefundBondError(c *C) {
ctx, _ := setupKeeperForTest(c)
// active node should not refund bond
pk := GetRandomPubKey()
na := GetRandomNodeAccount(NodeActive)
na.Bond = sdk.NewUint(12098 * common.One)
na.PubKeySet.Secp256k1 = pk
na.Bond = sdk.NewUint(100 * common.One)
txOut := NewTxStoreDummy()
tx := GetRandomTx()
keeper1 := &TestRefundBondKeeper{}
c.Assert(refundBond(ctx, tx, na, keeper1, txOut), IsNil)
// fail to get vault should return an error
na.UpdateStatus(NodeStandby, ctx.BlockHeight())
keeper1.na = na
c.Assert(refundBond(ctx, tx, na, keeper1, txOut), NotNil)
// if the vault is not a yggdrasil pool , it should return an error
ygg := NewVault(ctx.BlockHeight(), ActiveVault, AsgardVault, pk)
ygg.Coins = common.Coins{}
keeper1.ygg = ygg
c.Assert(refundBond(ctx, tx, na, keeper1, txOut), NotNil)
// fail to get pool should fail
ygg = NewVault(ctx.BlockHeight(), ActiveVault, YggdrasilVault, pk)
ygg.Coins = common.Coins{
common.NewCoin(common.RuneAsset(), sdk.NewUint(27*common.One)),
common.NewCoin(common.BNBAsset, sdk.NewUint(27*common.One)),
}
keeper1.ygg = ygg
c.Assert(refundBond(ctx, tx, na, keeper1, txOut), NotNil)
// when ygg asset in RUNE is more then bond , thorchain should slash the node account with all their bond
keeper1.pool = Pool{
Asset: common.BNBAsset,
BalanceRune: sdk.NewUint(1024 * common.One),
BalanceAsset: sdk.NewUint(167 * common.One),
}
c.Assert(refundBond(ctx, tx, na, keeper1, txOut), IsNil)
// make sure no tx has been generated for refund
c.Assert(txOut.GetOutboundItems(), HasLen, 0)
// make sure vault had been removed
_, err := keeper1.GetVault(ctx, pk)
c.Assert(err, NotNil)
}
func (s *HelperSuite) TestRefundBondHappyPath(c *C) {
ctx, _ := setupKeeperForTest(c)
na := GetRandomNodeAccount(NodeActive)
na.Bond = sdk.NewUint(12098 * common.One)
txOut := NewTxStoreDummy()
pk := GetRandomPubKey()
na.PubKeySet.Secp256k1 = pk
ygg := NewVault(ctx.BlockHeight(), ActiveVault, YggdrasilVault, pk)
ygg.Coins = common.Coins{
common.NewCoin(common.RuneAsset(), sdk.NewUint(3946*common.One)),
common.NewCoin(common.BNBAsset, sdk.NewUint(27*common.One)),
......@@ -62,11 +215,21 @@ func (s *HelperSuite) TestRefundBond(c *C) {
}
na.Status = NodeStandby
tx := GetRandomTx()
err := refundBond(ctx, tx, na, keeper, txOut)
yggAssetInRune, err := getTotalYggValueInRune(ctx, keeper, ygg)
c.Assert(err, IsNil)
err = refundBond(ctx, tx, na, keeper, txOut)
slashAmt := yggAssetInRune.MulUint64(3).QuoUint64(2)
c.Assert(err, IsNil)
c.Assert(txOut.GetOutboundItems(), HasLen, 1)
outCoin := txOut.GetOutboundItems()[0].Coin
c.Check(outCoin.Amount.Equal(sdk.NewUint(40981137725)), Equals, true)
p, err := keeper.GetPool(ctx, common.BNBAsset)
c.Assert(err, IsNil)
expectedPoolRune := sdk.NewUint(23789 * common.One).Sub(sdk.NewUint(3946 * common.One)).Add(slashAmt)
c.Assert(p.BalanceRune.Equal(expectedPoolRune), Equals, true, Commentf("expect %s however we got %s", expectedPoolRune, p.BalanceRune))
expectedPoolBNB := sdk.NewUint(167 * common.One).Sub(sdk.NewUint(27 * common.One))
c.Assert(p.BalanceAsset.Equal(expectedPoolBNB), Equals, true, Commentf("expected BNB in pool %s , however we got %s", expectedPoolBNB, p.BalanceAsset))
}
func (s *HelperSuite) TestEnableNextPool(c *C) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment