diff --git a/poll-v6/Cargo.toml b/poll-v6/Cargo.toml new file mode 100644 index 00000000..18911bbc --- /dev/null +++ b/poll-v6/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "poll" +version = "0.1.0" +authors = ["Use Ink "] +edition = "2021" + +[dependencies] +ink = { version = "6.0.0-beta", default-features = false } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { version = "6.0.0-beta" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/poll-v6/README.md b/poll-v6/README.md new file mode 100644 index 00000000..271ef452 --- /dev/null +++ b/poll-v6/README.md @@ -0,0 +1,239 @@ +# Secure Poll Contract (Built with ink! v6) + +![Status](https://img.shields.io/badge/Status-Completed-success) +![Tech Stack](https://img.shields.io/badge/Polkadot-ink!_v6-e6007a) +![VM](https://img.shields.io/badge/VM-RISC--V_PolkaVM-blue) + +## Project Overview + +This project implements a decentralized **Voting System** using **ink! v6**, the latest smart contract language for the Polkadot ecosystem. + +Unlike previous versions of ink! that relied on WebAssembly (Wasm), **ink! v6 compiles to RISC-V** to run on **PolkaVM**. This architectural shift (`pallet-revive`) allows for native Solidity ABI compatibility, higher performance, and a future-proof execution environment. + +### Core Features +* **ADMIN Access Control:** strictly restricted capabilities (Start/End Poll) to the contract creator. +* **State Machine Logic:** The poll transitions between `Closed` → `Open` → `Closed` states. +* **Double-Voting Prevention:** Utilizes on-chain storage Mappings to track voter participation. +* **Ethereum Compatibility:** Uses `H160` address types, making the contract compatible with Ethereum wallets (MetaMask) and tools. + +--- + +## Architecture & Logic + +The contract is designed with a strict separation of concerns between the **Administrator** and the **Public**. + +### 1. Storage Layout +The contract state is stored in the high-performance PolkaVM storage: + +```rust +#[ink(storage)] +pub struct Poll { + admin: H160, // The owner (Alice) + is_active: bool, // The State Switch + yes_votes: u32, // Counter + no_votes: u32, // Counter + has_voted: Mapping // Anti-Sybil mechanism +} +``` + +### 2. The "Ensure Admin" Pattern +Security is enforced not by `modifiers` (like in Solidity), but by a Rust helper function that panics or returns an error if the caller is unauthorized. + +```rust +fn ensure_admin(&self) -> Result<()> { + if self.env().caller() != self.admin { + return Err(Error::NotAdmin); + } + Ok(()) +} +``` + +--- + +## Prerequisites & Setup + +Before running this project, ensure your environment is configured for **ink! v6**. + +### 1. System Requirements +* **Rust:** Stable channel. +* **cargo-contract:** Version 6.0.0-beta (or newer). +* **ink-node:** The specialized Substrate node supporting `pallet-revive`. + +### 2. Installation Commands +```bash +# 1. Update Rust +rustup update +rustup target add wasm32-unknown-unknown +rustup component add rust-src + +# 2. Install ink! CLI v6 +cargo install --force --locked --version 6.0.0-beta cargo-contract + +# 3. Download the ink-node (Linux/Mac) +wget https://github.com/use-ink/ink-node/releases/download/v0.1.0/ink-node +chmod +x ink-node +``` + +--- + +## Phase 1: Building and Testing + +We utilize Rust's built-in testing framework to simulate blockchain interactions off-chain. This ensures logic allows the Admin to start polls and prevents double voting. + +### Running Unit Tests +Execute the test suite to verify the logic: + +```bash +cargo test +``` + +**Expected Output:** +All tests (`admin_can_start_poll`, `voting_works`, `double_voting_fails`) should pass. + +![Placeholder: Screenshot of terminal showing 'test result: ok. 3 passed'](./images/poll-test.JPG) + +### Compiling for Release +Compile the Rust code into a `.contract` bundle (which includes the RISC-V binary and JSON metadata). + +```bash +cargo contract build +``` + +**Successful Build Output:** +Look for the generated files in `./target/ink/`: +* `poll.contract` (The bundle to upload) +* `poll.json` (The Metadata/ABI) + +![Placeholder: Screenshot of terminal showing 'Your contract artifacts are ready'](./images/poll-build.JPG) + +--- + +## Phase 2: Local Blockchain Deployment + +This section details how to run the contract on a local development node. + +### 1. Start the Local Node +Run the `ink-node` binary in development mode. This creates a temporary blockchain on your machine. + +```bash +./ink-node --dev +``` + +* **RPC Port:** `9933` (HTTP) +* **WS Port:** `9944` (WebSocket) + +### 2. Configure the UI (ui.use.ink) +We use the hosted interface at **[ui.use.ink](https://ui.use.ink/)**. + +1. Open the URL in Chrome or Firefox. +2. **Toggle the Version:** Ensure the sidebar toggle is set to **ink! v6**. +3. **Connect Network:** Click the top-left dropdown and select **Local Node** (`ws://127.0.0.1:9944`). + +### 3. CRITICAL: Wallet Setup (The "Rich Alice" Fix) +*Standard development nodes come with a pre-funded account named Alice. However, browser wallets often import the wrong "derived" path, resulting in a $0 balance.* + +To pay for deployment, you must import **Alice's Raw Private Key**: + +1. Open **Polkadot.js Extension** (or SubWallet/Talisman). +2. Select **"Import Account from Pre-existing Seed"**. +3. **DO NOT** use the mnemonic words. +4. **Paste this Hex Seed:** + ```text + 0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a + ``` +5. Name the account **"Real Alice"**. +6. You should now see a balance of millions of units. + +### 4. Upload and Instantiate +1. Click **"Add New Contract"** -> **"Upload New Contract Code"**. +2. Upload `poll.contract`. +3. Select **"Real Alice"** as the deployment account. +4. Name the instance "My Poll". +5. Click **Next** -> **Upload and Instantiate**. + +![Placeholder: Screenshot of ui.use.ink showing the 'Upload' screen with the Green Checkmark] (./images/upload_success.png) + +--- + +## Phase 3: Interaction Walkthrough + +Once deployed, the contract lands on the "Interact" page. Here is the lifecycle of a poll. + +### Step 1: Check Status (Poll Closed) +By default, the poll is inactive. +* **Message:** `isActive()` +* **Click:** Read/Call +* **Result:** `false` + +### Step 2: Start the Poll (Admin Only) +Only Alice can do this. +* **Message:** `startPoll()` +* **Caller:** Real Alice +* **Action:** Click **Call Contract** and sign. + +### Step 3: Voting (Public Action) +Simulate a user voting "Yes". +* **Message:** `vote(choice: bool)` +* **Argument:** `true` (for Yes) +* **Caller:** Real Alice (or any funded account). +* **Action:** Click **Call Contract**. + +*Note: If you try to vote again with the same account, the transaction will fail with `Error: AlreadyVoted`.* + +### Step 4: Live Tally +Check the vote count. +* **Message:** `getResults()` +* **Action:** Read +* **Result:** `(1, 0)` indicating 1 Yes, 0 No. + +![Placeholder: Screenshot of 'getResults' output showing (1,0)] (./images/get_results.png) + +### Step 5: End Poll +* **Message:** `endPoll()` +* **Action:** Call Contract. +* **Result:** `isActive` returns `false` again. Voting is now disabled. + +--- + +## Troubleshooting Common Errors + +During the development of this contract, several specific errors were encountered and resolved. + +### 1. `StorageDepositNotEnoughFunds` +**Symptom:** The UI refuses to instantiate the contract, claiming the origin doesn't have enough balance. +**Cause:** You are using the default "Alice" account which has $0 because the derivation path was missing during import. +**Solution:** Import the Account using the **Raw Hex Seed** (provided in the Wallet Setup section above) to access the pre-funded developer funds. + +### 2. `WASM interface has not been initialized` +**Symptom:** A Javascript error popup appears when trying to sign a transaction in the browser. +**Cause:** The Polkadot.js cryptography libraries inside the browser haven't loaded before the UI tried to use them. +**Solution:** +1. Refresh the page (`Ctrl + R`). +2. Switch the account dropdown to a different user and back to Alice. +3. Ideally, use a browser extension (SubWallet/Talisman) instead of the UI's built-in "development accounts." + +### 3. `Module(NotAdmin)` +**Symptom:** Transaction fails when calling `startPoll` or `endPoll`. +**Cause:** You are calling the function with an account that is not the contract Creator. +**Solution:** Ensure the "Caller" dropdown is set to the same account that instantiated the contract (Alice). + +--- + +## API Reference + +| Message Name | Type | Arguments | Returns | Description | +| :--- | :--- | :--- | :--- | :--- | +| `new()` | Constructor | None | Self | Initializes admin as caller. | +| `start_poll()` | Transact | None | Result | **Admin Only.** Resets votes, opens poll. | +| `end_poll()` | Transact | None | Result | **Admin Only.** Closes poll. | +| `vote()` | Transact | `choice: bool` | Result | Records vote. Fails if closed/already voted. | +| `get_results()`| Query | None | `(u32, u32)` | Returns `(yes_votes, no_votes)`. | +| `is_active()` | Query | None | `bool` | Returns `true` if voting is open. | + +--- + +## License + +This project is open-source and available under the MIT License. + +*Built with ❤️ using [ink!](https://use.ink/) v6.* \ No newline at end of file diff --git a/poll-v6/images/get_results.png b/poll-v6/images/get_results.png new file mode 100644 index 00000000..d7abbf66 Binary files /dev/null and b/poll-v6/images/get_results.png differ diff --git a/poll-v6/images/poll-build.JPG b/poll-v6/images/poll-build.JPG new file mode 100644 index 00000000..002d3a84 Binary files /dev/null and b/poll-v6/images/poll-build.JPG differ diff --git a/poll-v6/images/poll-test.JPG b/poll-v6/images/poll-test.JPG new file mode 100644 index 00000000..91c28afb Binary files /dev/null and b/poll-v6/images/poll-test.JPG differ diff --git a/poll-v6/images/upload_success.png b/poll-v6/images/upload_success.png new file mode 100644 index 00000000..2530f27d Binary files /dev/null and b/poll-v6/images/upload_success.png differ diff --git a/poll-v6/lib.rs b/poll-v6/lib.rs new file mode 100644 index 00000000..1b101c40 --- /dev/null +++ b/poll-v6/lib.rs @@ -0,0 +1,175 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod poll { + use ink::storage::Mapping; + use ink::primitives::H160; + + #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + NotAdmin, + PollNotActive, + AlreadyVoted, + PollAlreadyActive, + } + + pub type Result = core::result::Result; + + #[ink(event)] + pub struct Voted { + #[ink(topic)] + voter: H160, + choice: bool, // true = yes, false = no + } + + #[ink(event)] + pub struct PollStateChanged { + is_active: bool, + } + + #[ink(storage)] + pub struct Poll { + /// The boss. Only this account can open/close polls. + admin: H160, + /// Is voting currently allowed? + is_active: bool, + /// Total "Yes" votes. + yes_votes: u32, + /// Total "No" votes. + no_votes: u32, + /// Tracks who has voted to prevent double voting. + has_voted: Mapping, + } + + impl Poll { + #[ink(constructor)] + pub fn new() -> Self { + let caller = Self::env().caller(); + Self { + admin: caller, + is_active: false, // Poll starts closed + yes_votes: 0, + no_votes: 0, + has_voted: Mapping::default(), + } + } + + /// ADMIN ONLY: Starts a new poll (resets counters). + #[ink(message)] + pub fn start_poll(&mut self) -> Result<()> { + self.ensure_admin()?; + + if self.is_active { + return Err(Error::PollAlreadyActive); + } + + // Reset state for new poll + self.is_active = true; + self.yes_votes = 0; + self.no_votes = 0; + // Note: In a real app, clearing a Mapping is expensive. + // Usually, we would use a PollID to separate polls. + // For this simple example, we accept previous voters can't vote again + // or we manually assume a reset logic (omitted for simplicity). + + self.env().emit_event(PollStateChanged { is_active: true }); + Ok(()) + } + + /// ADMIN ONLY: Stops the poll. + #[ink(message)] + pub fn end_poll(&mut self) -> Result<()> { + self.ensure_admin()?; + self.is_active = false; + self.env().emit_event(PollStateChanged { is_active: false }); + Ok(()) + } + + /// PUBLIC: Vote Yes (true) or No (false). + #[ink(message)] + pub fn vote(&mut self, choice: bool) -> Result<()> { + if !self.is_active { + return Err(Error::PollNotActive); + } + + let caller = self.env().caller(); + if self.has_voted.contains(caller) { + return Err(Error::AlreadyVoted); + } + + // Record vote + if choice { + self.yes_votes += 1; + } else { + self.no_votes += 1; + } + + // Mark voter as "done" + self.has_voted.insert(caller, &true); + + self.env().emit_event(Voted { + voter: caller, + choice, + }); + + Ok(()) + } + + /// PUBLIC: View current results. + #[ink(message)] + pub fn get_results(&self) -> (u32, u32) { + (self.yes_votes, self.no_votes) + } + + /// PUBLIC: Check if poll is active. + #[ink(message)] + pub fn is_active(&self) -> bool { + self.is_active + } + + // --- Helper to protect Admin functions --- + fn ensure_admin(&self) -> Result<()> { + if self.env().caller() != self.admin { + Err(Error::NotAdmin) + } else { + Ok(()) + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn admin_can_start_poll() { + let mut poll = Poll::new(); + assert_eq!(poll.is_active(), false); + + assert_eq!(poll.start_poll(), Ok(())); + assert_eq!(poll.is_active(), true); + } + + #[ink::test] + fn voting_works() { + let mut poll = Poll::new(); + poll.start_poll().unwrap(); + + // Vote Yes + assert_eq!(poll.vote(true), Ok(())); + + // Check results: (1 Yes, 0 No) + assert_eq!(poll.get_results(), (1, 0)); + } + + #[ink::test] + fn double_voting_fails() { + let mut poll = Poll::new(); + poll.start_poll().unwrap(); + + assert_eq!(poll.vote(true), Ok(())); + assert_eq!(poll.vote(false), Err(Error::AlreadyVoted)); + } + } +}