Skip to content

Commit

Permalink
feat(delegation): auto associate when possible (#284)
Browse files Browse the repository at this point in the history
* feat(delegation): auto associate when possible

Whenever the asset bearing `ExocoreAssetID` is delegated by a staker to
an operator, we should check if the staker address is the same as the
operator address. If a match is found, they should be automatically
associated.

* doc(delegation): update comment about duplicate

- It is not possible to overwrite an association of the `stakerID`
  ending with `0x0` because such an association cannot be made using any
  other mechanism.
- Even if it exists, the overwriting will do nothing of import due to
  the equality check.

* test(delegation): check operator share auto assoc

Validate that the operator share is correctly set to the expected value
when self-staking ExocoreAssetID

* doc(assets): clarify StakerAssetInfo

- for ExocoreAssetID, the `TotalDepositAmount` is post-slashing, if any
  was applied.
- for other asset IDs, all components are pre-slashing.
  • Loading branch information
MaxMustermann2 authored Jan 14, 2025
1 parent 59241ba commit 4225ead
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 16 deletions.
12 changes: 6 additions & 6 deletions testutil/fund.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ package testutil

import (
"cosmossdk.io/math"
"github.com/ExocoreNetwork/exocore/utils"
exominttypes "github.com/ExocoreNetwork/exocore/x/exomint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
"github.com/evmos/evmos/v16/utils"
inflationtypes "github.com/evmos/evmos/v16/x/inflation/v1/types"
)

// FundAccount is a utility function that funds an account by minting and
// sending the coins to the address.
func FundAccount(ctx sdk.Context, bankKeeper bankkeeper.Keeper, addr sdk.AccAddress, amounts sdk.Coins) error {
if err := bankKeeper.MintCoins(ctx, inflationtypes.ModuleName, amounts); err != nil {
if err := bankKeeper.MintCoins(ctx, exominttypes.ModuleName, amounts); err != nil {
return err
}

return bankKeeper.SendCoinsFromModuleToAccount(ctx, inflationtypes.ModuleName, addr, amounts)
return bankKeeper.SendCoinsFromModuleToAccount(ctx, exominttypes.ModuleName, addr, amounts)
}

// FundAccountWithBaseDenom is a utility function that uses the FundAccount function
Expand All @@ -30,9 +30,9 @@ func FundAccountWithBaseDenom(ctx sdk.Context, bankKeeper bankkeeper.Keeper, add
// FundModuleAccount is a utility function that funds a module account by
// minting and sending the coins to the address.
func FundModuleAccount(ctx sdk.Context, bankKeeper bankkeeper.Keeper, recipientMod string, amounts sdk.Coins) error {
if err := bankKeeper.MintCoins(ctx, inflationtypes.ModuleName, amounts); err != nil {
if err := bankKeeper.MintCoins(ctx, exominttypes.ModuleName, amounts); err != nil {
return err
}

return bankKeeper.SendCoinsFromModuleToModule(ctx, inflationtypes.ModuleName, recipientMod, amounts)
return bankKeeper.SendCoinsFromModuleToModule(ctx, exominttypes.ModuleName, recipientMod, amounts)
}
12 changes: 11 additions & 1 deletion x/assets/keeper/staker_asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,14 @@ func (k Keeper) GetStakerSpecifiedAssetInfo(ctx sdk.Context, stakerID string, as
if err != nil {
return nil, errorsmod.Wrap(err, "failed to GetOperatorSpecifiedAssetInfo")
}
// the `undelegatableTokens` are currently delegated tokens. they are post-slashing, if any is applied.
// this is because slashing is applied to an operator's total amount, of which, the share of a staker is kept
// unchanged.
undelegatableTokens, err := delegationkeeper.TokensFromShares(record.UndelegatableShare, operatorAssetInfo.TotalShare, operatorAssetInfo.TotalAmount)
if err != nil {
return nil, errorsmod.Wrap(err, "failed to get shares from token")
}
// this amount is post-slashing, as explained above.
info.TotalDepositAmount = info.TotalDepositAmount.Add(undelegatableTokens).Add(record.WaitUndelegationAmount)
info.PendingUndelegationAmount = info.PendingUndelegationAmount.Add(record.WaitUndelegationAmount)
}
Expand All @@ -119,7 +123,13 @@ func (k Keeper) GetStakerSpecifiedAssetInfo(ctx sdk.Context, stakerID string, as
if value == nil {
return nil, errorsmod.Wrap(assetstype.ErrNoStakerAssetKey, fmt.Sprintf("the key is:%s", key))
}

// when there is a slashing, we do not modify `StakerAssetInfo`.
// hence, all the amounts below are pre-slashing. however, when
// an undelegation is matured, the post-slashing amount is added
// to the withdrawable amount and the pre-slashed amount is removed
// from the amount pending undelegation.
// if a staker were to exit the system, they would leave behind
// `TotalDepositAmount` == lifetime slashing amount.
ret := assetstype.StakerAssetInfo{}
k.cdc.MustUnmarshal(value, &ret)
return &ret, nil
Expand Down
20 changes: 20 additions & 0 deletions x/delegation/keeper/delegation.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"bytes"
"fmt"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -62,6 +63,20 @@ func (k *Keeper) delegateTo(
if err := k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, params.StakerAddress, delegationtype.DelegatedPoolName, coins); err != nil {
return err
}
// auto associate it, if there is a match. note that both are byte versions of bech32
// AccAddress. there is no need to check for an existing association because:
// (1) at this point, the `params.ClientChainID` is 0 and such a `stakerID` ending with
// this clientChainID can not be associated with an operator using the standard
// precompile method due to the `ClientChainExists` check.
// (2) an existing association will be overwritten by the exact same association due to
// the equality check below.
if bytes.Equal(params.StakerAddress, params.OperatorAddress[:]) {
// always returns nil.
err := k.SetAssociatedOperator(ctx, stakerID, params.OperatorAddress.String())
if err != nil {
return err
}
}
}
// calculate the share from the delegation amount
share, err := k.CalculateShare(ctx, params.OperatorAddress, assetID, params.OpAmount)
Expand Down Expand Up @@ -149,6 +164,11 @@ func (k *Keeper) UndelegateFrom(ctx sdk.Context, params *delegationtype.Delegati
}
r.CompletedEpochIdentifier = completedEpochID
r.CompletedEpochNumber = completedEpochNumber
// the hold count is relevant to async AVSs instead of sync AVSs. for example, the dogfood AVS is sync since it
// runs only on this chain. meanwhile, x/appchain-based AVSs are async because of the IBC's in-built communication
// lag. the hold count is used to ensure that the undelegation is not processed until the AVS has completed its
// unbonding period.
// TODO: remove the hold count increment for x/dogfood AVS.
err = k.SetUndelegationRecords(ctx, false, []delegationtype.UndelegationAndHoldCount{
{
Undelegation: &r,
Expand Down
78 changes: 69 additions & 9 deletions x/delegation/keeper/delegation_op_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ package keeper_test

import (
"fmt"
epochtypes "github.com/ExocoreNetwork/exocore/x/epochs/types"
"math"
"time"

"github.com/ExocoreNetwork/exocore/testutil"
epochtypes "github.com/ExocoreNetwork/exocore/x/epochs/types"

utiltx "github.com/ExocoreNetwork/exocore/testutil/tx"
avstypes "github.com/ExocoreNetwork/exocore/x/avs/types"

errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
assetskeeper "github.com/ExocoreNetwork/exocore/x/assets/keeper"
assetstypes "github.com/ExocoreNetwork/exocore/x/assets/types"

"github.com/ExocoreNetwork/exocore/x/assets/types"
delegationtype "github.com/ExocoreNetwork/exocore/x/delegation/types"
Expand Down Expand Up @@ -100,9 +102,9 @@ func (suite *DelegationTestSuite) prepareOptingInDogfood(assetID string) (sdkmat

func (suite *DelegationTestSuite) prepareDelegationNativeToken() *delegationtype.DelegationOrUndelegationParams {
delegationEvent := &delegationtype.DelegationOrUndelegationParams{
ClientChainID: assetstypes.ExocoreChainLzID,
ClientChainID: types.ExocoreChainLzID,
Action: types.DelegateTo,
AssetsAddress: common.HexToAddress(assetstypes.ExocoreAssetAddr).Bytes(),
AssetsAddress: common.HexToAddress(types.ExocoreAssetAddr).Bytes(),
OperatorAddress: suite.opAccAddr,
StakerAddress: suite.accAddr[:],
OpAmount: suite.delegationAmount,
Expand Down Expand Up @@ -174,9 +176,9 @@ func (suite *DelegationTestSuite) TestDelegateTo() {

// delegate exocore-native-token
delegationParams = &delegationtype.DelegationOrUndelegationParams{
ClientChainID: assetstypes.ExocoreChainLzID,
ClientChainID: types.ExocoreChainLzID,
Action: types.DelegateTo,
AssetsAddress: common.HexToAddress(assetstypes.ExocoreAssetAddr).Bytes(),
AssetsAddress: common.HexToAddress(types.ExocoreAssetAddr).Bytes(),
OperatorAddress: opAccAddr,
StakerAddress: suite.accAddr[:],
OpAmount: sdkmath.NewInt(50),
Expand All @@ -188,7 +190,7 @@ func (suite *DelegationTestSuite) TestDelegateTo() {
stakerID, assetID = types.GetStakerIDAndAssetID(delegationParams.ClientChainID, delegationParams.StakerAddress, delegationParams.AssetsAddress)
restakerState, err = suite.App.AssetsKeeper.GetStakerSpecifiedAssetInfo(suite.Ctx, stakerID, assetID)
suite.NoError(err)
balance := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.accAddr, assetstypes.ExocoreAssetDenom)
balance := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.accAddr, types.ExocoreAssetDenom)
suite.Equal(types.StakerAssetInfo{
TotalDepositAmount: balance.Amount.Add(delegationParams.OpAmount),
WithdrawableAmount: balance.Amount,
Expand All @@ -215,6 +217,64 @@ func (suite *DelegationTestSuite) TestDelegateTo() {
suite.Equal(delegationParams.OpAmount, totalDelegationAmount)
}

func (suite *DelegationTestSuite) TestAutoAssociate() {
genAddr := utiltx.GenerateAddress()
opAccAddr := sdk.AccAddress(genAddr[:])

registerReq := &operatortype.RegisterOperatorReq{
FromAddress: opAccAddr.String(),
Info: &operatortype.OperatorInfo{
EarningsAddr: opAccAddr.String(),
},
}
_, err := s.OperatorMsgServer.RegisterOperator(s.Ctx, registerReq)
suite.NoError(err)

// self delegate exocore-native-token
err = testutil.FundAccountWithBaseDenom(
suite.Ctx, suite.App.BankKeeper, opAccAddr, math.MaxInt64,
)
suite.NoError(err)
delegationParams := &delegationtype.DelegationOrUndelegationParams{
ClientChainID: types.ExocoreChainLzID,
Action: types.DelegateTo,
AssetsAddress: common.HexToAddress(types.ExocoreAssetAddr).Bytes(),
OperatorAddress: opAccAddr,
StakerAddress: opAccAddr,
OpAmount: sdkmath.NewInt(50),
TxHash: common.HexToHash("0x24c4a315d757249c12a7a1d7b6fb96261d49deee26f06a3e1787d008b445c3ac"),
}
err = suite.App.DelegationKeeper.DelegateTo(suite.Ctx, delegationParams)
suite.NoError(err)
stakerID, assetID := types.GetStakerIDAndAssetID(delegationParams.ClientChainID, delegationParams.StakerAddress, delegationParams.AssetsAddress)
operator, err := suite.App.DelegationKeeper.GetAssociatedOperator(suite.Ctx, stakerID)
suite.NoError(err)
suite.Equal(opAccAddr.String(), operator)

// check state
balance := suite.App.BankKeeper.GetBalance(suite.Ctx, opAccAddr, types.ExocoreAssetDenom)
restakerState, err := suite.App.AssetsKeeper.GetStakerSpecifiedAssetInfo(suite.Ctx, stakerID, assetID)
suite.NoError(err)
suite.Equal(
types.StakerAssetInfo{
TotalDepositAmount: balance.Amount.Add(delegationParams.OpAmount),
WithdrawableAmount: balance.Amount,
PendingUndelegationAmount: sdkmath.ZeroInt(),
}, *restakerState,
)

// ensure operator share is credited
operatorState, err := suite.App.AssetsKeeper.GetOperatorSpecifiedAssetInfo(suite.Ctx, opAccAddr, assetID)
suite.NoError(err)
suite.Equal(types.OperatorAssetInfo{
TotalAmount: delegationParams.OpAmount,
PendingUndelegationAmount: sdkmath.ZeroInt(),
TotalShare: sdkmath.LegacyNewDecFromBigInt(delegationParams.OpAmount.BigInt()),
OperatorShare: sdkmath.LegacyNewDecFromBigInt(delegationParams.OpAmount.BigInt()),
}, *operatorState)

}

func (suite *DelegationTestSuite) TestUndelegateFrom() {
suite.basicPrepare()
suite.prepareDeposit(suite.depositAmount)
Expand Down Expand Up @@ -284,7 +344,7 @@ func (suite *DelegationTestSuite) TestUndelegateFrom() {
stakerID, assetID = types.GetStakerIDAndAssetID(delegationEvent.ClientChainID, delegationEvent.StakerAddress, delegationEvent.AssetsAddress)
restakerState, err = suite.App.AssetsKeeper.GetStakerSpecifiedAssetInfo(suite.Ctx, stakerID, assetID)
suite.NoError(err)
balance := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.accAddr, assetstypes.ExocoreAssetDenom)
balance := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.accAddr, types.ExocoreAssetDenom)
suite.Equal(types.StakerAssetInfo{
TotalDepositAmount: balance.Amount.Add(delegationEvent.OpAmount),
WithdrawableAmount: balance.Amount,
Expand Down Expand Up @@ -436,7 +496,7 @@ func (suite *DelegationTestSuite) TestCompleteUndelegation() {
restakerState, err = suite.App.AssetsKeeper.GetStakerSpecifiedAssetInfo(suite.Ctx, stakerID, assetID)
suite.NoError(err)

balance := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.accAddr, assetstypes.ExocoreAssetDenom)
balance := suite.App.BankKeeper.GetBalance(suite.Ctx, suite.accAddr, types.ExocoreAssetDenom)
suite.Equal(types.StakerAssetInfo{
TotalDepositAmount: balance.Amount,
WithdrawableAmount: balance.Amount,
Expand Down

0 comments on commit 4225ead

Please sign in to comment.