Skip to content

Commit

Permalink
Add PENDING status to IBC / ICA function calls (#296)
Browse files Browse the repository at this point in the history
## Context and purpose of the change

Currently, our ICA / IBC logic assumes efficient relayers. In practice, this has not been the case, and packets sometimes fail to relay. To improve the stability of our ICA / IBC logic and make it easier to reason about the current state, this PR adds a PENDING flag to all ICA / IBC function calls. Specifically, the PENDING status is tracked on various records.

The following functions are async and need to be accounted for in a PENDING state:

**Staking direction** (represented on `DepositRecord`s)
- IBC transfer from Stride --> Host Zone
- ICA bank send from withdrawal account -> delegation account (NOTE: we don't need a PENDING state here, since the record is created in the callback upon a successful MsgSend)
- ICA delegation call

**Unstaking direction** (represented on `EpochUnbondingRecords`s)
- ICA unstaking call
- ICA bank send from the delegation ICA to the redemption ICA

Claims already have a pending state (to prevent double claims).

The logic is as follows: after the ICA / IBC message is triggered, the state of the record is set to `PENDING`. Then, in the callback, the state either (1) progresses based on the action taken, if the ICA / IBC call was a success, or (2) reverts to the previous state, if the call failed.

Took the opportunity here to rename some of the flags to make them more clear.
Also note, the updates to PENDING status should happen as close to the ICA call site as possible. Please check this as you review!

## Brief Changelog

Hooks changes
- Add `PENDING` flag to `DepositRecord`
`TRANSFER` -> `TRANSFER_QUEUE`
`STAKE` -> `DELEGATION_QUEUE`
```
enum Status {
    // in transfer queue to be sent to the delegation ICA
    TRANSFER_QUEUE = 0;
    // transfer in progress (IBC packet sent, ack not received)
    TRANSFER_IN_PROGRESS = 2;
    // in staking queue on delegation ICA
    DELEGATION_QUEUE = 1;
    // staking in progress (ICA packet sent, ack not received)
    DELEGATION_IN_PROGRESS = 3;
  }
```
- Add `PENDING` flag to `EpochUnbondingRecord`
`BONDED`  -> `UNBONDING_QUEUE`
`UNBONDED` -> `EXIT_TRANSFER_QUEUE`
`TRANSFERRED` -> `CLAIMABLE`
```
enum Status {
    // tokens bonded on delegate account
    UNBONDING_QUEUE = 0;
    UNBONDING_IN_PROGRESS = 3;
    // unbonding completed on delegate account
    EXIT_TRANSFER_QUEUE = 1;
    EXIT_TRANSFER_IN_PROGRESS = 4;
    // transfer success
    CLAIMABLE = 2;
  }
```
- In `TransferExistingDepositsToHostZones`, after IBC transferring tokens to the host zone
- In `StakeExistingDepositsOnHostZones`, set the deposit record to status `PENDING` after sending a delegation ICA
- Set `HostZoneUnbonding`s to `PENDING` after transferring tokens to the redemption account in `SweepAllUnbondedTokens`
- Set `HostZoneUnbonding`s to `PENDING` after sending unstaking ICAs

Callback  changes
- Update `DepositRecord` status in `DelegateCallback`
- Update `DepositRecord` status in `TransferCallback`
- Update `HostZoneUnbonding` in `UndelegateCallback`
- Update `HostZoneUnbonding` in `RedemptionCallback`

## Tests
Force (1) timeout and (2) transaction failure for 
- IBC transfer
1. DepostRecord has status TRANSFER_QUEUE
2. DepostRecord has status TRANSFER_IN_PROGRESS
3. DepostRecord has status TRANSFER_QUEUE
timeout: set IBC transfer timeout very low
```
// set timeout to 1s
// observe record change from
TRANSFER_QUEUE -> TRANSFER_IN_PROGRESS -> TRANSFER_QUEUE
```
failure: transfer to an invalid account
```
// set delegateAddress to osmo1j6vwz24hddx4t6kukmzgmk0764x0evs74002e3
// observe record change from
TRANSFER_QUEUE -> TRANSFER_IN_PROGRESS -> TRANSFER_QUEUE
```

- ICA MsgDelegate
1. DepostRecord has status DELEGATION_QUEUE
2. DepostRecord has status DELEGATION_IN_PROGRESS
3. DepostRecord has status DELEGATION_QUEUE
timeout: set ICA timeout very low
```
if callbackId == "delegate" {
	timeoutTimestamp = uint64(ctx.BlockTime().UnixNano()) + uint64(1000000000)
}
// observe the following state transition
DELEGATION_QUEUE -> DELEGATION_IN_PROGRESS -> DELEGATION_QUEUE 
```
failure: stake to non-existent validator 
```
// stake to stridevaloper1dqpg0hfva0k0awzhxwarjpgzj82hlmvrnxlzsd
// observe the following state transition
DELEGATION_QUEUE -> DELEGATION_IN_PROGRESS -> DELEGATION_QUEUE
```

- ICA MsgUndelegate
1. HZU has status UNBONDING_QUEUE
2. HZU has status UNBONDING_IN_PROGRESS
3. HZU has status UNBONDING_QUEUE
timeout: set ICA timeout very low
```
if callbackId == "undelegate" {
	timeoutTimestamp = uint64(ctx.BlockTime().UnixNano()) + uint64(1000000000)
}
// observe the following state transition

```
failure: unstake from non-existent validator 
```
// unstake from stridevaloper1dqpg0hfva0k0awzhxwarjpgzj82hlmvrnxlzsd
// observe the following state transition
UNBONDING_QUEUE -> UNBONDING_IN_PROGRESS -> UNBONDING_QUEUE
```

- ICA MsgSend
1. HZU has status EXIT_TRANSFER_QUEUE
2. HZU has status EXIT_TRANSFER_IN_PROGRESS
3. HZU has status EXIT_TRANSFER_QUEUE
timeout: set ICA timeout very low
```
// set timeout to 1s 
// observe the following state transition
EXIT_TRANSFER_QUEUE -> EXIT_TRANSFER_IN_PROGRESS -> EXIT_TRANSFER_QUEUE
```
failure: send to invalid address
```
// MsgSend to the wrong address
// observe the following state transition
EXIT_TRANSFER_QUEUE -> EXIT_TRANSFER_IN_PROGRESS -> EXIT_TRANSFER_QUEUE
```

## Author's Checklist

I have...

- [ ] Run and PASSED locally all GAIA integration tests
- [ ] If the change is contentful, I either:
    - [ ] Added a new unit test OR 
    - [ ] Added test cases to existing unit tests
- [ ] OR this change is a trivial rework / code cleanup without any test coverage

If skipped any of the tests above, explain.


## Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] manually tested (if applicable)
- [ ] confirmed the author wrote unit tests for new logic
- [ ] reviewed documentation exists and is accurate


## Documentation and Release Note

  - [ ] Does this pull request introduce a new feature or user-facing behavior changes? 
  - [ ] Is a relevant changelog entry added to the `Unreleased` section in `CHANGELOG.md`?
  - [ ] This pull request updates existing proto field values (and require a backend and frontend migration)? 
  - [ ] Does this pull request change existing proto field names (and require a frontend migration)?
  How is the feature or change documented? 
      - [ ] not applicable
      - [ ] jira ticket `XXX` 
      - [ ] specification (`x/<module>/spec/`) 
      - [ ] README.md 
      - [ ] not documented
  • Loading branch information
asalzmann authored Oct 20, 2022
1 parent ceff377 commit 6660f60
Show file tree
Hide file tree
Showing 39 changed files with 488 additions and 335 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ require (
github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-kit/kit v0.12.0 // indirect
github.com/go-kit/log v0.2.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
Expand All @@ -68,6 +69,7 @@ require (
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.1.2 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
Expand Down Expand Up @@ -509,6 +510,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
Expand Down
20 changes: 13 additions & 7 deletions proto/records/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ message DepositRecord {
string denom = 3;
string hostZoneId = 4;
enum Status {
// pending transfer to delegate account
TRANSFER = 0;
// pending staking on delegate account
STAKE = 1;
// in transfer queue to be sent to the delegation ICA
TRANSFER_QUEUE = 0;
// transfer in progress (IBC packet sent, ack not received)
TRANSFER_IN_PROGRESS = 2;
// in staking queue on delegation ICA
DELEGATION_QUEUE = 1;
// staking in progress (ICA packet sent, ack not received)
DELEGATION_IN_PROGRESS = 3;
}
enum Source {
STRIDE = 0;
Expand All @@ -75,11 +79,13 @@ message DepositRecord {
message HostZoneUnbonding {
enum Status {
// tokens bonded on delegate account
BONDED = 0;
UNBONDING_QUEUE = 0;
UNBONDING_IN_PROGRESS = 3;
// unbonding completed on delegate account
UNBONDED = 1;
EXIT_TRANSFER_QUEUE = 1;
EXIT_TRANSFER_IN_PROGRESS = 4;
// transfer success
TRANSFERRED = 2;
CLAIMABLE = 2;
}
uint64 stTokenAmount = 1;
uint64 nativeTokenAmount = 2;
Expand Down
2 changes: 1 addition & 1 deletion scripts/tests/gaia_tests.bats
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ setup() {
# TODO check that the UserRedemptionRecord has isClaimable = true

# grab the epoch number for the first deposit record in the list od DRs
EPOCH=$(strided q records list-user-redemption-record | grep -Fiw 'epochNumber' | head -n 1 | grep -o -E '[0-9]+')
EPOCH=$($STRIDE_MAIN_CMD q records list-user-redemption-record | grep -Fiw 'epochNumber' | head -n 1 | grep -o -E '[0-9]+')
# claim the record
$STRIDE_MAIN_CMD tx stakeibc claim-undelegated-tokens GAIA $EPOCH $SENDER_ACCT --from val1 --keyring-backend test --chain-id STRIDE -y
WAIT_FOR_STRING $STRIDE_LOGS '\[CLAIM\] success on GAIA'
Expand Down
2 changes: 1 addition & 1 deletion utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

var ADMINS = map[string]bool{
"stride1k8c2m5cn322akk5wy8lpt87dd2f4yh9azg7jlh": true, // F5
"stride1u20df3trc2c2zdhm8qvh2hdjx9ewh00sv6eyy8": true, // F5
"stride10d07y265gmmuvt4z0w9aw880jnsr700jefnezl": true, // gov module
}

Expand Down
32 changes: 12 additions & 20 deletions x/epochs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@ parent:

While using the SDK, we often want to run certain code periodically. The `epochs` module allows other modules to be configured such that they are signaled once every period. So another module can specify it wants to execute code once a week, starting at UTC-time = x. `epochs` creates a generalized epoch interface to other modules so that they can easily be signalled upon such events.


## Contents

1. **[Concepts](#concepts)**
2. **[State](#state)**
3. **[Events](#events)**
4. **[Keeper](#keeper)**
5. **[Hooks](#hooks)**
6. **[Queries](#queries)**
4. **[Keeper](#keeper)**
5. **[Hooks](#hooks)**
6. **[Queries](#queries)**
7. **[Future Improvements](#future-improvements)**


## Concepts

Epochs are on-chain timers that have timer ticks at specific time intervals, triggering the execution of certain logic that is constrained by a specific epoch. The purpose of the `epochs` module is to provide a generalized epoch interface to other modules so that they can easily implement epochs without keeping their own code for epochs.
Expand All @@ -31,6 +29,7 @@ Every epoch has a unique identifier. Every epoch will have a start time, and an
When an epoch triggers the execution of code, that code is executed at the first block whose blocktime is greater than `end_time`. It follows that the `start_time` of the following epoch will be the `end_time` of the previous epoch.

Stride uses three epoch identifiers as found in `x/epochs/genesis.go`

1. `DAY_EPOCH`: this identifies an epoch that lasts 24 hours.
2. `STRIDE_EPOCH`: this identifies an epoch that lasts 5 minutes on local mode tesnet (although this may be changed) and longer on public testnet and mainnet, and is used in the `x/stakeibc/` module as a time interval in accordance with which the Stride app chain performs certain functions, such as autocompound stakig rewards.

Expand Down Expand Up @@ -67,7 +66,7 @@ message EpochInfo {
}
```

`EpochInfo` keeps `identifier`, `start_time`,`duration`, `current_epoch`, `current_epoch_start_time`, `epoch_counting_started`, `current_epoch_start_height`.
`EpochInfo` keeps `identifier`, `start_time`,`duration`, `current_epoch`, `current_epoch_start_time`, `epoch_counting_started`, `current_epoch_start_height`.

1. `identifier` keeps epoch identification string.
2. `start_time` keeps epoch counting start time, if block time passes `start_time`, `epoch_counting_started` is set.
Expand All @@ -76,6 +75,7 @@ message EpochInfo {
5. `current_epoch_start_time` keeps the start time of current epoch.
6. `epoch_number` is counted only when `epoch_counting_started` flag is set.
7. `current_epoch_start_height` keeps the start block height of current epoch.

---

## Events
Expand All @@ -91,10 +91,9 @@ The `epochs` module emits the following events:

### EndBlocker

| Type | Attribute Key | Attribute Value |
| ----------- | ------------- | --------------- |
| epoch_end | epoch_number | {epoch_number} |

| Type | Attribute Key | Attribute Value |
| --------- | ------------- | --------------- |
| epoch_end | epoch_number | {epoch_number} |

## Keeper

Expand All @@ -108,7 +107,7 @@ type Keeper interface {
// GetEpochInfo returns epoch info by identifier
GetEpochInfo(ctx sdk.Context, identifier string) types.EpochInfo
// SetEpochInfo set epoch info
SetEpochInfo(ctx sdk.Context, epoch types.EpochInfo)
SetEpochInfo(ctx sdk.Context, epoch types.EpochInfo)
// DeleteEpochInfo delete epoch info
DeleteEpochInfo(ctx sdk.Context, identifier string)
// IterateEpochInfo iterate through epochs
Expand All @@ -118,7 +117,6 @@ type Keeper interface {
}
```


## Hooks

```go
Expand All @@ -131,26 +129,20 @@ type Keeper interface {
The `BeforeEpochStart` hook does different things depending on the identifier.

If in a `day` identifier it:

1. begins unbondings
2. sweeps unbonded tokens to the redemption account
3. cleans up old records
4. creates empty epoch unbonding records for the next day

If in a `stride_epoch` identifier it:
5. creates and deposits records on each host zone
6. sets withdrawal addresses
7. updates redemption rates (if the epoch coincides with the correct interval)
8. processes `TRANSFER` deposit records to the delegation Interchain Account (if the epoch coincides with the correct interval)
9. processes `STAKE` deposit records to the delegation Interchain Account (if the epoch coincides with the correct interval)
10. Query the rewards account using interchain queries, with the transfer callback to a delegation account as a staked record (if at proper interval)
If in a `stride_epoch` identifier it: 5. creates and deposits records on each host zone 6. sets withdrawal addresses 7. updates redemption rates (if the epoch coincides with the correct interval) 8. processes `TRANSFER_QUEUE` deposit records to the delegation Interchain Account (if the epoch coincides with the correct interval) 9. processes `DELEGATION_QUEUE` deposit records to the delegation Interchain Account (if the epoch coincides with the correct interval) 10. Query the rewards account using interchain queries, with the transfer callback to a delegation account as a staked record (if at proper interval)

### How modules receive hooks

On the hook receiver functions of other modules, they need to filter `epochIdentifier` and execute for only a specific `epochIdentifier`.
Filtering `epochIdentifier` could be in `Params` of other modules so that they can be modified by governance.
Governance can change an epoch from `week` to `day` as needed.


## Queries

`epochs` module provides the below queries to check the module's state
Expand Down
43 changes: 27 additions & 16 deletions x/records/keeper/callback_transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,37 +33,48 @@ func (k Keeper) UnmarshalTransferCallbackArgs(ctx sdk.Context, delegateCallback

func TransferCallback(k Keeper, ctx sdk.Context, packet channeltypes.Packet, ack *channeltypes.Acknowledgement, args []byte) error {
k.Logger(ctx).Info("TransferCallback executing", "packet", packet)
if ack.GetError() != "" {
k.Logger(ctx).Error(fmt.Sprintf("TransferCallback does not handle errors %s", ack.GetError()))
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "TransferCallback does not handle errors: %s", ack.GetError())

// deserialize the args
transferCallbackData, err := k.UnmarshalTransferCallbackArgs(ctx, args)
if err != nil {
return sdkerrors.Wrapf(types.ErrUnmarshalFailure, "cannot unmarshal transfer callback args: %s", err.Error())
}
k.Logger(ctx).Info(fmt.Sprintf("TransferCallback %v", transferCallbackData))
depositRecord, found := k.GetDepositRecord(ctx, transferCallbackData.DepositRecordId)
if !found {
k.Logger(ctx).Error(fmt.Sprintf("TransferCallback deposit record not found, packet %v", packet))
return sdkerrors.Wrapf(types.ErrUnknownDepositRecord, "deposit record not found %d", transferCallbackData.DepositRecordId)
}

if ack == nil {
// timeout
// put record back in the TRANSFER_QUEUE
depositRecord.Status = types.DepositRecord_TRANSFER_QUEUE
k.SetDepositRecord(ctx, depositRecord)
k.Logger(ctx).Error(fmt.Sprintf("TransferCallback timeout, ack is nil, packet %v", packet))
return nil
}

if _, ok := ack.Response.(*channeltypes.Acknowledgement_Error); ok {
// error on host chain
// put record back in the TRANSFER_QUEUE
depositRecord.Status = types.DepositRecord_TRANSFER_QUEUE
k.SetDepositRecord(ctx, depositRecord)
k.Logger(ctx).Error(fmt.Sprintf("Error %s", ack.GetError()))
return nil
}

var data ibctransfertypes.FungibleTokenPacketData
if err := ibctransfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
k.Logger(ctx).Error(fmt.Sprintf("Error unmarshalling packet %v", err.Error()))
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal ICS-20 transfer packet data: %s", err.Error())
}
k.Logger(ctx).Info(fmt.Sprintf("TransferCallback unmarshalled FungibleTokenPacketData %v", data))

// deserialize the args
transferCallbackData, err := k.UnmarshalTransferCallbackArgs(ctx, args)
if err != nil {
return sdkerrors.Wrapf(types.ErrUnmarshalFailure, "cannot unmarshal transfer callback args: %s", err.Error())
}
k.Logger(ctx).Info(fmt.Sprintf("TransferCallback %v", transferCallbackData))
depositRecord, found := k.GetDepositRecord(ctx, transferCallbackData.DepositRecordId)
if !found {
k.Logger(ctx).Error(fmt.Sprintf("TransferCallback deposit record not found, packet %v", packet))
return sdkerrors.Wrapf(types.ErrUnknownDepositRecord, "deposit record not found %d", transferCallbackData.DepositRecordId)
}
depositRecord.Status = types.DepositRecord_STAKE
// put the deposit record in the DELEGATION_QUEUE
depositRecord.Status = types.DepositRecord_DELEGATION_QUEUE
k.SetDepositRecord(ctx, depositRecord)
k.Logger(ctx).Info(fmt.Sprintf("\t [IBC-TRANSFER] Deposit record updated: {%v}", depositRecord.Id))
k.Logger(ctx).Info(fmt.Sprintf("\t [IBC-TRANSFER] Deposit record updated: {%v}, status: {%s}", depositRecord.Id, depositRecord.Status.String()))
k.Logger(ctx).Info(fmt.Sprintf("[IBC-TRANSFER] success to %s", depositRecord.HostZoneId))
return nil
}
23 changes: 13 additions & 10 deletions x/records/keeper/callback_transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (s *KeeperTestSuite) SetupTransferCallback() TransferCallbackTestCase {
DepositEpochNumber: 1,
HostZoneId: chainId,
Amount: balanceToStake,
Status: recordtypes.DepositRecord_TRANSFER,
Status: recordtypes.DepositRecord_TRANSFER_QUEUE,
}
s.App.RecordsKeeper.SetDepositRecord(s.Ctx(), depositRecord)
packet := channeltypes.Packet{Data: s.MarshalledICS20PacketData()}
Expand Down Expand Up @@ -66,36 +66,39 @@ func (s *KeeperTestSuite) TestTransferCallback_Successful() {
err := recordskeeper.TransferCallback(s.App.RecordsKeeper, s.Ctx(), validArgs.packet, validArgs.ack, validArgs.args)
s.Require().NoError(err)

// Confirm deposit record has been updated to STAKE
// Confirm deposit record has been updated to DELEGATION_QUEUE
record, found := s.App.RecordsKeeper.GetDepositRecord(s.Ctx(), initialState.callbackArgs.DepositRecordId)
s.Require().True(found)
s.Require().Equal(record.Status, recordtypes.DepositRecord_STAKE, "deposit record status should be STAKE")
s.Require().Equal(record.Status, recordtypes.DepositRecord_DELEGATION_QUEUE, "deposit record status should be DELEGATION_QUEUE")
}

func (s *KeeperTestSuite) checkTransferStateIfCallbackFailed(tc TransferCallbackTestCase) {
record, found := s.App.RecordsKeeper.GetDepositRecord(s.Ctx(), tc.initialState.callbackArgs.DepositRecordId)
s.Require().True(found)
s.Require().Equal(record.Status, recordtypes.DepositRecord_TRANSFER, "deposit record status should be TRANSFER")
s.Require().Equal(record.Status, recordtypes.DepositRecord_TRANSFER_QUEUE, "deposit record status should be TRANSFER_QUEUE")
}

func (s *KeeperTestSuite) TestTransferCallback_TransferCallbackTimeout() {
tc := s.SetupTransferCallback()
invalidArgs := tc.validArgs
timeoutArgs := tc.validArgs
// a nil ack means the request timed out
invalidArgs.ack = nil
err := recordskeeper.TransferCallback(s.App.RecordsKeeper, s.Ctx(), invalidArgs.packet, invalidArgs.ack, invalidArgs.args)
timeoutArgs.ack = nil
err := recordskeeper.TransferCallback(s.App.RecordsKeeper, s.Ctx(), timeoutArgs.packet, timeoutArgs.ack, timeoutArgs.args)
s.Require().NoError(err)
s.checkTransferStateIfCallbackFailed(tc)
}

func (s *KeeperTestSuite) TestTransferCallback_TransferCallbackErrorOnHost() {
tc := s.SetupTransferCallback()
invalidArgs := tc.validArgs
errorArgs := tc.validArgs
// an error ack means the tx failed on the host
errorAck := channeltypes.Acknowledgement{Response: &channeltypes.Acknowledgement_Error{Error: "error"}}

err := recordskeeper.TransferCallback(s.App.RecordsKeeper, s.Ctx(), invalidArgs.packet, &errorAck, invalidArgs.args)
s.Require().EqualError(err, "TransferCallback does not handle errors: error: invalid request")
err := recordskeeper.TransferCallback(s.App.RecordsKeeper, s.Ctx(), errorArgs.packet, &errorAck, errorArgs.args)
s.Require().NoError(err)
record, found := s.App.RecordsKeeper.GetDepositRecord(s.Ctx(), tc.initialState.callbackArgs.DepositRecordId)
s.Require().True(found)
s.Require().Equal(record.Status, types.DepositRecord_TRANSFER_QUEUE, "DepositRecord is put back in the TRANSFER_QUEUE after a failed transfer")
s.checkTransferStateIfCallbackFailed(tc)
}

Expand Down
28 changes: 28 additions & 0 deletions x/records/keeper/epoch_unbonding_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package keeper

import (
"encoding/binary"
"fmt"

"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

stakeibctypes "github.com/Stride-Labs/stride/x/stakeibc/types"

"github.com/Stride-Labs/stride/x/records/types"
)
Expand Down Expand Up @@ -114,3 +118,27 @@ func (k Keeper) AddHostZoneToEpochUnbondingRecord(ctx sdk.Context, epochNumber u
}
return &epochUnbondingRecord, true
}

// TODO: unittest
func (k Keeper) SetHostZoneUnbondings(ctx sdk.Context, zone stakeibctypes.HostZone, epochUnbondingRecordIds []uint64, status types.HostZoneUnbonding_Status) error {
for _, epochUnbondingRecordId := range epochUnbondingRecordIds {
k.Logger(ctx).Info(fmt.Sprintf("Updating host zone unbondings on EpochUnbondingRecord %d to status %s", epochUnbondingRecordId, status.String()))
// fetch the host zone unbonding
hostZoneUnbonding, found := k.GetHostZoneUnbondingByChainId(ctx, epochUnbondingRecordId, zone.ChainId)
if !found {
errMsg := fmt.Sprintf("Error fetching host zone unbonding record for epoch: %d, host zone: %s", epochUnbondingRecordId, zone.ChainId)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrapf(stakeibctypes.ErrHostZoneNotFound, errMsg)
}
hostZoneUnbonding.Status = status
// save the updated hzu on the epoch unbonding record
updatedRecord, success := k.AddHostZoneToEpochUnbondingRecord(ctx, epochUnbondingRecordId, zone.ChainId, hostZoneUnbonding)
if !success {
errMsg := fmt.Sprintf("Error adding host zone unbonding record to epoch unbonding record: %d, host zone: %s", epochUnbondingRecordId, zone.ChainId)
k.Logger(ctx).Error(errMsg)
return sdkerrors.Wrap(types.ErrAddingHostZone, errMsg)
}
k.SetEpochUnbondingRecord(ctx, *updatedRecord)
}
return nil
}
9 changes: 7 additions & 2 deletions x/records/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (k *Keeper) ClaimCapability(ctx sdk.Context, cap *capabilitytypes.Capabilit
return k.scopedKeeper.ClaimCapability(ctx, cap, name)
}

func (k Keeper) Transfer(ctx sdk.Context, msg *ibctypes.MsgTransfer, depositRecordId uint64) error {
func (k Keeper) Transfer(ctx sdk.Context, msg *ibctypes.MsgTransfer, depositRecord types.DepositRecord) error {
goCtx := sdk.WrapSDKContext(ctx)

// because TransferKeeper.Transfer doesn't return a sequence number, we need to fetch it manually
Expand All @@ -99,7 +99,7 @@ func (k Keeper) Transfer(ctx sdk.Context, msg *ibctypes.MsgTransfer, depositReco

// add callback data
transferCallback := types.TransferCallback{
DepositRecordId: depositRecordId,
DepositRecordId: depositRecord.Id,
}
k.Logger(ctx).Info(fmt.Sprintf("Marshalling TransferCallback args: %v", transferCallback))
marshalledCallbackArgs, err := k.MarshalTransferCallbackArgs(ctx, transferCallback)
Expand All @@ -117,5 +117,10 @@ func (k Keeper) Transfer(ctx sdk.Context, msg *ibctypes.MsgTransfer, depositReco
}
k.Logger(ctx).Info(fmt.Sprintf("Storing callback data: %v", callback))
k.ICACallbacksKeeper.SetCallbackData(ctx, callback)

// update the record state to TRANSFER_IN_PROGRESS
depositRecord.Status = types.DepositRecord_TRANSFER_IN_PROGRESS
k.SetDepositRecord(ctx, depositRecord)

return nil
}
Loading

0 comments on commit 6660f60

Please sign in to comment.