diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0ca86b5..5626c11 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,6 +44,7 @@ jobs: # List your tests here - TestWithIbcEurekaTestSuite/TestDeploy - TestWithIbcEurekaTestSuite/TestICS20Transfer + - TestWithIbcEurekaTestSuite/TestICS20TransferNativeSdkCoin - TestWithIbcEurekaTestSuite/TestICS20Timeout name: ${{ matrix.test }} runs-on: ubuntu-latest @@ -69,4 +70,4 @@ jobs: SP1_PRIVATE_KEY: ${{ secrets.SP1_PRIVATE_KEY }} run: | cd e2e/interchaintestv8 - go test -v -mod=readonly . -run=${{ matrix.test }} -timeout 40m + go test -v -mod=readonly . -run '^${{ matrix.test }}$' -timeout 40m diff --git a/abi/ICS20Transfer.json b/abi/ICS20Transfer.json index 994c5df..3719f0f 100644 --- a/abi/ICS20Transfer.json +++ b/abi/ICS20Transfer.json @@ -386,28 +386,38 @@ "name": "packetData", "type": "tuple", "indexed": false, - "internalType": "struct ICS20Lib.UnwrappedFungibleTokenPacketData", + "internalType": "struct ICS20Lib.UnwrappedPacketData", "components": [ { - "name": "erc20ContractAddress", - "type": "address", - "internalType": "address" + "name": "denom", + "type": "string", + "internalType": "string" }, { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + "name": "originatorChainIsSource", + "type": "bool", + "internalType": "bool" }, { - "name": "sender", + "name": "erc20Contract", "type": "address", "internalType": "address" }, + { + "name": "sender", + "type": "string", + "internalType": "string" + }, { "name": "receiver", "type": "string", "internalType": "string" }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, { "name": "memo", "type": "string", @@ -432,13 +442,23 @@ "name": "packetData", "type": "tuple", "indexed": false, - "internalType": "struct ICS20Lib.PacketDataJSON", + "internalType": "struct ICS20Lib.UnwrappedPacketData", "components": [ { "name": "denom", "type": "string", "internalType": "string" }, + { + "name": "originatorChainIsSource", + "type": "bool", + "internalType": "bool" + }, + { + "name": "erc20Contract", + "type": "address", + "internalType": "address" + }, { "name": "sender", "type": "string", @@ -472,28 +492,38 @@ "name": "packetData", "type": "tuple", "indexed": false, - "internalType": "struct ICS20Lib.UnwrappedFungibleTokenPacketData", + "internalType": "struct ICS20Lib.UnwrappedPacketData", "components": [ { - "name": "erc20ContractAddress", - "type": "address", - "internalType": "address" + "name": "denom", + "type": "string", + "internalType": "string" }, { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + "name": "originatorChainIsSource", + "type": "bool", + "internalType": "bool" }, { - "name": "sender", + "name": "erc20Contract", "type": "address", "internalType": "address" }, + { + "name": "sender", + "type": "string", + "internalType": "string" + }, { "name": "receiver", "type": "string", "internalType": "string" }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, { "name": "memo", "type": "string", @@ -512,28 +542,38 @@ "name": "packetData", "type": "tuple", "indexed": false, - "internalType": "struct ICS20Lib.UnwrappedFungibleTokenPacketData", + "internalType": "struct ICS20Lib.UnwrappedPacketData", "components": [ { - "name": "erc20ContractAddress", - "type": "address", - "internalType": "address" + "name": "denom", + "type": "string", + "internalType": "string" }, { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + "name": "originatorChainIsSource", + "type": "bool", + "internalType": "bool" }, { - "name": "sender", + "name": "erc20Contract", "type": "address", "internalType": "address" }, + { + "name": "sender", + "type": "string", + "internalType": "string" + }, { "name": "receiver", "type": "string", "internalType": "string" }, + { + "name": "amount", + "type": "uint256", + "internalType": "uint256" + }, { "name": "memo", "type": "string", @@ -624,21 +664,21 @@ }, { "type": "error", - "name": "ICS20InvalidAmount", + "name": "ICS20DenomNotFound", "inputs": [ { - "name": "amount", - "type": "uint256", - "internalType": "uint256" + "name": "denom", + "type": "string", + "internalType": "string" } ] }, { "type": "error", - "name": "ICS20InvalidReceiver", + "name": "ICS20InvalidAddress", "inputs": [ { - "name": "receiver", + "name": "addr", "type": "string", "internalType": "string" } @@ -646,12 +686,12 @@ }, { "type": "error", - "name": "ICS20InvalidSender", + "name": "ICS20InvalidAmount", "inputs": [ { - "name": "sender", - "type": "string", - "internalType": "string" + "name": "amount", + "type": "uint256", + "internalType": "uint256" } ] }, diff --git a/bun.lockb b/bun.lockb index 3caf886..6f494bf 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/e2e/artifacts/genesis.json b/e2e/artifacts/genesis.json index 7173e3e..2aba33d 100644 --- a/e2e/artifacts/genesis.json +++ b/e2e/artifacts/genesis.json @@ -1,7 +1,7 @@ { "trustedClientState": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000012754500000000000000000000000000000000000000000000000000000000001baf800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000673696d642d310000000000000000000000000000000000000000000000000000", - "trustedConsensusState": "0000000000000000000000000000000000000000000000000000000066b24dfc0a376dcfbf32aa896d68958779c5d6ef4c73b5f0eb8c55e74eaeb1399a9f8250b29f00b95808b8b596d78d77bbb1f263250537c5561baf2dc879ae5226c9ccd7", - "updateClientVkey": "0x0068b9d316aced51c5923b2d50692f4a6a9bfefcd89392914b90e77545727fbe", - "membershipVkey": "0x00a4245d249b5c35c9782cc899c8e370a35d5d928187dc9e7acbab7096764b72", - "ucAndMembershipVkey": "0x00cea834e3408d45d29080a3146e4fb1fd0c06503d655bd787219caac86cf59c" + "trustedConsensusState": "0000000000000000000000000000000000000000000000000000000066b9eea063bcab1d2706362bf6a681e2d3290c2aabb5a4fb8bb1be180fde34c25bccd1af6d0f97857b6ffe08adeab2f8a4f8425638aa3d8d3eaeb59283abe81ab18417a7", + "updateClientVkey": "0x00ba8c461624efc02962f1fd9683d6576a73a3f126c5539832a725cc26071fba", + "membershipVkey": "0x00439c7d9afabe135aff8d86bae1e522beae2a66397fb27085233da1e45d0a02", + "ucAndMembershipVkey": "0x0065cb0619cfe50b6131a2cae2cad8ea153ef255fc3c6d0e83195ec4b64394cb" } \ No newline at end of file diff --git a/e2e/interchaintestv8/ibc_eureka_test.go b/e2e/interchaintestv8/ibc_eureka_test.go index 18dd550..fc8691e 100644 --- a/e2e/interchaintestv8/ibc_eureka_test.go +++ b/e2e/interchaintestv8/ibc_eureka_test.go @@ -168,19 +168,19 @@ func (s *IbcEurekaTestSuite) SetupSuite(ctx context.Context) { s.Require().Fail("invalid prover type: %s", prover) } - client, err := ethclient.Dial(eth.GetHostRPCAddress()) + ethClient, err := ethclient.Dial(eth.GetHostRPCAddress()) s.Require().NoError(err) s.contractAddresses = s.GetEthContractsFromDeployOutput(string(stdout)) - s.sp1Ics07Contract, err = sp1ics07tendermint.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics07Tendermint), client) + s.sp1Ics07Contract, err = sp1ics07tendermint.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics07Tendermint), ethClient) s.Require().NoError(err) - s.ics02Contract, err = ics02client.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics02Client), client) + s.ics02Contract, err = ics02client.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics02Client), ethClient) s.Require().NoError(err) - s.ics26Contract, err = ics26router.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics26Router), client) + s.ics26Contract, err = ics26router.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics26Router), ethClient) s.Require().NoError(err) - s.ics20Contract, err = ics20transfer.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics20Transfer), client) + s.ics20Contract, err = ics20transfer.NewContract(ethcommon.HexToAddress(s.contractAddresses.Ics20Transfer), ethClient) s.Require().NoError(err) - s.erc20Contract, err = erc20.NewContract(ethcommon.HexToAddress(s.contractAddresses.Erc20), client) + s.erc20Contract, err = erc20.NewContract(ethcommon.HexToAddress(s.contractAddresses.Erc20), ethClient) s.Require().NoError(err) })) @@ -364,9 +364,9 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { transferEvent, err := e2esuite.GetEvmEvent(receipt, s.ics20Contract.ParseICS20Transfer) s.Require().NoError(err) - s.Require().Equal(s.contractAddresses.Erc20, strings.ToLower(transferEvent.PacketData.Erc20ContractAddress.Hex())) + s.Require().Equal(s.contractAddresses.Erc20, strings.ToLower(transferEvent.PacketData.Erc20Contract.Hex())) s.Require().Equal(transferAmount, transferEvent.PacketData.Amount) - s.Require().Equal(userAddress, transferEvent.PacketData.Sender) + s.Require().Equal(strings.ToLower(userAddress.Hex()), strings.ToLower(transferEvent.PacketData.Sender)) s.Require().Equal(receiver.FormattedAddress(), transferEvent.PacketData.Receiver) s.Require().Equal("testmemo", transferEvent.PacketData.Memo) @@ -602,6 +602,307 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { })) } +func (s *IbcEurekaTestSuite) TestICS20TransferNativeSdkCoin() { + ctx := context.Background() + + s.SetupSuite(ctx) + + eth, simd := s.ChainA, s.ChainB + + ics20Address := ethcommon.HexToAddress(s.contractAddresses.Ics20Transfer) + transferAmount := big.NewInt(testvalues.TransferAmount) + userAddress := crypto.PubkeyToAddress(s.key.PublicKey) + sendMemo := "nonnativesend" + + var sendPacket channeltypes.Packet + var transferCoin sdk.Coin + s.Require().True(s.Run("Transfer from cosmos side", func() { + // We need the timeout to be a whole number of seconds to be received by eth + timeout := uint64(time.Now().Add(30*time.Minute).Unix() * 1_000_000_000) + transferCoin = sdk.NewCoin(s.ChainB.Config().Denom, sdkmath.NewIntFromBigInt(transferAmount)) + + msgTransfer := transfertypes.MsgTransfer{ + SourcePort: transfertypes.PortID, + SourceChannel: s.simdClientID, + Token: transferCoin, + Sender: s.UserB.FormattedAddress(), + Receiver: strings.ToLower(userAddress.Hex()), + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: timeout, + Memo: sendMemo, + DestPort: transfertypes.PortID, + DestChannel: s.ethClientID, + } + + txResp, err := s.BroadcastMessages(ctx, simd, s.UserB, 200_000, &msgTransfer) + s.Require().NoError(err) + + sendPacket, err = ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + + s.Require().Equal(uint64(1), sendPacket.Sequence) + s.Require().Equal(transfertypes.PortID, sendPacket.SourcePort) + s.Require().Equal(s.simdClientID, sendPacket.SourceChannel) + s.Require().Equal(transfertypes.PortID, sendPacket.DestinationPort) + s.Require().Equal(s.ethClientID, sendPacket.DestinationChannel) + s.Require().Equal(clienttypes.Height{}, sendPacket.TimeoutHeight) + s.Require().Equal(timeout, sendPacket.TimeoutTimestamp) + + var transferPacketData transfertypes.FungibleTokenPacketData + err = json.Unmarshal(sendPacket.Data, &transferPacketData) + s.Require().NoError(err) + s.Require().Equal(transferCoin.Denom, transferPacketData.Denom) + s.Require().Equal(transferAmount.String(), transferPacketData.Amount) + s.Require().Equal(s.UserB.FormattedAddress(), transferPacketData.Sender) + s.Require().Equal(strings.ToLower(userAddress.Hex()), transferPacketData.Receiver) + s.Require().Equal(sendMemo, transferPacketData.Memo) + + s.Require().True(s.Run("Verify balances", func() { + // Check the balance of UserB + resp, err := e2esuite.GRPCQuery[banktypes.QueryBalanceResponse](ctx, simd, &banktypes.QueryBalanceRequest{ + Address: s.UserB.FormattedAddress(), + Denom: transferCoin.Denom, + }) + s.Require().NoError(err) + s.Require().NotNil(resp.Balance) + s.Require().Equal(testvalues.StartingTokenAmount-testvalues.TransferAmount, resp.Balance.Amount.Int64()) + })) + })) + + var ethReceiveAckEvent *ics26router.ContractWriteAcknowledgement + var ethReceiveTransferPacket ics20transfer.ICS20LibUnwrappedPacketData + var ibcDenom string + var ibcERC20Contract *erc20.Contract + s.Require().True(s.Run("Receive packet on Ethereum side", func() { + clientState, err := s.sp1Ics07Contract.GetClientState(nil) + s.Require().NoError(err) + + trustedHeight := clientState.LatestHeight.RevisionHeight + latestHeight, err := simd.Height(ctx) + s.Require().NoError(err) + + packetCommitmentPath := ibchost.PacketCommitmentPath(sendPacket.SourcePort, sendPacket.SourceChannel, sendPacket.Sequence) + proofHeight, ucAndMemProof, err := operator.UpdateClientAndMembershipProof( + uint64(trustedHeight), uint64(latestHeight), packetCommitmentPath, + "--trust-level", testvalues.DefaultTrustLevel.String(), + "--trusting-period", strconv.Itoa(testvalues.DefaultTrustPeriod), + ) + s.Require().NoError(err) + + msg := ics26router.IICS26RouterMsgsMsgRecvPacket{ + Packet: ics26router.IICS26RouterMsgsPacket{ + Sequence: uint32(sendPacket.Sequence), + TimeoutTimestamp: sendPacket.TimeoutTimestamp / 1_000_000_000, + SourcePort: sendPacket.SourcePort, + SourceChannel: sendPacket.SourceChannel, + DestPort: sendPacket.DestinationPort, + DestChannel: sendPacket.DestinationChannel, + Version: transfertypes.Version, + Data: sendPacket.Data, + }, + ProofCommitment: ucAndMemProof, + ProofHeight: *proofHeight, + } + + tx, err := s.ics26Contract.RecvPacket(s.GetTransactOpts(s.key), msg) + s.Require().NoError(err) + + receipt := s.GetTxReciept(ctx, eth, tx.Hash()) + s.Require().Equal(ethtypes.ReceiptStatusSuccessful, receipt.Status) + + ethReceiveAckEvent, err = e2esuite.GetEvmEvent(receipt, s.ics26Contract.ParseWriteAcknowledgement) + s.Require().NoError(err) + + ethReceiveTransferEvent, err := e2esuite.GetEvmEvent(receipt, s.ics20Contract.ParseICS20ReceiveTransfer) + s.Require().NoError(err) + + ethReceiveTransferPacket = ethReceiveTransferEvent.PacketData + ibcDenom = ethReceiveTransferPacket.Denom + // TODO: Change to IBCDenom ? + s.Require().Equal(fmt.Sprintf("%s/%s/%s", sendPacket.DestinationPort, sendPacket.DestinationChannel, transferCoin.Denom), ibcDenom) + s.Require().True(ethReceiveTransferPacket.OriginatorChainIsSource) + s.Require().Equal(transferAmount, ethReceiveTransferPacket.Amount) + s.Require().NotNil(ethReceiveTransferPacket.Erc20Contract) + s.Require().Equal(s.UserB.FormattedAddress(), ethReceiveTransferPacket.Sender) + s.Require().Equal(strings.ToLower(userAddress.Hex()), strings.ToLower(ethReceiveTransferPacket.Receiver)) + s.Require().Equal(sendMemo, ethReceiveTransferPacket.Memo) + + s.True(s.Run("Verify balances", func() { + ethClient, err := ethclient.Dial(eth.GetHostRPCAddress()) + s.Require().NoError(err) + ibcERC20Contract, err = erc20.NewContract(ethReceiveTransferPacket.Erc20Contract, ethClient) + s.Require().NoError(err) + + userBalance, err := ibcERC20Contract.BalanceOf(nil, userAddress) + s.Require().NoError(err) + s.Require().Equal(transferAmount, userBalance) + + ics20TransferBalance, err := ibcERC20Contract.BalanceOf(nil, ics20Address) + s.Require().NoError(err) + s.Require().Equal(int64(0), ics20TransferBalance.Int64()) + })) + })) + + // TODO: When using a non-mock light client on the cosmos side, the client there needs to be updated at this point + + s.Require().True(s.Run("ack back to cosmos", func() { + resp, err := e2esuite.GRPCQuery[clienttypes.QueryClientStateResponse](ctx, simd, &clienttypes.QueryClientStateRequest{ + ClientId: s.simdClientID, + }) + s.Require().NoError(err) + var clientState mock.ClientState + err = simd.Config().EncodingConfig.Codec.Unmarshal(resp.ClientState.Value, &clientState) + s.Require().NoError(err) + + txResp, err := s.BroadcastMessages(ctx, simd, s.UserB, 200_000, &channeltypes.MsgAcknowledgement{ + Packet: sendPacket, + Acknowledgement: ethReceiveAckEvent.Acknowledgement, + ProofAcked: []byte("doesn't matter"), // Because mock light client + ProofHeight: clienttypes.Height{}, + Signer: s.UserB.FormattedAddress(), + }) + s.Require().NoError(err) + s.Require().Equal(uint32(0), txResp.Code) + })) + + s.Require().True(s.Run("Approve the ICS20Transfer contract to spend the erc20 tokens", func() { + tx, err := ibcERC20Contract.Approve(s.GetTransactOpts(s.key), ics20Address, transferAmount) + s.Require().NoError(err) + receipt := s.GetTxReciept(ctx, eth, tx.Hash()) + s.Require().Equal(ethtypes.ReceiptStatusSuccessful, receipt.Status) + + allowance, err := ibcERC20Contract.Allowance(nil, userAddress, ics20Address) + s.Require().NoError(err) + s.Require().Equal(transferAmount, allowance) + })) + + var returnPacket ics26router.IICS26RouterMsgsPacket + returnMemo := "testreturnmemo" + s.Require().True(s.Run("sendTransfer on Ethereum side", func() { + timeout := uint64(time.Now().Add(30 * time.Minute).Unix()) + msgSendTransfer := ics20transfer.IICS20TransferMsgsSendTransferMsg{ + Denom: ibcDenom, + Amount: transferAmount, + Receiver: s.UserB.FormattedAddress(), + SourceChannel: s.ethClientID, + DestPort: transfertypes.PortID, + TimeoutTimestamp: timeout, + Memo: returnMemo, + } + + tx, err := s.ics20Contract.SendTransfer(s.GetTransactOpts(s.key), msgSendTransfer) + s.Require().NoError(err) + receipt := s.GetTxReciept(ctx, eth, tx.Hash()) + s.Require().Equal(ethtypes.ReceiptStatusSuccessful, receipt.Status) + + transferEvent, err := e2esuite.GetEvmEvent(receipt, s.ics20Contract.ParseICS20Transfer) + s.Require().NoError(err) + s.Require().Equal(ethReceiveTransferPacket.Erc20Contract, transferEvent.PacketData.Erc20Contract) + s.Require().Equal(transferAmount, transferEvent.PacketData.Amount) + s.Require().Equal(strings.ToLower(userAddress.Hex()), strings.ToLower(transferEvent.PacketData.Sender)) + s.Require().Equal(s.UserB.FormattedAddress(), transferEvent.PacketData.Receiver) + s.Require().Equal(returnMemo, transferEvent.PacketData.Memo) + + sendPacketEvent, err := e2esuite.GetEvmEvent(receipt, s.ics26Contract.ParseSendPacket) + s.Require().NoError(err) + returnPacket = sendPacketEvent.Packet + s.Require().Equal(uint32(1), returnPacket.Sequence) + s.Require().Equal(timeout, returnPacket.TimeoutTimestamp) + s.Require().Equal(transfertypes.PortID, returnPacket.SourcePort) + s.Require().Equal(s.ethClientID, returnPacket.SourceChannel) + s.Require().Equal(transfertypes.PortID, returnPacket.DestPort) + s.Require().Equal(s.simdClientID, returnPacket.DestChannel) + s.Require().Equal(transfertypes.Version, returnPacket.Version) + + s.True(s.Run("Verify balances", func() { + userBalance, err := ibcERC20Contract.BalanceOf(nil, userAddress) + s.Require().NoError(err) + s.Require().Equal(int64(0), userBalance.Int64()) + + // the whole balance should have been burned + ics20TransferBalance, err := ibcERC20Contract.BalanceOf(nil, ics20Address) + s.Require().NoError(err) + s.Require().Equal(int64(0), ics20TransferBalance.Int64()) + })) + })) + + // TODO: When using a non-mock light client on the cosmos side, the client there needs to be updated at this point + + var cosmosReceiveAck []byte + s.Require().True(s.Run("recvPacket on Cosmos side", func() { + resp, err := e2esuite.GRPCQuery[clienttypes.QueryClientStateResponse](ctx, simd, &clienttypes.QueryClientStateRequest{ + ClientId: s.simdClientID, + }) + s.Require().NoError(err) + var clientState mock.ClientState + err = simd.Config().EncodingConfig.Codec.Unmarshal(resp.ClientState.Value, &clientState) + s.Require().NoError(err) + + txResp, err := s.BroadcastMessages(ctx, simd, s.UserB, 200_000, &channeltypes.MsgRecvPacket{ + Packet: channeltypes.Packet{ + Sequence: uint64(returnPacket.Sequence), + SourcePort: returnPacket.SourcePort, + SourceChannel: returnPacket.SourceChannel, + DestinationPort: returnPacket.DestPort, + DestinationChannel: returnPacket.DestChannel, + Data: returnPacket.Data, + TimeoutHeight: clienttypes.Height{}, + TimeoutTimestamp: returnPacket.TimeoutTimestamp * 1_000_000_000, + }, + ProofCommitment: []byte("doesn't matter"), + ProofHeight: clienttypes.Height{}, + Signer: s.UserB.FormattedAddress(), + }) + s.Require().NoError(err) + + cosmosReceiveAck, err = ibctesting.ParseAckFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(cosmosReceiveAck) + + s.Require().True(s.Run("Verify balances", func() { + // Check the balance of UserB + resp, err := e2esuite.GRPCQuery[banktypes.QueryBalanceResponse](ctx, simd, &banktypes.QueryBalanceRequest{ + Address: s.UserB.FormattedAddress(), + Denom: transferCoin.Denom, + }) + s.Require().NoError(err) + s.Require().NotNil(resp.Balance) + s.Require().Equal(testvalues.StartingTokenAmount, resp.Balance.Amount.Int64()) + })) + })) + + s.Require().True(s.Run("acknowledgePacket on Ethereum side", func() { + clientState, err := s.sp1Ics07Contract.GetClientState(nil) + s.Require().NoError(err) + + trustedHeight := clientState.LatestHeight.RevisionHeight + latestHeight, err := simd.Height(ctx) + s.Require().NoError(err) + + // This will be a membership proof since the acknowledgement is written + packetAckPath := ibchost.PacketAcknowledgementPath(returnPacket.DestPort, returnPacket.DestChannel, uint64(returnPacket.Sequence)) + proofHeight, ucAndMemProof, err := operator.UpdateClientAndMembershipProof( + uint64(trustedHeight), uint64(latestHeight), packetAckPath, + "--trust-level", testvalues.DefaultTrustLevel.String(), + "--trusting-period", strconv.Itoa(testvalues.DefaultTrustPeriod), + ) + s.Require().NoError(err) + + msg := ics26router.IICS26RouterMsgsMsgAckPacket{ + Packet: returnPacket, + Acknowledgement: cosmosReceiveAck, + ProofAcked: ucAndMemProof, + ProofHeight: *proofHeight, + } + + tx, err := s.ics26Contract.AckPacket(s.GetTransactOpts(s.key), msg) + s.Require().NoError(err) + + receipt := s.GetTxReciept(ctx, eth, tx.Hash()) + s.Require().Equal(ethtypes.ReceiptStatusSuccessful, receipt.Status) + })) +} + func (s *IbcEurekaTestSuite) TestICS20Timeout() { ctx := context.Background() @@ -645,9 +946,9 @@ func (s *IbcEurekaTestSuite) TestICS20Timeout() { transferEvent, err := e2esuite.GetEvmEvent(receipt, s.ics20Contract.ParseICS20Transfer) s.Require().NoError(err) - s.Require().Equal(s.contractAddresses.Erc20, strings.ToLower(transferEvent.PacketData.Erc20ContractAddress.Hex())) + s.Require().Equal(s.contractAddresses.Erc20, strings.ToLower(transferEvent.PacketData.Erc20Contract.Hex())) s.Require().Equal(transferAmount, transferEvent.PacketData.Amount) - s.Require().Equal(userAddress, transferEvent.PacketData.Sender) + s.Require().Equal(strings.ToLower(userAddress.Hex()), strings.ToLower(transferEvent.PacketData.Sender)) s.Require().Equal(receiver.FormattedAddress(), transferEvent.PacketData.Receiver) s.Require().Equal("testmemo", transferEvent.PacketData.Memo) diff --git a/e2e/interchaintestv8/types/ics20transfer/contract.go b/e2e/interchaintestv8/types/ics20transfer/contract.go index 02617bf..a02c19e 100644 --- a/e2e/interchaintestv8/types/ics20transfer/contract.go +++ b/e2e/interchaintestv8/types/ics20transfer/contract.go @@ -29,22 +29,15 @@ var ( _ = abi.ConvertType ) -// ICS20LibPacketDataJSON is an auto generated low-level Go binding around an user-defined struct. -type ICS20LibPacketDataJSON struct { - Denom string - Sender string - Receiver string - Amount *big.Int - Memo string -} - -// ICS20LibUnwrappedFungibleTokenPacketData is an auto generated low-level Go binding around an user-defined struct. -type ICS20LibUnwrappedFungibleTokenPacketData struct { - Erc20ContractAddress common.Address - Amount *big.Int - Sender common.Address - Receiver string - Memo string +// ICS20LibUnwrappedPacketData is an auto generated low-level Go binding around an user-defined struct. +type ICS20LibUnwrappedPacketData struct { + Denom string + OriginatorChainIsSource bool + Erc20Contract common.Address + Sender string + Receiver string + Amount *big.Int + Memo string } // IIBCAppCallbacksOnAcknowledgementPacketCallback is an auto generated low-level Go binding around an user-defined struct. @@ -97,7 +90,7 @@ type IICS26RouterMsgsPacket struct { // ContractMetaData contains all meta data concerning the Contract contract. var ContractMetaData = &bind.MetaData{ - ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"owner_\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onAcknowledgementPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnAcknowledgementPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"acknowledgement\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"relayer\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onRecvPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnRecvPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"relayer\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onSendPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnSendPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onTimeoutPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnTimeoutPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"relayer\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"sendTransfer\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIICS20TransferMsgs.SendTransferMsg\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"outputs\":[{\"name\":\"\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"ICS20Acknowledgement\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedFungibleTokenPacketData\",\"components\":[{\"name\":\"erc20ContractAddress\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"name\":\"acknowledgement\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ICS20ReceiveTransfer\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.PacketDataJSON\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sender\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ICS20Timeout\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedFungibleTokenPacketData\",\"components\":[{\"name\":\"erc20ContractAddress\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ICS20Transfer\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedFungibleTokenPacketData\",\"components\":[{\"name\":\"erc20ContractAddress\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"AddressEmptyCode\",\"inputs\":[{\"name\":\"target\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"AddressInsufficientBalance\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"FailedInnerCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ICS20BytesSliceOutOfBounds\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"start\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"end\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20BytesSliceOverflow\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidAmount\",\"inputs\":[{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidReceiver\",\"inputs\":[{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidSender\",\"inputs\":[{\"name\":\"sender\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidTokenContract\",\"inputs\":[{\"name\":\"tokenContract\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONClosingBraceNotFound\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONInvalidEscape\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONStringClosingDoubleQuoteNotFound\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONStringUnclosed\",\"inputs\":[{\"name\":\"bz\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONUnexpectedBytes\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"expected\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"actual\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"ICS20MsgSenderIsNotPacketSender\",\"inputs\":[{\"name\":\"msgSender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"packetSender\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ICS20UnexpectedERC20Balance\",\"inputs\":[{\"name\":\"expected\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20UnexpectedVersion\",\"inputs\":[{\"name\":\"expected\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20UnsupportedFeature\",\"inputs\":[{\"name\":\"feature\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"StringsInsufficientHexLength\",\"inputs\":[{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}]", + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"owner_\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onAcknowledgementPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnAcknowledgementPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"acknowledgement\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"relayer\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onRecvPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnRecvPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"relayer\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[{\"name\":\"\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onSendPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnSendPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"sender\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"onTimeoutPacket\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIIBCAppCallbacks.OnTimeoutPacketCallback\",\"components\":[{\"name\":\"packet\",\"type\":\"tuple\",\"internalType\":\"structIICS26RouterMsgs.Packet\",\"components\":[{\"name\":\"sequence\",\"type\":\"uint32\",\"internalType\":\"uint32\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"sourcePort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"data\",\"type\":\"bytes\",\"internalType\":\"bytes\"}]},{\"name\":\"relayer\",\"type\":\"address\",\"internalType\":\"address\"}]}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"owner\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"renounceOwnership\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"sendTransfer\",\"inputs\":[{\"name\":\"msg_\",\"type\":\"tuple\",\"internalType\":\"structIICS20TransferMsgs.SendTransferMsg\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"sourceChannel\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"destPort\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"timeoutTimestamp\",\"type\":\"uint64\",\"internalType\":\"uint64\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"outputs\":[{\"name\":\"\",\"type\":\"uint32\",\"internalType\":\"uint32\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"transferOwnership\",\"inputs\":[{\"name\":\"newOwner\",\"type\":\"address\",\"internalType\":\"address\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\"},{\"type\":\"event\",\"name\":\"ICS20Acknowledgement\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedPacketData\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"originatorChainIsSource\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"erc20Contract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"name\":\"acknowledgement\",\"type\":\"bytes\",\"indexed\":false,\"internalType\":\"bytes\"}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ICS20ReceiveTransfer\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedPacketData\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"originatorChainIsSource\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"erc20Contract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ICS20Timeout\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedPacketData\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"originatorChainIsSource\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"erc20Contract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"ICS20Transfer\",\"inputs\":[{\"name\":\"packetData\",\"type\":\"tuple\",\"indexed\":false,\"internalType\":\"structICS20Lib.UnwrappedPacketData\",\"components\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"originatorChainIsSource\",\"type\":\"bool\",\"internalType\":\"bool\"},{\"name\":\"erc20Contract\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"sender\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"receiver\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"memo\",\"type\":\"string\",\"internalType\":\"string\"}]}],\"anonymous\":false},{\"type\":\"event\",\"name\":\"OwnershipTransferred\",\"inputs\":[{\"name\":\"previousOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"},{\"name\":\"newOwner\",\"type\":\"address\",\"indexed\":true,\"internalType\":\"address\"}],\"anonymous\":false},{\"type\":\"error\",\"name\":\"AddressEmptyCode\",\"inputs\":[{\"name\":\"target\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"AddressInsufficientBalance\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"FailedInnerCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"ICS20BytesSliceOutOfBounds\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"start\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"end\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20BytesSliceOverflow\",\"inputs\":[{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20DenomNotFound\",\"inputs\":[{\"name\":\"denom\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidAddress\",\"inputs\":[{\"name\":\"addr\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidAmount\",\"inputs\":[{\"name\":\"amount\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20InvalidTokenContract\",\"inputs\":[{\"name\":\"tokenContract\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONClosingBraceNotFound\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONInvalidEscape\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONStringClosingDoubleQuoteNotFound\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"bytes1\",\"internalType\":\"bytes1\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONStringUnclosed\",\"inputs\":[{\"name\":\"bz\",\"type\":\"bytes\",\"internalType\":\"bytes\"},{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20JSONUnexpectedBytes\",\"inputs\":[{\"name\":\"position\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"expected\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"},{\"name\":\"actual\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}]},{\"type\":\"error\",\"name\":\"ICS20MsgSenderIsNotPacketSender\",\"inputs\":[{\"name\":\"msgSender\",\"type\":\"address\",\"internalType\":\"address\"},{\"name\":\"packetSender\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ICS20UnexpectedERC20Balance\",\"inputs\":[{\"name\":\"expected\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"actual\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]},{\"type\":\"error\",\"name\":\"ICS20UnexpectedVersion\",\"inputs\":[{\"name\":\"expected\",\"type\":\"string\",\"internalType\":\"string\"},{\"name\":\"version\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"ICS20UnsupportedFeature\",\"inputs\":[{\"name\":\"feature\",\"type\":\"string\",\"internalType\":\"string\"}]},{\"type\":\"error\",\"name\":\"OwnableInvalidOwner\",\"inputs\":[{\"name\":\"owner\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"OwnableUnauthorizedAccount\",\"inputs\":[{\"name\":\"account\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"ReentrancyGuardReentrantCall\",\"inputs\":[]},{\"type\":\"error\",\"name\":\"SafeERC20FailedOperation\",\"inputs\":[{\"name\":\"token\",\"type\":\"address\",\"internalType\":\"address\"}]},{\"type\":\"error\",\"name\":\"StringsInsufficientHexLength\",\"inputs\":[{\"name\":\"value\",\"type\":\"uint256\",\"internalType\":\"uint256\"},{\"name\":\"length\",\"type\":\"uint256\",\"internalType\":\"uint256\"}]}]", } // ContractABI is the input ABI used to generate the binding from. @@ -493,14 +486,14 @@ func (it *ContractICS20AcknowledgementIterator) Close() error { // ContractICS20Acknowledgement represents a ICS20Acknowledgement event raised by the Contract contract. type ContractICS20Acknowledgement struct { - PacketData ICS20LibUnwrappedFungibleTokenPacketData + PacketData ICS20LibUnwrappedPacketData Acknowledgement []byte Raw types.Log // Blockchain specific contextual infos } -// FilterICS20Acknowledgement is a free log retrieval operation binding the contract event 0x5d85ed7b743412cf36e53bae82d41ed211ac40dd06a30e7c4d2107efd4cd4635. +// FilterICS20Acknowledgement is a free log retrieval operation binding the contract event 0x0620319fadbad01462e6ef6729106763dc221a1cf68b712654737ba839e30ba1. // -// Solidity: event ICS20Acknowledgement((address,uint256,address,string,string) packetData, bytes acknowledgement) +// Solidity: event ICS20Acknowledgement((string,bool,address,string,string,uint256,string) packetData, bytes acknowledgement) func (_Contract *ContractFilterer) FilterICS20Acknowledgement(opts *bind.FilterOpts) (*ContractICS20AcknowledgementIterator, error) { logs, sub, err := _Contract.contract.FilterLogs(opts, "ICS20Acknowledgement") @@ -510,9 +503,9 @@ func (_Contract *ContractFilterer) FilterICS20Acknowledgement(opts *bind.FilterO return &ContractICS20AcknowledgementIterator{contract: _Contract.contract, event: "ICS20Acknowledgement", logs: logs, sub: sub}, nil } -// WatchICS20Acknowledgement is a free log subscription operation binding the contract event 0x5d85ed7b743412cf36e53bae82d41ed211ac40dd06a30e7c4d2107efd4cd4635. +// WatchICS20Acknowledgement is a free log subscription operation binding the contract event 0x0620319fadbad01462e6ef6729106763dc221a1cf68b712654737ba839e30ba1. // -// Solidity: event ICS20Acknowledgement((address,uint256,address,string,string) packetData, bytes acknowledgement) +// Solidity: event ICS20Acknowledgement((string,bool,address,string,string,uint256,string) packetData, bytes acknowledgement) func (_Contract *ContractFilterer) WatchICS20Acknowledgement(opts *bind.WatchOpts, sink chan<- *ContractICS20Acknowledgement) (event.Subscription, error) { logs, sub, err := _Contract.contract.WatchLogs(opts, "ICS20Acknowledgement") @@ -547,9 +540,9 @@ func (_Contract *ContractFilterer) WatchICS20Acknowledgement(opts *bind.WatchOpt }), nil } -// ParseICS20Acknowledgement is a log parse operation binding the contract event 0x5d85ed7b743412cf36e53bae82d41ed211ac40dd06a30e7c4d2107efd4cd4635. +// ParseICS20Acknowledgement is a log parse operation binding the contract event 0x0620319fadbad01462e6ef6729106763dc221a1cf68b712654737ba839e30ba1. // -// Solidity: event ICS20Acknowledgement((address,uint256,address,string,string) packetData, bytes acknowledgement) +// Solidity: event ICS20Acknowledgement((string,bool,address,string,string,uint256,string) packetData, bytes acknowledgement) func (_Contract *ContractFilterer) ParseICS20Acknowledgement(log types.Log) (*ContractICS20Acknowledgement, error) { event := new(ContractICS20Acknowledgement) if err := _Contract.contract.UnpackLog(event, "ICS20Acknowledgement", log); err != nil { @@ -628,13 +621,13 @@ func (it *ContractICS20ReceiveTransferIterator) Close() error { // ContractICS20ReceiveTransfer represents a ICS20ReceiveTransfer event raised by the Contract contract. type ContractICS20ReceiveTransfer struct { - PacketData ICS20LibPacketDataJSON + PacketData ICS20LibUnwrappedPacketData Raw types.Log // Blockchain specific contextual infos } -// FilterICS20ReceiveTransfer is a free log retrieval operation binding the contract event 0x9169ca6242a2d81c5bd346fe3e437825a5fcfb4b4845df61d0022c14bac4c393. +// FilterICS20ReceiveTransfer is a free log retrieval operation binding the contract event 0xa35d38be03b8b49e701113a4fcb6903af6c43c6e75087486a7ac24f512048bb4. // -// Solidity: event ICS20ReceiveTransfer((string,string,string,uint256,string) packetData) +// Solidity: event ICS20ReceiveTransfer((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) FilterICS20ReceiveTransfer(opts *bind.FilterOpts) (*ContractICS20ReceiveTransferIterator, error) { logs, sub, err := _Contract.contract.FilterLogs(opts, "ICS20ReceiveTransfer") @@ -644,9 +637,9 @@ func (_Contract *ContractFilterer) FilterICS20ReceiveTransfer(opts *bind.FilterO return &ContractICS20ReceiveTransferIterator{contract: _Contract.contract, event: "ICS20ReceiveTransfer", logs: logs, sub: sub}, nil } -// WatchICS20ReceiveTransfer is a free log subscription operation binding the contract event 0x9169ca6242a2d81c5bd346fe3e437825a5fcfb4b4845df61d0022c14bac4c393. +// WatchICS20ReceiveTransfer is a free log subscription operation binding the contract event 0xa35d38be03b8b49e701113a4fcb6903af6c43c6e75087486a7ac24f512048bb4. // -// Solidity: event ICS20ReceiveTransfer((string,string,string,uint256,string) packetData) +// Solidity: event ICS20ReceiveTransfer((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) WatchICS20ReceiveTransfer(opts *bind.WatchOpts, sink chan<- *ContractICS20ReceiveTransfer) (event.Subscription, error) { logs, sub, err := _Contract.contract.WatchLogs(opts, "ICS20ReceiveTransfer") @@ -681,9 +674,9 @@ func (_Contract *ContractFilterer) WatchICS20ReceiveTransfer(opts *bind.WatchOpt }), nil } -// ParseICS20ReceiveTransfer is a log parse operation binding the contract event 0x9169ca6242a2d81c5bd346fe3e437825a5fcfb4b4845df61d0022c14bac4c393. +// ParseICS20ReceiveTransfer is a log parse operation binding the contract event 0xa35d38be03b8b49e701113a4fcb6903af6c43c6e75087486a7ac24f512048bb4. // -// Solidity: event ICS20ReceiveTransfer((string,string,string,uint256,string) packetData) +// Solidity: event ICS20ReceiveTransfer((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) ParseICS20ReceiveTransfer(log types.Log) (*ContractICS20ReceiveTransfer, error) { event := new(ContractICS20ReceiveTransfer) if err := _Contract.contract.UnpackLog(event, "ICS20ReceiveTransfer", log); err != nil { @@ -762,13 +755,13 @@ func (it *ContractICS20TimeoutIterator) Close() error { // ContractICS20Timeout represents a ICS20Timeout event raised by the Contract contract. type ContractICS20Timeout struct { - PacketData ICS20LibUnwrappedFungibleTokenPacketData + PacketData ICS20LibUnwrappedPacketData Raw types.Log // Blockchain specific contextual infos } -// FilterICS20Timeout is a free log retrieval operation binding the contract event 0xe593536479534bcca0405e240e028fe7709ece5e1cfcb82bcee63ec6065c911a. +// FilterICS20Timeout is a free log retrieval operation binding the contract event 0xf9c9474c4626bf96890c3640c0f2f29eeb4d445bcca4d3d7ea465b108959c04c. // -// Solidity: event ICS20Timeout((address,uint256,address,string,string) packetData) +// Solidity: event ICS20Timeout((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) FilterICS20Timeout(opts *bind.FilterOpts) (*ContractICS20TimeoutIterator, error) { logs, sub, err := _Contract.contract.FilterLogs(opts, "ICS20Timeout") @@ -778,9 +771,9 @@ func (_Contract *ContractFilterer) FilterICS20Timeout(opts *bind.FilterOpts) (*C return &ContractICS20TimeoutIterator{contract: _Contract.contract, event: "ICS20Timeout", logs: logs, sub: sub}, nil } -// WatchICS20Timeout is a free log subscription operation binding the contract event 0xe593536479534bcca0405e240e028fe7709ece5e1cfcb82bcee63ec6065c911a. +// WatchICS20Timeout is a free log subscription operation binding the contract event 0xf9c9474c4626bf96890c3640c0f2f29eeb4d445bcca4d3d7ea465b108959c04c. // -// Solidity: event ICS20Timeout((address,uint256,address,string,string) packetData) +// Solidity: event ICS20Timeout((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) WatchICS20Timeout(opts *bind.WatchOpts, sink chan<- *ContractICS20Timeout) (event.Subscription, error) { logs, sub, err := _Contract.contract.WatchLogs(opts, "ICS20Timeout") @@ -815,9 +808,9 @@ func (_Contract *ContractFilterer) WatchICS20Timeout(opts *bind.WatchOpts, sink }), nil } -// ParseICS20Timeout is a log parse operation binding the contract event 0xe593536479534bcca0405e240e028fe7709ece5e1cfcb82bcee63ec6065c911a. +// ParseICS20Timeout is a log parse operation binding the contract event 0xf9c9474c4626bf96890c3640c0f2f29eeb4d445bcca4d3d7ea465b108959c04c. // -// Solidity: event ICS20Timeout((address,uint256,address,string,string) packetData) +// Solidity: event ICS20Timeout((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) ParseICS20Timeout(log types.Log) (*ContractICS20Timeout, error) { event := new(ContractICS20Timeout) if err := _Contract.contract.UnpackLog(event, "ICS20Timeout", log); err != nil { @@ -896,13 +889,13 @@ func (it *ContractICS20TransferIterator) Close() error { // ContractICS20Transfer represents a ICS20Transfer event raised by the Contract contract. type ContractICS20Transfer struct { - PacketData ICS20LibUnwrappedFungibleTokenPacketData + PacketData ICS20LibUnwrappedPacketData Raw types.Log // Blockchain specific contextual infos } -// FilterICS20Transfer is a free log retrieval operation binding the contract event 0xc8b9eb1385efc079eca447ddeae64f092898d23ffbbddc3915af3e5febb049ee. +// FilterICS20Transfer is a free log retrieval operation binding the contract event 0x2cf3150880b14aa74189911fb7970e158143356e3c09f4658dea6984af065430. // -// Solidity: event ICS20Transfer((address,uint256,address,string,string) packetData) +// Solidity: event ICS20Transfer((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) FilterICS20Transfer(opts *bind.FilterOpts) (*ContractICS20TransferIterator, error) { logs, sub, err := _Contract.contract.FilterLogs(opts, "ICS20Transfer") @@ -912,9 +905,9 @@ func (_Contract *ContractFilterer) FilterICS20Transfer(opts *bind.FilterOpts) (* return &ContractICS20TransferIterator{contract: _Contract.contract, event: "ICS20Transfer", logs: logs, sub: sub}, nil } -// WatchICS20Transfer is a free log subscription operation binding the contract event 0xc8b9eb1385efc079eca447ddeae64f092898d23ffbbddc3915af3e5febb049ee. +// WatchICS20Transfer is a free log subscription operation binding the contract event 0x2cf3150880b14aa74189911fb7970e158143356e3c09f4658dea6984af065430. // -// Solidity: event ICS20Transfer((address,uint256,address,string,string) packetData) +// Solidity: event ICS20Transfer((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) WatchICS20Transfer(opts *bind.WatchOpts, sink chan<- *ContractICS20Transfer) (event.Subscription, error) { logs, sub, err := _Contract.contract.WatchLogs(opts, "ICS20Transfer") @@ -949,9 +942,9 @@ func (_Contract *ContractFilterer) WatchICS20Transfer(opts *bind.WatchOpts, sink }), nil } -// ParseICS20Transfer is a log parse operation binding the contract event 0xc8b9eb1385efc079eca447ddeae64f092898d23ffbbddc3915af3e5febb049ee. +// ParseICS20Transfer is a log parse operation binding the contract event 0x2cf3150880b14aa74189911fb7970e158143356e3c09f4658dea6984af065430. // -// Solidity: event ICS20Transfer((address,uint256,address,string,string) packetData) +// Solidity: event ICS20Transfer((string,bool,address,string,string,uint256,string) packetData) func (_Contract *ContractFilterer) ParseICS20Transfer(log types.Log) (*ContractICS20Transfer, error) { event := new(ContractICS20Transfer) if err := _Contract.contract.UnpackLog(event, "ICS20Transfer", log); err != nil { diff --git a/justfile b/justfile index 0120a81..003d057 100644 --- a/justfile +++ b/justfile @@ -55,4 +55,4 @@ generate-abi: test-e2e testname: just clean @echo "Running {{testname}} test..." - cd e2e/interchaintestv8 && go test -v -run=TestWithIbcEurekaTestSuite/{{testname}} -timeout 40m + cd e2e/interchaintestv8 && go test -v -run '^TestWithIbcEurekaTestSuite/{{testname}}$' -timeout 40m diff --git a/src/ICS20Transfer.sol b/src/ICS20Transfer.sol index d096c50..d88386c 100644 --- a/src/ICS20Transfer.sol +++ b/src/ICS20Transfer.sol @@ -12,22 +12,28 @@ import { IICS20Transfer } from "./interfaces/IICS20Transfer.sol"; import { IICS26Router } from "./interfaces/IICS26Router.sol"; import { IICS26RouterMsgs } from "./msgs/IICS26RouterMsgs.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { IBCERC20 } from "./utils/IBCERC20.sol"; using SafeERC20 for IERC20; /* * Things not handled yet: - * - Prefixed denoms (source chain is not the source) and the burning of tokens related to that * - Separate escrow balance tracking * - Related to escrow ^: invariant checking (where to implement that?) - * - Receiving packets */ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, ReentrancyGuard { + /// @notice Mapping of non-native denoms to their respective IBCERC20 contracts created here + mapping(string denom => IBCERC20 ibcERC20Contract) private _foreignDenomContracts; + /// @param owner_ The owner of the contract constructor(address owner_) Ownable(owner_) { } /// @inheritdoc IICS20Transfer function sendTransfer(SendTransferMsg calldata msg_) external override returns (uint32) { + if (msg_.amount == 0) { + revert ICS20InvalidAmount(msg_.amount); + } + IICS26Router ibcRouter = IICS26Router(owner()); string memory sender = Strings.toHexString(msg.sender); @@ -57,38 +63,46 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr revert ICS20UnexpectedVersion(ICS20Lib.ICS20_VERSION, msg_.packet.version); } - ICS20Lib.UnwrappedFungibleTokenPacketData memory packetData = ICS20Lib.unwrapPacketData(msg_.packet.data); - - // TODO: Maybe have a "ValidateBasic" type of function that checks the packet data, could be done in unwrapping? + ICS20Lib.UnwrappedPacketData memory packetData = _unwrapSendPacketData(msg_.packet); if (packetData.amount == 0) { revert ICS20InvalidAmount(packetData.amount); } - // TODO: Handle prefixed denoms (source chain is not the source) and burn + address sender = ICS20Lib.mustHexStringToAddress(packetData.sender); // The packet sender has to be either the packet data sender or the contract itself // The scenarios are either the sender sent the packet directly to the router (msg_.sender == packetData.sender) // or sender used the sendTransfer function, which makes this contract the sender (msg_.sender == address(this)) - if (msg_.sender != packetData.sender && msg_.sender != address(this)) { - revert ICS20MsgSenderIsNotPacketSender(msg_.sender, packetData.sender); + if (msg_.sender != sender && msg_.sender != address(this)) { + revert ICS20MsgSenderIsNotPacketSender(msg_.sender, sender); } - _transferFrom(packetData.sender, address(this), packetData.erc20ContractAddress, packetData.amount); + // transfer the tokens to us (requires the allowance to be set) + _transferFrom(sender, address(this), packetData.erc20Contract, packetData.amount); + + if (!packetData.originatorChainIsSource) { + // receiver chain is source: burn the vouchers + // TODO: Implement escrow balance tracking (#6) + IBCERC20 ibcERC20Contract = IBCERC20(packetData.erc20Contract); + ibcERC20Contract.burn(packetData.amount); + } emit ICS20Transfer(packetData); } /// @inheritdoc IIBCApp function onRecvPacket(OnRecvPacketCallback calldata msg_) external onlyOwner nonReentrant returns (bytes memory) { - // TODO Emit error event + // Since this function mostly returns acks, also when it fails, the ics26router (the caller) will log the ack if (keccak256(abi.encodePacked(msg_.packet.version)) != keccak256(abi.encodePacked(ICS20Lib.ICS20_VERSION))) { + // TODO: Figure out if should actually error out, or if just error acking is enough return ICS20Lib.errorAck(abi.encodePacked("unexpected version: ", msg_.packet.version)); } - ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(msg_.packet.data); + ICS20Lib.UnwrappedPacketData memory packetData = _unwrapReceivePacketData(msg_.packet); + if (packetData.amount == 0) { - return ICS20Lib.errorAck(abi.encodePacked("invalid amount: 0")); + return ICS20Lib.errorAck("invalid amount: 0"); } (address receiver, bool receiverConvertSuccess) = ICS20Lib.hexStringToAddress(packetData.receiver); @@ -96,31 +110,16 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr return ICS20Lib.errorAck(abi.encodePacked("invalid receiver: ", packetData.receiver)); } - // TODO: Handle non-contract denoms (destination chain is not source) - bytes memory denomPrefix = ICS20Lib.getDenomPrefix(msg_.packet.sourcePort, msg_.packet.sourceChannel); - bytes memory denom = bytes(packetData.denom); - if ( - denom.length >= denomPrefix.length - && ICS20Lib.equal(ICS20Lib.slice(denom, 0, denomPrefix.length), denomPrefix) - ) { - // sender chain is not the source, unescrow tokens - // TODO: Implement escrow balance tracking (#6) - - string memory unprefixedDenom = - string(ICS20Lib.slice(denom, denomPrefix.length, denom.length - denomPrefix.length)); - (address tokenContract, bool tokenContractConvertSuccess) = ICS20Lib.hexStringToAddress(unprefixedDenom); - if (!tokenContractConvertSuccess) { - return ICS20Lib.errorAck(abi.encodePacked("invalid token contract: ", unprefixedDenom)); - } - - IERC20(tokenContract).safeTransfer(receiver, packetData.amount); - } else { - // sender chain is the source, mint vouchers - // TODO: Implement escrow balance tracking (#6) - // TODO: Implement creating (new erc20 contracts), looking up and minting of vouchers - revert ICS20UnsupportedFeature("sender denom is source"); + // TODO: Implement escrow balance tracking (#6) + if (packetData.originatorChainIsSource) { + // sender is source, so we mint vouchers + // NOTE: The unwrap function already created a new contract if it didn't exist already + IBCERC20(packetData.erc20Contract).mint(packetData.amount); } + // transfer the tokens to the receiver + IERC20(packetData.erc20Contract).safeTransfer(receiver, packetData.amount); + emit ICS20ReceiveTransfer(packetData); return ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON; @@ -128,7 +127,7 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr /// @inheritdoc IIBCApp function onAcknowledgementPacket(OnAcknowledgementPacketCallback calldata msg_) external onlyOwner nonReentrant { - ICS20Lib.UnwrappedFungibleTokenPacketData memory packetData = ICS20Lib.unwrapPacketData(msg_.packet.data); + ICS20Lib.UnwrappedPacketData memory packetData = _unwrapSendPacketData(msg_.packet); if (keccak256(msg_.acknowledgement) != ICS20Lib.KECCAK256_SUCCESSFUL_ACKNOWLEDGEMENT_JSON) { _refundTokens(packetData); @@ -140,7 +139,7 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr /// @inheritdoc IIBCApp function onTimeoutPacket(OnTimeoutPacketCallback calldata msg_) external onlyOwner nonReentrant { - ICS20Lib.UnwrappedFungibleTokenPacketData memory packetData = ICS20Lib.unwrapPacketData(msg_.packet.data); + ICS20Lib.UnwrappedPacketData memory packetData = _unwrapSendPacketData(msg_.packet); _refundTokens(packetData); emit ICS20Timeout(packetData); @@ -148,9 +147,9 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr /// @notice Refund the tokens to the sender /// @param data The packet data - function _refundTokens(ICS20Lib.UnwrappedFungibleTokenPacketData memory data) private { - address refundee = data.sender; - IERC20(data.erc20ContractAddress).safeTransfer(refundee, data.amount); + function _refundTokens(ICS20Lib.UnwrappedPacketData memory data) private { + address refundee = ICS20Lib.mustHexStringToAddress(data.sender); + IERC20(data.erc20Contract).safeTransfer(refundee, data.amount); } /// @notice Transfer tokens from sender to receiver @@ -175,4 +174,126 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr revert ICS20UnexpectedERC20Balance(expectedEndingBalance, actualEndingBalance); } } + + /// @notice Unwrap the packet data for sending, including finding the correct erc20 contract to use + /// @param packet The packet to unwrap + /// @return The unwrapped packet data + function _unwrapSendPacketData( + IICS26RouterMsgs.Packet calldata packet + ) + private + view + returns (ICS20Lib.UnwrappedPacketData memory) + { + ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(packet.data); + ICS20Lib.UnwrappedPacketData memory receivePacketData = ICS20Lib.UnwrappedPacketData({ + denom: packetData.denom, + originatorChainIsSource: false, + erc20Contract: address(0), + sender: packetData.sender, + receiver: packetData.receiver, + amount: packetData.amount, + memo: packetData.memo + }); + + // if the denom is NOT prefixed by the port and channel on which we are sending the token, + // then the we are the the source of the token + // otherwise the receiving chain is the source (i.e we need to burn when sending, or mint when refunding) + bytes memory denomPrefix = ICS20Lib.getDenomPrefix(packet.sourcePort, packet.sourceChannel); + receivePacketData.originatorChainIsSource = !ICS20Lib.hasPrefix(bytes(packetData.denom), denomPrefix); + if (receivePacketData.originatorChainIsSource) { + // we are the source of this token, so we unwrap and look for the token contract address + receivePacketData.erc20Contract = findOrExtractERC20Address(packetData.denom); + } else { + // receiving chain is source of the token, so we will find the address in the mapping + receivePacketData.erc20Contract = address(_foreignDenomContracts[packetData.denom]); + if (receivePacketData.erc20Contract == address(0)) { + revert ICS20DenomNotFound(packetData.denom); + } + } + + return receivePacketData; + } + + /// @notice Unwrap the packet data for receiving, including finding or instantiating the erc20 contract to use + /// @param packet The packet to unwrap + /// @return The unwrapped packet data + function _unwrapReceivePacketData( + IICS26RouterMsgs.Packet calldata packet + ) + private + returns (ICS20Lib.UnwrappedPacketData memory) + { + ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(packet.data); + ICS20Lib.UnwrappedPacketData memory receivePacketData = ICS20Lib.UnwrappedPacketData({ + denom: "", + originatorChainIsSource: false, + erc20Contract: address(0), + sender: packetData.sender, + receiver: packetData.receiver, + amount: packetData.amount, + memo: packetData.memo + }); + + bytes memory denomBz = bytes(packetData.denom); + // NOTE: We use sourcePort and sourceChannel here, because the counterparty + // chain would have prefixed with DestPort and DestChannel when originally + // receiving this token. + bytes memory denomPrefix = ICS20Lib.getDenomPrefix(packet.sourcePort, packet.sourceChannel); + + receivePacketData.originatorChainIsSource = !ICS20Lib.hasPrefix(denomBz, denomPrefix); + + if (receivePacketData.originatorChainIsSource) { + // we are not the source of this token, so we add a denom trace and find or create a new token contract + bytes memory newDenomPrefix = ICS20Lib.getDenomPrefix(packet.destPort, packet.destChannel); + receivePacketData.denom = string(abi.encodePacked(newDenomPrefix, packetData.denom)); + + receivePacketData.erc20Contract = findOrCreateERC20Address(receivePacketData.denom); + } else { + // we are the source of this token, so we unwrap the denom and find the token contract + // either in the mapping or by converting the denom to an address + receivePacketData.denom = + string(ICS20Lib.slice(denomBz, denomPrefix.length, denomBz.length - denomPrefix.length)); + + receivePacketData.erc20Contract = findOrExtractERC20Address(receivePacketData.denom); + } + + return receivePacketData; + } + + /// @notice Finds a contract in the foreign mapping, or expects the denom to be a token contract address + /// @notice This function will never return address(0) + /// @param denom The denom to find or extract the address from + /// @return The address of the contract + function findOrExtractERC20Address(string memory denom) internal view returns (address) { + // check if denom already has a foreign registered contract + address erc20Contract = address(_foreignDenomContracts[denom]); + if (erc20Contract == address(0)) { + // this denom is not created by us, so we expect the denom to be a token contract address + bool convertSuccess; + (erc20Contract, convertSuccess) = ICS20Lib.hexStringToAddress(denom); + if (!convertSuccess) { + revert ICS20InvalidTokenContract(denom); + } + } + + return erc20Contract; + } + + /// @notice Finds a contract in the foreign mapping, or creates a new IBCERC20 contract + /// @notice This function will never return address(0) + /// @param denom The denom to find or create the contract for + /// @return The address of the erc20 contract + function findOrCreateERC20Address(string memory denom) internal returns (address) { + // check if denom already has a foreign registered contract + address erc20Contract = address(_foreignDenomContracts[denom]); + if (erc20Contract == address(0)) { + // nothing exists, so we create new erc20 contract and register it in the mapping + IBCERC20 ibcERC20 = new IBCERC20(IICS20Transfer(address(this))); + _foreignDenomContracts[denom] = ibcERC20; + erc20Contract = address(ibcERC20); + } + + return erc20Contract; + } } diff --git a/src/errors/IICS20Errors.sol b/src/errors/IICS20Errors.sol index 0b02672..a5b9f7a 100644 --- a/src/errors/IICS20Errors.sol +++ b/src/errors/IICS20Errors.sol @@ -7,13 +7,9 @@ interface IICS20Errors { /// @param packetSender Address of the packet sender error ICS20MsgSenderIsNotPacketSender(address msgSender, address packetSender); - /// @notice Invalid sender address - /// @param sender Address whose tokens are being transferred - error ICS20InvalidSender(string sender); - - /// @notice Invalid receiver address - /// @param receiver Address receiving the tokens - error ICS20InvalidReceiver(string receiver); + /// @notice Invalid address + /// @param addr Address of the sender or receiver + error ICS20InvalidAddress(string addr); /// @notice Invalid transfer amount /// @param amount Amount of tokens being transferred @@ -33,6 +29,10 @@ interface IICS20Errors { /// @param actual Actual balance of the ERC20 token for ICS20Transfer error ICS20UnexpectedERC20Balance(uint256 expected, uint256 actual); + /// @notice this error happens when the denom has no foreign ibcERC20 contract (i.e. we don't know this denom) + /// @param denom Denomination of the token being transferred, for which we have no foreign ibcERC20 contract + error ICS20DenomNotFound(string denom); + /// @notice Unsupported feature /// @param feature Unsupported feature error ICS20UnsupportedFeature(string feature); diff --git a/src/interfaces/IIBCERC20.sol b/src/interfaces/IIBCERC20.sol new file mode 100644 index 000000000..85139d1 --- /dev/null +++ b/src/interfaces/IIBCERC20.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.25; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IIBCERC20 is IERC20 { + /// @notice Mint new tokens to the ICS20Transfer contract + /// @param amount Amount of tokens to mint + function mint(uint256 amount) external; + + /// @notice Burn tokens from the ICS20Transfer contract + /// @param amount Amount of tokens to burn + function burn(uint256 amount) external; +} diff --git a/src/interfaces/IICS20Transfer.sol b/src/interfaces/IICS20Transfer.sol index 4c72906..250fad5 100644 --- a/src/interfaces/IICS20Transfer.sol +++ b/src/interfaces/IICS20Transfer.sol @@ -7,20 +7,20 @@ import { IICS20TransferMsgs } from "../msgs/IICS20TransferMsgs.sol"; interface IICS20Transfer is IICS20TransferMsgs { /// @notice Called when a packet is handled in onSendPacket and a transfer has been initiated /// @param packetData The transfer packet data - event ICS20Transfer(ICS20Lib.UnwrappedFungibleTokenPacketData packetData); + event ICS20Transfer(ICS20Lib.UnwrappedPacketData packetData); /// @notice Called when a packet is received in onReceivePacket /// @param packetData The transfer packet data - event ICS20ReceiveTransfer(ICS20Lib.PacketDataJSON packetData); + event ICS20ReceiveTransfer(ICS20Lib.UnwrappedPacketData packetData); /// @notice Called after handling acknowledgement in onAcknowledgementPacket /// @param packetData The transfer packet data /// @param acknowledgement The acknowledgement data - event ICS20Acknowledgement(ICS20Lib.UnwrappedFungibleTokenPacketData packetData, bytes acknowledgement); + event ICS20Acknowledgement(ICS20Lib.UnwrappedPacketData packetData, bytes acknowledgement); /// @notice Called after handling a timeout in onTimeoutPacket /// @param packetData The transfer packet data - event ICS20Timeout(ICS20Lib.UnwrappedFungibleTokenPacketData packetData); + event ICS20Timeout(ICS20Lib.UnwrappedPacketData packetData); /// @notice Send a transfer /// @param msg The message for sending a transfer diff --git a/src/utils/IBCERC20.sol b/src/utils/IBCERC20.sol new file mode 100644 index 000000000..0636868 --- /dev/null +++ b/src/utils/IBCERC20.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.25; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IICS20Transfer } from "../interfaces/IICS20Transfer.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IIBCERC20 } from "../interfaces/IIBCERC20.sol"; + +contract IBCERC20 is IIBCERC20, ERC20, Ownable { + // TODO: Figure out naming and symbol for IBC denoms + constructor(IICS20Transfer owner_) ERC20("IBC Token", "IBC") Ownable(address(owner_)) { } + + /// @inheritdoc IIBCERC20 + function mint(uint256 amount) external onlyOwner { + _mint(owner(), amount); + } + + /// @inheritdoc IIBCERC20 + function burn(uint256 amount) external onlyOwner { + _burn(owner(), amount); + } +} diff --git a/src/utils/ICS20Lib.sol b/src/utils/ICS20Lib.sol index 8c3544d..49ad2af 100644 --- a/src/utils/ICS20Lib.sol +++ b/src/utils/ICS20Lib.sol @@ -25,16 +25,20 @@ library ICS20Lib { } /// @notice Convenience type used after unmarshalling the packet data and converting addresses - /// @param erc20ContractAddress The address of the ERC20 contract + /// @param denom The denom of the token + /// @param originatorChainIsSource True if origniating chain is source of token + /// @param erc20Contract The address of the ERC20 contract /// @param amount The amount of tokens /// @param sender The sender of the tokens /// @param receiver The receiver of the tokens /// @param memo Optional memo - struct UnwrappedFungibleTokenPacketData { - address erc20ContractAddress; - uint256 amount; - address sender; + struct UnwrappedPacketData { + string denom; + bool originatorChainIsSource; + address erc20Contract; + string sender; string receiver; + uint256 amount; string memo; } @@ -150,7 +154,7 @@ library ICS20Lib { /// @param bz JSON bytes /// @return Unmarshalled PacketData function unmarshalJSON(bytes calldata bz) internal pure returns (PacketDataJSON memory) { - // TODO: Consider if this should support other orders of fields (currently fixed order: denom, amount, etc) + // TODO: Consider if this should support other orders of fields (currently fixed order: denom, amount...) (#22) PacketDataJSON memory pd; uint256 pos = 0; @@ -310,6 +314,17 @@ library ICS20Lib { return (address(uint160(addr)), true); } + /// @notice mustHexStringToAddress converts a hex string to an address and reverts on failure. + /// @param addrHexString hex address string + /// @return address the converted address + function mustHexStringToAddress(string memory addrHexString) internal pure returns (address) { + (address addr, bool success) = hexStringToAddress(addrHexString); + if (!success) { + revert IICS20Errors.ICS20InvalidAddress(addrHexString); + } + return addr; + } + /// @notice slice returns a slice of the original bytes from `start` to `start + length`. /// @dev This is a copy from https://github.com/GNSPS/solidity-bytes-utils/blob/v0.8.0/contracts/BytesLib.sol /// @param _bytes bytes @@ -387,29 +402,15 @@ library ICS20Lib { return keccak256(a) == keccak256(b); } - /// @notice unwrapPacketData unmarshals packet data and converts addresses. - /// @param data Packet data - /// @return UnwrappedFungibleTokenPacketData - function unwrapPacketData(bytes calldata data) internal pure returns (UnwrappedFungibleTokenPacketData memory) { - ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(data); - - (address tokenContract, bool tokenContractConvertSuccess) = ICS20Lib.hexStringToAddress(packetData.denom); - if (!tokenContractConvertSuccess) { - revert IICS20Errors.ICS20InvalidTokenContract(packetData.denom); + /// @notice hasPrefix checks a denom for a prefix + /// @param denomBz the denom to check + /// @param prefix the prefix to check with + /// @return true if `denomBz` has the prefix `prefix` + function hasPrefix(bytes memory denomBz, bytes memory prefix) internal pure returns (bool) { + if (denomBz.length < prefix.length) { + return false; } - - (address sender, bool senderConvertSuccess) = ICS20Lib.hexStringToAddress(packetData.sender); - if (!senderConvertSuccess) { - revert IICS20Errors.ICS20InvalidSender(packetData.sender); - } - - return UnwrappedFungibleTokenPacketData({ - erc20ContractAddress: tokenContract, - amount: packetData.amount, - sender: sender, - receiver: packetData.receiver, - memo: packetData.memo - }); + return equal(slice(denomBz, 0, prefix.length), prefix); } /// @notice errorAck returns an error acknowledgement. @@ -419,11 +420,11 @@ library ICS20Lib { return abi.encodePacked("{\"error\":\"", reason, "\"}"); } - /// @notice getDenomPrefix returns the prefix for a denom. - /// @param portId Port identifier - /// @param channelId Channel identifier + /// @notice getDenomPrefix returns an ibc path prefix + /// @param port Port + /// @param channel Channel /// @return Denom prefix - function getDenomPrefix(string calldata portId, string calldata channelId) internal pure returns (bytes memory) { - return abi.encodePacked(portId, "/", channelId, "/"); + function getDenomPrefix(string calldata port, string calldata channel) internal pure returns (bytes memory) { + return abi.encodePacked(port, "/", channel, "/"); } } diff --git a/test/IBCERC20Test.t.sol b/test/IBCERC20Test.t.sol new file mode 100644 index 000000000..37b3c50 --- /dev/null +++ b/test/IBCERC20Test.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.25 <0.9.0; + +// solhint-disable custom-errors,max-line-length + +import { Test } from "forge-std/Test.sol"; +import { IBCERC20 } from "../src/utils/IBCERC20.sol"; +import { IICS20Transfer } from "../src/interfaces/IICS20Transfer.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; + +contract IBCERC20Test is Test, IICS20Transfer { + IBCERC20 public ibcERC20; + + function setUp() public { + ibcERC20 = new IBCERC20(IICS20Transfer(this)); + } + + function test_ERC20Metadata() public view { + assertEq(ibcERC20.owner(), address(this)); + assertEq(ibcERC20.name(), "IBC Token"); + assertEq(ibcERC20.symbol(), "IBC"); + assertEq(0, ibcERC20.totalSupply()); + } + + function testFuzz_success_Mint(uint256 amount) public { + ibcERC20.mint(amount); + assertEq(ibcERC20.balanceOf(address(this)), amount); + assertEq(ibcERC20.totalSupply(), amount); + } + + // Just to document the behaviour + function test_MintZero() public { + ibcERC20.mint(0); + assertEq(ibcERC20.balanceOf(address(this)), 0); + assertEq(ibcERC20.totalSupply(), 0); + } + + function testFuzz_unauthorized_Mint(uint256 amount) public { + address notICS20Transfer = makeAddr("notICS20Transfer"); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, notICS20Transfer)); + vm.prank(notICS20Transfer); + ibcERC20.mint(amount); + assertEq(ibcERC20.balanceOf(notICS20Transfer), 0); + assertEq(ibcERC20.balanceOf(address(this)), 0); + assertEq(ibcERC20.totalSupply(), 0); + } + + function testFuzz_success_Burn(uint256 startingAmount, uint256 burnAmount) public { + burnAmount = bound(burnAmount, 0, startingAmount); + ibcERC20.mint(startingAmount); + assertEq(ibcERC20.balanceOf(address(this)), startingAmount); + + ibcERC20.burn(burnAmount); + uint256 leftOver = startingAmount - burnAmount; + assertEq(ibcERC20.balanceOf(address(this)), leftOver); + assertEq(ibcERC20.totalSupply(), leftOver); + + if (leftOver != 0) { + ibcERC20.burn(leftOver); + assertEq(ibcERC20.balanceOf(address(this)), 0); + assertEq(ibcERC20.totalSupply(), 0); + } + } + + function testFuzz_unauthorized_Burn(uint256 startingAmount, uint256 burnAmount) public { + burnAmount = bound(burnAmount, 0, startingAmount); + ibcERC20.mint(startingAmount); + assertEq(ibcERC20.balanceOf(address(this)), startingAmount); + + address notICS20Transfer = makeAddr("notICS20Transfer"); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, notICS20Transfer)); + vm.prank(notICS20Transfer); + ibcERC20.burn(burnAmount); + assertEq(ibcERC20.balanceOf(notICS20Transfer), 0); + assertEq(ibcERC20.balanceOf(address(this)), startingAmount); + assertEq(ibcERC20.totalSupply(), startingAmount); + } + + // Just to document the behaviour + function test_BurnZero() public { + ibcERC20.burn(0); + assertEq(ibcERC20.balanceOf(address(this)), 0); + assertEq(ibcERC20.totalSupply(), 0); + + ibcERC20.mint(1000); + ibcERC20.burn(0); + assertEq(ibcERC20.balanceOf(address(this)), 1000); + assertEq(ibcERC20.totalSupply(), 1000); + } + + function test_failure_Burn() public { + // test burn with zero balance + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(this), 0, 1)); + ibcERC20.burn(1); + + // mint some to test other cases + ibcERC20.mint(1000); + + // test burn with insufficient balance + vm.expectRevert( + abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(this), 1000, 1001) + ); + ibcERC20.burn(1001); + } + + // Dummy implementation of IICS20Transfer + function sendTransfer(SendTransferMsg calldata) external pure returns (uint32 sequence) { + return 0; + } +} diff --git a/test/ICS20TransferTest.t.sol b/test/ICS20TransferTest.t.sol index 1c49afc..3c8da50 100644 --- a/test/ICS20TransferTest.t.sol +++ b/test/ICS20TransferTest.t.sol @@ -9,10 +9,12 @@ import { IIBCAppCallbacks } from "../src/msgs/IIBCAppCallbacks.sol"; import { IICS20Transfer } from "../src/interfaces/IICS20Transfer.sol"; import { ICS20Transfer } from "../src/ICS20Transfer.sol"; import { TestERC20, MalfunctioningERC20 } from "./TestERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Errors } from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import { ICS20Lib } from "../src/utils/ICS20Lib.sol"; import { IICS20Errors } from "../src/errors/IICS20Errors.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Vm } from "forge-std/Vm.sol"; contract ICS20TransferTest is Test { ICS20Transfer public ics20Transfer; @@ -21,10 +23,11 @@ contract ICS20TransferTest is Test { address public sender; string public senderStr; - string public receiver; + string public receiver = "receiver"; uint256 public defaultAmount = 100; bytes public data; IICS26RouterMsgs.Packet public packet; + ICS20Lib.UnwrappedPacketData public expectedDefaultSendPacketData; function setUp() public { ics20Transfer = new ICS20Transfer(address(this)); @@ -46,6 +49,16 @@ contract ICS20TransferTest is Test { version: ICS20Lib.ICS20_VERSION, data: data }); + + expectedDefaultSendPacketData = ICS20Lib.UnwrappedPacketData({ + denom: erc20AddressStr, + originatorChainIsSource: true, + erc20Contract: address(erc20), + sender: senderStr, + receiver: receiver, + amount: defaultAmount, + memo: "memo" + }); } function test_success_onSendPacket() public { @@ -60,7 +73,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceBefore, 0); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); ics20Transfer.onSendPacket(IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: sender })); uint256 senderBalanceAfter = erc20.balanceOf(sender); @@ -101,7 +114,7 @@ contract ICS20TransferTest is Test { // test invalid sender data = ICS20Lib.marshalJSON(erc20AddressStr, defaultAmount, "invalid", receiver, "memo"); packet.data = data; - vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidSender.selector, "invalid")); + vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidAddress.selector, "invalid")); ics20Transfer.onSendPacket(IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: sender })); // test msg sender is not packet sender @@ -152,7 +165,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceBefore, 0); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); ics20Transfer.onSendPacket(IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: sender })); uint256 senderBalanceAfterSend = erc20.balanceOf(sender); @@ -161,7 +174,9 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceAfterSend, defaultAmount); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement( + expectedDefaultSendPacketData, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON + ); ics20Transfer.onAcknowledgementPacket( IIBCAppCallbacks.OnAcknowledgementPacketCallback({ packet: packet, @@ -189,7 +204,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceBefore, 0); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); ics20Transfer.onSendPacket(IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: sender })); uint256 senderBalanceAfterSend = erc20.balanceOf(sender); @@ -198,7 +213,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceAfterSend, defaultAmount); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement(expectedDefaultSendPacketData, ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON); ics20Transfer.onAcknowledgementPacket( IIBCAppCallbacks.OnAcknowledgementPacketCallback({ packet: packet, @@ -242,7 +257,7 @@ contract ICS20TransferTest is Test { // test invalid sender data = ICS20Lib.marshalJSON(erc20AddressStr, defaultAmount, "invalid", receiver, "memo"); packet.data = data; - vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidSender.selector, "invalid")); + vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidAddress.selector, "invalid")); ics20Transfer.onAcknowledgementPacket( IIBCAppCallbacks.OnAcknowledgementPacketCallback({ packet: packet, @@ -264,7 +279,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceBefore, 0); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); ics20Transfer.onSendPacket(IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: sender })); uint256 senderBalanceAfterSend = erc20.balanceOf(sender); @@ -273,7 +288,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceAfterSend, defaultAmount); vm.expectEmit(); - emit IICS20Transfer.ICS20Timeout(_getPacketData()); + emit IICS20Transfer.ICS20Timeout(expectedDefaultSendPacketData); ics20Transfer.onTimeoutPacket( IIBCAppCallbacks.OnTimeoutPacketCallback({ packet: packet, relayer: makeAddr("relayer") }) ); @@ -305,13 +320,13 @@ contract ICS20TransferTest is Test { // test invalid sender data = ICS20Lib.marshalJSON(erc20AddressStr, defaultAmount, "invalid", receiver, "memo"); packet.data = data; - vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidSender.selector, "invalid")); + vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidAddress.selector, "invalid")); ics20Transfer.onTimeoutPacket( IIBCAppCallbacks.OnTimeoutPacketCallback({ packet: packet, relayer: makeAddr("relayer") }) ); } - function test_success_onRecvPacket() public { + function test_success_onRecvPacketWithSourceDenom() public { erc20.mint(sender, defaultAmount); vm.prank(sender); @@ -323,7 +338,7 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceBefore, 0); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); ics20Transfer.onSendPacket(IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: sender })); uint256 senderBalanceAfterSend = erc20.balanceOf(sender); @@ -336,6 +351,8 @@ contract ICS20TransferTest is Test { string memory newSourceChannel = packet.destChannel; string memory ibcDenom = string(abi.encodePacked(newSourcePort, "/", newSourceChannel, "/", erc20AddressStr)); + string memory backSenderStr = receiver; + string memory backReceiverStr = senderStr; bytes memory receiveData = ICS20Lib.marshalJSON(ibcDenom, defaultAmount, receiver, senderStr, "memo"); packet.data = receiveData; packet.destPort = packet.sourcePort; @@ -343,10 +360,22 @@ contract ICS20TransferTest is Test { packet.sourcePort = newSourcePort; packet.sourceChannel = newSourceChannel; + vm.expectEmit(); + emit IICS20Transfer.ICS20ReceiveTransfer( + ICS20Lib.UnwrappedPacketData({ + denom: erc20AddressStr, + originatorChainIsSource: false, + erc20Contract: address(erc20), + sender: backSenderStr, + receiver: backReceiverStr, + amount: defaultAmount, + memo: "memo" + }) + ); bytes memory ack = ics20Transfer.onRecvPacket( IIBCAppCallbacks.OnRecvPacketCallback({ packet: packet, relayer: makeAddr("relayer") }) ); - assertEq(string(ack), "{\"result\":\"AQ==\"}"); + assertEq(ack, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); // the tokens should have been transferred back again uint256 senderBalanceAfterReceive = erc20.balanceOf(sender); @@ -355,6 +384,54 @@ contract ICS20TransferTest is Test { assertEq(contractBalanceAfterReceive, 0); } + function test_success_onRecvPacketWithForeignDenom() public { + string memory foreignDenom = "uatom"; + + string memory senderAddrStr = "cosmos1mhmwgrfrcrdex5gnr0vcqt90wknunsxej63feh"; + address receiverAddr = makeAddr("receiver_of_foreign_denom"); + string memory receiverAddrStr = Strings.toHexString(receiverAddr); + bytes memory receiveData = + ICS20Lib.marshalJSON(foreignDenom, defaultAmount, senderAddrStr, receiverAddrStr, "memo"); + packet.data = receiveData; + packet.destPort = "transfer"; + packet.destChannel = "dest-channel"; + packet.sourcePort = "transfer"; + packet.sourceChannel = "source-channel"; + + string memory ibcDenom = "transfer/source-channel/uatom"; + + vm.expectEmit(true, true, true, false); // Not checking data because we don't know the address yet + emit IICS20Transfer.ICS20ReceiveTransfer( + ICS20Lib.UnwrappedPacketData({ + denom: ibcDenom, + originatorChainIsSource: true, + erc20Contract: address(0), // unknown + sender: senderAddrStr, + receiver: receiverAddrStr, + amount: defaultAmount, + memo: "memo" + }) + ); + vm.recordLogs(); + bytes memory ack = ics20Transfer.onRecvPacket( + IIBCAppCallbacks.OnRecvPacketCallback({ packet: packet, relayer: makeAddr("relayer") }) + ); + assertEq(ack, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + + // find and extract data from the ICS20ReceiveTransfer event + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 4); + Vm.Log memory receiveTransferLog = entries[3]; + assertEq(receiveTransferLog.topics[0], IICS20Transfer.ICS20ReceiveTransfer.selector); + (ICS20Lib.UnwrappedPacketData memory receivePacketData) = + abi.decode(receiveTransferLog.data, (ICS20Lib.UnwrappedPacketData)); + IERC20 ibcERC20 = IERC20(receivePacketData.erc20Contract); + + // finally, verify balances have been updated as expected + assertEq(ibcERC20.totalSupply(), defaultAmount); + assertEq(ibcERC20.balanceOf(receiverAddr), defaultAmount); + } + function test_failure_onRecvPacket() public { string memory ibcDenom = string(abi.encodePacked(packet.sourcePort, "/", packet.sourceChannel, "/", erc20AddressStr)); @@ -390,10 +467,10 @@ contract ICS20TransferTest is Test { string(abi.encodePacked(packet.sourcePort, "/", packet.sourceChannel, "/invalid")); data = ICS20Lib.marshalJSON(invalidErc20Denom, defaultAmount, receiver, senderStr, "memo"); packet.data = data; - ack = ics20Transfer.onRecvPacket( + vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20InvalidTokenContract.selector, "invalid")); + ics20Transfer.onRecvPacket( IIBCAppCallbacks.OnRecvPacketCallback({ packet: packet, relayer: makeAddr("relayer") }) ); - assertEq(string(ack), "{\"error\":\"invalid token contract: invalid\"}"); // test invalid receiver data = ICS20Lib.marshalJSON(ibcDenom, defaultAmount, receiver, "invalid", "memo"); @@ -403,23 +480,28 @@ contract ICS20TransferTest is Test { ); assertEq(string(ack), "{\"error\":\"invalid receiver: invalid\"}"); - // just to document current limitations: sender chain is the source is not supported - string memory sourceDenom = "uatom"; - data = ICS20Lib.marshalJSON(sourceDenom, defaultAmount, receiver, senderStr, "memo"); - packet.data = data; - vm.expectRevert(abi.encodeWithSelector(IICS20Errors.ICS20UnsupportedFeature.selector, "sender denom is source")); + // just to document current limitations: JSON needs to be in a very specific order + bytes memory wrongOrderJSON = abi.encodePacked( + "{\"amount\":\"", + Strings.toString(defaultAmount), + "\",\"denom\":\"", + ibcDenom, + "\",\"memo\":\"", + "memo", + "\",\"receiver\":\"", + receiver, + "\",\"sender\":\"", + senderStr, + "\"}" + ); + packet.data = wrongOrderJSON; + vm.expectRevert( + abi.encodeWithSelector( + IICS20Errors.ICS20JSONUnexpectedBytes.selector, 0, bytes32("{\"denom\":\""), bytes32("{\"amount\":") + ) + ); ics20Transfer.onRecvPacket( IIBCAppCallbacks.OnRecvPacketCallback({ packet: packet, relayer: makeAddr("relayer") }) ); } - - function _getPacketData() internal view returns (ICS20Lib.UnwrappedFungibleTokenPacketData memory) { - return ICS20Lib.UnwrappedFungibleTokenPacketData({ - sender: sender, - receiver: receiver, - erc20ContractAddress: address(erc20), - amount: defaultAmount, - memo: "memo" - }); - } } diff --git a/test/IntegrationTest.t.sol b/test/IntegrationTest.t.sol index 5c94e60..421acbe 100644 --- a/test/IntegrationTest.t.sol +++ b/test/IntegrationTest.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.25 <0.9.0; -// solhint-disable custom-errors,max-line-length +// solhint-disable custom-errors,max-line-length,max-states-count import { Test } from "forge-std/Test.sol"; import { IICS02Client } from "../src/interfaces/IICS02Client.sol"; @@ -10,6 +10,7 @@ import { ICS20Transfer } from "../src/ICS20Transfer.sol"; import { IICS20Transfer } from "../src/interfaces/IICS20Transfer.sol"; import { IICS20TransferMsgs } from "../src/msgs/IICS20TransferMsgs.sol"; import { TestERC20 } from "./TestERC20.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ICS02Client } from "../src/ICS02Client.sol"; import { IICS26Router } from "../src/ICS26Router.sol"; import { IICS26RouterErrors } from "../src/errors/IICS26RouterErrors.sol"; @@ -20,6 +21,7 @@ import { ILightClientMsgs } from "../src/msgs/ILightClientMsgs.sol"; import { ICS20Lib } from "../src/utils/ICS20Lib.sol"; import { ICS24Host } from "../src/utils/ICS24Host.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Vm } from "forge-std/Vm.sol"; contract IntegrationTest is Test { IICS02Client public ics02Client; @@ -38,6 +40,7 @@ contract IntegrationTest is Test { string public receiver = "someReceiver"; bytes public data; IICS26RouterMsgs.MsgSendPacket public msgSendPacket; + ICS20Lib.UnwrappedPacketData public expectedDefaultSendPacketData; function setUp() public { ics02Client = new ICS02Client(address(this)); @@ -69,6 +72,16 @@ contract IntegrationTest is Test { timeoutTimestamp: uint64(block.timestamp + 1000), version: ICS20Lib.ICS20_VERSION }); + + expectedDefaultSendPacketData = ICS20Lib.UnwrappedPacketData({ + denom: erc20AddressStr, + originatorChainIsSource: true, + erc20Contract: address(erc20), + sender: senderStr, + receiver: receiver, + amount: defaultAmount, + memo: "memo" + }); } function test_success_sendICS20PacketDirectlyFromRouter() public { @@ -82,7 +95,7 @@ contract IntegrationTest is Test { assertEq(contractBalanceBefore, 0); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); uint32 sequence = ics26Router.sendPacket(msgSendPacket); assertEq(sequence, 1); @@ -99,7 +112,9 @@ contract IntegrationTest is Test { proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // dummy client will accept }); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement( + expectedDefaultSendPacketData, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON + ); ics26Router.ackPacket(ackMsg); // commitment should be deleted storedCommitment = ics26Router.getCommitment(path); @@ -121,7 +136,9 @@ contract IntegrationTest is Test { proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // dummy client will accept }); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement( + expectedDefaultSendPacketData, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON + ); ics26Router.ackPacket(ackMsg); // commitment should be deleted bytes32 path = ICS24Host.packetCommitmentKeyCalldata( @@ -146,7 +163,7 @@ contract IntegrationTest is Test { proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // dummy client will accept }); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement(expectedDefaultSendPacketData, ICS20Lib.FAILED_ACKNOWLEDGEMENT_JSON); ics26Router.ackPacket(ackMsg); // commitment should be deleted bytes32 path = ICS24Host.packetCommitmentKeyCalldata( @@ -174,7 +191,7 @@ contract IntegrationTest is Test { proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // dummy client will accept }); vm.expectEmit(); - emit IICS20Transfer.ICS20Timeout(_getPacketData()); + emit IICS20Transfer.ICS20Timeout(expectedDefaultSendPacketData); ics26Router.timeoutPacket(timeoutMsg); // commitment should be deleted bytes32 path = ICS24Host.packetCommitmentKeyCalldata( @@ -190,7 +207,7 @@ contract IntegrationTest is Test { assertEq(contractBalanceAfterTimeout, 0); } - function test_success_receiveICS20PacketWithKnownDenom() public { + function test_success_receiveICS20PacketWithSourceDenom() public { IICS26RouterMsgs.Packet memory packet = _sendICS20Transfer(); IICS26RouterMsgs.MsgAckPacket memory ackMsg = IICS26RouterMsgs.MsgAckPacket({ @@ -200,7 +217,9 @@ contract IntegrationTest is Test { proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // dummy client will accept }); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement( + expectedDefaultSendPacketData, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON + ); ics26Router.ackPacket(ackMsg); // commitment should be deleted @@ -235,16 +254,21 @@ contract IntegrationTest is Test { }); vm.expectEmit(); emit IICS20Transfer.ICS20ReceiveTransfer( - ICS20Lib.PacketDataJSON({ - denom: ibcDenom, - amount: defaultAmount, + ICS20Lib.UnwrappedPacketData({ + denom: erc20AddressStr, // Because unwrapped now + originatorChainIsSource: false, + erc20Contract: address(erc20), sender: backSender, receiver: backReceiverStr, + amount: defaultAmount, memo: "backmemo" }) ); vm.expectEmit(); emit IICS26Router.WriteAcknowledgement(packet, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + vm.expectEmit(); + emit IICS26Router.RecvPacket(packet); + ics26Router.recvPacket( IICS26RouterMsgs.MsgRecvPacket({ packet: packet, @@ -266,6 +290,131 @@ contract IntegrationTest is Test { assertEq(storedAck, ICS24Host.packetAcknowledgementCommitmentBytes32(ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON)); } + function test_success_receiveICS20PacketWithForeignDenom() public { + string memory foreignDenom = "uatom"; + + string memory senderAddrStr = "cosmos1mhmwgrfrcrdex5gnr0vcqt90wknunsxej63feh"; + address receiverAddr = makeAddr("receiver_of_foreign_denom"); + string memory receiverAddrStr = Strings.toHexString(receiverAddr); + bytes memory receiveData = + ICS20Lib.marshalJSON(foreignDenom, defaultAmount, senderAddrStr, receiverAddrStr, "memo"); + + // For the packet back we pretend this is ibc-go and that the timeout is in nanoseconds + IICS26RouterMsgs.Packet memory receivePacket = IICS26RouterMsgs.Packet({ + sequence: 1, + timeoutTimestamp: uint64(block.timestamp + 1000), + sourcePort: "transfer", + sourceChannel: counterpartyClient, + destPort: "transfer", + destChannel: clientIdentifier, + version: ICS20Lib.ICS20_VERSION, + data: receiveData + }); + + string memory ibcDenom = + string(abi.encodePacked(receivePacket.destPort, "/", receivePacket.destChannel, "/", foreignDenom)); + + vm.expectEmit(true, true, true, false); // Not checking data because we don't know the address yet + emit IICS20Transfer.ICS20ReceiveTransfer( + ICS20Lib.UnwrappedPacketData({ + denom: ibcDenom, + originatorChainIsSource: false, + erc20Contract: address(0), // This one we don't know yet + sender: senderAddrStr, + receiver: receiverAddrStr, + amount: defaultAmount, + memo: "memo" + }) + ); + vm.expectEmit(); + emit IICS26Router.WriteAcknowledgement(receivePacket, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + vm.expectEmit(); + emit IICS26Router.RecvPacket(receivePacket); + + vm.recordLogs(); + ics26Router.recvPacket( + IICS26RouterMsgs.MsgRecvPacket({ + packet: receivePacket, + proofCommitment: bytes("doesntmatter"), // dummy client will accept + proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // will accept + }) + ); + + // Check that the ack is written + bytes32 ackPath = ICS24Host.packetAcknowledgementCommitmentKeyCalldata( + receivePacket.destPort, receivePacket.destChannel, receivePacket.sequence + ); + bytes32 storedAck = ics26Router.getCommitment(ackPath); + assertEq(storedAck, ICS24Host.packetAcknowledgementCommitmentBytes32(ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON)); + + // find and extract data from the ICS20ReceiveTransfer event + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 6); + Vm.Log memory receiveTransferLog = entries[3]; + assertEq(receiveTransferLog.topics[0], IICS20Transfer.ICS20ReceiveTransfer.selector); + + (ICS20Lib.UnwrappedPacketData memory receivePacketData) = + abi.decode(receiveTransferLog.data, (ICS20Lib.UnwrappedPacketData)); + assertEq(receivePacketData.denom, ibcDenom); + + IERC20 ibcERC20 = IERC20(receivePacketData.erc20Contract); + assertEq(ibcERC20.totalSupply(), defaultAmount); + assertEq(ibcERC20.balanceOf(receiverAddr), defaultAmount); + + // Send out again + address backSender = receiverAddr; + string memory backSenderStr = receiverAddrStr; + string memory backReceiverStr = senderAddrStr; + + vm.prank(backSender); + ibcERC20.approve(address(ics20Transfer), defaultAmount); + + IICS20TransferMsgs.SendTransferMsg memory msgSendTransfer = IICS20TransferMsgs.SendTransferMsg({ + denom: ibcDenom, + amount: defaultAmount, + receiver: backReceiverStr, + sourceChannel: clientIdentifier, + destPort: "transfer", + timeoutTimestamp: uint64(block.timestamp + 1000), + memo: "backmemo" + }); + + IICS26RouterMsgs.Packet memory expectedPacketSent = IICS26RouterMsgs.Packet({ + sequence: 1, + timeoutTimestamp: msgSendTransfer.timeoutTimestamp, + sourcePort: "transfer", + sourceChannel: clientIdentifier, + destPort: "transfer", + destChannel: counterpartyClient, + version: ICS20Lib.ICS20_VERSION, + data: ICS20Lib.marshalJSON(ibcDenom, defaultAmount, backSenderStr, backReceiverStr, "backmemo") + }); + + vm.expectEmit(); + emit IICS20Transfer.ICS20Transfer( + ICS20Lib.UnwrappedPacketData({ + denom: ibcDenom, + originatorChainIsSource: false, + erc20Contract: address(ibcERC20), + sender: backSenderStr, + receiver: backReceiverStr, + amount: defaultAmount, + memo: "backmemo" + }) + ); + vm.expectEmit(); + emit IICS26Router.SendPacket(expectedPacketSent); + vm.prank(backSender); + uint32 sequence = ics20Transfer.sendTransfer(msgSendTransfer); + assertEq(sequence, expectedPacketSent.sequence); + + bytes32 path = ICS24Host.packetCommitmentKeyCalldata( + expectedPacketSent.sourcePort, expectedPacketSent.sourceChannel, expectedPacketSent.sequence + ); + bytes32 storedCommitment = ics26Router.getCommitment(path); + assertEq(storedCommitment, ICS24Host.packetCommitmentBytes32(expectedPacketSent)); + } + function test_failure_receiveICS20PacketHasTimedOut() public { IICS26RouterMsgs.Packet memory packet = _sendICS20Transfer(); @@ -276,7 +425,9 @@ contract IntegrationTest is Test { proofHeight: IICS02ClientMsgs.Height({ revisionNumber: 1, revisionHeight: 42 }) // dummy client will accept }); vm.expectEmit(); - emit IICS20Transfer.ICS20Acknowledgement(_getPacketData(), ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON); + emit IICS20Transfer.ICS20Acknowledgement( + expectedDefaultSendPacketData, ICS20Lib.SUCCESSFUL_ACKNOWLEDGEMENT_JSON + ); ics26Router.ackPacket(ackMsg); // commitment should be deleted @@ -345,7 +496,7 @@ contract IntegrationTest is Test { vm.startPrank(sender); vm.expectEmit(); - emit IICS20Transfer.ICS20Transfer(_getPacketData()); + emit IICS20Transfer.ICS20Transfer(expectedDefaultSendPacketData); uint32 sequence = ics20Transfer.sendTransfer(msgSendTransfer); assertEq(sequence, 1); @@ -377,14 +528,4 @@ contract IntegrationTest is Test { data: _msgSendPacket.data }); } - - function _getPacketData() internal view returns (ICS20Lib.UnwrappedFungibleTokenPacketData memory) { - return ICS20Lib.UnwrappedFungibleTokenPacketData({ - sender: sender, - receiver: receiver, - erc20ContractAddress: address(erc20), - amount: defaultAmount, - memo: "memo" - }); - } }