Skip to content

Conversation

@linguists
Copy link
Contributor

@linguists linguists commented May 12, 2025

NEAR Validator Voting Contract

The purpose of this contract is for validators to vote on any specific proposal. Validators can call vote function to vote for yes or no with the staked amount on the validator. If there are more than 2/3 of the stake at any given moment voting for yes, the voting is done. After the voting is finished or the voting deadline has passed, no one can further modify the contract. The voting contract is recommended to be pinged every epoch to make sure the latest stake is updated in the contract.

Summary by CodeRabbit

  • New Features
    • Introduced a NEAR Validator Voting smart contract enabling stake-weighted voting with automatic finalization upon reaching quorum.
    • Added voting, vote withdrawal, stake update, and status retrieval functionalities.
    • Emitted structured events for voting actions and proposal approval.
    • Provided detailed documentation and usage instructions in the README.
    • Added Rust toolchain and project configuration files.
  • Chores
    • Implemented CI workflows for building, formatting, linting, and testing.
    • Simplified the .gitignore file to exclude only build artifacts.
  • Tests
    • Included comprehensive unit tests and a commented-out integration test scaffold.

@linguists linguists changed the title feat: voting contract feat: validator voting contract May 13, 2025

/// Method for validators to vote or withdraw the vote.
/// Votes for if `is_vote` is true, or withdraws the vote if `is_vote` is false.
pub fn vote(&mut self, is_vote: bool) {
Copy link
Contributor

@arkff202310 arkff202310 May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we change the code as following?

pub fn vote(&mut self, is_vote: bool) {
    let account_id = env::predecessor_account_id();
    if is_vote {
        self.votes.insert(&account_id, &0); // or Balance::MAX
    } else {
        self.votes.remove(&account_id);
    }
    self.ping();
}

Copy link
Contributor Author

@linguists linguists May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work well.

In this way, if ping() is already invoked in the current epoch, the total_voted_stake will not be updated right away after a new vote().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think remove the condition if cur_epoch_height != self.last_epoch_height can solve this issue, this is unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the condition to save gas cost for vote() function call. It's not necessary to update stake balances very frequently.

@linguists
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented May 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented May 14, 2025

Walkthrough

This update introduces the initial project setup for a NEAR smart contract enabling stake-weighted validator voting on proposals. It adds comprehensive CI workflows for building, formatting, linting, and testing. The contract logic, configuration files, build instructions, and documentation are established, with unit tests and a commented integration test scaffold included.

Changes

File(s) Change Summary
.github/workflows/build.yml Added a GitHub Actions workflow to build the project on push and pull request events using cargo-near.
.github/workflows/test.yml Added a GitHub Actions workflow for code formatting checks, linting with Clippy, and running tests on push and pull request events.
.gitignore Simplified to only ignore the /target directory, removing all other ignore patterns.
Cargo.toml Introduced the project manifest specifying metadata, dependencies (notably near-sdk), reproducible build config, and a custom release profile for the "validator-voting" contract.
README.md Rewritten to provide a detailed overview of the contract, voting mechanism, state mutability, and instructions for building, testing, and deploying with cargo-near.
rust-toolchain.toml Added to specify the Rust toolchain channel, components, and WASM target for consistent builds.
src/lib.rs Implemented the NEAR validator voting contract, including vote tracking, stake updates, voting finalization logic, view methods, and comprehensive unit tests.
src/events.rs Added event definitions and an emit method for structured logging of voting actions and proposal approval events.
tests/test_basics.rs Added a (commented-out) integration test scaffold using near_workspaces for contract deployment and method calls in a sandbox environment.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Contract
    participant NEAR_Env

    User->>Contract: new(proposal, deadline)
    Note right of Contract: Initializes contract state

    User->>Contract: vote(is_vote)
    Contract->>NEAR_Env: Query validator stake for caller
    Contract->>Contract: Update votes & total_voted_stake
    Contract->>Contract: Check if threshold met
    Contract->>Contract: Emit Voted or VoteWithdrawn event

    User->>Contract: ping()
    Contract->>NEAR_Env: Query all validator stakes
    Contract->>Contract: Update votes & total_voted_stake
    Contract->>Contract: Check if threshold met
    Contract->>Contract: Emit ProposalApproved event if threshold met

    User->>Contract: get_total_voted_stake()
    Contract-->>User: (voted_stake, total_stake)

    User->>Contract: get_votes()
    Contract-->>User: votes map

    User->>Contract: get_result()
    Contract-->>User: result timestamp or None
Loading

Poem

In the meadow of code, a contract appears,
Where validators vote and the outcome clears.
With workflows that test, and builds that are neat,
The voting is fair, the logic complete.
A hop and a skip, the project takes flight—
Rabbits rejoice, for the code feels just right!
🐇✨

Tip

⚡️ Faster reviews with caching
  • CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.

Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (10)
.github/workflows/build.yml (1)

13-20: Add caching to speed up CI builds.
To reduce build times, consider caching the Cargo registry and generated artifacts between runs. For example:

+      - name: Cache Cargo registry
+        uses: actions/cache@v3
+        with:
+          path: ~/.cargo/registry
+          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+      - name: Cache build artifacts
+        uses: actions/cache@v3
+        with:
+          path: target
+          key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }}

This can significantly speed up subsequent builds.

README.md (1)

3-3: Fix missing article in description.
Change “Validators can call vote function” to “Validators can call the vote function” for grammatical clarity.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~3-~3: You might be missing the article “the” here.
Context: ... specific proposal. Validators can call vote function to vote for yes or no wit...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

.github/workflows/test.yml (1)

13-16: Speed up CI by caching dependencies and build artifacts.
Introduce caching after the checkout step in each job. For example:

+      - name: Cache Cargo registry
+        uses: actions/cache@v3
+        with:
+          path: ~/.cargo/registry
+          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+      - name: Cache build artifacts
+        uses: actions/cache@v3
+        with:
+          path: target
+          key: ${{ runner.os }}-cargo-target-${{ hashFiles('**/Cargo.lock') }}

This will significantly reduce job execution time on subsequent runs.

Also applies to: 21-27, 32-35

tests/test_basics.rs (1)

1-29: Entire test file is commented-out – enable or delete to avoid bit-rot

cargo test will silently ignore this file because every line is commented. Either

  1. uncomment the code (and add near-workspaces back to dev-dependencies) so that the integration test actually protects the contract, or
  2. remove the file / gate it behind #[cfg(feature = "e2e")] to keep the test suite signal-to-noise ratio high.

Running dead code through CI wastes time and may mislead new contributors.

Cargo.toml (2)

6-10: Fill in the repository field

Tooling such as cargo about, NEAR explorer metadata and cargo-near pick up the
repository URL to surface links back to source. Leaving a placeholder degrades UX
for downstream integrators.


44-45: Upgrade Tokio – 1.12 is ~3 years old

Tokio 1.12 lacks many perf & soundness fixes. Unless you have a lock-in
constraint, bumping to the latest 1.x (currently 1.37) is a drop-in upgrade.

-tokio = { version = "1.12.0", features = ["full"] }
+tokio = { version = "1.37.0", features = ["full"] }
src/lib.rs (4)

31-39: Initialize last_epoch_height with the current epoch

Setting it to 0 means the very first vote() triggers an unnecessary full
re-scan in ping(). Capturing env::epoch_height() once saves gas on the first
call.

-            last_epoch_height: 0,
+            last_epoch_height: env::epoch_height(),

41-50: Duplicate deadline / result checks – consider DRYing

vote() and ping() repeat the same require! guards. Extracting an internal
assert_voting_open() would reduce byte-code size and future maintenance
foot-guns.


75-95: ping() reallocates and re-serialises the vote map every epoch

std::mem::take followed by reinsertion forces two full writes of the map per
epoch. When switching to UnorderedMap, you can iterate keys in-place and only
touch entries whose stake actually changed, cutting gas roughly in half.


103-107: Emit an event when voting finalises

External indexers and off-chain clients have no efficient way to learn that
result flipped to Some(_). Emitting a JSON-serialised event via
near_sdk::env::log_str keeps the contract stateless yet discoverable.

if self.total_voted_stake > total_stake * 2 / 3 {
    self.result = Some(env::block_timestamp_ms());
    env::log_str(&format!("VOTING_FINISHED:{}", self.result.unwrap()));
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b1b51e and 2901dde.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .github/workflows/build.yml (1 hunks)
  • .github/workflows/test.yml (1 hunks)
  • .gitignore (1 hunks)
  • Cargo.toml (1 hunks)
  • README.md (1 hunks)
  • rust-toolchain.toml (1 hunks)
  • src/lib.rs (1 hunks)
  • tests/test_basics.rs (1 hunks)
🧰 Additional context used
🪛 LanguageTool
README.md

[uncategorized] ~3-~3: You might be missing the article “the” here.
Context: ... specific proposal. Validators can call vote function to vote for yes or no wit...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

🔇 Additional comments (3)
.gitignore (1)

1-1: Ignore build artifacts as expected.
The simplified .gitignore correctly excludes the target directory where Rust artifacts are placed.

rust-toolchain.toml (1)

1-4: Toolchain specification is clear and appropriate.
Pinning the stable channel with rustfmt and the wasm32-unknown-unknown target ensures consistent builds and formatting across all environments.

Cargo.toml (1)

41-45: Enable near-workspaces for integration tests

The scaffold in tests/test_basics.rs depends on near-workspaces, but the crate
is commented out here. Uncomment to avoid compilation errors when the test is
re-enabled.

-# near-workspaces = { version = "0.18", features = ["unstable"] }
+near-workspaces = { version = "0.18", features = ["unstable"] }

Comment on lines +12 to +18
proposal: String,
deadline_timestamp_ms: Timestamp,
votes: HashMap<AccountId, Balance>,
total_voted_stake: Balance,
result: Option<Timestamp>,
last_epoch_height: EpochHeight,
}
Copy link

@coderabbitai coderabbitai bot May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Persisting votes in std::collections::HashMap is gas-inefficient

Each insert/remove rewrites the whole map because the entire structure is stored
as a single Borsh blob. With >300 validators this quickly balloons write costs.
Prefer near_sdk::collections::UnorderedMap/LookupMap, which stores each entry
under its own key and scales O(1) per operation.

Refactor sketch:

-use std::collections::HashMap;
-...
-votes: HashMap<AccountId, Balance>,
+use near_sdk::collections::UnorderedMap;
+...
+votes: UnorderedMap<AccountId, Balance>,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There're indeed potential gas issues at scale. That's something we'd like to test in integration test in another PR to confirm whether the gas assumption works well or not.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@linguists That's a sensible approach. Integration testing will provide empirical data on the actual gas costs of the current implementation. This can help determine if the optimization is necessary given your specific validator set size and usage patterns. Let me know if you'd like any guidance on setting up those tests when you get to that PR.


✏️ Learnings added
Learnt from: linguists
PR: linear-protocol/validator-voting-contract#1
File: src/lib.rs:12-18
Timestamp: 2025-05-14T03:23:47.294Z
Learning: The team is aware of potential gas inefficiency with using `std::collections::HashMap` instead of `near_sdk::collections::UnorderedMap` or `LookupMap` for storing validator votes in the validator-voting contract. They plan to test the actual gas usage in integration tests in a future PR before deciding whether to implement this optimization.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/lib.rs (1)

20-24: HashMap in contract state is gas-inefficient (repeat observation)

std::collections::HashMap stores the whole map as one Borsh blob,
causing every insert/remove to rewrite all votes. UnorderedMap or
LookupMap stores each key under its own trie node and scales O(1).

Previous reviews captured this, so tagging as duplicate.

🧹 Nitpick comments (2)
src/events.rs (2)

17-32: Consider owning the payload inside Event to drop the lifetime parameter

Storing references means every call-site has to keep the referenced
values alive until the event is emitted, which complicates usage and
prevents the enum from being passed around or queued for later logging.

Making the fields AccountId, String, U64, U128, … (owned
values) removes the need for the <'a> lifetime and produces a more
ergonomic API with negligible extra gas (serialization already copies
the data).

-pub enum Event<'a> {
-    Voted {
-        validator_id: &'a AccountId,
-    },
+pub enum Event {
+    Voted {
+        validator_id: AccountId,
+    },
     ...
 }

35-45: Handle (de-)serialization errors explicitly & avoid a double json! pass

json!(self) allocates a Value, which is then re-wrapped in another
Value before being stringified. This creates two intermediate
allocations and silently ignores serialization errors.

-        let json = json!(self);
-        let event_json = json!({
-            "standard": EVENT_STANDARD,
-            "version": EVENT_STANDARD_VERSION,
-            "event": json["event"],
-            "data": [json["data"]]
-        })
-        .to_string();
+        // single pass – fails early if serialization ever breaks
+        let event_json = serde_json::to_string(&serde_json::json!({
+            "standard": EVENT_STANDARD,
+            "version": EVENT_STANDARD_VERSION,
+            "event": self,            // `serde` extracts the tag automatically
+        }))
+        .expect("event serialization must succeed");

This keeps the gas footprint unchanged while guaranteeing that any
serialization regression surfaces immediately.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2901dde and 4cb0a9b.

📒 Files selected for processing (3)
  • Cargo.toml (1 hunks)
  • src/events.rs (1 hunks)
  • src/lib.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Cargo.toml
🧰 Additional context used
🧠 Learnings (1)
src/lib.rs (1)
Learnt from: linguists
PR: linear-protocol/validator-voting-contract#1
File: src/lib.rs:12-18
Timestamp: 2025-05-14T03:23:47.294Z
Learning: The team is aware of potential gas inefficiency with using `std::collections::HashMap` instead of `near_sdk::collections::UnorderedMap` or `LookupMap` for storing validator votes in the validator-voting contract. They plan to test the actual gas usage in integration tests in a future PR before deciding whether to implement this optimization.
🔇 Additional comments (1)
src/lib.rs (1)

73-83: check_result() is skipped on vote withdrawal – re-evaluate?

When a validator withdraws (is_vote == false) the contract does not
re-run check_result(). If >⅔ stake had already been reached within
the same epoch and a large withdrawal pushes it back under the
threshold, the contract will not notice until the next epoch’s ping().

If this is intentional (i.e. once a super-majority is reached in-epoch
the proposal is irrevocably approved), please add a comment. Otherwise
consider invoking self.check_result() unconditionally after
total_voted_stake is updated.

@linguists linguists merged commit ed87409 into main May 14, 2025
5 checks passed
@linguists linguists deleted the feat/voting-contract branch May 14, 2025 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants