/
wasm_hook.go
204 lines (169 loc) · 6.8 KB
/
wasm_hook.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package ibc_hooks
import (
"encoding/json"
"fmt"
wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types"
"github.com/osmosis-labs/osmosis/v13/osmoutils"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
sdk "github.com/cosmos/cosmos-sdk/types"
transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v3/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v3/modules/core/exported"
"github.com/osmosis-labs/osmosis/v13/x/ibc-hooks/types"
)
type ContractAck struct {
ContractResult []byte `json:"contract_result"`
IbcAck []byte `json:"ibc_ack"`
}
type WasmHooks struct {
ContractKeeper *wasmkeeper.PermissionedKeeper
}
func NewWasmHooks(contractKeeper *wasmkeeper.PermissionedKeeper) WasmHooks {
return WasmHooks{ContractKeeper: contractKeeper}
}
func (h WasmHooks) OnRecvPacketOverride(im IBCMiddleware, ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement {
if h.ContractKeeper == nil {
// Not configured
return im.App.OnRecvPacket(ctx, packet, relayer)
}
isIcs20, data := isIcs20Packet(packet)
if !isIcs20 {
return im.App.OnRecvPacket(ctx, packet, relayer)
}
// Validate the memo
isWasmRouted, contractAddr, msgBytes, err := ValidateAndParseMemo(data.GetMemo(), data.Receiver)
if !isWasmRouted {
return im.App.OnRecvPacket(ctx, packet, relayer)
}
if err != nil {
return channeltypes.NewErrorAcknowledgement(err.Error())
}
if msgBytes == nil || contractAddr == nil { // This should never happen
return channeltypes.NewErrorAcknowledgement("error in wasmhook message validation")
}
// The funds sent on this packet need to be transferred to the wasm hooks module address/
// For this, we override the ICS20 packet's Receiver (essentially hijacking the funds for the module)
// and execute the underlying OnRecvPacket() call (which should eventually land on the transfer app's
// relay.go and send the sunds to the module.
//
// If that succeeds, we make the contract call
data.Receiver = WasmHookModuleAccountAddr.String()
bz, err := json.Marshal(data)
if err != nil {
return channeltypes.NewErrorAcknowledgement(fmt.Sprintf("cannot marshal the ICS20 packet: %s", err.Error()))
}
packet.Data = bz
// Execute the receive
ack := im.App.OnRecvPacket(ctx, packet, relayer)
if !ack.Success() {
return ack
}
amount, ok := sdk.NewIntFromString(data.GetAmount())
if !ok {
// This should never happen, as it should've been caught in the underlaying call to OnRecvPacket,
// but returning here for completeness
return channeltypes.NewErrorAcknowledgement("Invalid packet data: Amount is not an int")
}
// The packet's denom is the denom in the sender chain. This needs to be converted to the local denom.
denom := osmoutils.MustExtractDenomFromPacketOnRecv(packet)
funds := sdk.NewCoins(sdk.NewCoin(denom, amount))
execMsg := wasmtypes.MsgExecuteContract{
Sender: WasmHookModuleAccountAddr.String(),
Contract: contractAddr.String(),
Msg: msgBytes,
Funds: funds,
}
response, err := h.execWasmMsg(ctx, &execMsg)
if err != nil {
return channeltypes.NewErrorAcknowledgement(err.Error())
}
fullAck := ContractAck{ContractResult: response.Data, IbcAck: ack.Acknowledgement()}
bz, err = json.Marshal(fullAck)
if err != nil {
return channeltypes.NewErrorAcknowledgement(fmt.Sprintf(types.ErrBadResponse, err.Error()))
}
return channeltypes.NewResultAcknowledgement(bz)
}
func (h WasmHooks) execWasmMsg(ctx sdk.Context, execMsg *wasmtypes.MsgExecuteContract) (*wasmtypes.MsgExecuteContractResponse, error) {
if err := execMsg.ValidateBasic(); err != nil {
return nil, fmt.Errorf(types.ErrBadExecutionMsg, err.Error())
}
wasmMsgServer := wasmkeeper.NewMsgServerImpl(h.ContractKeeper)
return wasmMsgServer.ExecuteContract(sdk.WrapSDKContext(ctx), execMsg)
}
func isIcs20Packet(packet channeltypes.Packet) (isIcs20 bool, ics20data transfertypes.FungibleTokenPacketData) {
var data transfertypes.FungibleTokenPacketData
if err := json.Unmarshal(packet.GetData(), &data); err != nil {
return false, data
}
return true, data
}
func isMemoWasmRouted(memo string) (isWasmRouted bool, metadata map[string]interface{}) {
metadata = make(map[string]interface{})
// If there is no memo, the packet was either sent with an earlier version of IBC, or the memo was
// intentionally left blank. Nothing to do here. Ignore the packet and pass it down the stack.
if len(memo) == 0 {
return false, metadata
}
// the metadata must be a valid JSON object
err := json.Unmarshal([]byte(memo), &metadata)
if err != nil {
return false, metadata
}
// If the key "wasm" doesn't exist, there's nothing to do on this hook. Continue by passing the packet
// down the stack
_, ok := metadata["wasm"]
if !ok {
return false, metadata
}
return true, metadata
}
func ValidateAndParseMemo(memo string, receiver string) (isWasmRouted bool, contractAddr sdk.AccAddress, msgBytes []byte, err error) {
isWasmRouted, metadata := isMemoWasmRouted(memo)
if !isWasmRouted {
return isWasmRouted, sdk.AccAddress{}, nil, nil
}
wasmRaw := metadata["wasm"]
// Make sure the wasm key is a map. If it isn't, ignore this packet
wasm, ok := wasmRaw.(map[string]interface{})
if !ok {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, "wasm metadata is not a valid JSON map object")
}
// Get the contract
contract, ok := wasm["contract"].(string)
if !ok {
// The tokens will be returned
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["contract"]`)
}
contractAddr, err = sdk.AccAddressFromBech32(contract)
if err != nil {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] is not a valid bech32 address`)
}
// The contract and the receiver should be the same for the packet to be valid
if contract != receiver {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["contract"] should be the same as the receiver of the packet`)
}
// Ensure the message key is provided
if wasm["msg"] == nil {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `Could not find key wasm["msg"]`)
}
// Make sure the msg key is a map. If it isn't, return an error
_, ok = wasm["msg"].(map[string]interface{})
if !ok {
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, `wasm["msg"] is not a map object`)
}
// Get the message string by serializing the map
msgBytes, err = json.Marshal(wasm["msg"])
if err != nil {
// The tokens will be returned
return isWasmRouted, sdk.AccAddress{}, nil,
fmt.Errorf(types.ErrBadMetadataFormatMsg, memo, err.Error())
}
return isWasmRouted, contractAddr, msgBytes, nil
}