diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 55966951..b3c37a80 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -43,6 +43,7 @@ jobs: # List your tests here - TestWithIbcEurekaTestSuite/TestDeploy - TestWithIbcEurekaTestSuite/TestICS20Transfer + - TestWithIbcEurekaTestSuite/TestICS20Timeout name: ${{ matrix.test }} runs-on: ubuntu-latest steps: diff --git a/bun.lockb b/bun.lockb index dfd5b5d9..dd518ab9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/e2e/artifacts/genesis.json b/e2e/artifacts/genesis.json index bc7ddaf3..aa50f724 100644 --- a/e2e/artifacts/genesis.json +++ b/e2e/artifacts/genesis.json @@ -1,6 +1,6 @@ { "trustedClientState": "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000012754500000000000000000000000000000000000000000000000000000000001baf800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000673696d642d310000000000000000000000000000000000000000000000000000", - "trustedConsensusState": "0000000000000000000000000000000000000000000000000000000066b21a6180d26d76e2ef512de8af9baf444a87782fee9b50f3f455d552ced6ffbaafada76076e95e1d308f10802c0140af31825da40f208e5012e88322a27563ebfb64c6", + "trustedConsensusState": "0000000000000000000000000000000000000000000000000000000066b0b92862093024debc13c1fab2cce33d04895624033edca5cc22f7266085eca4c6461606c79e13ccd4e467cae775cbc073ccca2735947069c0255e78337c50dcdee823", "updateClientVkey": "0x0068b9d316aced51c5923b2d50692f4a6a9bfefcd89392914b90e77545727fbe", "membershipVkey": "0x00a4245d249b5c35c9782cc899c8e370a35d5d928187dc9e7acbab7096764b72", "ucAndMembershipVkey": "0x00cea834e3408d45d29080a3146e4fb1fd0c06503d655bd787219caac86cf59c" diff --git a/e2e/interchaintestv8/ibc_eureka_test.go b/e2e/interchaintestv8/ibc_eureka_test.go index 1a265398..34c49776 100644 --- a/e2e/interchaintestv8/ibc_eureka_test.go +++ b/e2e/interchaintestv8/ibc_eureka_test.go @@ -300,10 +300,6 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { transferAmount := big.NewInt(testvalues.TransferAmount) userAddress := crypto.PubkeyToAddress(s.key.PublicKey) receiver := s.UserB - var sendPacket ics26router.IICS26RouterMsgsPacket - var returnPacket channeltypes.Packet - var recvAck []byte - var ibcDenom string s.Require().True(s.Run("Approve the ICS20Transfer contract to spend the erc20 tokens", func() { tx, err := s.erc20Contract.Approve(s.GetTransactOpts(s.key), ics20Address, transferAmount) @@ -316,8 +312,9 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { s.Require().Equal(transferAmount, allowance) })) + var sendPacket ics26router.IICS26RouterMsgsPacket s.Require().True(s.Run("sendTransfer on Ethereum side", func() { - timeout := uint64(time.Now().Add(30 * time.Minute).UnixNano()) + timeout := uint64(time.Now().Add(30 * time.Minute).Unix()) msgSendTransfer := ics20transfer.IICS20TransferMsgsSendTransferMsg{ Denom: s.contractAddresses.Erc20, Amount: transferAmount, @@ -365,6 +362,8 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { // TODO: When using a non-mock light client on the cosmos side, the client there needs to be updated at this point + var recvAck []byte + var ibcDenom string s.Require().True(s.Run("recvPacket on Cosmos side", func() { resp, err := e2esuite.GRPCQuery[clienttypes.QueryClientStateResponse](ctx, simd, &clienttypes.QueryClientStateRequest{ ClientId: s.simdClientID, @@ -383,7 +382,7 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { DestinationChannel: sendPacket.DestChannel, Data: sendPacket.Data, TimeoutHeight: clienttypes.Height{}, - TimeoutTimestamp: sendPacket.TimeoutTimestamp, + TimeoutTimestamp: sendPacket.TimeoutTimestamp * 1_000_000_000, }, ProofCommitment: []byte("doesn't matter"), ProofHeight: clientState.LatestHeight, @@ -420,6 +419,7 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { latestHeight, err := simd.Height(ctx) s.Require().NoError(err) + // This will be a membership proof since the acknowledgement is written packetAckPath := ibchost.PacketAcknowledgementPath(sendPacket.DestPort, sendPacket.DestChannel, uint64(sendPacket.Sequence)) proofHeight, ucAndMemProof, err := operator.UpdateClientAndMembershipProof( uint64(trustedHeight), uint64(latestHeight), packetAckPath, @@ -442,6 +442,7 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { s.Require().Equal(ethtypes.ReceiptStatusSuccessful, receipt.Status) })) + var returnPacket channeltypes.Packet s.Require().True(s.Run("Transfer back", func() { timeout := uint64(time.Now().Add(30 * time.Minute).UnixNano()) ibcCoin := sdk.NewCoin(ibcDenom, sdkmath.NewIntFromBigInt(transferAmount)) @@ -522,7 +523,7 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { msg := ics26router.IICS26RouterMsgsMsgRecvPacket{ Packet: ics26router.IICS26RouterMsgsPacket{ Sequence: uint32(returnPacket.Sequence), - TimeoutTimestamp: returnPacket.TimeoutTimestamp, + TimeoutTimestamp: returnPacket.TimeoutTimestamp / 1_000_000_000, // Nano to seconds SourcePort: returnPacket.SourcePort, SourceChannel: returnPacket.SourceChannel, DestPort: returnPacket.DestinationPort, @@ -576,3 +577,98 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { s.Require().Equal(0, txResp.Code) })) } + +func (s *IbcEurekaTestSuite) TestICS20Timeout() { + ctx := context.Background() + + s.SetupSuite(ctx) + + eth, simd := s.ChainA, s.ChainB + + transferAmount := big.NewInt(testvalues.TransferAmount) + userAddress := crypto.PubkeyToAddress(s.key.PublicKey) + receiver := s.UserB + + var packet ics26router.IICS26RouterMsgsPacket + s.Require().True(s.Run("Approve the ICS20Transfer contract to spend the erc20 tokens", func() { + ics20Address := ethcommon.HexToAddress(s.contractAddresses.Ics20Transfer) + tx, err := s.erc20Contract.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 := s.erc20Contract.Allowance(nil, userAddress, ics20Address) + s.Require().NoError(err) + s.Require().Equal(transferAmount, allowance) + })) + + s.Require().True(s.Run("sendTransfer on Ethereum side", func() { + timeout := uint64(time.Now().Add(45 * time.Second).Unix()) + msgSendTransfer := ics20transfer.IICS20TransferMsgsSendTransferMsg{ + Denom: s.contractAddresses.Erc20, + Amount: transferAmount, + Receiver: receiver.FormattedAddress(), + SourceChannel: s.ethClientID, + DestPort: "transfer", + TimeoutTimestamp: timeout, + Memo: "testmemo", + } + + 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(s.contractAddresses.Erc20, strings.ToLower(transferEvent.PacketData.Erc20ContractAddress.Hex())) + s.Require().Equal(transferAmount, transferEvent.PacketData.Amount) + s.Require().Equal(userAddress, transferEvent.PacketData.Sender) + s.Require().Equal(receiver.FormattedAddress(), transferEvent.PacketData.Receiver) + s.Require().Equal("testmemo", transferEvent.PacketData.Memo) + + sendPacketEvent, err := e2esuite.GetEvmEvent(receipt, s.ics26Contract.ParseSendPacket) + s.Require().NoError(err) + packet = sendPacketEvent.Packet + s.Require().Equal(uint32(1), packet.Sequence) + s.Require().Equal(timeout, packet.TimeoutTimestamp) + s.Require().Equal("transfer", packet.SourcePort) + s.Require().Equal(s.ethClientID, packet.SourceChannel) + s.Require().Equal("transfer", packet.DestPort) + s.Require().Equal(s.simdClientID, packet.DestChannel) + s.Require().Equal(transfertypes.Version, packet.Version) + })) + + // sleep for 60 seconds to let the packet timeout + time.Sleep(60 * time.Second) + + s.True(s.Run("timeoutPacket on Ethereum", 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 non-membership proof since no packets have been sent + packetReceiptPath := ibchost.PacketReceiptPath(packet.DestPort, packet.DestChannel, uint64(packet.Sequence)) + proofHeight, ucAndMemProof, err := operator.UpdateClientAndMembershipProof( + uint64(trustedHeight), uint64(latestHeight), packetReceiptPath, + "--trust-level", testvalues.DefaultTrustLevel.String(), + "--trusting-period", strconv.Itoa(testvalues.DefaultTrustPeriod), + ) + s.Require().NoError(err) + + msg := ics26router.IICS26RouterMsgsMsgTimeoutPacket{ + Packet: packet, + ProofTimeout: ucAndMemProof, + ProofHeight: *proofHeight, + } + + tx, err := s.ics26Contract.TimeoutPacket(s.GetTransactOpts(s.key), msg) + s.Require().NoError(err) + + receipt := s.GetTxReciept(ctx, eth, tx.Hash()) + s.Require().Equal(ethtypes.ReceiptStatusSuccessful, receipt.Status) + })) +} diff --git a/package.json b/package.json index 3aa01cd0..c93ee8b2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "solhint": "^5.0.2", - "@cosmos/sp1-ics07-tendermint": "github:cosmos/sp1-ics07-tendermint#64ad2b9f09cfe640e8c387037809298d8f1c16fa", + "@cosmos/sp1-ics07-tendermint": "github:cosmos/sp1-ics07-tendermint#dfba448b5f6bd08e2917e7ddeca3aaed78f9ad7f", "sp1-contracts": "github:succinctlabs/sp1-contracts#v1.0.1" }, "keywords": [ diff --git a/src/ICS26Router.sol b/src/ICS26Router.sol index 2f078b73..77c525b4 100644 --- a/src/ICS26Router.sol +++ b/src/ICS26Router.sol @@ -69,9 +69,8 @@ contract ICS26Router is IICS26Router, IBCStore, Ownable, IICS26RouterErrors, Ree string memory counterpartyId = ics02Client.getCounterparty(msg_.sourceChannel).clientId; // TODO: validate all identifiers - uint64 nanoTimestamp = uint64(block.timestamp * 1_000_000_000); - if (msg_.timeoutTimestamp <= nanoTimestamp) { - revert IBCInvalidTimeoutTimestamp(msg_.timeoutTimestamp, nanoTimestamp); + if (msg_.timeoutTimestamp <= block.timestamp) { + revert IBCInvalidTimeoutTimestamp(msg_.timeoutTimestamp, block.timestamp); } uint32 sequence = IBCStore.nextSequenceSend(msg_.sourcePort, msg_.sourceChannel); @@ -128,10 +127,8 @@ contract ICS26Router is IICS26Router, IBCStore, Ownable, IICS26RouterErrors, Ree } catch (bytes memory reason) { revert IBCMembershipProofVerificationFailed(msg_.packet, membershipMsg, reason); } - - uint64 nanoTimestamp = uint64(block.timestamp * 1_000_000_000); - if (msg_.packet.timeoutTimestamp <= nanoTimestamp) { - revert IBCInvalidTimeoutTimestamp(msg_.packet.timeoutTimestamp, nanoTimestamp); + if (msg_.packet.timeoutTimestamp <= block.timestamp) { + revert IBCInvalidTimeoutTimestamp(msg_.packet.timeoutTimestamp, block.timestamp); } try app.onRecvPacket(IIBCAppCallbacks.OnRecvPacketCallback({ packet: msg_.packet, relayer: msg.sender })) returns (bytes memory ack) { diff --git a/src/msgs/IICS20TransferMsgs.sol b/src/msgs/IICS20TransferMsgs.sol index 778316a2..ffb168de 100644 --- a/src/msgs/IICS20TransferMsgs.sol +++ b/src/msgs/IICS20TransferMsgs.sol @@ -14,7 +14,7 @@ interface IICS20TransferMsgs { string sourceChannel; /// The destination port on the counterparty chain string destPort; - /// The absolute timeout timestamp in unix nano seconds + /// The absolute timeout timestamp in unix seconds uint64 timeoutTimestamp; /// Optional memo string memo; diff --git a/src/utils/ICS24Host.sol b/src/utils/ICS24Host.sol index bd9617f0..972da234 100644 --- a/src/utils/ICS24Host.sol +++ b/src/utils/ICS24Host.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.25; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IICS26RouterMsgs } from "../msgs/IICS26RouterMsgs.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; // @title ICS24 Host Path Generators // @notice ICS24Host is a library that provides commitment path generators for ICS24 host requirements. @@ -101,7 +102,7 @@ library ICS24Host { function packetCommitmentBytes32(IICS26RouterMsgs.Packet memory packet) internal pure returns (bytes32) { return sha256( abi.encodePacked( - uint64(packet.timeoutTimestamp), + SafeCast.toUint64(uint256(packet.timeoutTimestamp) * 1_000_000_000), uint64(0), uint64(0), sha256(packet.data), diff --git a/test/IntegrationTest.t.sol b/test/IntegrationTest.t.sol index a39833d7..de42d488 100644 --- a/test/IntegrationTest.t.sol +++ b/test/IntegrationTest.t.sol @@ -60,13 +60,12 @@ contract IntegrationTest is Test { sender = makeAddr("sender"); senderStr = Strings.toHexString(sender); data = ICS20Lib.marshalJSON(erc20AddressStr, defaultAmount, senderStr, receiver, "memo"); - uint64 nanoTimestamp = uint64((block.timestamp + 1000) * 1_000_000_000); msgSendPacket = IICS26RouterMsgs.MsgSendPacket({ sourcePort: "transfer", sourceChannel: clientIdentifier, destPort: "transfer", data: data, - timeoutTimestamp: nanoTimestamp, + timeoutTimestamp: uint64(block.timestamp + 1000), version: ICS20Lib.ICS20_VERSION }); } @@ -166,7 +165,7 @@ contract IntegrationTest is Test { IICS26RouterMsgs.Packet memory packet = _sendICS20Transfer(); // make light client return timestamp that is after our timeout - lightClient.setMembershipResult(msgSendPacket.timeoutTimestamp + uint64(1_000_000_000), false); + lightClient.setMembershipResult(msgSendPacket.timeoutTimestamp + 1, false); IICS26RouterMsgs.MsgTimeoutPacket memory timeoutMsg = IICS26RouterMsgs.MsgTimeoutPacket({ packet: packet, @@ -223,7 +222,7 @@ contract IntegrationTest is Test { data = ICS20Lib.marshalJSON(ibcDenom, defaultAmount, backSender, backReceiverStr, "backmemo"); packet = IICS26RouterMsgs.Packet({ sequence: 1, - timeoutTimestamp: uint64((block.timestamp + 1000) * 1_000_000_000), + timeoutTimestamp: uint64(block.timestamp + 1000), sourcePort: "transfer", sourceChannel: counterpartyClient, destPort: "transfer", @@ -277,7 +276,7 @@ contract IntegrationTest is Test { receiver: receiver, sourceChannel: clientIdentifier, destPort: "transfer", - timeoutTimestamp: uint64((block.timestamp + 1000) * 1_000_000_000), + timeoutTimestamp: uint64(block.timestamp + 1000), memo: "memo" });