Skip to content

Commit

Permalink
imp(stride): Transactions implementation and events unit tests (evmos…
Browse files Browse the repository at this point in the history
…#1932)

* chore(stride): Add stride outpost unit testing setup

* fix: add the registering in the setup

* CHANGELOG

* imp(stride): Transaction implementations and events unit tests

* fix: add custom errors

* CHANGELOG

* fix: fix linter issues

* fix: add license to errors file

* fix: slim down test boilerplate

* run make format

* Apply suggestions from code review

Co-authored-by: stepit <[email protected]>

---------

Co-authored-by: Vvaradinov <[email protected]>
Co-authored-by: stepit <[email protected]>
  • Loading branch information
3 people authored Oct 24, 2023
1 parent fa43a10 commit 6d4b9cc
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (ics20) [#1917](https://github.com/evmos/evmos/pull/1917) Make timeout height a const in the ics20 precompile.
- (stride-outpost) [#1926](https://github.com/evmos/evmos/pull/1926) Refactor event names and definitions.
- (stride-outpost) [#1931](https://github.com/evmos/evmos/pull/1931) Add Stride unit testing setup.
- (stride-outpost) [#1932](https://github.com/evmos/evmos/pull/1932) Add transaction implementation and events unit tests.

### Bug Fixes

Expand Down
12 changes: 12 additions & 0 deletions precompiles/outposts/stride/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)

package stride

const (
// ErrTokenPairNotFound is the error returned when a token pair is not found
// #nosec G101
ErrTokenPairNotFound = "token pair not found for %s"
// ErrUnsupportedToken is the error returned when a token is not supported
ErrUnsupportedToken = "unsupported token %s. The only supported token contract for Stride Outpost v1 is %s"
)
105 changes: 105 additions & 0 deletions precompiles/outposts/stride/events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package stride_test

import (
"fmt"
"math/big"

transfertypes "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types"

"github.com/ethereum/go-ethereum/common"
cmn "github.com/evmos/evmos/v15/precompiles/common"
"github.com/evmos/evmos/v15/precompiles/outposts/stride"
"github.com/evmos/evmos/v15/utils"
)

const receiver = "stride1rhe5leyt5w0mcwd9rpp93zqn99yktsxvyaqgd0"

func (s *PrecompileTestSuite) TestLiquidStakeEvent() {
denomID := s.app.Erc20Keeper.GetDenomMap(s.ctx, utils.BaseDenom)
tokenPair, ok := s.app.Erc20Keeper.GetTokenPair(s.ctx, denomID)
s.Require().True(ok, "expected token pair to be found")

//nolint:dupl
testCases := []struct {
name string
postCheck func()
}{
{
"success",
func() {
liquidStakeLog := s.stateDB.Logs()[0]
s.Require().Equal(liquidStakeLog.Address, s.precompile.Address())
// Check event signature matches the one emitted
event := s.precompile.ABI.Events[stride.EventTypeLiquidStake]
s.Require().Equal(event.ID, common.HexToHash(liquidStakeLog.Topics[0].Hex()))
s.Require().Equal(liquidStakeLog.BlockNumber, uint64(s.ctx.BlockHeight()))

var liquidStakeEvent stride.EventLiquidStake
err := cmn.UnpackLog(s.precompile.ABI, &liquidStakeEvent, stride.EventTypeLiquidStake, *liquidStakeLog)
s.Require().NoError(err)
s.Require().Equal(common.BytesToAddress(s.address.Bytes()), liquidStakeEvent.Sender)
s.Require().Equal(common.HexToAddress(tokenPair.Erc20Address), liquidStakeEvent.Token)
s.Require().Equal(big.NewInt(1e18), liquidStakeEvent.Amount)
},
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTest()

err := s.precompile.EmitLiquidStakeEvent(s.ctx, s.stateDB, s.address, common.HexToAddress(tokenPair.Erc20Address), big.NewInt(1e18))
s.Require().NoError(err)
tc.postCheck()
})
}
}

func (s *PrecompileTestSuite) TestRedeemEvent() {
bondDenom := s.app.StakingKeeper.BondDenom(s.ctx)
denomTrace := transfertypes.DenomTrace{
Path: fmt.Sprintf("%s/%s", portID, channelID),
BaseDenom: "st" + bondDenom,
}

stEvmos := denomTrace.IBCDenom()

denomID := s.app.Erc20Keeper.GetDenomMap(s.ctx, stEvmos)
tokenPair, ok := s.app.Erc20Keeper.GetTokenPair(s.ctx, denomID)
s.Require().True(ok, "expected token pair to be found")

//nolint:dupl
testCases := []struct {
name string
postCheck func()
}{
{
"success",
func() {
redeemLog := s.stateDB.Logs()[0]
s.Require().Equal(redeemLog.Address, s.precompile.Address())
// Check event signature matches the one emitted
event := s.precompile.ABI.Events[stride.EventTypeRedeem]
s.Require().Equal(event.ID, common.HexToHash(redeemLog.Topics[0].Hex()))
s.Require().Equal(redeemLog.BlockNumber, uint64(s.ctx.BlockHeight()))

var redeemEvent stride.EventRedeem
err := cmn.UnpackLog(s.precompile.ABI, &redeemEvent, stride.EventTypeRedeem, *redeemLog)
s.Require().NoError(err)
s.Require().Equal(common.BytesToAddress(s.address.Bytes()), redeemEvent.Sender)
s.Require().Equal(common.HexToAddress(tokenPair.Erc20Address), redeemEvent.Token)
s.Require().Equal(big.NewInt(1e18), redeemEvent.Amount)
},
},
}

for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTest()

err := s.precompile.EmitRedeemEvent(s.ctx, s.stateDB, s.address, common.HexToAddress(tokenPair.Erc20Address), receiver, big.NewInt(1e18))
s.Require().NoError(err)
tc.postCheck()
})
}
}
6 changes: 0 additions & 6 deletions precompiles/outposts/stride/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import (
"github.com/evmos/evmos/v15/x/evm/statedb"
evmtypes "github.com/evmos/evmos/v15/x/evm/types"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/suite"
)

Expand Down Expand Up @@ -60,10 +58,6 @@ type PrecompileTestSuite struct {
func TestPrecompileTestSuite(t *testing.T) {
s = new(PrecompileTestSuite)
suite.Run(t, s)

// Run Ginkgo integration tests
RegisterFailHandler(Fail)
RunSpecs(t, "ICS20 Precompile Suite")
}

func (s *PrecompileTestSuite) SetupTest() {
Expand Down
217 changes: 203 additions & 14 deletions precompiles/outposts/stride/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@
package stride

import (
"fmt"

"github.com/evmos/evmos/v15/utils"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/evmos/evmos/v15/precompiles/ics20"
)

const (
Expand All @@ -24,26 +29,210 @@ const (
// LiquidStake is a transaction that liquid stakes tokens using
// a ICS20 transfer with a custom memo field that will trigger Stride's Autopilot middleware
func (p Precompile) LiquidStake(
_ sdk.Context,
_ common.Address,
_ vm.StateDB,
_ *vm.Contract,
_ *abi.Method,
_ []interface{},
ctx sdk.Context,
origin common.Address,
stateDB vm.StateDB,
contract *vm.Contract,
method *abi.Method,
args []interface{},
) ([]byte, error) {
return nil, nil
sender, token, amount, receiver, err := parseLiquidStakeArgs(args)
if err != nil {
return nil, err
}

// The provided sender address should always be equal to the origin address.
// In case the contract caller address is the same as the sender address provided,
// update the sender address to be equal to the origin address.
// Otherwise, if the provided sender address is different from the origin address,
// return an error because is a forbidden operation
sender, err = ics20.CheckOriginAndSender(contract, origin, sender)
if err != nil {
return nil, err
}

bondDenom := p.stakingKeeper.BondDenom(ctx)

tokenPairID := p.erc20Keeper.GetDenomMap(ctx, bondDenom)

tokenPair, found := p.erc20Keeper.GetTokenPair(ctx, tokenPairID)
// NOTE this should always exist
if !found {
return nil, fmt.Errorf(ErrTokenPairNotFound, tokenPairID)
}

// NOTE: for v1 we only support the native EVM (and staking) denomination (WEVMOS/WTEVMOS).
if token != tokenPair.GetERC20Contract() {
return nil, fmt.Errorf(ErrUnsupportedToken, token, tokenPair.Erc20Address)
}

coin := sdk.Coin{Denom: tokenPair.Denom, Amount: sdk.NewIntFromBigInt(amount)}

// Create the memo for the ICS20 transfer packet
memo, err := CreateMemo(LiquidStakeAction, receiver)
if err != nil {
return nil, err
}

// Build the MsgTransfer with the memo and coin
msg, err := ics20.CreateAndValidateMsgTransfer(
p.portID,
p.channelID,
coin,
sdk.AccAddress(sender.Bytes()).String(),
receiver,
p.timeoutHeight,
0,
memo,
)
if err != nil {
return nil, err
}

// no need to have authorization when the contract caller is the same as origin (owner of funds)
// and the sender is the origin
accept, expiration, err := ics20.CheckAndAcceptAuthorizationIfNeeded(ctx, contract, origin, p.AuthzKeeper, msg)
if err != nil {
return nil, err
}

// Execute the ICS20 Transfer
res, err := p.transferKeeper.Transfer(sdk.WrapSDKContext(ctx), msg)
if err != nil {
return nil, err
}

// Update grant only if is needed
if err := ics20.UpdateGrantIfNeeded(ctx, contract, p.AuthzKeeper, origin, expiration, accept); err != nil {
return nil, err
}

// Emit the IBC transfer Event
if err := ics20.EmitIBCTransferEvent(
ctx,
stateDB,
p.ABI.Events[ics20.EventTypeIBCTransfer],
p.Address(),
sender,
msg.Receiver,
msg.SourcePort,
msg.SourceChannel,
coin,
memo,
); err != nil {
return nil, err
}

// Emit the custom LiquidStake Event
if err := p.EmitLiquidStakeEvent(ctx, stateDB, sender, token, amount); err != nil {
return nil, err
}

return method.Outputs.Pack(res.Sequence, true)
}

// Redeem is a transaction that redeems the native tokens using the liquid stake
// tokens. It executes a ICS20 transfer with a custom memo field that will
// trigger Stride's Autopilot middleware
func (p Precompile) Redeem(
_ sdk.Context,
_ common.Address,
_ vm.StateDB,
_ *vm.Contract,
_ *abi.Method,
_ []interface{},
ctx sdk.Context,
origin common.Address,
stateDB vm.StateDB,
contract *vm.Contract,
method *abi.Method,
args []interface{},
) ([]byte, error) {
return nil, nil
sender, token, amount, receiver, err := parseLiquidStakeArgs(args)
if err != nil {
return nil, err
}

// The provided sender address should always be equal to the origin address.
// In case the contract caller address is the same as the sender address provided,
// update the sender address to be equal to the origin address.
// Otherwise, if the provided sender address is different from the origin address,
// return an error because is a forbidden operation
sender, err = ics20.CheckOriginAndSender(contract, origin, sender)
if err != nil {
return nil, err
}

bondDenom := p.stakingKeeper.BondDenom(ctx)
stToken := "st" + bondDenom

ibcDenom := utils.ComputeIBCDenom(p.portID, p.channelID, stToken)

tokenPairID := p.erc20Keeper.GetDenomMap(ctx, ibcDenom)
tokenPair, found := p.erc20Keeper.GetTokenPair(ctx, tokenPairID)
if !found {
return nil, fmt.Errorf(ErrTokenPairNotFound, ibcDenom)
}

if token != tokenPair.GetERC20Contract() {
return nil, fmt.Errorf(ErrUnsupportedToken, token, tokenPair.Erc20Address)
}

coin := sdk.Coin{Denom: tokenPair.Denom, Amount: sdk.NewIntFromBigInt(amount)}

// Create the memo for the ICS20 transfer
memo, err := CreateMemo(RedeemAction, receiver)
if err != nil {
return nil, err
}

// Build the MsgTransfer with the memo and coin
msg, err := ics20.CreateAndValidateMsgTransfer(
p.portID,
p.channelID,
coin,
sdk.AccAddress(sender.Bytes()).String(),
receiver,
p.timeoutHeight,
0,
memo,
)
if err != nil {
return nil, err
}

// no need to have authorization when the contract caller is the same as origin (owner of funds)
// and the sender is the origin
accept, expiration, err := ics20.CheckAndAcceptAuthorizationIfNeeded(ctx, contract, origin, p.AuthzKeeper, msg)
if err != nil {
return nil, err
}

// Execute the ICS20 Transfer
res, err := p.transferKeeper.Transfer(sdk.WrapSDKContext(ctx), msg)
if err != nil {
return nil, err
}

// Update grant only if is needed
if err := ics20.UpdateGrantIfNeeded(ctx, contract, p.AuthzKeeper, origin, expiration, accept); err != nil {
return nil, err
}

// Emit the IBC transfer Event
if err := ics20.EmitIBCTransferEvent(
ctx,
stateDB,
p.ABI.Events[ics20.EventTypeIBCTransfer],
p.Address(),
sender,
msg.Receiver,
msg.SourcePort,
msg.SourceChannel,
coin,
memo,
); err != nil {
return nil, err
}

// Emit the custom Redeem Event
if err := p.EmitRedeemEvent(ctx, stateDB, sender, token, receiver, amount); err != nil {
return nil, err
}

return method.Outputs.Pack(res.Sequence, true)
}
2 changes: 0 additions & 2 deletions precompiles/outposts/stride/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ type RawPacketMetadata struct {
}

// parseLiquidStakeArgs parses the arguments from the Liquid Stake method call
//
//nolint:unused
func parseLiquidStakeArgs(args []interface{}) (common.Address, common.Address, *big.Int, string, error) {
if len(args) != 4 {
return common.Address{}, common.Address{}, nil, "", fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 4, len(args))
Expand Down
Loading

0 comments on commit 6d4b9cc

Please sign in to comment.