diff --git a/dictionary.txt b/dictionary.txt index 8133c66aeb..a7d2034716 100644 --- a/dictionary.txt +++ b/dictionary.txt @@ -761,6 +761,7 @@ txpool typecheck typemap typenum +typeof typesparams uatom ucli diff --git a/evm/contracts/apps/ucs/00-pingpong/PingPong.sol b/evm/contracts/apps/ucs/00-pingpong/PingPong.sol index 9ca411514b..f83cbe43c3 100644 --- a/evm/contracts/apps/ucs/00-pingpong/PingPong.sol +++ b/evm/contracts/apps/ucs/00-pingpong/PingPong.sol @@ -3,61 +3,62 @@ pragma solidity ^0.8.23; import "../../Base.sol"; import "../../../core/25-handler/IBCHandler.sol"; +// Protocol specific packet struct PingPongPacket { bool ping; - uint64 counterpartyTimeoutRevisionNumber; - uint64 counterpartyTimeoutRevisionHeight; + uint64 counterpartyTimeout; } -library PingPongPacketLib { +library PingPongLib { + bytes1 public constant ACK_SUCCESS = 0x01; + + error ErrOnlyOneChannel(); + error ErrInvalidAck(); + error ErrNoChannel(); + error ErrInfiniteGame(); + + event Ring(bool ping); + event TimedOut(); + event Acknowledged(); + function encode( PingPongPacket memory packet ) internal pure returns (bytes memory) { - return - abi.encode( - packet.ping, - packet.counterpartyTimeoutRevisionNumber, - packet.counterpartyTimeoutRevisionHeight - ); + return abi.encode(packet.ping, packet.counterpartyTimeout); } function decode( bytes memory packet ) internal pure returns (PingPongPacket memory) { - ( - bool ping, - uint64 counterpartyTimeoutRevisionNumber, - uint64 counterpartyTimeoutRevisionHeight - ) = abi.decode(packet, (bool, uint64, uint64)); + (bool ping, uint64 counterpartyTimeout) = abi.decode( + packet, + (bool, uint64) + ); return PingPongPacket({ ping: ping, - counterpartyTimeoutRevisionNumber: counterpartyTimeoutRevisionNumber, - counterpartyTimeoutRevisionHeight: counterpartyTimeoutRevisionHeight + counterpartyTimeout: counterpartyTimeout }); } } contract PingPong is IBCAppBase { - using PingPongPacketLib for PingPongPacket; + using PingPongLib for *; IBCHandler private ibcHandler; string private portId; string private channelId; uint64 private revisionNumber; - uint64 private numberOfBlockBeforePongTimeout; - - event Ring(bool ping); - event TimedOut(); + uint64 private timeout; constructor( IBCHandler _ibcHandler, uint64 _revisionNumber, - uint64 _numberOfBlockBeforePongTimeout + uint64 _timeout ) { ibcHandler = _ibcHandler; revisionNumber = _revisionNumber; - numberOfBlockBeforePongTimeout = _numberOfBlockBeforePongTimeout; + timeout = _timeout; } function ibcAddress() public view virtual override returns (address) { @@ -66,21 +67,22 @@ contract PingPong is IBCAppBase { function initiate( PingPongPacket memory packet, - uint64 counterpartyTimeoutRevisionNumber, - uint64 counterpartyTimeoutRevisionHeight + uint64 localTimeout ) public { - require( - bytes(channelId).length != 0, - "pingpong: channel must be opened" - ); + if (bytes(channelId).length == 0) { + revert PingPongLib.ErrNoChannel(); + } ibcHandler.sendPacket( portId, channelId, + // No height timeout IbcCoreClientV1Height.Data({ - revision_number: counterpartyTimeoutRevisionNumber, - revision_height: counterpartyTimeoutRevisionHeight + revision_number: 0, + revision_height: 0 }), - 0, + // Timestamp timeout + localTimeout, + // Raw protocol packet packet.encode() ); } @@ -89,36 +91,50 @@ contract PingPong is IBCAppBase { IbcCoreChannelV1Packet.Data calldata packet, address relayer ) external virtual override onlyIBC returns (bytes memory acknowledgement) { - PingPongPacket memory pp = PingPongPacketLib.decode(packet.data); - emit Ring(pp.ping); - uint64 counterpartyTimeoutRevisionNumber = pp - .counterpartyTimeoutRevisionNumber; - uint64 counterpartyTimeoutRevisionHeight = pp - .counterpartyTimeoutRevisionHeight; + PingPongPacket memory pp = PingPongLib.decode(packet.data); + + emit PingPongLib.Ring(pp.ping); + + uint64 localTimeout = pp.counterpartyTimeout; + pp.ping = !pp.ping; - pp.counterpartyTimeoutRevisionNumber = revisionNumber; - pp.counterpartyTimeoutRevisionHeight = - uint64(block.number) + - numberOfBlockBeforePongTimeout; - initiate( - pp, - counterpartyTimeoutRevisionNumber, - counterpartyTimeoutRevisionHeight - ); - return hex"01"; + pp.counterpartyTimeout = uint64(block.timestamp) + timeout; + + // Send back the packet after having reversed the bool and set the counterparty timeout + initiate(pp, localTimeout); + + // Return protocol specific successful acknowledgement + return abi.encodePacked(PingPongLib.ACK_SUCCESS); } function onAcknowledgementPacket( IbcCoreChannelV1Packet.Data calldata packet, bytes calldata acknowledgement, address relayer - ) external virtual override onlyIBC {} + ) external virtual override onlyIBC { + /* + In practice, a more sophisticated protocol would check + and execute code depending on the counterparty outcome (refund etc...). + In our case, the acknowledgement will always be ACK_SUCCESS + */ + if ( + keccak256(acknowledgement) != + keccak256(abi.encodePacked(PingPongLib.ACK_SUCCESS)) + ) { + revert PingPongLib.ErrInvalidAck(); + } + emit PingPongLib.Acknowledged(); + } function onTimeoutPacket( IbcCoreChannelV1Packet.Data calldata packet, address relayer ) external virtual override onlyIBC { - emit TimedOut(); + /* + Similarly to the onAcknowledgementPacket function, this indicates a failure to deliver the packet in expected time. + A sophisticated protocol would revert the action done before sending this packet. + */ + emit PingPongLib.TimedOut(); } function onChanOpenInit( @@ -129,10 +145,10 @@ contract PingPong is IBCAppBase { IbcCoreChannelV1Counterparty.Data calldata, string calldata ) external virtual override onlyIBC { - require( - bytes(channelId).length == 0, - "pingpong: only one channel can be opened" - ); + // This protocol is only accepting a single counterparty. + if (bytes(channelId).length != 0) { + revert PingPongLib.ErrOnlyOneChannel(); + } } function onChanOpenTry( @@ -144,10 +160,10 @@ contract PingPong is IBCAppBase { string calldata, string calldata ) external virtual override onlyIBC { - require( - bytes(channelId).length == 0, - "pingpong: only one channel can be opened" - ); + // Symmetric to onChanOpenInit + if (bytes(channelId).length != 0) { + revert PingPongLib.ErrOnlyOneChannel(); + } } function onChanOpenAck( @@ -156,6 +172,7 @@ contract PingPong is IBCAppBase { string calldata _counterpartyChannelId, string calldata _counterpartyVersion ) external virtual override onlyIBC { + // Store the port/channel needed to send packets. portId = _portId; channelId = _channelId; } @@ -164,6 +181,7 @@ contract PingPong is IBCAppBase { string calldata _portId, string calldata _channelId ) external virtual override onlyIBC { + // Symmetric to onChanOpenAck portId = _portId; channelId = _channelId; } @@ -172,13 +190,15 @@ contract PingPong is IBCAppBase { string calldata _portId, string calldata _channelId ) external virtual override onlyIBC { - revert("This game is infinite"); + // The ping-pong is infinite, closing the channel is disallowed. + revert PingPongLib.ErrInfiniteGame(); } function onChanCloseConfirm( string calldata _portId, string calldata _channelId ) external virtual override onlyIBC { - revert("This game is infinite"); + // Symmetric to onChanCloseInit + revert PingPongLib.ErrInfiniteGame(); } } diff --git a/evm/contracts/core/05-port/IIBCModule.sol b/evm/contracts/core/05-port/IIBCModule.sol index aab66264a8..4009e68e4e 100644 --- a/evm/contracts/core/05-port/IIBCModule.sol +++ b/evm/contracts/core/05-port/IIBCModule.sol @@ -4,17 +4,8 @@ import "../../proto/ibc/core/channel/v1/channel.sol"; // IIBCModule defines an interface that implements all the callbacks // that modules must define as specified in ICS-26 +// https://github.com/cosmos/ibc/blob/2921c5cec7b18e4ef77677e16a6b693051ae3b35/spec/core/ics-026-routing-module/README.md interface IIBCModule { - // onChanOpenInit will verify that the relayer-chosen parameters - // are valid and perform any custom INIT logic. - // It may return an error if the chosen parameters are invalid - // in which case the handshake is aborted. - // If the provided version string is non-empty, OnChanOpenInit should return - // the version string if valid or an error if the provided version is invalid. - // If the version string is empty, OnChanOpenInit is expected to - // return a default version string representing the version(s) it supports. - // If there is no default version string for the application, - // it should return an error if provided version is empty string. function onChanOpenInit( IbcCoreChannelV1GlobalEnums.Order, string[] calldata connectionHops, @@ -24,14 +15,6 @@ interface IIBCModule { string calldata version ) external; - // OnChanOpenTry will verify the relayer-chosen parameters along with the - // counterparty-chosen version string and perform custom TRY logic. - // If the relayer-chosen parameters are invalid, the callback must return - // an error to abort the handshake. If the counterparty-chosen version is not - // compatible with this modules supported versions, the callback must return - // an error to abort the handshake. If the versions are compatible, the try callback - // must select the final version string and return it to core IBC. - // OnChanOpenTry may also perform custom initialization logic function onChanOpenTry( IbcCoreChannelV1GlobalEnums.Order, string[] calldata connectionHops, @@ -42,8 +25,6 @@ interface IIBCModule { string calldata counterpartyVersion ) external; - // OnChanOpenAck will error if the counterparty selected version string - // is invalid to abort the handshake. It may also perform custom ACK logic. function onChanOpenAck( string calldata portId, string calldata channelId, @@ -51,7 +32,6 @@ interface IIBCModule { string calldata counterpartyVersion ) external; - // OnChanOpenConfirm will perform custom CONFIRM logic and may error to abort the handshake. function onChanOpenConfirm( string calldata portId, string calldata channelId @@ -67,11 +47,6 @@ interface IIBCModule { string calldata channelId ) external; - // OnRecvPacket must return an acknowledgement that implements the Acknowledgement interface. - // In the case of an asynchronous acknowledgement, nil should be returned. - // If the acknowledgement returned is successful, the state changes on callback are written, - // otherwise the application state changes are discarded. In either case the packet is received - // and the acknowledgement is written (in synchronous cases). function onRecvPacket( IbcCoreChannelV1Packet.Data calldata, address relayer diff --git a/evm/evm.nix b/evm/evm.nix index 428ee916e9..806ba75829 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -352,6 +352,7 @@ text = '' ${ensureAtRepositoryRoot} FOUNDRY_CONFIG="${foundryConfig}/foundry.toml" \ + FOUNDRY_SRC="evm/contracts" \ FOUNDRY_PROFILE="test" \ FOUNDRY_TEST="evm/tests/src" \ forge test -vvv --gas-report diff --git a/site/astro.config.ts b/site/astro.config.ts index 9833e9b331..7276952244 100644 --- a/site/astro.config.ts +++ b/site/astro.config.ts @@ -108,6 +108,12 @@ export default defineConfig({ }, ], }, + { + label: "Integration", + autogenerate: { + directory: "/docs/integration", + }, + }, { label: "Demos", autogenerate: { diff --git a/site/site.nix b/site/site.nix index 986ba9e920..b0aebb3d31 100644 --- a/site/site.nix +++ b/site/site.nix @@ -8,6 +8,11 @@ site = pkgs.buildNpmPackage { npmDepsHash = "sha256-MnKC0nRMAa4tWVYXYhvfpb73BJ5GXEbATQtqzmQxI4E="; src = ./.; + srcs = [ + ./. + ./../evm/. + ]; + sourceRoot = "site"; pname = "site"; version = "0.0.1"; PUPPETEER_SKIP_DOWNLOAD = true; diff --git a/site/src/components/EmbedCodeSnippet.astro b/site/src/components/EmbedCodeSnippet.astro new file mode 100644 index 0000000000..cc9387c2ae --- /dev/null +++ b/site/src/components/EmbedCodeSnippet.astro @@ -0,0 +1,24 @@ +--- +import { Code } from 'astro:components' +import { getFileContent } from '#/lib/utilities.ts' + +interface Props { + filepath: string + filename: string + language: Parameters[0]['lang'] +} + +/** + * TODO: add tab support + */ +const { filepath, filename, language } = Astro.props + +const fileContent = await getFileContent({ filepath }) +--- + + diff --git a/site/src/content/docs/docs/demos/pingpong.mdx b/site/src/content/docs/docs/demos/pingpong.mdx index a1d6d78f4d..d9065d0aa8 100644 --- a/site/src/content/docs/docs/demos/pingpong.mdx +++ b/site/src/content/docs/docs/demos/pingpong.mdx @@ -2,6 +2,8 @@ title: "PingPong" --- +import EmbedCodeSnippet from "#/components/EmbedCodeSnippet.astro"; + ## Overview The `PingPong` contract implements a simple ping pong protocol that alternates between sending "ping" and "pong" messages between two contracts deployed on two different blockchains connected via IBC. @@ -10,9 +12,9 @@ The `PingPong` contract implements a simple ping pong protocol that alternates b The contract consists of the following components: -1. **Data Structures and Library**: Defines a `PingPongPacket` struct to represent the ping pong packet data and a library `PingPongPacketLib` for encoding and decoding packets. +1. **Data Structures and Library**: Defines a `PingPongPacket` struct to represent the ping pong packet data. -2. **Contract Definition**: The `PingPong` contract inherits from `IBCAppBase` and implements the IBC callbacks. +2. **Contract Definition**: The `PingPong` contract inherits from `IBCAppBase` and implements the callbacks from `IIBCModule`. 3. **Constructor**: Initializes the contract with the IBC handler, revision number, and the number of blocks before pong timeout. @@ -30,3 +32,13 @@ The contract consists of the following components: ## Conclusion The `PingPong` contract showcases a basic example of cross-chain communication over IBC using Solidity. It demonstrates how developers can implement a simple ping pong protocol, exchanging packets and alternating between ping and pong messages. This example serves as a starting point for building more complex cross-chain applications using IBC and Solidity. + +## Implementation + +The interface used by the implementation is the one described in [the Solidity integration section.](../integration/solidity) + + diff --git a/site/src/content/docs/docs/integration/solidity.mdx b/site/src/content/docs/docs/integration/solidity.mdx new file mode 100644 index 0000000000..158052c830 --- /dev/null +++ b/site/src/content/docs/docs/integration/solidity.mdx @@ -0,0 +1,37 @@ +--- +title: "IBC Enabled Solidity" +--- + +import EmbedCodeSnippet from "#/components/EmbedCodeSnippet.astro"; + +Enabling IBC for Solidity contracts has never been easier. We provide a +simple interface for IBC Channel/Packet lifecycle that protocols must implement +in order to open channels, send and receive packets. + +:::danger + +Contracts MUST use an **onlyIBC** modifier for IBC callbacks. This modifier ensures that the caller is the IBC handler contract. + +::: + +[The interface is following the official IBC ICS26 specification](https://github.com/cosmos/ibc/blob/2921c5cec7b18e4ef77677e16a6b693051ae3b35/spec/core/ics-026-routing-module/README.md) + + + +On top of this interface, we also provide an abstract contract that we recommend to use. + +:::note + +An example implementation is available [in the Solidity demo.](../demos/pingpong) + +::: + + diff --git a/site/src/lib/utilities.ts b/site/src/lib/utilities.ts index 938a49f635..0cbd3b2828 100644 --- a/site/src/lib/utilities.ts +++ b/site/src/lib/utilities.ts @@ -1,3 +1,9 @@ +import url from "node:url"; +import path from "node:path"; +import fs from "node:fs/promises"; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + export function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -9,3 +15,9 @@ export const roundNumber = (_number: number, decimalPlaces: number) => export const generateRandomNumber = (min: number, max: number) => Math.random() * (max - min) + min; + +export async function getFileContent({ filepath }: { filepath: string }) { + const file = path.resolve(__dirname, filepath); + if (!fs.stat(file)) throw new Error(`File not found: ${file}`); + return await fs.readFile(file, { encoding: "utf8" }); +}