/
msg_server_redeem_stake.go
149 lines (131 loc) · 7.13 KB
/
msg_server_redeem_stake.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package keeper
import (
"context"
"fmt"
recordstypes "github.com/Stride-Labs/stride/v12/x/records/types"
"github.com/Stride-Labs/stride/v12/x/stakeibc/types"
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/Stride-Labs/stride/v12/utils"
)
func (k msgServer) RedeemStake(goCtx context.Context, msg *types.MsgRedeemStake) (*types.MsgRedeemStakeResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
k.Logger(ctx).Info(fmt.Sprintf("redeem stake: %s", msg.String()))
// get our addresses, make sure they're valid
sender, err := sdk.AccAddressFromBech32(msg.Creator)
if err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "creator address is invalid: %s. err: %s", msg.Creator, err.Error())
}
// then make sure host zone is valid
hostZone, found := k.GetHostZone(ctx, msg.HostZone)
if !found {
return nil, errorsmod.Wrapf(types.ErrInvalidHostZone, "host zone is invalid: %s", msg.HostZone)
}
if hostZone.Halted {
k.Logger(ctx).Error(fmt.Sprintf("Host Zone halted for zone (%s)", msg.HostZone))
return nil, errorsmod.Wrapf(types.ErrHaltedHostZone, "halted host zone found for zone (%s)", msg.HostZone)
}
// first construct a user redemption record
epochTracker, found := k.GetEpochTracker(ctx, "day")
if !found {
return nil, errorsmod.Wrapf(types.ErrEpochNotFound, "epoch tracker found: %s", "day")
}
senderAddr := sender.String()
redemptionId := recordstypes.UserRedemptionRecordKeyFormatter(hostZone.ChainId, epochTracker.EpochNumber, senderAddr)
_, found = k.RecordsKeeper.GetUserRedemptionRecord(ctx, redemptionId)
if found {
return nil, errorsmod.Wrapf(recordstypes.ErrRedemptionAlreadyExists, "user already redeemed this epoch: %s", redemptionId)
}
// ensure the recipient address is a valid bech32 address on the hostZone
// TODO(TEST-112) do we need to check the hostZone before this check? Would need access to keeper
_, err = utils.AccAddressFromBech32(msg.Receiver, hostZone.Bech32Prefix)
if err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid receiver address (%s)", err)
}
// construct desired unstaking amount from host zone
stDenom := types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
nativeAmount := sdk.NewDecFromInt(msg.Amount).Mul(hostZone.RedemptionRate).RoundInt()
if nativeAmount.GT(hostZone.StakedBal) {
return nil, errorsmod.Wrapf(types.ErrInvalidAmount, "cannot unstake an amount g.t. staked balance on host zone: %v", msg.Amount)
}
// safety check: redemption rate must be within safety bounds
rateIsSafe, err := k.IsRedemptionRateWithinSafetyBounds(ctx, hostZone)
if !rateIsSafe || (err != nil) {
errMsg := fmt.Sprintf("IsRedemptionRateWithinSafetyBounds check failed. hostZone: %s, err: %s", hostZone.String(), err.Error())
return nil, errorsmod.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg)
}
// TODO(TEST-112) bigint safety
coinString := nativeAmount.String() + stDenom
inCoin, err := sdk.ParseCoinNormalized(coinString)
if err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "could not parse inCoin: %s. err: %s", coinString, err.Error())
}
// safety checks on the coin
// - Redemption amount must be positive
if !nativeAmount.IsPositive() {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "amount must be greater than 0. found: %v", msg.Amount)
}
// - Creator owns at least "amount" stAssets
balance := k.bankKeeper.GetBalance(ctx, sender, stDenom)
k.Logger(ctx).Info(fmt.Sprintf("Redemption issuer IBCDenom balance: %v%s", balance.Amount, balance.Denom))
k.Logger(ctx).Info(fmt.Sprintf("Redemption requested redemotion amount: %v%s", inCoin.Amount, inCoin.Denom))
if balance.Amount.LT(msg.Amount) {
return nil, errorsmod.Wrapf(sdkerrors.ErrInvalidCoins, "balance is lower than redemption amount. redemption amount: %v, balance %v: ", msg.Amount, balance.Amount)
}
// UNBONDING RECORD KEEPING
userRedemptionRecord := recordstypes.UserRedemptionRecord{
Id: redemptionId,
Sender: senderAddr,
Receiver: msg.Receiver,
Amount: nativeAmount,
Denom: hostZone.HostDenom,
HostZoneId: hostZone.ChainId,
EpochNumber: epochTracker.EpochNumber,
// claimIsPending represents whether a redemption is currently being claimed,
// contingent on the host zone unbonding having status CLAIMABLE
ClaimIsPending: false,
}
// then add undelegation amount to epoch unbonding records
epochUnbondingRecord, found := k.RecordsKeeper.GetEpochUnbondingRecord(ctx, epochTracker.EpochNumber)
if !found {
k.Logger(ctx).Error("latest epoch unbonding record not found")
return nil, errorsmod.Wrapf(recordstypes.ErrEpochUnbondingRecordNotFound, "latest epoch unbonding record not found")
}
// get relevant host zone on this epoch unbonding record
hostZoneUnbonding, found := k.RecordsKeeper.GetHostZoneUnbondingByChainId(ctx, epochUnbondingRecord.EpochNumber, hostZone.ChainId)
if !found {
return nil, errorsmod.Wrapf(types.ErrInvalidHostZone, "host zone not found in unbondings: %s", hostZone.ChainId)
}
hostZoneUnbonding.NativeTokenAmount = hostZoneUnbonding.NativeTokenAmount.Add(nativeAmount)
hostZoneUnbonding.UserRedemptionRecords = append(hostZoneUnbonding.UserRedemptionRecords, userRedemptionRecord.Id)
// Escrow user's balance
redeemCoin := sdk.NewCoins(sdk.NewCoin(stDenom, msg.Amount))
bech32ZoneAddress, err := sdk.AccAddressFromBech32(hostZone.Address)
if err != nil {
return nil, fmt.Errorf("could not bech32 decode address %s of zone with id: %s", hostZone.Address, hostZone.ChainId)
}
err = k.bankKeeper.SendCoins(ctx, sender, bech32ZoneAddress, redeemCoin)
if err != nil {
k.Logger(ctx).Error("Failed to send sdk.NewCoins(inCoins) from account to module")
return nil, errorsmod.Wrapf(types.ErrInsufficientFunds, "couldn't send %v derivative %s tokens to module account. err: %s", msg.Amount, hostZone.HostDenom, err.Error())
}
// record the number of stAssets that should be burned after unbonding
hostZoneUnbonding.StTokenAmount = hostZoneUnbonding.StTokenAmount.Add(msg.Amount)
// Actually set the records, we wait until now to prevent any errors
k.RecordsKeeper.SetUserRedemptionRecord(ctx, userRedemptionRecord)
// Set the UserUnbondingRecords on the proper HostZoneUnbondingRecord
hostZoneUnbondings := epochUnbondingRecord.GetHostZoneUnbondings()
if hostZoneUnbondings == nil {
hostZoneUnbondings = []*recordstypes.HostZoneUnbonding{}
epochUnbondingRecord.HostZoneUnbondings = hostZoneUnbondings
}
updatedEpochUnbondingRecord, success := k.RecordsKeeper.AddHostZoneToEpochUnbondingRecord(ctx, epochUnbondingRecord.EpochNumber, hostZone.ChainId, hostZoneUnbonding)
if !success {
k.Logger(ctx).Error(fmt.Sprintf("Failed to set host zone epoch unbonding record: epochNumber %d, chainId %s, hostZoneUnbonding %v", epochUnbondingRecord.EpochNumber, hostZone.ChainId, hostZoneUnbonding))
return nil, errorsmod.Wrapf(types.ErrEpochNotFound, "couldn't set host zone epoch unbonding record. err: %s", err.Error())
}
k.RecordsKeeper.SetEpochUnbondingRecord(ctx, *updatedEpochUnbondingRecord)
k.Logger(ctx).Info(fmt.Sprintf("executed redeem stake: %s", msg.String()))
return &types.MsgRedeemStakeResponse{}, nil
}