diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a48ab0b..5952b59 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,4 +1,18 @@ version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +version: 2 updates: - package-ecosystem: cargo directory: / diff --git a/.github/workflows/dependency-scan.yml b/.github/workflows/dependency-scan.yml new file mode 100644 index 0000000..4adf2c9 --- /dev/null +++ b/.github/workflows/dependency-scan.yml @@ -0,0 +1,20 @@ +name: Dependency Scan + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Dependency Review Action + uses: github/dependency-review-action@v1 + - name: Run cargo-audit + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/contracts/governance/src/lib.rs b/contracts/governance/src/lib.rs index 8abf167..5dad691 100644 --- a/contracts/governance/src/lib.rs +++ b/contracts/governance/src/lib.rs @@ -397,11 +397,13 @@ impl GovernanceContract { return Err(ContractError::NoVotingPower); } - // Weight = voter's own balance (delegators' balances are added via get_delegated_weight). - // Since we cannot enumerate all delegators on-chain, governance uses an empty list here; - // the voter's own balance is always included. Delegators who want their weight counted - // must have their delegate cast the vote on their behalf. - let weight = token_client.get_delegated_weight(&voter, &Vec::new(&env)); + // Use the stored proposal snapshot ledger to read historical balances. + // Record the voter's snapshot balance as their voting weight. Delegated + // weight is not enumerated on-chain; off-chain tooling should provide + // delegator lists if needed. Using `balance_at` prevents manipulation + // of balances after proposal creation from affecting the tally. + let snapshot_ledger = proposal.snapshot_ledger as u64; + let weight = token_client.balance_at(&voter, &snapshot_ledger); if weight <= 0 { return Err(ContractError::NoVotingPower); } diff --git a/contracts/governance/src/test.rs b/contracts/governance/src/test.rs index ace1f52..da25cd2 100644 --- a/contracts/governance/src/test.rs +++ b/contracts/governance/src/test.rs @@ -371,6 +371,48 @@ fn test_cast_vote_yes() { assert_eq!(record.weight, 10_000_000); } +#[test] +fn test_vote_uses_snapshot_balance() { + let env = Env::default(); + env.mock_all_auths(); + let admin = Address::generate(&env); + let voter = Address::generate(&env); + + let token_id = env.register(TokenContract, ()); + let token = TokenContractClient::new(&env, &token_id); + token.initialize( + &admin, + &1_000_000_000i128, + &String::from_str(&env, "CosmosVote"), + &String::from_str(&env, "VOTE"), + &7u32, + ); + // initial balance 10M + token.mint(&admin, &voter, &10_000_000i128); + + let gov_id = env.register(GovernanceContract, ()); + let gov = GovernanceContractClient::new(&env, &gov_id); + gov.initialize(&admin, &token_id, &0i128, &0u64, &0u32, &false, &None); + + // Create proposal -> snapshot captured now + let id = gov.create_proposal( + &voter, + &String::from_str(&env, "Snapshot Test"), + &String::from_str(&env, "Balances after snapshot should not count"), + &1i128, + &3600u64, + &None, + ); + + // Mint more tokens after proposal creation + token.mint(&admin, &voter, &5_000_000i128); + + // Vote — weight must equal snapshot (10M), not current (15M) + gov.cast_vote(&voter, &id, &Vote::Yes); + let record = gov.get_vote(&id, &voter); + assert_eq!(record.weight, 10_000_000i128); +} + #[test] fn test_cast_vote_no() { let env = Env::default(); diff --git a/contracts/treasury/Cargo.toml b/contracts/treasury/Cargo.toml index 9ea45a7..db21dce 100644 --- a/contracts/treasury/Cargo.toml +++ b/contracts/treasury/Cargo.toml @@ -12,6 +12,8 @@ soroban-sdk = { version = "26.0.1" } [dev-dependencies] soroban-sdk = { version = "26.0.1", features = ["testutils"] } +cosmosvote-governance = { path = "../governance", features = ["testutils"] } +cosmosvote-token = { path = "../token", features = ["testutils"] } [features] testutils = ["soroban-sdk/testutils"] diff --git a/contracts/treasury/src/lib.rs b/contracts/treasury/src/lib.rs index a38b134..95b8349 100644 --- a/contracts/treasury/src/lib.rs +++ b/contracts/treasury/src/lib.rs @@ -123,4 +123,78 @@ mod test { client.initialize(&governance); client.initialize(&governance); } + + #[test] + fn test_governance_triggers_treasury_disbursement() { + use cosmosvote_governance::{GovernanceContract, GovernanceContractClient}; + use cosmosvote_governance::types::{TreasuryAction as GovTreasuryAction, TreasuryAsset as GovTreasuryAsset}; + use cosmosvote_token::{TokenContract, TokenContractClient}; + + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let proposer = Address::generate(&env); + let recipient = Address::generate(&env); + + // Deploy token contract and mint funds to the treasury later + let token_id = env.register(TokenContract, ()); + let token = TokenContractClient::new(&env, &token_id); + token.initialize( + &admin, + &1_000_000_000i128, + &soroban_sdk::String::from_str(&env, "CosmosVote"), + &soroban_sdk::String::from_str(&env, "VOTE"), + &7u32, + ); + + // Register governance and treasury contracts + let gov_id = env.register(GovernanceContract, ()); + let treasury_id = env.register(TreasuryContract, ()); + + // Initialize treasury with governance address + let treasury_client = TreasuryContractClient::new(&env, &treasury_id); + treasury_client.initialize(&gov_id); + + // Mint some tokens to the treasury so it can disburse + token.mint(&admin, &treasury_client.address, &1_000i128); + + // Initialize governance with treasury address and token + let gov = GovernanceContractClient::new(&env, &gov_id); + gov.initialize(&admin, &token_id, &0i128, &0u64, &0u32, &false, &Some(treasury_id)); + + // Create a proposal that disburses 500 tokens to `recipient` + let action = GovTreasuryAction { + recipient: recipient.clone(), + amount: 500i128, + asset: GovTreasuryAsset::Token(token_id.clone()), + }; + + let id = gov.create_proposal( + &proposer, + &soroban_sdk::String::from_str(&env, "Treasury Disburse"), + &soroban_sdk::String::from_str(&env, "Disburse tokens to recipient"), + &1i128, + &3600u64, + &Some(action), + ); + + // Give proposer a small balance and vote yes + token.mint(&admin, &proposer, &1i128); + gov.cast_vote(&proposer, &id, &cosmosvote_governance::types::Vote::Yes); + + // Fast-forward time and finalize + let proposal = gov.get_proposal(&id); + env.ledger().set_timestamp(proposal.end_time + 1); + gov.finalise(&id); + + // Execute as admin (admin has auth via mock_all_auths) + gov.execute(&admin, &id); + + // Verify recipient received tokens and treasury balance decreased + let recipient_bal = token.balance(&recipient); + assert_eq!(recipient_bal, 500i128); + let treasury_bal = token.balance(&treasury_client.address); + assert_eq!(treasury_bal, 500i128); + } }