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

add RedeemStake to autopilot #1012

Merged
merged 16 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 24 additions & 1 deletion dockernet/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ IBC_DYDX_CHANNEL_1_DENOM='ibc/78B7A771A2ECBF5D10DC6AB35568A7AC4161DB21B3A848DA47
IBC_DYDX_CHANNEL_2_DENOM='ibc/748465E0D883217048DB25F4C3825D03F682A06FE292E21072BF678E249DAC18'
IBC_DYDX_CHANNEL_3_DENOM='ibc/6301148031C0AC9A392C2DDB1B2D1F11B3B9D0A3ECF20C6B5122685D9E4CC631'

IBC_GAIA_STDENOM='ibc/054A44EC8D9B68B9A6F0D5708375E00A5569A28F21E0064FF12CADC3FEF1D04F'
IBC_HOST_STDENOM='ibc/E3AF56419340E719710C088D3855F65C4717E1A0C3B405F0C1D16F2A54E89421'

# COIN TYPES
# Coin types can be found at https://github.com/satoshilabs/slips/blob/master/slip-0044.md
COSMOS_COIN_TYPE=118
Expand Down Expand Up @@ -421,6 +424,26 @@ WAIT_FOR_STRING() {
( tail -f -n0 $1 & ) | grep -q "$2"
}

# Helper function to ensure there's enough time left in the epoch for operations to complete
# This will check how much time is remaining in the epoch, and if there's enough time,
# it will do nothing, otherwise it will sleep until the next epoch begins
# Ex: if you need at least 30 seconds in the day epoch to complete the test,
# you can run `AVOID_EPOCH_BOUNDARY day 30``
AVOID_EPOCH_BOUNDARY() {
epoch_type="$1"
buffer_required="$2"

seconds_remaining_in_epoch=$($STRIDE_MAIN_CMD q epochs seconds-remaining $epoch_type)

# If there's enough time left, no need to sleep
if [[ $seconds_remaining_in_epoch -gt $buffer_required ]]; then
return
fi

# Otherwise, wait for the next epoch
sleep $((seconds_remaining_in_epoch+5))
}

# Sleep until the balance has changed
# Optionally provide a minimum amount it must change by (to ignore interest)
WAIT_FOR_BALANCE_CHANGE() {
Expand Down Expand Up @@ -573,7 +596,7 @@ NUMBERS_ONLY() {
}

GETBAL() {
head -n 1 | grep -o -E '[0-9]+' || "0"
head -n 1 | grep -o -E '[0-9]+' || echo "0"
}

GETSTAKE() {
Expand Down
106 changes: 101 additions & 5 deletions dockernet/tests/integration_tests.bats
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ setup_file() {
STRIDE_TRANSFER_CHANNEL="channel-${TRANSFER_CHANNEL_NUMBER}"
HOST_TRANSFER_CHANNEL="channel-0"

# IBC sttoken is only needed for autopilot tests which run on GAIA and HOST
IBC_STTOKEN="N/A"
if [[ "$CHAIN_NAME" == "GAIA" || "$CHAIN_NAME" == "HOST" ]]; then
IBC_STTOKEN=$(GET_VAR_VALUE IBC_${CHAIN_NAME}_STDENOM)
fi

TRANSFER_AMOUNT=50000000
STAKE_AMOUNT=10000000
REDEEM_AMOUNT=10000
Expand Down Expand Up @@ -86,7 +92,7 @@ setup_file() {
##############################################################################################


@test "[INTEGRATION-BASIC-$CHAIN_NAME] ibc transfer updates all balances" {
@test "[INTEGRATION-BASIC-$CHAIN_NAME] ibc transfer" {
# get initial balances
sval_strd_balance_start=$($STRIDE_MAIN_CMD q bank balances $(STRIDE_ADDRESS) --denom $STRIDE_DENOM | GETBAL)
hval_strd_balance_start=$($HOST_MAIN_CMD q bank balances $HOST_VAL_ADDRESS --denom $IBC_STRD_DENOM | GETBAL)
Expand Down Expand Up @@ -181,7 +187,7 @@ setup_file() {
}

# check that tokens on the host are staked
@test "[INTEGRATION-BASIC-$CHAIN_NAME] tokens on $CHAIN_NAME were staked" {
@test "[INTEGRATION-BASIC-$CHAIN_NAME] delegation on $CHAIN_NAME" {
# wait for another epoch to pass so that tokens are staked
WAIT_FOR_STRING $STRIDE_LOGS "\[DELEGATION\] success on $HOST_CHAIN_ID"
WAIT_FOR_BLOCK $STRIDE_LOGS 4
Expand Down Expand Up @@ -275,7 +281,7 @@ setup_file() {
assert_equal "$sttoken_balance_diff" "$STAKE_AMOUNT"
}

@test "[INTEGRATION-BASIC-$CHAIN_NAME] packet forwarding automatically liquid stakes" {
@test "[INTEGRATION-BASIC-$CHAIN_NAME] autopilot liquid stake" {
memo='{ "autopilot": { "receiver": "'"$(STRIDE_ADDRESS)"'", "stakeibc": { "action": "LiquidStake" } } }'

# get initial balances
Expand Down Expand Up @@ -303,14 +309,104 @@ setup_file() {
assert_equal "$sttoken_balance_diff" "$PACKET_FORWARD_STAKE_AMOUNT"
}

@test "[INTEGRATION-BASIC-$CHAIN_NAME] autopilot liquid stake and transfer" {
riley-stride marked this conversation as resolved.
Show resolved Hide resolved
if [[ "$CHAIN_NAME" != "GAIA" && "$CHAIN_NAME" != "HOST" ]]; then
skip "Packet forward liquid stake test is only run on GAIA and HOST"
fi

memo='{ "autopilot": { "receiver": "'"$(STRIDE_ADDRESS)"'", "stakeibc": { "action": "LiquidStake", "ibc_receiver": "'$HOST_VAL_ADDRESS'" } } }'

# get initial balances
stibctoken_balance_start=$($HOST_MAIN_CMD q bank balances $HOST_VAL_ADDRESS --denom $IBC_STTOKEN 2>/dev/null | GETBAL)

# Send the IBC transfer with the JSON memo
transfer_msg_prefix="$HOST_MAIN_CMD tx ibc-transfer transfer transfer $HOST_TRANSFER_CHANNEL"
if [[ "$CHAIN_NAME" == "GAIA" ]]; then
# For GAIA (ibc-v3), pass the memo into the receiver field
$transfer_msg_prefix "$memo" ${PACKET_FORWARD_STAKE_AMOUNT}${HOST_DENOM} --from $HOST_VAL -y
elif [[ "$CHAIN_NAME" == "HOST" ]]; then
# For HOST (ibc-v5), pass an address for a receiver and the memo in the --memo field
$transfer_msg_prefix $(STRIDE_ADDRESS) ${PACKET_FORWARD_STAKE_AMOUNT}${HOST_DENOM} --memo "$memo" --from $HOST_VAL -y
fi

# Wait for the transfer to complete
WAIT_FOR_BALANCE_CHANGE $CHAIN_NAME $HOST_VAL_ADDRESS $IBC_STTOKEN

# make sure stATOM balance increased
stibctoken_balance_end=$($HOST_MAIN_CMD q bank balances $HOST_VAL_ADDRESS --denom $IBC_STTOKEN 2>/dev/null | GETBAL)
stibctoken_balance_diff=$(($stibctoken_balance_end-$stibctoken_balance_start))
assert_equal "$stibctoken_balance_diff" "$PACKET_FORWARD_STAKE_AMOUNT"
}

@test "[INTEGRATION-BASIC-$CHAIN_NAME] autopilot redeem stake" {
# Over the next two tests, we will run two redemptions in a row and we want both to occur in the same epoch
# To ensure we don't accidentally cross the epoch boundary, we'll make sure there's enough of a buffer here
# between the two redemptions
AVOID_EPOCH_BOUNDARY day 25

# get initial balances
stibctoken_balance_start=$($HOST_MAIN_CMD q bank balances $HOST_VAL_ADDRESS --denom $IBC_STTOKEN | GETBAL)

memo='{ "autopilot": { "receiver": "'"$(STRIDE_ADDRESS)"'", "stakeibc": { "action": "RedeemStake", "ibc_receiver": "'$HOST_RECEIVER_ADDRESS'" } } }'

# do IBC transfer
transfer_msg_prefix="$HOST_MAIN_CMD tx ibc-transfer transfer transfer $HOST_TRANSFER_CHANNEL"
if [[ "$CHAIN_NAME" == "GAIA" ]]; then
# For GAIA (ibc-v3), pass the memo into the receiver field
$transfer_msg_prefix "$memo" ${REDEEM_AMOUNT}${IBC_STTOKEN} --from $HOST_VAL -y
elif [[ "$CHAIN_NAME" == "HOST" ]]; then
# For HOST (ibc-v5), pass an address for a receiver and the memo in the --memo field
$transfer_msg_prefix $(STRIDE_ADDRESS)${REDEEM_AMOUNT}${IBC_STTOKEN} --memo "$memo" --from $HOST_VAL -y
else
# For all other hosts, skip this test
skip "Packet forward liquid stake test is only run on GAIA and HOST"
fi
WAIT_FOR_BLOCK $STRIDE_LOGS 2

# make sure stATOM balance decreased
stibctoken_balance_mid=$($HOST_MAIN_CMD q bank balances $HOST_VAL_ADDRESS --denom $IBC_STTOKEN | GETBAL)
stibctoken_balance_diff=$(($stibctoken_balance_start-$stibctoken_balance_mid))
assert_equal "$stibctoken_balance_diff" "$REDEEM_AMOUNT"

WAIT_FOR_BLOCK $STRIDE_LOGS 5

# check that a user redemption record was created
redemption_record_amount=$($STRIDE_MAIN_CMD q records list-user-redemption-record | grep -Fiw 'amount' | head -n 1 | grep -o -E '[0-9]+')
amount_positive=$(($redemption_record_amount > 0))
assert_equal "$amount_positive" "1"

# attempt to redeem with an invalid receiver address to invoke a failure
invalid_memo='{ "autopilot": { "receiver": "'"$(STRIDE_ADDRESS)"'", "stakeibc": { "action": "RedeemStake", "ibc_receiver": "XXX" } } }'
$transfer_msg_prefix "$invalid_memo" ${REDEEM_AMOUNT}${IBC_STTOKEN} --from $HOST_VAL -y
WAIT_FOR_BLOCK $STRIDE_LOGS 10

# Confirm the stATOM balance was refunded
stibctoken_balance_end=$($HOST_MAIN_CMD q bank balances $HOST_VAL_ADDRESS --denom $IBC_STTOKEN | GETBAL)
assert_equal "$stibctoken_balance_end" "$stibctoken_balance_mid"
}

# check that redemptions and claims work
@test "[INTEGRATION-BASIC-$CHAIN_NAME] redemption works" {
@test "[INTEGRATION-BASIC-$CHAIN_NAME] redemption and undelegation on $CHAIN_NAME" {
# get initial balance of redemption ICA
redemption_ica_balance_start=$($HOST_MAIN_CMD q bank balances $(GET_ICA_ADDR $HOST_CHAIN_ID redemption) --denom $HOST_DENOM | GETBAL)

# call redeem-stake
$STRIDE_MAIN_CMD tx stakeibc redeem-stake $REDEEM_AMOUNT $HOST_CHAIN_ID $HOST_RECEIVER_ADDRESS \
--from $STRIDE_VAL --keyring-backend test --chain-id $STRIDE_CHAIN_ID -y
WAIT_FOR_BLOCK $STRIDE_LOGS 2

# Check that the redemption record created from the autopilot redeem above was incremented
# and that there is still only one record
num_records=$($STRIDE_MAIN_CMD q records list-user-redemption-record | grep -c "amount")
assert_equal "$num_records" "1"

# The amount in the redemption record is denominated in native tokens, but amount in the
# redeem message is denominated in stTokens (and the redemption rate may be greater than 1)
# So when checking the amount, we make sure the amount in the record is greater than or
# equal to 2 * REDEEM_AMOUNT (since there were two redemptions - one from autopilot, one here)
redemption_record_amount=$($STRIDE_MAIN_CMD q records list-user-redemption-record | grep -Fiw 'amount' | head -n 1 | grep -o -E '[0-9]+')
expected_record_minimum=$(echo "$REDEEM_AMOUNT * 2" | bc)
assert_equal "$(($redemption_record_amount > $expected_record_minimum))" "1"

WAIT_FOR_STRING $STRIDE_LOGS "\[REDEMPTION] completed on $HOST_CHAIN_ID"
WAIT_FOR_BLOCK $STRIDE_LOGS 2
Expand All @@ -321,7 +417,7 @@ setup_file() {
assert_equal "$diff_positive" "1"
}

@test "[INTEGRATION-BASIC-$CHAIN_NAME] claimed tokens are properly distributed" {
@test "[INTEGRATION-BASIC-$CHAIN_NAME] claim redeemed tokens" {
# get balance before claim
start_balance=$($HOST_MAIN_CMD q bank balances $HOST_RECEIVER_ADDRESS --denom $HOST_DENOM | GETBAL)

Expand Down
7 changes: 7 additions & 0 deletions x/autopilot/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import (
"github.com/Stride-Labs/stride/v16/x/autopilot/types"
)

const (
HostChainId = "chain-0"
HostBechPrefix = "cosmos"
HostAddress = "cosmos16plylpsgxechajltx9yeseqexzdzut9g8vla4k"
HostDenom = "uatom"
)

type KeeperTestSuite struct {
apptesting.AppTestHelper
QueryClient types.QueryClient
Expand Down
4 changes: 2 additions & 2 deletions x/autopilot/keeper/liquidstake.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (k Keeper) TryLiquidStaking(

hostZone, err := k.stakeibcKeeper.GetHostZoneFromHostDenom(ctx, token.Denom)
if err != nil {
return fmt.Errorf("host zone not found for denom (%s)", token.Denom)
return err
}

if hostZone.IbcDenom != ibcDenom {
Expand Down Expand Up @@ -77,7 +77,7 @@ func (k Keeper) RunLiquidStake(ctx sdk.Context, addr sdk.AccAddress, token sdk.C
msg,
)
if err != nil {
return err
return errorsmod.Wrapf(err, "failed to liquid stake")
}

if packetMetadata.IbcReceiver == "" {
Expand Down
80 changes: 80 additions & 0 deletions x/autopilot/keeper/redeem_stake.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package keeper

import (
"fmt"

errorsmod "cosmossdk.io/errors"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types"

"github.com/Stride-Labs/stride/v16/x/autopilot/types"
stakeibckeeper "github.com/Stride-Labs/stride/v16/x/stakeibc/keeper"
stakeibctypes "github.com/Stride-Labs/stride/v16/x/stakeibc/types"
)

func (k Keeper) TryRedeemStake(
ctx sdk.Context,
packet channeltypes.Packet,
transferPacketData transfertypes.FungibleTokenPacketData,
autopilotMetadata types.StakeibcPacketMetadata,
) error {
params := k.GetParams(ctx)
if !params.StakeibcActive {
return fmt.Errorf("packet forwarding param is not active")
}

// At this point in the stack, the denom's in the packet data appear as they existed on the sender zone,
// but as a denom trace instead of a hash
// Meaning, for native stTokens, the port and channel on the host zone are part of the denom
// (e.g. transfer/{channel-on-hub}/stuatom)
// Only stride native stTokens can be redeemed, so we confirm that the denom's prefix matches
// the packet's "source" channel (i.e. the channel on the host zone)
if !transfertypes.ReceiverChainIsSource(packet.GetSourcePort(), packet.GetSourceChannel(), transferPacketData.Denom) {
return fmt.Errorf("the ibc token %s is not supported for redeem stake", transferPacketData.Denom)
}

voucherPrefix := transfertypes.GetDenomPrefix(packet.GetSourcePort(), packet.GetSourceChannel())
stAssetDenom := transferPacketData.Denom[len(voucherPrefix):]
if !k.stakeibcKeeper.CheckIsStToken(ctx, stAssetDenom) {
return fmt.Errorf("not a liquid staking token")
}

hostZoneDenom := stakeibctypes.HostZoneDenomFromStAssetDenom(stAssetDenom)

amount, ok := sdk.NewIntFromString(transferPacketData.Amount)
if !ok {
return fmt.Errorf("not a parsable amount field")
}

strideAddress := transferPacketData.Receiver
redemptionReceiver := autopilotMetadata.IbcReceiver

return k.RunRedeemStake(ctx, strideAddress, redemptionReceiver, hostZoneDenom, amount)
}

func (k Keeper) RunRedeemStake(ctx sdk.Context, strideAddress string, redemptionReceiver string, hostZoneDenom string, amount sdkmath.Int) error {
hostZone, err := k.stakeibcKeeper.GetHostZoneFromHostDenom(ctx, hostZoneDenom)
if err != nil {
return err
}

msg := &stakeibctypes.MsgRedeemStake{
Creator: strideAddress,
Amount: amount,
HostZone: hostZone.ChainId,
Receiver: redemptionReceiver,
}

if err := msg.ValidateBasic(); err != nil {
return err
}

msgServer := stakeibckeeper.NewMsgServerImpl(k.stakeibcKeeper)
if _, err = msgServer.RedeemStake(sdk.WrapSDKContext(ctx), msg); err != nil {
return errorsmod.Wrapf(err, "redeem stake failed")
}

return nil
}
Loading
Loading