-
Notifications
You must be signed in to change notification settings - Fork 208
/
Copy pathmsg_server_redeem_stake.go
155 lines (138 loc) · 7.36 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
150
151
152
153
154
155
package keeper
import (
"context"
"fmt"
"github.com/spf13/cast"
recordstypes "github.com/Stride-Labs/stride/x/records/types"
"github.com/Stride-Labs/stride/x/stakeibc/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/Stride-Labs/stride/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, sdkerrors.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, sdkerrors.Wrapf(types.ErrInvalidHostZone, "host zone is invalid: %s", msg.HostZone)
}
// first construct a user redemption record
epochTracker, found := k.GetEpochTracker(ctx, "day")
if !found {
return nil, sdkerrors.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, sdkerrors.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, sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid receiver address (%s)", err)
}
if msg.Amount > hostZone.StakedBal {
return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, "cannot unstake an amount g.t. staked balance on host zone: %d", msg.Amount)
}
amt, err := cast.ToInt64E(msg.Amount)
if err != nil {
k.Logger(ctx).Error(fmt.Sprintf("error casting RedeemStake msg.Amount to int64, err: %s", err.Error()))
return nil, sdkerrors.Wrapf(types.ErrInvalidAmount, fmt.Sprintf("invalid amount: %s", err.Error()))
}
// 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, sdkerrors.Wrapf(types.ErrRedemptionRateOutsideSafetyBounds, errMsg)
}
// construct desired unstaking amount from host zone
coinDenom := types.StAssetDenomFromHostZoneDenom(hostZone.HostDenom)
nativeAmount := sdk.NewDec(amt).Mul(hostZone.RedemptionRate).RoundInt()
// TODO(TEST-112) bigint safety
coinString := nativeAmount.String() + coinDenom
inCoin, err := sdk.ParseCoinNormalized(coinString)
if err != nil {
return nil, sdkerrors.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, sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "amount must be greater than 0. found: %d", msg.Amount)
}
// - Creator owns at least "amount" stAssets
balance := k.bankKeeper.GetBalance(ctx, sender, coinDenom)
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(sdk.NewInt(amt)) {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "balance is lower than redemption amount. redemption amount: %d, balance %d: ", msg.Amount, balance.Amount)
}
// UNBONDING RECORD KEEPING
userRedemptionRecord := recordstypes.UserRedemptionRecord{
Id: redemptionId,
Sender: senderAddr,
Receiver: msg.Receiver,
Amount: nativeAmount.Uint64(),
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, sdkerrors.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, sdkerrors.Wrapf(types.ErrInvalidHostZone, "host zone not found in unbondings: %s", hostZone.ChainId)
}
hostZoneUnbonding.NativeTokenAmount += nativeAmount.Uint64()
hostZoneUnbonding.UserRedemptionRecords = append(hostZoneUnbonding.UserRedemptionRecords, userRedemptionRecord.Id)
// Escrow user's balance
redeemCoin := sdk.NewCoins(sdk.NewCoin(coinDenom, sdk.NewInt(amt)))
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, sdkerrors.Wrapf(types.ErrInsufficientFunds, "couldn't send %d 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
stTokenAmount, err := cast.ToUint64E(msg.Amount)
if err != nil {
errMsg := fmt.Sprintf("Could not convert redemption amount to int64 in redeem stake | %s", err.Error())
k.Logger(ctx).Error(errMsg)
return nil, sdkerrors.Wrapf(types.ErrIntCast, errMsg)
}
hostZoneUnbonding.StTokenAmount += stTokenAmount
// 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, sdkerrors.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
}