Skip to content

feat(vault): Implement Core Vault Contract Operations with Unit Tests #3

Description

@Adamsonoladipupo

Implement the core operations of the Arbellar Vault contract — the accounting and custody layer of the protocol.

The vault contract currently exists only as scaffolding: contracts/vault/src/lib.rs contains an empty #[contract] struct with no logic. The complete behavior specification is documented in contracts/vault/README.md.

This issue covers the user-facing vault lifecycle: initialize, create_vault, deposit, withdraw, and get_balance, together with a focused unit-test suite. Internal execution hooks (transfer_for_execution / return_from_execution) are intentionally out of scope — they belong to a later execution-focused issue.

The vault must enforce Arbellar's core principles: non-custodial ownership, explicit authorization, and accurate on-chain accounting.


📍 File Scope

Applies to:

  • contracts/vault/src/lib.rs
  • contracts/vault/Cargo.toml (only if a dependency adjustment is strictly required)
  • Unit tests may live in contracts/vault/src/lib.rs (#[cfg(test)]) or a contracts/vault/src/test.rs module

Use the shared types from the arbellar-types package (require Issue 1 to be merged, or depend on the types specified in packages/types/README.md).


🎯 Objective

Provide the on-chain foundation for the Arbellar protocol:

  • One vault per user, with the user as the sole owner
  • User deposits and withdrawals of supported assets (Phase 1: USDC, XLM)
  • Strict authorization — only the vault owner can deposit to / withdraw from their vault
  • Accurate balance accounting that maintains the accounting invariant
  • A clear initialize entry point that records the protocol admin and treasury
  • Events emitted for every state change

📝 Implementation Requirements

Contract Entry Points

Implement the following public functions:

pub fn initialize(env: Env, admin: Address, treasury: Address);

pub fn create_vault(env: Env, user: Address) -> VaultId;

pub fn deposit(env: Env, user: Address, asset: Address, amount: i128) -> Result<(), Error>;

pub fn withdraw(env: Env, user: Address, asset: Address, amount: i128) -> Result<(), Error>;

pub fn get_balance(env: Env, user: Address, asset: Address) -> i128;

Behavior Details

  • initialize — must be callable exactly once; stores the admin and treasury addresses from arbellar-types::ProtocolConfig-style configuration.
  • create_vault — generates a unique VaultId, creates a Vault with zero balances and VaultStatus::Active, and emits a VaultCreated event.
  • deposit — requires the user's authorization, validates that the amount is positive and the asset is supported (USDC 0x... / XLM per the asset registry; see the spec), transfers the tokens from the user wallet into the contract, updates the vault balance by exactly amount, and emits a Deposit event.
  • withdraw — requires the user's authorization, enforces vault ownership, validates the amount, checks the vault is not InExecution, enforces the principal/balance limit, transfers tokens back to the user, decreases the balance by exactly amount, and emits a Withdrawal event.
  • get_balance — read-only query, returns the current vault balance for the asset (and 0 for a missing vault/asset).
  • Error — reuse arbellar_types::Error (e.g., Unauthorized, VaultNotFound, InvalidAsset, InsufficientBalance if extended consistently with the spec) for Result returns.

Key Security Requirements

  • All state-changing operations require user.require_auth().
  • The vault owner is the only address authorized to withdraw.
  • No overflow/underflow — the workspace release profile already enables overflow-checks, but logic must handle bounded amounts defensively.
  • Never let a failed or unauthorized operation change state.
  • Emit events with env.events().publish(...) using short symbols such as VaultCreated, Deposit, Withdrawal.

Suggested Storage Layout

Use a single storage key for the vault registry (e.g., DataKey::Vault(Address)), a counter for VaultId generation, and the arbellar-types enums for status. Document any storage-key decision with a short comment or add it to the module docs.


🧪 Verification

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features --package vault
cargo build --target wasm32-unknown-unknown --release

Add at minimum the following unit tests (using the Soroban test framework, Env::default()):

  • test_initialize_sets_admin_and_treasury
  • test_create_vault_assigns_unique_ids
  • test_deposit_increases_balance
  • test_deposit_unsupported_asset_fails
  • test_deposit_zero_or_negative_amount_fails
  • test_withdraw_by_owner
  • test_withdraw_by_non_owner_fails
  • test_withdraw_exceeding_balance_fails
  • test_get_balance_returns_zero_for_missing_vault

Verify:

  • Unit tests pass
  • No TypeScript-equivalent compile warnings; no clippy warnings
  • The contract builds for wasm32-unknown-unknown in release mode
  • Events are emitted with the expected topics/data

✅ Acceptance Criteria

  • initialize is implemented and guarded against double initialization
  • create_vault creates a single-owner vault with a unique VaultId
  • deposit transfers tokens, validates amount/asset, and updates vault balance
  • withdraw enforces ownership, balance limits, and correct token transfer
  • get_balance returns accurate balance for the requested asset
  • Only the vault owner can withdraw; unauthorized attempts fail with Error::Unauthorized
  • No state changes occur on failed operations
  • Events (VaultCreated, Deposit, Withdrawal) are emitted on success
  • Unit test suite covers the cases listed above and passes
  • cargo fmt --all -- --check passes
  • cargo clippy --all-targets --all-features -- -D warnings passes
  • cargo build --target wasm32-unknown-unknown --release succeeds
  • Uses arbellar-types shared types rather than redefining them in the contract

🌿 Suggested Branch

git checkout -b feat/vault-core-operations

📝 Suggested Commit Message

feat(vault): implement core vault operations with unit tests

📦 PR Requirements

  • Assignment required before starting
  • Time Limit: ETA 24 hrs


Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions