├── .env.example ├── .gitignore ├── .gitmodules ├── Justfile ├── README.md ├── config.json ├── config ├── x-ballot-nft-UC.json └── x-ballot-nft.json ├── contracts ├── XCounter.sol ├── XCounterUC.sol ├── arguments.js ├── base │ ├── CustomChanIbcApp.sol │ ├── GeneralMiddleware.sol │ └── UniversalChanIbcApp.sol ├── x-ballot-nft-UC │ ├── XBallotUC.sol │ └── XProofOfVoteNFTUC.sol └── x-ballot-nft │ ├── XBallot.sol │ └── XProofOfVoteNFT.sol ├── foundry.toml ├── hardhat.config.js ├── ibc-app-template.md ├── ibc.json ├── img └── gh_template.png ├── package-lock.json ├── package.json ├── remappings.txt └── scripts ├── deploy.js ├── private ├── _create-channel-config.js ├── _create-channel.js ├── _deploy-config.js ├── _events.js ├── _helpers.js ├── _sanity-check-custom.js ├── _sanity-check-universal.js ├── _sanity-check.js ├── _send-packet-config.js ├── _send-vote-info-config.js ├── _set-contracts-config.js ├── _switch-clients.js ├── _update-vibc-address.js └── _vibc-helpers.js ├── send-packet.js ├── send-universal-packet.js └── x-ballot-nft ├── _app-events-UC.js ├── _app-events.js ├── send-universal-vote-info.js └── send-vote-info.js /.env.example: -------------------------------------------------------------------------------- 1 | # Make sure to rename this file to .env before adding your private keys!!! 2 | PRIVATE_KEY_1='' 3 | PRIVATE_KEY_2='' 4 | PRIVATE_KEY_3='' 5 | # Add more if your project requires more private keys 6 | 7 | # API keys for developer tooling and infra 8 | OP_ALCHEMY_API_KEY='' 9 | BASE_ALCHEMY_API_KEY='' 10 | OP_BLOCKSCOUT_API_KEY='' 11 | BASE_BLOCKSCOUT_API_KEY='' 12 | # TENDERLY_TOKEN='' 13 | 14 | # Contract addresses last updated on 2024-03-05, for public testnet launch 15 | OP_DISPATCHER='0x58f1863f75c9db1c7266dc3d7b43832b58f35e83' 16 | BASE_DISPATCHER='0xfc1d3e02e00e0077628e8cc9edb6812f95db05dc' 17 | 18 | OP_UC_MW='0x34a0e37cCCEdaC70EC1807e5a1f6A4a91D4AE0Ce' 19 | BASE_UC_MW='0x50E32e236bfE4d514f786C9bC80061637dd5AF98' 20 | 21 | # Contract addresses for the sim-client 22 | OP_DISPATCHER_SIM="0x6C9427E8d770Ad9e5a493D201280Cc178125CEc0" 23 | BASE_DISPATCHER_SIM="0x0dE926fE2001B2c96e9cA6b79089CEB276325E9F" 24 | 25 | OP_UC_MW_SIM='0xC3318ce027C560B559b09b1aA9cA4FEBDDF252F5' 26 | BASE_UC_MW_SIM='0x5031fb609569b67608Ffb9e224754bb317f174cD' 27 | 28 | # Configuration file the scripts will use, defaulting to config.json when not set 29 | CONFIG_PATH='config.json' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | # Foundry files 17 | /out 18 | /cache_forge -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/vibc-core-smart-contracts"] 2 | path = lib/vibc-core-smart-contracts 3 | url = https://github.com/open-ibc/vibc-core-smart-contracts 4 | [submodule "lib/forge-std"] 5 | path = lib/forge-std 6 | url = https://github.com/foundry-rs/forge-std 7 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | # Install dependencies 2 | install: 3 | echo "Installing dependencies" 4 | npm install 5 | forge install --shallow 6 | 7 | # Compile contracts using the specified compiler or default to Hardhat 8 | # The compiler argument is optional; if not provided, it defaults to "hardhat". 9 | # Usage: just compile [compiler] 10 | compile COMPILER='hardhat': 11 | #!/usr/bin/env sh 12 | if test "{{COMPILER}}" = "hardhat"; then 13 | echo "Compiling contracts with Hardhat..." 14 | npx hardhat compile 15 | elif test "{{COMPILER}}" = "foundry"; then 16 | echo "Compiling contracts with Foundry..." 17 | forge build 18 | else 19 | echo "Unknown compiler: {{COMPILER}}" 20 | exit 1 21 | fi 22 | 23 | # Update the config.json file with the contract type for a specified chain/rollup 24 | # The chain and contract-type arguments are REQUIRED; 25 | # The universal argument is optional; if not provided, it defaults to "true". 26 | # It indicates whether the contracts to deploy are using custom or universal IBC channels to send packets. 27 | # Usage: just set-contracts [chain] [contract-type] [universal] 28 | set-contracts CHAIN CONTRACT_TYPE UNIVERSAL='true': 29 | echo "Updating config.json with contract type..." 30 | node scripts/private/_set-contracts-config.js {{CHAIN}} {{CONTRACT_TYPE}} {{UNIVERSAL}} 31 | 32 | # Deploy the contracts in the /contracts folder using Hardhat and updating the config.json file 33 | # The source and destination arguments are REQUIRED; 34 | # Usage: just deploy [source] [destination] 35 | deploy SOURCE DESTINATION: 36 | echo "Deploying contracts with Hardhat..." 37 | node scripts/private/_deploy-config.js {{SOURCE}} {{DESTINATION}} 38 | 39 | # Run the sanity check script to verify that configuration (.env) files match with deployed contracts' stored values 40 | # Usage: just sanity-check 41 | sanity-check: 42 | echo "Running sanity check..." 43 | node scripts/private/_sanity-check.js 44 | 45 | # Update the dispatcher or universal channel handler address on the IBC application, with that from the .env file 46 | # The chain argument is REQUIRED; 47 | # Usage: just update-vibc [chain] 48 | update-vibc CHAIN: 49 | echo "Updating the dispatcher or universal channel handler address..." 50 | npx hardhat run scripts/private/_update-vibc-address.js --network {{CHAIN}} 51 | 52 | # Create a channel by triggering a channel handshake from the source and with parameters found in the config.json file 53 | # Usage: just create-channel 54 | create-channel: 55 | echo "Attempting to create a channel with the values from the config..." 56 | node scripts/private/_create-channel-config.js 57 | 58 | # Send a packet over the universal channel or a custom channel as defined in the config.json file 59 | # The source argument is REQUIRED; 60 | # Usage: just send-packet [source] 61 | send-packet SOURCE: 62 | echo "Sending a packet with the values from the config..." 63 | node scripts/private/_send-packet-config.js {{SOURCE}} 64 | 65 | # Send a packet over the universal channel or a custom channel as defined in the config.json file 66 | # The source argument is REQUIRED; 67 | # Usage: just send-packet [source] 68 | send-vote-info: 69 | echo "Sending an IBC packet with vote information to mint a Proof-of-Vote NFT..." 70 | node scripts/private/_send-vote-info-config.js 71 | 72 | # Switch between the sim client and the client with proofs 73 | # Usage: just switch-client 74 | switch-client: 75 | echo "Switching between sim client and client with proofs..." 76 | npx hardhat run scripts/private/_update-vibc-address.js --network optimism 77 | npx hardhat run scripts/private/_update-vibc-address.js --network base 78 | node scripts/private/_switch-clients.js 79 | 80 | # Run the full E2E flow by setting the contracts, deploying them, creating a channel, and sending a packet 81 | # Usage: just do-it 82 | do-it: 83 | echo "Running the full E2E flow..." 84 | just set-contracts optimism XCounter false && just set-contracts base XCounter false 85 | just deploy optimism base 86 | just sanity-check 87 | just create-channel 88 | just send-packet optimism 89 | echo "You've done it!" 90 | 91 | # Clean up the environment by removing the artifacts and cache folders and running the forge clean command 92 | # Usage: just clean 93 | clean: 94 | echo "Cleaning up environment..." 95 | rm -rf artifacts cache 96 | forge clean 97 | 98 | # Fully clean the environment by removing the artifacts, the dependencies, and cache folders and running the forge clean-all command 99 | # Usage: just clean-all 100 | clean-all: 101 | echo "Cleaning up environment..." 102 | rm -rf artifacts cache 103 | forge clean 104 | rm -rf node_modules 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo dApps for Polymer 2 | 3 | Welcome to the official repository for [Polymer](https://polymerlabs.org) demo applications! This repository serves as a centralized hub for the official (maintained by the Polymer Labs team and used in the official docs) demo apps, showcasing the capabilities and use cases of Polymer x [IBC](https://ibcprotocol.dev) interoperability. 4 | 5 | ### 🦸🏼🦸🏾‍♂️ Community projects 🦸🏾‍♀️🦸🏻 6 | 7 | We highly encourage our community to build new demos and showcase them! To help visibility of these projects, they can be added to the [community demo dApps repo](https://github.com/polymerdevs/community-demo-dapps) in the PolymerDevs GitHub org. 8 | 9 | ## 📚 Documentation 10 | 11 | This repository is forked from [the IBC app template repo](https://open-ibc/ibc-app-solidity-template) so check it out if you haven't or find its docs [here](ibc-app-template.md). 12 | 13 | There's some basic information here in the README but all of the dApps found here are documented more extensively in [the official Polymer documentation](https://docs.polymerlabs.org/docs/quickstart/start). 14 | 15 | ## 📋 Prerequisites 16 | 17 | The demo dapps repository has been based off of the project structure found in the [IBC app template for Solidity](https://github.com/open-ibc/ibc-app-solidity-template) so it has the same requirements: 18 | 19 | - Have [git](https://git-scm.com/downloads) installed 20 | - Have [node](https://nodejs.org) installed (v18+) 21 | - Have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (Hardhat will be installed when running `npm install`) 22 | - Have [just](https://just.systems/man/en/chapter_1.html) installed (recommended but not strictly necessary) 23 | 24 | Some basic knowledge of all of these tools is also required, although the details are abstracted away for basic usage. 25 | 26 | ## 🧱 Repository Structure 27 | 28 | This repository has a project structure that set it up to be compatible with the Hardhat and Foundry EVM development environments, as in the [IBC app template for Solidity repo](https://github.com/open-ibc/ibc-app-solidity-template). 29 | 30 | The main logic specific to the dApps can be found in the `/contracts` directory: 31 | ```bash 32 | # Example tree structure with only one custom dApp, x-ballot-nft 33 | contracts 34 | ├── XCounter.sol 35 | ├── XCounterUC.sol 36 | ├── base 37 | │   ├── CustomChanIbcApp.sol 38 | │   ├── GeneralMiddleware.sol 39 | │   └── UniversalChanIbcApp.sol 40 | ├── x-ballot-nft 41 | │   ├── XBallot.sol 42 | │   └── XProofOfVoteNFT.sol 43 | └── x-ballot-nft-UC 44 | ├── XBallotUC.sol 45 | └── XProofOfVoteNFTUC.sol 46 | ``` 47 | 48 | The `/contracts` directory always contains a `/base` directory where custom developed contract will inherit from to quickly get IBC compatibility. Additionally, you'll find the x-counter example from the IBC app template repo. 49 | 50 | The additional folders are the custom developed applications. 51 | 52 | ## 🦮 Dependency management 53 | 54 | This repo depends on Polymer's [vibc-core-smart-contracts](https://github.com/open-ibc/vibc-core-smart-contracts) which are tracked as git submodules. 55 | 56 | There are two ways to install these dependencies. 57 | 58 | ### Using IBC app template just recipe 59 | 60 | If you have Node and Foundry installed, simply run: 61 | ```bash 62 | just install 63 | ``` 64 | 65 | To install the required dependencies. 66 | 67 | ### Using git submodules directly 68 | 69 | If you prefer not to use Foundry / Forge, you can use git submodules directly. 70 | 71 | After cloning the repo, run this command additionally: 72 | ```bash 73 | git submodule update --init --recursive 74 | ``` 75 | 76 | Find more documentation on using git submodules from the [official docs](https://git-scm.com/book/en/v2/Git-Tools-Submodules) or [in this tutorial](https://www.atlassian.com/git/tutorials/git-submodule). 77 | 78 | Also run `npm install` additionally. 79 | 80 | ## 💻 Interacting with demos 81 | 82 | To interact with any of the demos, there's a couple of things to do. (Assuming the dependencies have been installed). 83 | 84 | 1. Convert the `.env.example` file into an `.env` file. This will ignore the file for future git commits as well as expose the environment variables. Add your private keys and update the other values if you want to customize (advanced usage feature). 85 | 86 | 2. Check out the configuration file; `config.json` or the alternate configs in the `/config` directory. Depending on which application you'll want to interact with, update the contract type in the `deploy` field to the desired contract (use the `just set-contracts` recipe for that). For example, when interacting with x-ballot-nft-UC you would put 'XBallotUC' and 'XProofOfVoteNFTUC' for optimism or base (which one you pick where does not matter). 87 | 88 | 3. Once the configuration file is updated and saved, you can look at the `just` commands with `just --list`. Alternatively follow the instructions in [the official Polymer documentation](https://docs.polymerlabs.org/docs/quickstart/start). 89 | 90 | ## 🤝 Contributing 91 | 92 | We welcome and encourage contributions from our community! Here’s how you can contribute. 93 | 94 | ### Option 1: Improve IBC Solidity app template 95 | 96 | Have ideas how to improve the project environment itself, not just application logic? Feel free to drop an issue or a PR (after forking) in the [IBC app template for Solidity](https://github.com/open-ibc/ibc-app-solidity-template) repository. 97 | 98 | ### Option 2: Contribute to Polymer official demo-dapps 99 | 100 | Have you seen an issue with any of the demo apps listed in this repo? Feel free to drop an issue or implement the changes yourself. Also if you feel you have a great addition that could live in the official docs, drop a PR and we'll investigate (it's possible we'll ask you to add it to the [community demo dApps repo](https://github.com/polymerdevs/community-demo-dapps) instead). 101 | 102 | Please follow these steps when you do submit code changes: 103 | 104 | 1. **Fork the Repository:** Start by forking this repository. 105 | 2. **Add Your Demo App or make changes:** Place your demo app in the `/contracts` directory (separate directory for your project) or update the code that can be improved. 106 | 3. **Create a Pull Request:** Once you've added your demo or updated the code, create a pull request to the main repository with a detailed description of your app. 107 | 108 | ## 💡 Questions or Suggestions? 109 | 110 | Feel free to open an issue for questions, suggestions, or discussions related to this repository. For further discussion as well as a showcase of some community projects, check out the [Polymer developer forum](https://forum.polymerlabs.org). 111 | 112 | Thank you for being a part of our community! 113 | 114 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "proofsEnabled": false, 3 | "deploy": { 4 | "optimism": "XCounterUC", 5 | "base": "XCounterUC" 6 | }, 7 | "isUniversal": true, 8 | "createChannel": { 9 | "srcChain": "optimism", 10 | "srcAddr": "0x1234567890AbCdEf1234567890aBcDeF12345678", 11 | "dstChain": "base", 12 | "dstAddr": "0x1234567890AbCdEf1234567890aBcDeF12345678", 13 | "version": "1.0", 14 | "ordering": 0, 15 | "fees": false 16 | }, 17 | "sendPacket": { 18 | "optimism": { 19 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 20 | "channelId": "channel-n", 21 | "timeout": 36000 22 | }, 23 | "base": { 24 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 25 | "channelId": "channel-n", 26 | "timeout": 36000 27 | } 28 | }, 29 | "sendUniversalPacket": { 30 | "optimism": { 31 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 32 | "channelId": "channel-x", 33 | "timeout": 36000 34 | }, 35 | "base": { 36 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 37 | "channelId": "channel-y", 38 | "timeout": 36000 39 | } 40 | }, 41 | "backup": {} 42 | } -------------------------------------------------------------------------------- /config/x-ballot-nft-UC.json: -------------------------------------------------------------------------------- 1 | { 2 | "proofsEnabled": false, 3 | "deploy": { 4 | "optimism": "XBallotUC", 5 | "base": "XProofOfVoteNFTUC" 6 | }, 7 | "isUniversal": true, 8 | "createChannel": { 9 | "srcChain": "optimism", 10 | "srcAddr": "0x1234567890AbCdEf1234567890aBcDeF12345678", 11 | "dstChain": "base", 12 | "dstAddr": "0x1234567890AbCdEf1234567890aBcDeF12345678", 13 | "version": "1.0", 14 | "ordering": 0, 15 | "fees": false 16 | }, 17 | "sendPacket": { 18 | "optimism": { 19 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 20 | "channelId": "channel-n", 21 | "timeout": 36000 22 | }, 23 | "base": { 24 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 25 | "channelId": "channel-n", 26 | "timeout": 36000 27 | } 28 | }, 29 | "sendUniversalPacket": { 30 | "optimism": { 31 | "portAddr": "0x80FA9cB428b3873df748628Ca992194451473159", 32 | "channelId": "channel-10", 33 | "timeout": 36000 34 | }, 35 | "base": { 36 | "portAddr": "0x4EF4C3C0Fe45a24b531e36C53e1AbF962eDBCFEb", 37 | "channelId": "channel-11", 38 | "timeout": 36000 39 | } 40 | }, 41 | "backup": {} 42 | } -------------------------------------------------------------------------------- /config/x-ballot-nft.json: -------------------------------------------------------------------------------- 1 | { 2 | "proofsEnabled": false, 3 | "deploy": { 4 | "optimism": "XBallot", 5 | "base": "XProofOfVoteNFT" 6 | }, 7 | "isUniversal": false, 8 | "createChannel": { 9 | "srcChain": "optimism", 10 | "srcAddr": "0xC4B32EE322beA5F48b59b7B3A13B2281B7BA6244", 11 | "dstChain": "base", 12 | "dstAddr": "0x051A88c9fBe5934dfE3Af835D23592C2d8cca1fF", 13 | "version": "1.0", 14 | "ordering": 0, 15 | "fees": false 16 | }, 17 | "sendPacket": { 18 | "optimism": { 19 | "portAddr": "0xC4B32EE322beA5F48b59b7B3A13B2281B7BA6244", 20 | "channelId": "channel-37224", 21 | "timeout": 36000 22 | }, 23 | "base": { 24 | "portAddr": "0x051A88c9fBe5934dfE3Af835D23592C2d8cca1fF", 25 | "channelId": "channel-37225", 26 | "timeout": 36000 27 | } 28 | }, 29 | "sendUniversalPacket": { 30 | "optimism": { 31 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 32 | "channelId": "channel-x", 33 | "timeout": 36000 34 | }, 35 | "base": { 36 | "portAddr": "0x1234567890abcdef1234567890abcdef12345678", 37 | "channelId": "channel-y", 38 | "timeout": 36000 39 | } 40 | }, 41 | "backup": {} 42 | } -------------------------------------------------------------------------------- /contracts/XCounter.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import './base/CustomChanIbcApp.sol'; 6 | 7 | contract XCounter is CustomChanIbcApp { 8 | // app specific state 9 | uint64 public counter; 10 | mapping (uint64 => address) public counterMap; 11 | 12 | 13 | constructor(IbcDispatcher _dispatcher) CustomChanIbcApp(_dispatcher) {} 14 | 15 | // app specific logic 16 | function resetCounter() internal { 17 | counter = 0; 18 | } 19 | 20 | function increment() internal { 21 | counter++; 22 | } 23 | 24 | // IBC logic 25 | 26 | /** 27 | * @dev Sends a packet with the caller address over a specified channel. 28 | * @param channelId The ID of the channel (locally) to send the packet to. 29 | * @param timeoutSeconds The timeout in seconds (relative). 30 | */ 31 | 32 | function sendPacket( bytes32 channelId, uint64 timeoutSeconds) external { 33 | // incrementing counter on source chain 34 | increment(); 35 | 36 | // encoding the caller address to update counterMap on destination chain 37 | bytes memory payload = abi.encode(msg.sender); 38 | 39 | // setting the timeout timestamp at 10h from now 40 | uint64 timeoutTimestamp = uint64((block.timestamp + timeoutSeconds) * 1000000000); 41 | 42 | // calling the Dispatcher to send the packet 43 | dispatcher.sendPacket(channelId, payload, timeoutTimestamp); 44 | } 45 | 46 | /** 47 | * @dev Packet lifecycle callback that implements packet receipt logic and returns and acknowledgement packet. 48 | * MUST be overriden by the inheriting contract. 49 | * 50 | * @param packet the IBC packet encoded by the source and relayed by the relayer. 51 | */ 52 | function onRecvPacket(IbcPacket memory packet) external override onlyIbcDispatcher returns (AckPacket memory ackPacket) { 53 | recvedPackets.push(packet); 54 | address _caller = abi.decode(packet.data, (address)); 55 | counterMap[packet.sequence] = _caller; 56 | 57 | increment(); 58 | 59 | return AckPacket(true, abi.encode(counter)); 60 | } 61 | 62 | /** 63 | * @dev Packet lifecycle callback that implements packet acknowledgment logic. 64 | * MUST be overriden by the inheriting contract. 65 | * 66 | * @param ack the acknowledgment packet encoded by the destination and relayed by the relayer. 67 | */ 68 | function onAcknowledgementPacket(IbcPacket calldata, AckPacket calldata ack) external override onlyIbcDispatcher { 69 | ackPackets.push(ack); 70 | 71 | (uint64 _counter) = abi.decode(ack.data, (uint64)); 72 | 73 | if (_counter != counter) { 74 | resetCounter(); 75 | } 76 | } 77 | 78 | /** 79 | * @dev Packet lifecycle callback that implements packet receipt logic and return and acknowledgement packet. 80 | * MUST be overriden by the inheriting contract. 81 | * NOT SUPPORTED YET 82 | * 83 | * @param packet the IBC packet encoded by the counterparty and relayed by the relayer 84 | */ 85 | function onTimeoutPacket(IbcPacket calldata packet) external override onlyIbcDispatcher { 86 | timeoutPackets.push(packet); 87 | // do logic 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /contracts/XCounterUC.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import "./base/UniversalChanIbcApp.sol"; 6 | 7 | contract XCounterUC is UniversalChanIbcApp { 8 | // application specific state 9 | uint64 public counter; 10 | mapping(uint64 => address) public counterMap; 11 | 12 | constructor(address _middleware) UniversalChanIbcApp(_middleware) {} 13 | 14 | // application specific logic 15 | function resetCounter() internal { 16 | counter = 0; 17 | } 18 | 19 | function increment() internal { 20 | counter++; 21 | } 22 | 23 | // IBC logic 24 | 25 | /** 26 | * @dev Sends a packet with the caller's address over the universal channel. 27 | * @param destPortAddr The address of the destination application. 28 | * @param channelId The ID of the channel to send the packet to. 29 | * @param timeoutSeconds The timeout in seconds (relative). 30 | */ 31 | function sendUniversalPacket(address destPortAddr, bytes32 channelId, uint64 timeoutSeconds) external { 32 | increment(); 33 | bytes memory payload = abi.encode(msg.sender, counter); 34 | 35 | uint64 timeoutTimestamp = uint64((block.timestamp + timeoutSeconds) * 1000000000); 36 | 37 | IbcUniversalPacketSender(mw).sendUniversalPacket( 38 | channelId, IbcUtils.toBytes32(destPortAddr), payload, timeoutTimestamp 39 | ); 40 | } 41 | 42 | /** 43 | * @dev Packet lifecycle callback that implements packet receipt logic and returns and acknowledgement packet. 44 | * MUST be overriden by the inheriting contract. 45 | * 46 | * @param channelId the ID of the channel (locally) the packet was received on. 47 | * @param packet the Universal packet encoded by the source and relayed by the relayer. 48 | */ 49 | function onRecvUniversalPacket(bytes32 channelId, UniversalPacket calldata packet) 50 | external 51 | override 52 | onlyIbcMw 53 | returns (AckPacket memory ackPacket) 54 | { 55 | recvedPackets.push(UcPacketWithChannel(channelId, packet)); 56 | 57 | (address payload, uint64 c) = abi.decode(packet.appData, (address, uint64)); 58 | counterMap[c] = payload; 59 | 60 | increment(); 61 | 62 | return AckPacket(true, abi.encode(counter)); 63 | } 64 | 65 | /** 66 | * @dev Packet lifecycle callback that implements packet acknowledgment logic. 67 | * MUST be overriden by the inheriting contract. 68 | * 69 | * @param channelId the ID of the channel (locally) the ack was received on. 70 | * @param packet the Universal packet encoded by the source and relayed by the relayer. 71 | * @param ack the acknowledgment packet encoded by the destination and relayed by the relayer. 72 | */ 73 | function onUniversalAcknowledgement(bytes32 channelId, UniversalPacket memory packet, AckPacket calldata ack) 74 | external 75 | override 76 | onlyIbcMw 77 | { 78 | ackPackets.push(UcAckWithChannel(channelId, packet, ack)); 79 | 80 | // decode the counter from the ack packet 81 | (uint64 _counter) = abi.decode(ack.data, (uint64)); 82 | 83 | if (_counter != counter) { 84 | resetCounter(); 85 | } 86 | } 87 | 88 | /** 89 | * @dev Packet lifecycle callback that implements packet receipt logic and return and acknowledgement packet. 90 | * MUST be overriden by the inheriting contract. 91 | * NOT SUPPORTED YET 92 | * 93 | * @param channelId the ID of the channel (locally) the timeout was submitted on. 94 | * @param packet the Universal packet encoded by the counterparty and relayed by the relayer 95 | */ 96 | function onTimeoutUniversalPacket(bytes32 channelId, UniversalPacket calldata packet) external override onlyIbcMw { 97 | timeoutPackets.push(UcPacketWithChannel(channelId, packet)); 98 | // do logic 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /contracts/arguments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "XCounter": [], 3 | "XCounterUC": [], 4 | // Add your contract types here, along with the list of custom constructor arguments 5 | // DO NOT ADD THE DISPATCHER OR UNIVERSAL CHANNEL HANDLER ADDRESSES HERE!!! 6 | // These will be added in the deploy script at $ROOT/scripts/deploy.js 7 | "XBallot": [ 8 | [ 9 | '0x506f6c796d657220697320612062726964676500000000000000000000000000', // "Polymer is a bridge" 10 | '0x506f6c796d6572206973206e6f74206120627269646765000000000000000000' // "Polymer is not a bridge" 11 | ] 12 | ], 13 | "XProofOfVoteNFT": [ 14 | 'https://picsum.photos/id/' 15 | ], 16 | "XBallotUC": [ 17 | [ 18 | '0x506f6c796d657220697320612062726964676500000000000000000000000000', // "Polymer is a bridge" 19 | '0x506f6c796d6572206973206e6f74206120627269646765000000000000000000' // "Polymer is not a bridge" 20 | ] 21 | ], 22 | "XProofOfVoteNFTUC": [ 23 | 'https://picsum.photos/id/' 24 | ] 25 | }; -------------------------------------------------------------------------------- /contracts/base/CustomChanIbcApp.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import '@open-ibc/vibc-core-smart-contracts/contracts/libs/Ibc.sol'; 6 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcReceiver.sol'; 7 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcDispatcher.sol'; 8 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/ProofVerifier.sol'; 9 | 10 | // CustomChanIbcApp is a contract that can be used as a base contract 11 | // for IBC-enabled contracts that send packets over a custom IBC channel. 12 | contract CustomChanIbcApp is IbcReceiverBase, IbcReceiver { 13 | // received packet as chain B 14 | IbcPacket[] public recvedPackets; 15 | // received ack packet as chain A 16 | AckPacket[] public ackPackets; 17 | // received timeout packet as chain A 18 | IbcPacket[] public timeoutPackets; 19 | 20 | struct ChannelMapping { 21 | bytes32 channelId; 22 | bytes32 cpChannelId; 23 | } 24 | 25 | // ChannelMapping array with the channel IDs of the connected channels 26 | ChannelMapping[] public connectedChannels; 27 | 28 | // add supported versions (format to be negotiated between apps) 29 | string[] supportedVersions = ['1.0']; 30 | 31 | constructor(IbcDispatcher _dispatcher) IbcReceiverBase(_dispatcher) {} 32 | 33 | function updateDispatcher(IbcDispatcher _dispatcher) external onlyOwner { 34 | dispatcher = _dispatcher; 35 | } 36 | 37 | function getConnectedChannels() external view returns (ChannelMapping[] memory) { 38 | return connectedChannels; 39 | } 40 | 41 | function updateSupportedVersions(string[] memory _supportedVersions) external onlyOwner { 42 | supportedVersions = _supportedVersions; 43 | } 44 | 45 | /** 46 | * @dev Implement a function to send a packet that calls the dispatcher.sendPacket function 47 | * It has the following function handle: 48 | * function sendPacket(bytes32 channelId, bytes calldata payload, uint64 timeoutTimestamp) external; 49 | */ 50 | 51 | /** 52 | * @dev Packet lifecycle callback that implements packet receipt logic and returns and acknowledgement packet. 53 | * MUST be overriden by the inheriting contract. 54 | * 55 | * @param packet the IBC packet encoded by the source and relayed by the relayer. 56 | */ 57 | function onRecvPacket(IbcPacket memory packet) external virtual onlyIbcDispatcher returns (AckPacket memory ackPacket) { 58 | recvedPackets.push(packet); 59 | // do logic 60 | return AckPacket(true, abi.encodePacked('{ "account": "account", "reply": "got the message" }')); 61 | } 62 | 63 | /** 64 | * @dev Packet lifecycle callback that implements packet acknowledgment logic. 65 | * MUST be overriden by the inheriting contract. 66 | * 67 | * @param packet the IBC packet encoded by the source and relayed by the relayer. 68 | * @param ack the acknowledgment packet encoded by the destination and relayed by the relayer. 69 | */ 70 | function onAcknowledgementPacket(IbcPacket calldata packet, AckPacket calldata ack) external virtual onlyIbcDispatcher { 71 | ackPackets.push(ack); 72 | // do logic 73 | } 74 | 75 | /** 76 | * @dev Packet lifecycle callback that implements packet receipt logic and return and acknowledgement packet. 77 | * MUST be overriden by the inheriting contract. 78 | * NOT SUPPORTED YET 79 | * 80 | * @param packet the IBC packet encoded by the counterparty and relayed by the relayer 81 | */ 82 | function onTimeoutPacket(IbcPacket calldata packet) external virtual onlyIbcDispatcher { 83 | timeoutPackets.push(packet); 84 | // do logic 85 | } 86 | 87 | /** 88 | * @dev Create a custom channel between two IbcReceiver contracts 89 | * @param local a CounterParty struct with the local chain's portId and version (channelId can be empty) 90 | * @param ordering the channel ordering (NONE, UNORDERED, ORDERED) equivalent to (0, 1, 2) 91 | * @param feeEnabled in production, you'll want to enable this to avoid spamming create channel calls (costly for relayers) 92 | * @param connectionHops 2 connection hops to connect to the destination via Polymer 93 | * @param counterparty the address of the destination chain contract you want to connect to 94 | * @param proof ICS23 proof struct with dummy data (only needed on ChanOpenTry) 95 | */ 96 | function createChannel( 97 | CounterParty calldata local, 98 | uint8 ordering, 99 | bool feeEnabled, 100 | string[] calldata connectionHops, 101 | CounterParty calldata counterparty, 102 | Ics23Proof calldata proof 103 | ) external virtual onlyOwner{ 104 | 105 | dispatcher.openIbcChannel( 106 | IbcChannelReceiver(address(this)), 107 | local, 108 | ChannelOrder(ordering), 109 | feeEnabled, 110 | connectionHops, 111 | counterparty, 112 | proof 113 | ); 114 | } 115 | 116 | function onOpenIbcChannel( 117 | string calldata version, 118 | ChannelOrder, 119 | bool, 120 | string[] calldata, 121 | CounterParty calldata counterparty 122 | ) external view virtual onlyIbcDispatcher returns (string memory selectedVersion) { 123 | if (bytes(counterparty.portId).length <= 8) { 124 | revert invalidCounterPartyPortId(); 125 | } 126 | /** 127 | * Version selection is determined by if the callback is invoked on behalf of ChanOpenInit or ChanOpenTry. 128 | * ChanOpenInit: self version should be provided whereas the counterparty version is empty. 129 | * ChanOpenTry: counterparty version should be provided whereas the self version is empty. 130 | * In both cases, the selected version should be in the supported versions list. 131 | */ 132 | bool foundVersion = false; 133 | selectedVersion = keccak256(abi.encodePacked(version)) == keccak256(abi.encodePacked('')) 134 | ? counterparty.version 135 | : version; 136 | for (uint256 i = 0; i < supportedVersions.length; i++) { 137 | if (keccak256(abi.encodePacked(selectedVersion)) == keccak256(abi.encodePacked(supportedVersions[i]))) { 138 | foundVersion = true; 139 | break; 140 | } 141 | } 142 | require(foundVersion, 'Unsupported version'); 143 | // if counterpartyVersion is not empty, then it must be the same foundVersion 144 | if (keccak256(abi.encodePacked(counterparty.version)) != keccak256(abi.encodePacked(''))) { 145 | require( 146 | keccak256(abi.encodePacked(counterparty.version)) == keccak256(abi.encodePacked(selectedVersion)), 147 | 'Version mismatch' 148 | ); 149 | } 150 | 151 | // do logic 152 | 153 | return selectedVersion; 154 | } 155 | 156 | function onConnectIbcChannel( 157 | bytes32 channelId, 158 | bytes32 counterpartyChannelId, 159 | string calldata counterpartyVersion 160 | ) external virtual onlyIbcDispatcher { 161 | // ensure negotiated version is supported 162 | bool foundVersion = false; 163 | for (uint256 i = 0; i < supportedVersions.length; i++) { 164 | if (keccak256(abi.encodePacked(counterpartyVersion)) == keccak256(abi.encodePacked(supportedVersions[i]))) { 165 | foundVersion = true; 166 | break; 167 | } 168 | } 169 | require(foundVersion, 'Unsupported version'); 170 | 171 | // do logic 172 | 173 | ChannelMapping memory channelMapping = ChannelMapping({ 174 | channelId: channelId, 175 | cpChannelId: counterpartyChannelId 176 | }); 177 | connectedChannels.push(channelMapping); 178 | } 179 | 180 | function onCloseIbcChannel(bytes32 channelId, string calldata, bytes32) external virtual onlyIbcDispatcher { 181 | // logic to determin if the channel should be closed 182 | bool channelFound = false; 183 | for (uint256 i = 0; i < connectedChannels.length; i++) { 184 | if (connectedChannels[i].channelId == channelId) { 185 | for (uint256 j = i; j < connectedChannels.length - 1; j++) { 186 | connectedChannels[j] = connectedChannels[j + 1]; 187 | } 188 | connectedChannels.pop(); 189 | channelFound = true; 190 | break; 191 | } 192 | } 193 | require(channelFound, 'Channel not found'); 194 | 195 | // do logic 196 | } 197 | 198 | /** 199 | * This func triggers channel closure from the dApp. 200 | * Func args can be arbitary, as long as dispatcher.closeIbcChannel is invoked propperly. 201 | */ 202 | function triggerChannelClose(bytes32 channelId) external virtual onlyOwner { 203 | dispatcher.closeIbcChannel(channelId); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /contracts/base/GeneralMiddleware.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import '@open-ibc/vibc-core-smart-contracts/contracts/libs/Ibc.sol'; 6 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcReceiver.sol'; 7 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcDispatcher.sol'; 8 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcMiddleware.sol'; 9 | 10 | contract GeneralMiddleware is IbcMwUser, IbcMiddleware, IbcMwEventsEmitter { 11 | /** 12 | * @dev MW_ID is the ID of MW contract on all supported virtual chains. 13 | * MW_ID must: 14 | * - be globally unique, ie. no two MWs should have the same MW_ID registered on Polymer chain. 15 | * - be identical on all supported virtual chains. 16 | * - be identical on one virtual chain across multiple deployed MW instances. Each MW instance belong exclusively to one MW stack. 17 | * - be 1 << N, where N is a non-negative integer, and not in conflict with other MWs. 18 | */ 19 | uint256 public MW_ID; 20 | 21 | /** 22 | * @param _middleware The middleware contract address this contract sends packets to and receives packets from. 23 | */ 24 | 25 | constructor(uint256 mwId, address _middleware) IbcMwUser(_middleware) { 26 | MW_ID = mwId; 27 | } 28 | 29 | function sendUniversalPacket( 30 | bytes32 channelId, 31 | bytes32 destPortAddr, 32 | bytes calldata appData, 33 | uint64 timeoutTimestamp 34 | ) external override { 35 | _sendPacket(channelId, IbcUtils.toBytes32(msg.sender), destPortAddr, 0, appData, timeoutTimestamp); 36 | } 37 | 38 | function sendMWPacket( 39 | bytes32 channelId, 40 | bytes32 srcPortAddr, 41 | bytes32 destPortAddr, 42 | uint256 srcMwIds, 43 | bytes calldata appData, 44 | uint64 timeoutTimestamp 45 | ) external override { 46 | _sendPacket(channelId, srcPortAddr, destPortAddr, srcMwIds, appData, timeoutTimestamp); 47 | } 48 | 49 | function onRecvMWPacket( 50 | bytes32 channelId, 51 | UniversalPacket calldata ucPacket, 52 | // address srcPortAddr, 53 | // address destPortAddr, 54 | // 0-based receiver middleware index in the MW stack. 55 | // 0 for the first MW directly called by UniversalChannel MW. 56 | // `mwIndex-1` is the last MW that delivers the packet to the non-MW dApp. 57 | // Each mw in the stack must increment mwIndex by 1 before calling the next MW. 58 | uint256 mwIndex, 59 | // bytes calldata appData, 60 | address[] calldata mwAddrs 61 | ) external onlyIbcMw returns (AckPacket memory ackPacket) { 62 | // extra MW custom logic here to process packet, eg. emit MW events, mutate state, etc. 63 | // implementer can emit custom data fields suitable for their use cases. 64 | // Here we use MW_ID as the custom MW data field. 65 | emit RecvMWPacket( 66 | channelId, 67 | ucPacket.srcPortAddr, 68 | ucPacket.destPortAddr, 69 | MW_ID, 70 | ucPacket.appData, 71 | abi.encodePacked(MW_ID) 72 | ); 73 | 74 | if (mwIndex == mwAddrs.length - 1) { 75 | // last MW in the stack, deliver packet to dApp 76 | return 77 | IbcUniversalPacketReceiver(IbcUtils.toAddress(ucPacket.destPortAddr)).onRecvUniversalPacket( 78 | channelId, 79 | ucPacket 80 | ); 81 | } else { 82 | // send packet to next MW 83 | return IbcMwPacketReceiver(mwAddrs[mwIndex + 1]).onRecvMWPacket(channelId, ucPacket, mwIndex + 1, mwAddrs); 84 | } 85 | } 86 | 87 | function onRecvMWAck( 88 | bytes32 channelId, 89 | UniversalPacket calldata ucPacket, 90 | // 0-based receiver middleware index in the MW stack. 91 | // 0 for the first MW directly called by UniversalChannel MW. 92 | // `mwIndex-1` is the last MW that delivers the packet to the non-MW dApp. 93 | // Each mw in the stack must increment mwIndex by 1 before calling the next MW. 94 | uint256 mwIndex, 95 | address[] calldata mwAddrs, 96 | AckPacket calldata ack 97 | ) external override onlyIbcMw { 98 | // extra MW custom logic here to process packet, eg. emit MW events, mutate state, etc. 99 | // implementer can emit custom data fields suitable for their use cases. 100 | // Here we use MW_ID as the custom MW data field. 101 | emit RecvMWAck( 102 | channelId, 103 | ucPacket.srcPortAddr, 104 | ucPacket.destPortAddr, 105 | MW_ID, 106 | ucPacket.appData, 107 | abi.encodePacked(MW_ID), 108 | ack 109 | ); 110 | 111 | if (mwIndex == mwAddrs.length - 1) { 112 | // last MW in the stack, deliver ack to dApp 113 | IbcUniversalPacketReceiver(IbcUtils.toAddress(ucPacket.srcPortAddr)).onUniversalAcknowledgement( 114 | channelId, 115 | ucPacket, 116 | ack 117 | ); 118 | } else { 119 | // send ack to next MW 120 | IbcMwPacketReceiver(mwAddrs[mwIndex + 1]).onRecvMWAck(channelId, ucPacket, mwIndex + 1, mwAddrs, ack); 121 | } 122 | } 123 | 124 | function onRecvMWTimeout( 125 | bytes32 channelId, 126 | UniversalPacket calldata ucPacket, 127 | uint256 mwIndex, 128 | address[] calldata mwAddrs 129 | ) external override onlyIbcMw { 130 | // extra MW custom logic here to process packet, eg. emit MW events, mutate state, etc. 131 | // implementer can emit custom data fields suitable for their use cases. 132 | // Here we use MW_ID as the custom MW data field. 133 | emit RecvMWTimeout( 134 | channelId, 135 | ucPacket.srcPortAddr, 136 | ucPacket.destPortAddr, 137 | MW_ID, 138 | ucPacket.appData, 139 | abi.encodePacked(MW_ID) 140 | ); 141 | 142 | if (mwIndex == mwAddrs.length - 1) { 143 | // last MW in the stack, deliver timeout to dApp 144 | IbcUniversalPacketReceiver(IbcUtils.toAddress(ucPacket.srcPortAddr)).onTimeoutUniversalPacket( 145 | channelId, 146 | ucPacket 147 | ); 148 | } else { 149 | // send timeout to next MW 150 | IbcMwPacketReceiver(mwAddrs[mwIndex + 1]).onRecvMWTimeout(channelId, ucPacket, mwIndex + 1, mwAddrs); 151 | } 152 | } 153 | 154 | function onRecvUniversalPacket( 155 | bytes32 channelId, 156 | UniversalPacket calldata ucPacket 157 | ) external override onlyIbcMw returns (AckPacket memory ackPacket) {} 158 | 159 | function onUniversalAcknowledgement( 160 | bytes32 channelId, 161 | UniversalPacket memory packet, 162 | AckPacket calldata ack 163 | ) external override onlyIbcMw {} 164 | 165 | function onTimeoutUniversalPacket(bytes32 channelId, UniversalPacket calldata packet) external override onlyIbcMw {} 166 | 167 | // internal function to send packet to next MW with MW Ids bit flipped 168 | // param srcMwIds: MW ID bitmap excluding this MW's ID 169 | function _sendPacket( 170 | bytes32 channelId, 171 | bytes32 srcPortAddr, 172 | bytes32 destPortAddr, 173 | uint256 srcMwIds, 174 | bytes calldata appData, 175 | uint64 timeoutTimestamp 176 | ) internal virtual { 177 | // extra MW custom logic here to process packet, eg. emit MW events, mutate state, etc. 178 | // implementer can emit custom data fields suitable for their use cases. 179 | // Here we use MW_ID as the custom MW data field. 180 | emit SendMWPacket( 181 | channelId, 182 | srcPortAddr, 183 | destPortAddr, 184 | MW_ID, 185 | appData, 186 | timeoutTimestamp, 187 | abi.encodePacked(MW_ID) 188 | ); 189 | 190 | // send packet to next MW 191 | IbcMwPacketSender(mw).sendMWPacket( 192 | channelId, 193 | srcPortAddr, 194 | destPortAddr, 195 | srcMwIds | MW_ID, 196 | appData, 197 | timeoutTimestamp 198 | ); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /contracts/base/UniversalChanIbcApp.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import '@open-ibc/vibc-core-smart-contracts/contracts/libs/Ibc.sol'; 6 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcReceiver.sol'; 7 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcDispatcher.sol'; 8 | import '@open-ibc/vibc-core-smart-contracts/contracts/interfaces/IbcMiddleware.sol'; 9 | 10 | // UniversalChanIbcApp is a contract that can be used as a base contract 11 | // for IBC-enabled contracts that send packets over the universal channel. 12 | contract UniversalChanIbcApp is IbcMwUser, IbcUniversalPacketReceiver { 13 | struct UcPacketWithChannel { 14 | bytes32 channelId; 15 | UniversalPacket packet; 16 | } 17 | 18 | struct UcAckWithChannel { 19 | bytes32 channelId; 20 | UniversalPacket packet; 21 | AckPacket ack; 22 | } 23 | 24 | // received packet as chain B 25 | UcPacketWithChannel[] public recvedPackets; 26 | // received ack packet as chain A 27 | UcAckWithChannel[] public ackPackets; 28 | // received timeout packet as chain A 29 | UcPacketWithChannel[] public timeoutPackets; 30 | 31 | constructor(address _middleware) IbcMwUser(_middleware) {} 32 | 33 | /** 34 | * @dev Implement a function to send a packet that calls the IbcUniversalPacketSender(mw).sendUniversalPacket function 35 | * It has the following function handle: 36 | * function sendUniversalPacket( 37 | bytes32 channelId, 38 | bytes32 destPortAddr, 39 | bytes calldata appData, 40 | uint64 timeoutTimestamp 41 | ) external; 42 | */ 43 | 44 | /** 45 | * @dev Packet lifecycle callback that implements packet receipt logic and returns and acknowledgement packet. 46 | * MUST be overriden by the inheriting contract. 47 | * 48 | * @param channelId the ID of the channel (locally) the packet was received on. 49 | * @param packet the Universal packet encoded by the source and relayed by the relayer. 50 | */ 51 | function onRecvUniversalPacket( 52 | bytes32 channelId, 53 | UniversalPacket calldata packet 54 | ) external virtual onlyIbcMw returns (AckPacket memory ackPacket) { 55 | recvedPackets.push(UcPacketWithChannel(channelId, packet)); 56 | // 1. decode the packet.data 57 | // 2. do logic 58 | // 3. encode the ack packet (encoding format should be agreed between the two applications) 59 | // below is an example, the actual ackpacket data should be implemented by the contract developer 60 | return AckPacket(true, abi.encodePacked(address(this), IbcUtils.toAddress(packet.srcPortAddr), 'ack-', packet.appData)); 61 | } 62 | 63 | /** 64 | * @dev Packet lifecycle callback that implements packet acknowledgment logic. 65 | * MUST be overriden by the inheriting contract. 66 | * 67 | * @param channelId the ID of the channel (locally) the ack was received on. 68 | * @param packet the Universal packet encoded by the source and relayed by the relayer. 69 | * @param ack the acknowledgment packet encoded by the destination and relayed by the relayer. 70 | */ 71 | function onUniversalAcknowledgement( 72 | bytes32 channelId, 73 | UniversalPacket memory packet, 74 | AckPacket calldata ack 75 | ) external virtual onlyIbcMw { 76 | ackPackets.push(UcAckWithChannel(channelId, packet, ack)); 77 | // 1. decode the ack.data 78 | // 2. do logic 79 | } 80 | 81 | /** 82 | * @dev Packet lifecycle callback that implements packet receipt logic and return and acknowledgement packet. 83 | * MUST be overriden by the inheriting contract. 84 | * NOT SUPPORTED YET 85 | * 86 | * @param channelId the ID of the channel (locally) the timeout was submitted on. 87 | * @param packet the Universal packet encoded by the counterparty and relayed by the relayer 88 | */ 89 | function onTimeoutUniversalPacket(bytes32 channelId, UniversalPacket calldata packet) external virtual onlyIbcMw { 90 | timeoutPackets.push(UcPacketWithChannel(channelId, packet)); 91 | // do logic 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/x-ballot-nft-UC/XBallotUC.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import '../base/UniversalChanIbcApp.sol'; 6 | 7 | contract XBallotUC is UniversalChanIbcApp { 8 | enum IbcPacketStatus {UNSENT, SENT, ACKED, TIMEOUT} 9 | 10 | struct Voter { 11 | uint weight; // weight is accumulated by delegation 12 | bool voted; // if true, that person already voted; 13 | address delegate; // person delegated to 14 | uint vote; // index of the voted proposal 15 | // additional 16 | IbcPacketStatus ibcPacketStatus; 17 | uint[] voteNFTIds; 18 | } 19 | 20 | struct Proposal { 21 | // If you can limit the length to a certain number of bytes, 22 | // always use one of bytes1 to bytes32 because they are much cheaper 23 | bytes32 name; // short name (up to 32 bytes) 24 | uint voteCount; // number of accumulated votes 25 | } 26 | 27 | address public chairperson; 28 | 29 | mapping(address => Voter) public voters; 30 | 31 | Proposal[] public proposals; 32 | 33 | modifier onlyChairperson() { 34 | require(msg.sender == chairperson, "Not chairperson."); 35 | _; 36 | } 37 | 38 | event Voted(address indexed voter, uint proposal); // Exposing the vote information for debugging; hide in production if you want private voting 39 | event SendVoteInfo(address indexed destPortAddr, address indexed voter, address indexed recipient, uint proposal); 40 | event AckNFTMint(address indexed destPortAddr, address indexed voter, uint voteNFTid); 41 | 42 | 43 | constructor(address _middleware, bytes32[] memory proposalNames) UniversalChanIbcApp(_middleware) { 44 | chairperson = msg.sender; 45 | voters[chairperson].weight = 1; 46 | 47 | for (uint i = 0; i < proposalNames.length; i++) { 48 | // 'Proposal({...})' creates a temporary 49 | // Proposal object and 'proposals.push(...)' 50 | // appends it to the end of 'proposals'. 51 | proposals.push(Proposal({ 52 | name: proposalNames[i], 53 | voteCount: 0 54 | })); 55 | } 56 | } 57 | 58 | /** 59 | * @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. 60 | * @param voter address of voter 61 | */ 62 | function giveRightToVote(address voter) public { 63 | require( 64 | msg.sender == chairperson, 65 | "Only chairperson can give right to vote." 66 | ); 67 | require( 68 | !voters[voter].voted, 69 | "The voter already voted." 70 | ); 71 | require(voters[voter].weight == 0); 72 | voters[voter].weight = 1; 73 | } 74 | 75 | /** 76 | * @dev Delegate your vote to the voter 'to'. 77 | * @param to address to which vote is delegated 78 | */ 79 | function delegate(address to) public { 80 | Voter storage sender = voters[msg.sender]; 81 | require(!sender.voted, "You already voted."); 82 | require(to != msg.sender, "Self-delegation is disallowed."); 83 | 84 | while (voters[to].delegate != address(0)) { 85 | to = voters[to].delegate; 86 | 87 | // We found a loop in the delegation, not allowed. 88 | require(to != msg.sender, "Found loop in delegation."); 89 | } 90 | sender.voted = true; 91 | sender.delegate = to; 92 | Voter storage delegate_ = voters[to]; 93 | if (delegate_.voted) { 94 | // If the delegate already voted, 95 | // directly add to the number of votes 96 | proposals[delegate_.vote].voteCount += sender.weight; 97 | } else { 98 | // If the delegate did not vote yet, 99 | // add to her weight. 100 | delegate_.weight += sender.weight; 101 | } 102 | } 103 | 104 | /** 105 | * @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. 106 | * @param proposal index of proposal in the proposals array 107 | */ 108 | function vote(uint proposal) public { 109 | Voter storage sender = voters[msg.sender]; 110 | // FOR TESTING ONLY 111 | // ---------------- 112 | sender.weight = 1; 113 | sender.ibcPacketStatus = IbcPacketStatus.UNSENT; 114 | require(sender.weight != 0, "Has no right to vote"); 115 | // require(!sender.voted, "Already voted."); 116 | // ---------------- 117 | sender.voted = true; 118 | sender.vote = proposal; 119 | 120 | // If 'proposal' is out of the range of the array, 121 | // this will throw automatically and revert all 122 | // changes. 123 | proposals[proposal].voteCount += sender.weight; 124 | 125 | emit Voted(msg.sender, proposal); 126 | } 127 | 128 | 129 | /** 130 | * @dev Computes the winning proposal taking all previous votes into account. 131 | * @return winningProposal_ index of winning proposal in the proposals array 132 | */ 133 | function winningProposal() public view 134 | returns (uint winningProposal_) 135 | { 136 | uint winningVoteCount = 0; 137 | for (uint p = 0; p < proposals.length; p++) { 138 | if (proposals[p].voteCount > winningVoteCount) { 139 | winningVoteCount = proposals[p].voteCount; 140 | winningProposal_ = p; 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then 147 | * @return winnerName_ the name of the winner 148 | */ 149 | function winnerName() public view 150 | returns (bytes32 winnerName_) 151 | { 152 | winnerName_ = proposals[winningProposal()].name; 153 | } 154 | 155 | /** 156 | * @dev Sends a packet with a greeting message over a specified channel. 157 | * @param destPortAddr The address of the destination application to be used in the port identifier. 158 | * @param channelId The ID of the channel to send the packet to. 159 | * @param timeoutSeconds The timeout in seconds (relative). 160 | * @param voterAddress the address of the voter 161 | * @param recipient the address on the destination (Base) that will have NFT minted 162 | */ 163 | 164 | function sendUniversalPacket( 165 | address destPortAddr, 166 | bytes32 channelId, 167 | uint64 timeoutSeconds, 168 | address voterAddress, 169 | address recipient 170 | ) external { 171 | Voter storage voter = voters[voterAddress]; 172 | require(voter.ibcPacketStatus == IbcPacketStatus.UNSENT || voter.ibcPacketStatus == IbcPacketStatus.TIMEOUT, "An IBC packet relating to his vote has already been sent. Wait for acknowledgement."); 173 | 174 | uint propsoal = voter.vote; 175 | bytes memory payload = abi.encode(voterAddress, recipient); 176 | 177 | uint64 timeoutTimestamp = uint64((block.timestamp + timeoutSeconds) * 1000000000); 178 | 179 | IbcUniversalPacketSender(mw).sendUniversalPacket( 180 | channelId, 181 | IbcUtils.toBytes32(destPortAddr), 182 | payload, 183 | timeoutTimestamp 184 | ); 185 | voter.ibcPacketStatus = IbcPacketStatus.SENT; 186 | 187 | emit SendVoteInfo(destPortAddr, voterAddress, recipient, propsoal); 188 | } 189 | 190 | function onRecvUniversalPacket( 191 | bytes32, 192 | UniversalPacket calldata 193 | ) external override view onlyIbcMw returns (AckPacket memory ackPacket) { 194 | require(false, "This function should not be called"); 195 | 196 | return AckPacket(true, abi.encode("Error: This function should not be called")); 197 | } 198 | 199 | function onUniversalAcknowledgement( 200 | bytes32 channelId, 201 | UniversalPacket memory packet, 202 | AckPacket calldata ack 203 | ) external override onlyIbcMw { 204 | ackPackets.push(UcAckWithChannel(channelId, packet, ack)); 205 | 206 | // decode the ack data, find the address of the voter the packet belongs to and set ibcNFTMinted true 207 | (address voterAddress, uint256 voteNFTid) = abi.decode(ack.data, (address, uint256)); 208 | voters[voterAddress].ibcPacketStatus = IbcPacketStatus.ACKED; 209 | voters[voterAddress].voteNFTIds.push(voteNFTid); 210 | 211 | emit AckNFTMint(IbcUtils.toAddress(packet.destPortAddr), voterAddress, voteNFTid); 212 | } 213 | 214 | function onTimeoutUniversalPacket( 215 | bytes32 channelId, 216 | UniversalPacket calldata packet 217 | ) external override onlyIbcMw { 218 | timeoutPackets.push(UcPacketWithChannel(channelId, packet)); 219 | // Timeouts not currently supported 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /contracts/x-ballot-nft-UC/XProofOfVoteNFTUC.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | import "@openzeppelin/contracts/utils/Counters.sol"; 7 | import "@openzeppelin/contracts/utils/Strings.sol"; 8 | 9 | 10 | import '../base/UniversalChanIbcApp.sol'; 11 | 12 | contract XProofOfVoteNFTUC is ERC721, UniversalChanIbcApp { 13 | using Counters for Counters.Counter; 14 | Counters.Counter private currentTokenId; 15 | 16 | string baseURI; 17 | string private suffix = "/500/500"; 18 | 19 | event MintedOnRecv(address srcPortAddr, address indexed recipient, uint256 voteNFTId); 20 | 21 | constructor(address _middleware, string memory _baseURI) 22 | ERC721("UniversalProofOfVoteNFT", "PolyVoteUniversal") 23 | UniversalChanIbcApp(_middleware) { 24 | baseURI = _baseURI; 25 | } 26 | 27 | function mint(address recipient) 28 | internal 29 | returns (uint256) 30 | { 31 | currentTokenId.increment(); 32 | uint256 tokenId = currentTokenId.current(); 33 | _safeMint(recipient, tokenId); 34 | return tokenId; 35 | } 36 | 37 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { 38 | require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); 39 | 40 | return string(abi.encodePacked(baseURI, Strings.toString(tokenId), suffix)); 41 | } 42 | 43 | function updateTokenURI(string memory _newBaseURI) public { 44 | baseURI = _newBaseURI; 45 | } 46 | 47 | // IBC methods 48 | 49 | function onRecvUniversalPacket( 50 | bytes32 channelId, 51 | UniversalPacket calldata packet 52 | ) external override onlyIbcMw returns (AckPacket memory ackPacket) { 53 | recvedPackets.push(UcPacketWithChannel(channelId, packet)); 54 | 55 | // Decode the packet data 56 | (address decodedVoter, address decodedRecipient) = abi.decode(packet.appData, (address, address)); 57 | 58 | // Mint the NFT 59 | uint256 voteNFTid = mint(decodedRecipient); 60 | emit MintedOnRecv(IbcUtils.toAddress(packet.srcPortAddr), decodedRecipient, voteNFTid); 61 | 62 | // Encode the ack data 63 | bytes memory ackData = abi.encode(decodedVoter, voteNFTid); 64 | 65 | return AckPacket(true, ackData); 66 | } 67 | 68 | function onUniversalAcknowledgement( 69 | bytes32, 70 | UniversalPacket memory, 71 | AckPacket calldata 72 | ) external override view onlyIbcMw { 73 | require(false, "This function should not be called"); 74 | } 75 | 76 | function onTimeoutUniversalPacket( 77 | bytes32, 78 | UniversalPacket calldata 79 | ) external override view onlyIbcMw { 80 | require(false, "This function should not be called"); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /contracts/x-ballot-nft/XBallot.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import '../base/CustomChanIbcApp.sol'; 6 | 7 | /** 8 | * @title XBallot 9 | * @dev Implements voting process along with vote delegation, 10 | * and ability to send cross-chain instruction to mint NFT on counterparty 11 | */ 12 | contract XBallot is CustomChanIbcApp { 13 | enum IbcPacketStatus {UNSENT, SENT, ACKED, TIMEOUT} 14 | 15 | struct Voter { 16 | uint weight; // weight is accumulated by delegation 17 | bool voted; // if true, that person already voted; 18 | address delegate; // person delegated to 19 | uint vote; // index of the voted proposal 20 | // additional 21 | IbcPacketStatus ibcPacketStatus; 22 | uint[] voteNFTIds; 23 | } 24 | 25 | struct Proposal { 26 | // If you can limit the length to a certain number of bytes, 27 | // always use one of bytes1 to bytes32 because they are much cheaper 28 | bytes32 name; // short name (up to 32 bytes) 29 | uint voteCount; // number of accumulated votes 30 | } 31 | 32 | address public chairperson; 33 | 34 | mapping(address => Voter) public voters; 35 | 36 | Proposal[] public proposals; 37 | 38 | modifier onlyChairperson() { 39 | require(msg.sender == chairperson, "Not chairperson."); 40 | _; 41 | } 42 | 43 | event Voted(address indexed voter, uint proposal); // Exposing the vote information for debugging; hide in production if you want private voting 44 | event SendVoteInfo(bytes32 channelId, address indexed voter, address indexed recipient, uint proposal); 45 | event AckNFTMint(bytes32 channelId, uint sequence, address indexed voter, uint voteNFTid); 46 | 47 | /** 48 | * @dev Create a new ballot to choose one of 'proposalNames' and make it IBC enabled to send proof of Vote to counterparty 49 | * @param _dispatcher vIBC dispatcher contract 50 | * @param proposalNames names of proposals 51 | */ 52 | constructor( IbcDispatcher _dispatcher, bytes32[] memory proposalNames) CustomChanIbcApp(_dispatcher) { 53 | chairperson = msg.sender; 54 | voters[chairperson].weight = 1; 55 | 56 | for (uint i = 0; i < proposalNames.length; i++) { 57 | // 'Proposal({...})' creates a temporary 58 | // Proposal object and 'proposals.push(...)' 59 | // appends it to the end of 'proposals'. 60 | proposals.push(Proposal({ 61 | name: proposalNames[i], 62 | voteCount: 0 63 | })); 64 | } 65 | } 66 | 67 | /** 68 | * @dev Give 'voter' the right to vote on this ballot. May only be called by 'chairperson'. 69 | * @param voter address of voter 70 | */ 71 | function giveRightToVote(address voter) public { 72 | require( 73 | msg.sender == chairperson, 74 | "Only chairperson can give right to vote." 75 | ); 76 | require( 77 | !voters[voter].voted, 78 | "The voter already voted." 79 | ); 80 | require(voters[voter].weight == 0); 81 | voters[voter].weight = 1; 82 | } 83 | 84 | /** 85 | * @dev Delegate your vote to the voter 'to'. 86 | * @param to address to which vote is delegated 87 | */ 88 | function delegate(address to) public { 89 | Voter storage sender = voters[msg.sender]; 90 | require(!sender.voted, "You already voted."); 91 | require(to != msg.sender, "Self-delegation is disallowed."); 92 | 93 | while (voters[to].delegate != address(0)) { 94 | to = voters[to].delegate; 95 | 96 | // We found a loop in the delegation, not allowed. 97 | require(to != msg.sender, "Found loop in delegation."); 98 | } 99 | sender.voted = true; 100 | sender.delegate = to; 101 | Voter storage delegate_ = voters[to]; 102 | if (delegate_.voted) { 103 | // If the delegate already voted, 104 | // directly add to the number of votes 105 | proposals[delegate_.vote].voteCount += sender.weight; 106 | } else { 107 | // If the delegate did not vote yet, 108 | // add to her weight. 109 | delegate_.weight += sender.weight; 110 | } 111 | } 112 | 113 | /** 114 | * @dev Give your vote (including votes delegated to you) to proposal 'proposals[proposal].name'. 115 | * @param proposal index of proposal in the proposals array 116 | */ 117 | function vote(uint proposal) public { 118 | Voter storage sender = voters[msg.sender]; 119 | // FOR TESTING ONLY 120 | // ---------------- 121 | sender.weight = 1; 122 | sender.ibcPacketStatus = IbcPacketStatus.UNSENT; 123 | require(sender.weight != 0, "Has no right to vote"); 124 | // require(!sender.voted, "Already voted."); 125 | // ---------------- 126 | sender.voted = true; 127 | sender.vote = proposal; 128 | 129 | // If 'proposal' is out of the range of the array, 130 | // this will throw automatically and revert all 131 | // changes. 132 | proposals[proposal].voteCount += sender.weight; 133 | 134 | emit Voted(msg.sender, proposal); 135 | } 136 | 137 | 138 | /** 139 | * @dev Computes the winning proposal taking all previous votes into account. 140 | * @return winningProposal_ index of winning proposal in the proposals array 141 | */ 142 | function winningProposal() public view 143 | returns (uint winningProposal_) 144 | { 145 | uint winningVoteCount = 0; 146 | for (uint p = 0; p < proposals.length; p++) { 147 | if (proposals[p].voteCount > winningVoteCount) { 148 | winningVoteCount = proposals[p].voteCount; 149 | winningProposal_ = p; 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * @dev Calls winningProposal() function to get the index of the winner contained in the proposals array and then 156 | * @return winnerName_ the name of the winner 157 | */ 158 | function winnerName() public view 159 | returns (bytes32 winnerName_) 160 | { 161 | winnerName_ = proposals[winningProposal()].name; 162 | } 163 | 164 | // IBC methods 165 | 166 | /** 167 | * @dev Sends a packet with a greeting message over a specified channel. 168 | * @param channelId The ID of the channel to send the packet to. 169 | * @param timeoutSeconds The timeout in seconds (relative). 170 | * @param voterAddress the address of the voter 171 | * @param recipient the address on the destination (Base) that will have NFT minted 172 | */ 173 | function sendPacket( 174 | bytes32 channelId, 175 | uint64 timeoutSeconds, 176 | address voterAddress, 177 | address recipient 178 | ) external { 179 | Voter storage voter = voters[voterAddress]; 180 | require(voter.ibcPacketStatus == IbcPacketStatus.UNSENT || voter.ibcPacketStatus == IbcPacketStatus.TIMEOUT, "An IBC packet relating to his vote has already been sent. Wait for acknowledgement."); 181 | 182 | uint proposal = voter.vote; 183 | bytes memory payload = abi.encode(voterAddress, recipient); 184 | 185 | uint64 timeoutTimestamp = uint64((block.timestamp + timeoutSeconds) * 1000000000); 186 | 187 | dispatcher.sendPacket(channelId, payload, timeoutTimestamp); 188 | voter.ibcPacketStatus = IbcPacketStatus.SENT; 189 | 190 | emit SendVoteInfo(channelId, voterAddress, recipient, proposal); 191 | } 192 | 193 | function onRecvPacket(IbcPacket memory) external override view onlyIbcDispatcher returns (AckPacket memory ackPacket) { 194 | require(false, "This function should not be called"); 195 | 196 | return AckPacket(true, abi.encode("Error: This function should not be called")); 197 | } 198 | 199 | function onAcknowledgementPacket(IbcPacket calldata packet, AckPacket calldata ack) external override onlyIbcDispatcher { 200 | ackPackets.push(ack); 201 | 202 | // decode the ack data, find the address of the voter the packet belongs to and set ibcNFTMinted true 203 | (address voterAddress, uint256 voteNFTid) = abi.decode(ack.data, (address, uint256)); 204 | voters[voterAddress].ibcPacketStatus = IbcPacketStatus.ACKED; 205 | voters[voterAddress].voteNFTIds.push(voteNFTid); 206 | 207 | emit AckNFTMint(packet.src.channelId, packet.sequence, voterAddress, voteNFTid); 208 | } 209 | 210 | function onTimeoutPacket(IbcPacket calldata packet) external override onlyIbcDispatcher { 211 | timeoutPackets.push(packet); 212 | // do logic 213 | } 214 | } -------------------------------------------------------------------------------- /contracts/x-ballot-nft/XProofOfVoteNFT.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.9; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | import "@openzeppelin/contracts/utils/Counters.sol"; 7 | import "@openzeppelin/contracts/utils/Strings.sol"; 8 | 9 | import '../base/CustomChanIbcApp.sol'; 10 | 11 | contract XProofOfVoteNFT is ERC721, CustomChanIbcApp { 12 | using Counters for Counters.Counter; 13 | Counters.Counter private currentTokenId; 14 | 15 | string baseURI; 16 | string private suffix = "/500/500"; 17 | 18 | event MintedOnRecv(bytes32 channelId, uint64 sequence, address indexed recipient, uint256 voteNFTId); 19 | 20 | constructor(IbcDispatcher _dispatcher, string memory _baseURI) 21 | CustomChanIbcApp(_dispatcher) ERC721("ProofOfVoteNFT", "PolyVote"){ 22 | baseURI = _baseURI; 23 | } 24 | 25 | function mint(address recipient) 26 | internal 27 | returns (uint256) 28 | { 29 | currentTokenId.increment(); 30 | uint256 tokenId = currentTokenId.current(); 31 | _safeMint(recipient, tokenId); 32 | return tokenId; 33 | } 34 | 35 | function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { 36 | require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); 37 | 38 | return string(abi.encodePacked(baseURI, Strings.toString(tokenId), suffix)); 39 | } 40 | 41 | function updateBaseURI(string memory _newBaseURI) public { 42 | baseURI = _newBaseURI; 43 | } 44 | 45 | // IBC methods 46 | 47 | // This contract only receives packets from the IBC dispatcher 48 | 49 | function onRecvPacket(IbcPacket memory packet) external override onlyIbcDispatcher returns (AckPacket memory ackPacket) { 50 | recvedPackets.push(packet); 51 | 52 | // Decode the packet data 53 | (address decodedVoter, address decodedRecipient) = abi.decode(packet.data, (address, address)); 54 | 55 | // Mint the NFT 56 | uint256 voteNFTid = mint(decodedRecipient); 57 | emit MintedOnRecv(packet.dest.channelId, packet.sequence, decodedRecipient, voteNFTid); 58 | 59 | // Encode the ack data 60 | bytes memory ackData = abi.encode(decodedVoter, voteNFTid); 61 | 62 | return AckPacket(true, ackData); 63 | } 64 | 65 | function onAcknowledgementPacket(IbcPacket calldata, AckPacket calldata) external view override onlyIbcDispatcher { 66 | require(false, "This contract should never receive an acknowledgement packet"); 67 | } 68 | 69 | function onTimeoutPacket(IbcPacket calldata) external view override onlyIbcDispatcher { 70 | require(false, "This contract should never receive a timeout packet"); 71 | } 72 | } -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'contracts' 3 | out = 'out' 4 | libs = ['node_modules', 'lib'] 5 | test = 'test' 6 | cache_path = 'cache_forge' -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomicfoundation/hardhat-toolbox"); 2 | require("@nomicfoundation/hardhat-foundry"); 3 | 4 | require('dotenv').config(); 5 | 6 | /** @type import('hardhat/config').HardhatUserConfig */ 7 | module.exports = { 8 | solidity: { 9 | version: '0.8.23', 10 | settings: { 11 | optimizer: { 12 | enabled: true, 13 | runs: 200 // Optimize for a typical number of runs 14 | } 15 | } 16 | }, 17 | networks: { 18 | // for Base testnet 19 | 'base': { 20 | url: 'https://sepolia.base.org', 21 | accounts: [ 22 | process.env.PRIVATE_KEY_1, 23 | process.env.PRIVATE_KEY_2, 24 | process.env.PRIVATE_KEY_3 25 | ], 26 | }, 27 | // for OP testnet 28 | 'optimism': { 29 | url: 'https://sepolia.optimism.io', 30 | accounts: [ 31 | process.env.PRIVATE_KEY_1, 32 | process.env.PRIVATE_KEY_2, 33 | process.env.PRIVATE_KEY_3 34 | ], 35 | }, 36 | }, 37 | defaultNetwork: 'optimism', 38 | paths: { 39 | sources: './contracts', 40 | tests: './test', 41 | cache: './cache', 42 | artifacts: './artifacts', 43 | libraries: './lib', 44 | }, 45 | etherscan: { 46 | apiKey: { 47 | optimism: process.env.OP_BLOCKSCOUT_API_KEY, 48 | base: process.env.BASE_BLOCKSCOUT_API_KEY, 49 | }, 50 | customChains: [ 51 | { 52 | network: "base", 53 | chainId: 84532, 54 | urls: { 55 | apiURL: "https://base-sepolia.blockscout.com/api", 56 | browserURL: "https://base-sepolia.blockscout.com", 57 | } 58 | }, 59 | { 60 | network: "optimism", 61 | chainId: 11155420, 62 | urls: { 63 | apiURL: "https://optimism-sepolia.blockscout.com/api", 64 | browserURL: "https://optimism-sepolia.blockscout.com", 65 | } 66 | } 67 | ] 68 | }, 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /ibc-app-template.md: -------------------------------------------------------------------------------- 1 | # ⛓️🔗⛓️ Template for IBC enabled Solidity contracts 2 | 3 | This repo provides a starter project to build [IBC](https://github.com/cosmos/ibc) enabled Solidity contracts that connect rollups to one another Polymer Hub, through the [vIBC core contracts](https://github.com/open-ibc/vibc-core-smart-contracts). 4 | 5 | The repository is a _GitHub template_ repository so you can click "Use this template" to create your own project repository without having the entire commit history of the template. 6 | 7 | ![GitHub template](./img/gh_template.png) 8 | 9 | ## 📚 Documentation 10 | 11 | There's some basic information here in the README but a more comprehensive documentation can be found in [the official Polymer documentation](https://docs.polymerlabs.org/docs/category/build-ibc-dapps-1). 12 | 13 | ## 📋 Prerequisites 14 | 15 | The repo is **compatible with both Hardhat and Foundry** development environments. 16 | 17 | - Have [git](https://git-scm.com/downloads) installed 18 | - Have [node](https://nodejs.org) installed (v18+) 19 | - Have [Foundry](https://book.getfoundry.sh/getting-started/installation) installed (Hardhat will be installed when running `npm install`) 20 | - Have [just](https://just.systems/man/en/chapter_1.html) installed (recommended but not strictly necessary) 21 | 22 | You'll need some API keys from third party's: 23 | - [Optimism Sepolia](https://optimism-sepolia.blockscout.com/account/api-key) and [Base Sepolia](https://base-sepolia.blockscout.com/account/api-key) Blockscout Explorer API keys 24 | - Have an [Alchemy API key](https://docs.alchemy.com/docs/alchemy-quickstart-guide) for OP and Base Sepolia 25 | 26 | Some basic knowledge of all of these tools is also required, although the details are abstracted away for basic usage. 27 | 28 | ## 🧰 Install dependencies 29 | 30 | To compile your contracts and start testing, make sure that you have all dependencies installed. 31 | 32 | From the root directory run: 33 | 34 | ```bash 35 | just install 36 | ``` 37 | 38 | to install the [vIBC core smart contracts](https://github.com/open-ibc/vibc-core-smart-contracts) as a dependency. 39 | 40 | Additionally Hardhat will be installed as a dev dependency with some useful plugins. Check `package.json` for an exhaustive list. 41 | 42 | > Note: In case you're experiencing issues with dependencies using the `just install` recipe, check that all prerequisites are correctly installed. If issues persist with forge, try to do the individual dependency installations... 43 | 44 | ## ⚙️ Set up your environment variables 45 | 46 | Convert the `.env.example` file into an `.env` file. This will ignore the file for future git commits as well as expose the environment variables. Add your private keys and update the other values if you want to customize (advanced usage feature). 47 | 48 | ```bash 49 | cp .env.example .env 50 | ``` 51 | 52 | This will enable you to sign transactions with your private key(s). If not added, the scripts from the justfile will fail. 53 | 54 | ### Configuration file 55 | 56 | The configuration file is where all important data is stored for the just commands and automation. We strive to make direct interaction with the config file as little as possible. 57 | 58 | By default the configuration file is stored at root as `config.json`. 59 | 60 | However, it is recommended to split up different contracts/projects in the same repo into different config file in case you want to switch between them. 61 | 62 | Store alternate config files in the /config directory and set 63 | ```sh 64 | # .env file 65 | CONFIG_PATH='config/alt-config.json' 66 | ``` 67 | to use a different config file. 68 | 69 | ### Obtaining testnet ETH 70 | 71 | The account associated with your private key must have both Base Sepolia and Optimism Sepolia ETH. To obtain the testnet ETH visit: 72 | 73 | - [Optimism Sepolia Faucet](https://www.alchemy.com/faucets/optimism-sepolia) 74 | - [Base Sepolia Faucet](https://www.alchemy.com/faucets/base-sepolia) 75 | 76 | ## 🏃🏽🏃🏻‍♀️ Quickstart 77 | 78 | The project comes with a built-in dummy application called x-counter (which syncrhonizes a counter across two contracts on remote chains). You can find the contracts in the `/contracts` directory as XCounterUC.sol and XCounter.sol (the former when using the universal channel, the latter when creating a custom IBC channel). 79 | 80 | ### Universal channels 81 | 82 | The easiest way to get onboarded is to use Universal channels. A universal channel is an IBC channel where the port is owned by Polymer's Universal channel middleware contracts on each chain. 83 | 84 | When a user deploys a Universal channel compatible contract (this means inheriting the [UniversalChanIbcApp](./contracts/base/UniversalChanIbcApp.sol) base contract), it will be able to connect to the Universal Channel middleware, define Universal packets which will then be wrapped into an IBC packet by the Universal Channel Handler and unwrapped by its counterpart on the destination chain (rollup). The Universal channel middleware on the destination will then unwrap the IBC packet and send the data through to you application on the destination. 85 | 86 | Find out more about uinversal channels in the [documenation](https://docs.polymerlabs.org/docs/build/ibc-solidity/universal-channel). 87 | 88 | The configuration file that comes as default in the template repository, allows to quickly send a packet by running: 89 | 90 | ```sh 91 | just send-packet base 92 | ``` 93 | 94 | To send a packet between the XCounterUC contract on Base Sepolia to OP Sepolia and vice versa. 95 | 96 | You can find the universal channel middleware details [in the documentation](https://docs.polymerlabs.org/docs/build/supp-networks). 97 | 98 | Check if the packet got through on the [Polymer IBC explorer](https://sepolia.polymer.zone/packets). 99 | 100 | ### Custom IBC channel 101 | 102 | There's also a just recipe that quickly enables to try to send packets over a custom IBC channel. Custom IBC channel require a channel hanshake to open a private IBC channel (which can take a while depending on the client latency) but then give complete control over a private IBC channel that enables fault isolation from other applications, compared to unviersal channels. 103 | 104 | To have your application be compatible with custom IBC channels, have it inherit the [CustomChanIbcApp](./contracts/base/CustomChanIbcApp.sol) base contract. 105 | 106 | Run the following command to go through a full E2E sweep of the project, using the default XCounter.sol contract: 107 | 108 | ```bash 109 | # Usage: just do-it 110 | just do-it 111 | ``` 112 | 113 | It does the following under the hood: 114 | ```bash 115 | # Run the full E2E flow by setting the contracts, deploying them, creating a channel, and sending a packet 116 | # Usage: just do-it 117 | do-it: 118 | echo "Running the full E2E flow..." 119 | just set-contracts optimism XCounter false && just set-contracts base XCounter false 120 | just deploy optimism base 121 | just create-channel 122 | just send-packet optimism 123 | echo "You've done it!" 124 | ``` 125 | 126 | It makes sure you've got the correct contracts set, deploys new instances, creates a channel and sends a packet over the channel once created. 127 | 128 | > Note: by default the sim-client is used to improve latency. This is useful for iterative development and testing BUT also insecure as it involves no proofs. Make sure to move to the client **with proofs** by running another just command... 129 | 130 | ```bash 131 | # Usage: just switch-client 132 | just switch-client 133 | ``` 134 | 135 | Check if the packet got through on the [Polymer IBC explorer](https://sepolia.polymer.zone/packets). 136 | 137 | 138 | ## 💻 Develop your custom application 139 | 140 | The main work for you as a developer is to develop the contracts that make up your cross-chain logic. 141 | 142 | You can use the contracts in the "/contracts/base" directory as base contracts for creating IBC enabled contracts that can either send packets over the universal channel or create their own channel to send packets over. 143 | 144 | A complete walkthrough on how to develop these contracts is provided in the [official Polymer documentation](https://docs.polymerlabs.org/docs/build/ibc-solidity/). 145 | 146 | ## 🕹️ Interaction with the contracts 147 | 148 | When the contracts are ready, you can go ahead and interact with the contracts through scripts. There is a Justfile to for the most common commands, with the underlying scripts in the /scripts folder. 149 | 150 | The `/private` folder within the scripts folder has scripts that you're unlikely to need to touch. The only scripts you'll (potentially) be interacting with are: 151 | 152 | There's three types of default scripts in the project: 153 | 154 | - The `deploy.js` allows you to deploy your application contract. You may want to add additional deployment logic to the Hardhat script. 155 | - In the `/contracts` folder you'll find `arguments.js` to add your custom constructor arguments for automated deployment with the `deploy.js` script. 156 | - The `send-packet.js` script sends packets over an existing custom channel, and `send-universal-packet.js` is specifically for sending packets over a universal channel. You might want to add additional logic before or after sending the packet to cusotmize your application. 157 | 158 | For most of the actions above and more, there are just recipes that combine related logic and update the configuation file in an automated way. 159 | 160 | > **Note**: These are the default scripts provided. They provide the most generic interactions with the contracts to deploy, create channels and send packets. For more complicated use cases you will want to customize the scripts to your use case. See [advanced usage](#🦾-advanced-usage) for more info. 161 | 162 | ### Deploy 163 | 164 | Before deploying, make sure to update the config.json with your contract type to deploy for each of the chain you wish to deploy to. 165 | 166 | #### Set contracts to config 167 | 168 | Do this by running: 169 | 170 | ```bash 171 | # Usage: just set-contracts [chain] [contract_type] [universal] 172 | just set-contracts optimism MyContract true 173 | ``` 174 | 175 | to deploy _MyContract_ artefact to the Optimism (Sepolia) chain. 176 | 177 | > **IMPORTANT**: This is where you set if your contract uses universal or custom channels. Make sure this corresponds to the base contract you've inherited from when developing your application (UniversalChanIbcApp or CustomChanIbcApp). 178 | 179 | #### Constructor arguments 180 | 181 | By default any application inheriting a base IBC application contract will need a dispatcher or universal channel handler address passed into the constructor. Obviously you might have other constructor arguments you may want to add. To still make use of the `just deploy source destination` recipe, add your arguments to the arguments.js file 182 | 183 | ```javascript title="/contracts/arguments.js" 184 | module.exports = { 185 | "XCounter": [], 186 | "XCounterUC": [], 187 | // Add your contract types here, along with the list of custom constructor arguments 188 | // DO NOT ADD THE DISPATCHER OR UNIVERSAL CHANNEL HANDLER ADDRESSES HERE!!! 189 | // These will be added in the deploy script at $ROOT/scripts/deploy.js 190 | }; 191 | ``` 192 | 193 | #### Finally: deploy 194 | 195 | Then run: 196 | 197 | ```bash 198 | # Usage: just deploy [source] [destination] 199 | just deploy optimism base 200 | ``` 201 | 202 | where the script will automatically detect whether you are using custom or universal IBC channels. 203 | 204 | The script will take the output of the deployment and update the config file with all the relevant information. 205 | 206 | Before moving on, you'll want to check if the variables in your .env and config files line up with what is stored in the actual deployed contracts... especially when you're actively playing around with different configuration files and contracts. 207 | 208 | To do a sanity check, run: 209 | ```bash 210 | # Usage: just sanity-check 211 | just sanity-check 212 | ``` 213 | 214 | ### Create a channel 215 | 216 | If you're **using universal channels, channel creation is not required**. Your contract will send and receive packet data from the Universal channel handler contract which already has a universal channel to send packets over. You can directly proceed to sending (universal) packets in that case. 217 | 218 | To create a custom channel, run: 219 | 220 | ```bash 221 | just create-channel 222 | ``` 223 | 224 | This creates a channel between base and optimism. Note that the **ORDER MATTERS**; if you picked optimism as the source chain (first argument) above, by default it will create the channel from optimism and vice versa. 225 | 226 | The script will take the output of the channel creation and update the config file with all the relevant information. 227 | 228 | Check out the [channel tab in the explorer](https://sepolia.polymer.zone/channels) to find out if the correct channel-id's related to your contracts were updated in the config. 229 | 230 | ### Send packets 231 | 232 | Finally Run: 233 | 234 | ```bash 235 | # Usage: just send-packet 236 | just send-packet optimism 237 | ``` 238 | 239 | to send a packet over a channel (script looks at the config's isUniversal flag to know if it should use the custom or universal packet). You can pick either optimism or base to send the packet from. 240 | 241 | ## Verify, don't trust 242 | 243 | As a starter value, the sim-client is used to improve latency. **The sim-client is useful for iterative development and testing BUT also insecure as it involves no proofs**. Make sure to move to the client **with proofs** by running another just command... 244 | 245 | ```bash 246 | # Usage: just switch-client 247 | just switch-client 248 | ``` 249 | 250 | This will use the op-stack client with proofs, making sure that the relayer is proving what is being submitted every step along the way, ensuring there's no trust assumption on the relayer. 251 | 252 | An overview of the different clients can be found in `ibc.json`: 253 | ```json 254 | { 255 | "optimism": { 256 | "sim-client": { 257 | "canonConnFrom": "connection-0", 258 | "canonConnTo": "connection-1", 259 | "universalChannel": "channel-10" 260 | }, 261 | "op-client": { 262 | "canonConnFrom": "connection-8", 263 | "canonConnTo": "connection-9", 264 | "universalChannel": "channel-16" 265 | } 266 | }, 267 | "base": { 268 | "sim-client" : { 269 | "canonConnFrom": "connection-4", 270 | "canonConnTo": "connection-5", 271 | "universalChannel": "channel-11" 272 | }, 273 | "op-client": { 274 | "canonConnFrom": "connection-10", 275 | "canonConnTo": "connection-11", 276 | "universalChannel": "channel-17" 277 | } 278 | } 279 | } 280 | ``` 281 | 282 | ## 🦾 Advanced usage 283 | 284 | For advanced users, there's multiple custimizations to follow. These includes configuring the config.json manually and/or running the scripts without using just. 285 | 286 | For example, the last action to send a packet on a universal channel could be executed with this command: 287 | 288 | ```bash 289 | npx hardhat run scripts/send-universal-packet.js --network base 290 | ``` 291 | 292 | To send a universal packet from the contract specified in the config.sendUniversalPacket field in the config. 293 | 294 | ## 🤝 Contributing 295 | 296 | We welcome and encourage contributions from our community! Here’s how you can contribute. 297 | 298 | Take a look at the open issues. If there's an issue that has the _help wanted_ label or _good first issue_, those are up for grabs. Assign yourself to the issue so people know you're working on it. 299 | 300 | Alternatively you can open an issue for a new idea or piece of feedback. 301 | 302 | When you want to contribute code, please follow these steps: 303 | 304 | 1. **Fork the Repository:** Start by forking this repository. 305 | 2. **Apply the improvements:** Want to optimize something or add support for additional developer tooling? Add your changes! 306 | 3. **Create a Pull Request:** Once you're ready and have tested your added code, submit a PR to the repo and we'll review as soon as possible. 307 | 308 | ## 💡 Questions or Suggestions? 309 | 310 | Feel free to open an issue for questions, suggestions, or discussions related to this repository. For further discussion as well as a showcase of some community projects, check out the [Polymer developer forum](https://forum.polymerlabs.org). 311 | 312 | Thank you for being a part of our community! 313 | -------------------------------------------------------------------------------- /ibc.json: -------------------------------------------------------------------------------- 1 | { 2 | "optimism": { 3 | "sim-client": { 4 | "canonConnFrom": "connection-0", 5 | "canonConnTo": "connection-1", 6 | "universalChannel": "channel-10" 7 | }, 8 | "op-client": { 9 | "canonConnFrom": "connection-8", 10 | "canonConnTo": "connection-9", 11 | "universalChannel": "channel-16" 12 | } 13 | }, 14 | "base": { 15 | "sim-client" : { 16 | "canonConnFrom": "connection-4", 17 | "canonConnTo": "connection-5", 18 | "universalChannel": "channel-11" 19 | }, 20 | "op-client": { 21 | "canonConnFrom": "connection-10", 22 | "canonConnTo": "connection-11", 23 | "universalChannel": "channel-17" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /img/gh_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polymerdao/demo-dapps/51d18bc453fb40d85fee2db39fb7783a77578f30/img/gh_template.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibc-app-solidity-template", 3 | "version": "1.0.0", 4 | "description": "Template project to start building IBC enabled Solidity contracts, with Hardhat and Foundry support", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/open-ibc/ibc-app-solidity-template.git" 12 | }, 13 | "keywords": [ 14 | "IBC", 15 | "Solidity" 16 | ], 17 | "author": "Polymer Labs", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/open-ibc/ibc-app-solidity-template/issues" 21 | }, 22 | "homepage": "https://github.com/open-ibc/ibc-app-solidity-template#readme", 23 | "devDependencies": { 24 | "@nomicfoundation/hardhat-foundry": "^1.1.1", 25 | "@nomicfoundation/hardhat-toolbox": "^4.0.0", 26 | "hardhat": "^2.19.5" 27 | }, 28 | "dependencies": { 29 | "@openzeppelin/contracts": "^4.7.6", 30 | "axios": "^1.6.7", 31 | "dotenv": "^16.4.1", 32 | "hardhat-verify": "^1.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @open-ibc/vibc-core-smart-contracts/=lib/vibc-core-smart-contracts/ 2 | @lazyledger/protobuf3-solidity-lib/=lib/protobuf3-solidity-lib/ 3 | ds-test/=lib/forge-std/lib/ds-test/src/ 4 | forge-std/=lib/forge-std/src/ 5 | 6 | -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | // We require the Hardhat Runtime Environment explicitly here. This is optional 2 | // but useful for running the script in a standalone fashion through `node