Skip to content

Latest commit

 

History

History
302 lines (243 loc) · 11.7 KB

File metadata and controls

302 lines (243 loc) · 11.7 KB

Changelog

All notable source-level changes to the EcoTask contracts are documented in this file.

The format is based on Keep a Changelog, and source releases follow Semantic Versioning. Soroban deployments are immutable: a breaking public API or storage layout change requires a new contract deployment and address, even when the source package version is bumped.

Security

eco-token, task-registry, and reward-engine

  • Admin rotation now uses a two-step handover. The current admin proposes a successor with propose_admin, and the proposed address must authenticate and call accept_admin before receiving control. The existing transfer_admin entry point remains as a compatibility alias for the proposal step. Successful proposals and acceptances emit AdminProposedEvent and AdminAcceptedEvent, respectively.

Fixed

reward-engine

  • Fixed documentation for the oracle field in the Verification struct to explicitly state it records the submitter.
  • Fixed setup() tuple unpacking in unit tests in verification.rs.

eco-token

  • [#67] Prevent self-transfer allowance drain in transfer_from and fix storage re-fetch TOCTOU in spend_allowance. transfer_from now panics with "token: cannot transfer to self" when from == to, preventing spenders from burning an owner's allowance without transferring tokens. spend_allowance in storage.rs now accepts the &Allowance struct directly instead of re-fetching from persistent storage with .unwrap(), eliminating a potential TOCTOU window.

task-registry

  • [#52] Unbounded CreatorTasks Vec replaced with indexed persistent storage. Each create_task call previously read and rewrote an ever-growing Vec<u64> under DataKey::CreatorTasks(creator). Prolific sponsors could drive storage and compute costs up without bound, and the serialized Vec would eventually exhaust transaction limits. The storage layout is now:

    Key Value Purpose
    DataKey::CreatorTaskCount(Address) u64 number of tasks for that creator
    DataKey::CreatorTask(Address, u64) u64 task id at the given 0-based index

    push_creator_task now does one read and two writes regardless of the creator's history size (O(1) per task). get_tasks_by_creator_paged reads only the indexed entries required for the requested page. The unpaged get_tasks_by_creator is retained for API compatibility but is deprecated and hard-capped at 50 entries to stay within the Soroban 100-entry footprint budget.

Added

eco-token

  • [#41] Added fuzz / property-based arithmetic tests and max supply boundary tests using proptest.
  • set_minter now emits a MinterUpdatedEvent (#[contractevent]) on every successful minter rotation, containing the admin (topic), previous_minter, and new_minter fields.

Changed

eco-token

  • set_minter now panics with "token: minter must differ from admin" when the caller attempts to set minter == admin, enforcing role separation.

reward-engine

  • set_user_cooldown(caller: Address, min_ledgers_between_rewards: u64) — admin-only; sets the minimum number of ledgers a user must wait between reward approvals. 0 disables the cooldown (the default).

Storage layout additions:

Scope Key Value
Instance DataKey::UserCooldown u64
Persistent DataKey::LastRewardLedger(Address) u64

Error strings:

  • engine: user cooldown active

reward-engine (proof CID validation)

  • submit_proof validates proof_cid length, rejecting empty or oversized (> MAX_CID_LEN bytes) CID strings before hashing/storage.

Error strings:

  • engine: proof cid must not be empty
  • engine: proof cid too long

0.1.0-alpha - 2026-08-17

This entry records the current source interface as the baseline for future contract releases. It does not designate any deployed contract address as a release.

Added

eco-token

Public API:

  • initialize(admin: Address, name: String, symbol: String, decimal: u32)
  • mint(to: Address, amount: i128)
  • transfer(from: Address, to: Address, amount: i128)
  • balance(id: Address) -> i128
  • total_supply() -> i128
  • max_supply() -> i128
  • set_max_supply(caller: Address, max_supply: i128)
  • name() -> String
  • symbol() -> String
  • decimal() -> u32
  • decimals() -> u32
  • set_metadata(caller: Address, name: String, symbol: String, decimal: u32)
  • admin() -> Address
  • transfer_admin(current_admin: Address, new_admin: Address)
  • minter() -> Address
  • set_minter(caller: Address, new_minter: Address)
  • burn(from: Address, amount: i128)
  • approve(owner: Address, spender: Address, amount: i128, expiration_ledger: u32)
  • allowance(owner: Address, spender: Address) -> i128
  • allowance_with_expiry(owner: Address, spender: Address) -> Option<(i128, u32)>
  • transfer_from(spender: Address, from: Address, to: Address, amount: i128)

Storage layout:

Scope Key Value
Instance "admin" Address
Instance "minter" Address
Instance "name" String
Instance "symbol" String
Instance "decimal" u32
Instance "supply" i128
Instance "maxsupply" i128
Persistent ("balance", Address) i128
Persistent ("allow", owner: Address, spender: Address) Allowance { amount: i128, expiration_ledger: u32 }

Error strings:

  • token: allowance expired
  • token: allowance not found
  • token: already initialized
  • token: amount must be non-negative
  • token: amount must be positive
  • token: expiration must be in the future
  • token: insufficient allowance
  • token: insufficient balance
  • token: max supply below current supply
  • token: max supply must be positive
  • token: new admin must be different
  • token: supply cap exceeded
  • token: unauthorized
  • allowance underflow
  • balance overflow
  • balance underflow
  • supply overflow
  • supply underflow

task-registry

Public API:

  • initialize(admin: Address)
  • add_sponsor(caller: Address, sponsor: Address)
  • remove_sponsor(caller: Address, sponsor: Address)
  • create_task(creator: Address, task_type: String, location_hash: BytesN<32>, reward_amount: i128, max_completions: u32, expires_at: u64) -> u64
  • get_task(task_id: u64) -> Task
  • get_task_live_status(task_id: u64) -> Task
  • complete_task(caller: Address, task_id: u64, user: Address)
  • expire_task(caller: Address, task_id: u64)
  • expire_task_permissionless(task_id: u64)
  • extend_task_expiry(caller: Address, task_id: u64, new_expires_at: u64)
  • cancel_task(caller: Address, task_id: u64)
  • admin_cancel_task(caller: Address, task_id: u64)
  • task_count() -> u64
  • is_task_completed(task_id: u64, user: Address) -> bool
  • get_tasks_by_creator(creator: Address) -> Vec<u64>
  • get_tasks_by_creator_paged(creator: Address, cursor: u32, limit: u32) -> Vec<u64>
  • list_tasks(cursor: u64, limit: u32) -> Vec<Task>
  • transfer_admin(current_admin: Address, new_admin: Address)

Storage layout:

Scope Key Value
Instance DataKey::TaskCount u64
Instance DataKey::Admin Address
Persistent DataKey::Task(task_id: u64) Task { id, creator, task_type, location_hash, reward_amount, max_completions, completions, status, created_at, expires_at }
Persistent DataKey::Sponsor(Address) bool
Persistent DataKey::Completion(task_id: u64, user: Address) bool
Persistent DataKey::CreatorTasks(Address) Vec<u64>

TaskStatus variants are Active, Completed, Expired, and Cancelled.

Error strings:

  • registry: already completed
  • registry: already initialized
  • registry: expiry must be in the future
  • registry: max completions must be positive
  • registry: max completions reached
  • registry: new admin must be different
  • registry: new expiry must extend the current one
  • registry: reward must be positive
  • registry: sponsor revoked
  • registry: task expired
  • registry: task is not active
  • registry: task not found
  • registry: task not yet expired
  • registry: task type must not be empty
  • registry: unauthorized

reward-engine

Public API:

  • initialize(admin: Address, token: Address, registry: Address, oracle: Address)
  • set_oracle(caller: Address, new_oracle: Address)
  • add_oracle(caller: Address, new_oracle: Address)
  • remove_oracle(caller: Address, oracle: Address)
  • get_oracles() -> Vec<Address>
  • is_oracle(addr: Address) -> bool
  • set_token(caller: Address, new_token: Address)
  • set_registry(caller: Address, new_registry: Address)
  • set_reward_range(caller: Address, min_reward: i128, max_reward: i128)
  • pause(caller: Address)
  • unpause(caller: Address)
  • is_paused() -> bool
  • submit_proof(oracle: Address, user: Address, task_id: u64, proof_cid: String)
  • approve_proof(oracle: Address, user: Address, task_id: u64, reward_amount: i128)
  • reject_proof(oracle: Address, user: Address, task_id: u64)
  • dispute_proof(caller: Address, user: Address, task_id: u64)
  • resolve_dispute(caller: Address, user: Address, task_id: u64, approve: bool, reward_amount: i128)
  • get_verification(task_id: u64, user: Address) -> Verification
  • get_verification_by_cid_hash(cid_hash: BytesN<32>) -> Verification
  • get_pending_verifications_paged(cursor: u32, limit: u32) -> Vec<Verification>
  • get_pending_verifications() -> Vec<Verification>
  • get_verifications_by_user(user: Address, cursor: u32, limit: u32) -> Vec<Verification>
  • total_paid() -> i128
  • transfer_admin(current_admin: Address, new_admin: Address)

Storage layout:

Scope Key Value
Instance DataKey::Admin Address
Instance DataKey::Token Address
Instance DataKey::Registry Address
Instance DataKey::Oracles Vec<Address>
Instance DataKey::MinReward i128
Instance DataKey::MaxReward i128
Instance DataKey::VerificationList Vec<VerificationKey>
Instance DataKey::TotalPaid i128
Instance DataKey::Paused bool
Persistent DataKey::Verification(task_id: u64, user: Address) Verification { task_id, user, proof_cid, reward_amount, status, submitted_at, resolved_at, oracle }
Persistent DataKey::CidHash(BytesN<32>) VerificationKey { task_id, user }
Persistent DataKey::UserVerifications(Address) Vec<u64>

VerificationStatus variants are Pending, Approved, Rejected, and Disputed. CID index entries are extended to 4,096 ledgers when written.

Error strings:

  • engine: already initialized
  • engine: cannot remove the last oracle
  • engine: contract is paused
  • engine: max reward must be >= min reward
  • engine: min reward must be positive
  • engine: new admin must be different
  • engine: not found
  • engine: oracle already registered
  • engine: oracle must be different from admin
  • engine: oracle not registered
  • engine: proof already submitted
  • engine: proof cid already submitted
  • engine: reward amount must be positive
  • engine: reward below minimum
  • engine: reward exceeds maximum
  • engine: reward exceeds task budget
  • engine: task has expired
  • engine: task is not active
  • engine: unauthorized
  • engine: verification is not disputable
  • engine: verification is not disputed
  • engine: verification is not pending
  • engine: verification not found
  • total_paid overflow