Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions contracts/payment_executor/tests/treasury_authorization_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use payment_executor::{ContractAddresses, PaymentError, PaymentExecutor, Payment
use payroll_registry::{PayrollRegistry, PayrollRegistryClient};
use proof_verifier::{ProofVerifier, ProofVerifierClient, VerificationKey};
use salary_commitment::SalaryCommitmentContract;
use soroban_sdk::testutils::{Address as _, Ledger, MockAuth, MockAuthInvoke};
use soroban_sdk::{Address, BytesN, Env, IntoVal, Vec};
use soroban_sdk::testutils::{Address as _, Events, Ledger, MockAuth, MockAuthInvoke};
use soroban_sdk::{Address, BytesN, Env, IntoVal, TryIntoVal, Vec};

fn mock_vk(env: &Env) -> VerificationKey {
VerificationKey {
Expand Down Expand Up @@ -105,6 +105,84 @@ fn amount_to_public_input(env: &Env, amount: i128) -> BytesN<32> {
BytesN::from_array(env, &bytes)
}

#[test]
fn test_treasury_asset_registration_defaults_to_supported_token_only() {
let env = Env::default();
let (
executor,
_registry,
_commitment,
_token,
_company_id,
_admin,
_treasury,
_employee,
token_id,
) = setup_system_no_auth(&env);
let unregistered_asset = Address::generate(&env);

assert!(executor.is_asset_allowed(&token_id));
assert!(!executor.is_asset_allowed(&unregistered_asset));
}

#[test]
fn test_treasury_asset_registration_can_be_updated_and_disabled() {
let env = Env::default();
let (
executor,
_registry,
_commitment,
_token,
_company_id,
_admin,
_treasury,
_employee,
_token_id,
) = setup_system_no_auth(&env);
let asset = Address::generate(&env);

executor.set_asset_allowed(&asset, &true);
assert!(executor.is_asset_allowed(&asset));

executor.set_asset_allowed(&asset, &false);
assert!(!executor.is_asset_allowed(&asset));
}

#[test]
fn test_treasury_asset_registration_emits_state_event() {
let env = Env::default();
let (
executor,
_registry,
_commitment,
_token,
_company_id,
_admin,
_treasury,
_employee,
_token_id,
) = setup_system_no_auth(&env);
let asset = Address::generate(&env);
let before = env.events().all().len();

executor.set_asset_allowed(&asset, &true);

let events = env.events().all();
assert_eq!(events.len(), before + 1);
let event = events.get(before).unwrap();
let topic: soroban_sdk::Symbol = event.1.get(0).unwrap().try_into_val(&env).unwrap();
let emitted_asset: Address = event.1.get(1).unwrap().try_into_val(&env).unwrap();
let (allowed, timestamp): (bool, u64) = event.2.try_into_val(&env).unwrap();

assert_eq!(
topic,
soroban_sdk::Symbol::new(&env, "TreasuryAssetAllowedUpdated")
);
assert_eq!(emitted_asset, asset);
assert!(allowed);
assert_eq!(timestamp, env.ledger().timestamp());
}

#[test]
fn test_execution_with_correct_treasury_context() {
let env = Env::default();
Expand Down
13 changes: 12 additions & 1 deletion docs/treasury.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@
This document outlines the expected treasury authorization behavior during payroll execution in `zk-payroll-contracts`.

## Overview
Payroll execution requires appropriate authorization from both the company administrator (to trigger the payroll) and the company treasury (to release the funds).

Payroll execution requires appropriate authorization from both the company administrator (to trigger the payroll) and the company treasury (to release the funds).

## Authorization Rules

1. **Valid Treasury Authorization**: Payroll execution must be authorized by the specific treasury account registered to the company in the `PayrollRegistry`. The execution logic pulls the `treasury` address directly from the registry and invokes a token transfer from it.
2. **Mismatched Treasury Rejection**: If an attacker or a different treasury account attempts to authorize the transfer, the token contract will reject it because the `from` address in `token.transfer(&company.treasury, &employee, &amount)` must match the authorized address.
3. **Mismatched Asset Rejection**: Only allowlisted tokens can be used for payments. If an unapproved asset is configured or attempted to be used, the execution panics with `Asset not allowed`.
4. **Stale Authorization**: Execution requires fresh proofs. Proofs older than the maximum proof age (7 days) will be rejected with a `ProofExpired` error.

## Treasury Asset Registration

The token configured during contract initialization is allowlisted automatically. Other assets start disabled and must be explicitly registered by the contract administrator with `set_asset_allowed(asset, true)`. The administrator can disable a registered asset with `set_asset_allowed(asset, false)`; disabled and unregistered assets are rejected before payment execution.

Each payment executor update emits `TreasuryAssetAllowedUpdated` with the asset address, the new allowlist state, and the ledger timestamp. These events contain asset configuration only and must not be used to publish payroll amounts, employee data, or proof material.

The integration tests cover the default-deny state, registration, update and disable transitions, lifecycle event contents, and rejection of payments using a disabled asset.

## Technical Implementation

- **Company Admin Auth**: Explicitly checked via `company.admin.require_auth()`.
- **Treasury Auth**: Implicitly enforced by the token contract when `token_client.transfer(&company.treasury, ...)` is called. Soroban's auth framework requires the `company.treasury` signature to be present in the transaction auth entries.
- **Asset Allowlist**: Checked via `is_asset_allowed()`.