diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72304a2..41a0c43 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,219 +1,105 @@ # Contributing to StellarForge -Thank you for your interest in contributing to StellarForge! This document provides guidelines for contributing to our collection of Soroban smart contracts. +Thank you for your interest in contributing to StellarForge! We welcome contributions that help make this collection of Soroban smart contract primitives more robust and easier to use. -## ๐Ÿš€ Quick Start +## ๐Ÿ› ๏ธ Prerequisites -1. **Fork the repository** -2. **Create a feature branch**: `git checkout -b feature/your-feature-name` -3. **Make your changes** -4. **Run tests**: `make test` or `cargo test --workspace` -5. **Submit a pull request** +To contribute to this project, you will need: +- **Rust:** Latest stable version (2021 edition) +- **Target:** `wasm32v1-none` +- **Stellar CLI:** v25.2.0 or higher +- **Make:** Optional, but recommended for running development commands -## ๐Ÿ“ฆ Shared Error Crate (forge-errors) +## ๐Ÿš€ Getting Started -When adding new common error variants to `forge-errors`: +1. **Fork the repository** on GitHub. +2. **Clone your fork** locally: + ```bash + git clone https://github.com/YOUR_USERNAME/stellarforge.git + cd stellarforge + ``` +3. **Set up the pre-commit hook** (recommended): + ```bash + cp src/scripts/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + ``` -1. **Consider if the error is truly common** across multiple contracts -2. **Add descriptive documentation** to the variant in `crates/forge-errors/src/lib.rs` -3. **Update error codes** to avoid conflicts with existing variants -4. **Test the change** across all affected contracts +### Pre-commit Hook (Optional but Recommended) -### Adding New Common Errors +We provide a git pre-commit hook that automatically checks code formatting and linting before each commit. This helps catch issues early. -If you identify an error pattern that appears in 3+ contracts, consider adding it to `CommonError`: +By default, the hook runs `cargo fmt` and `cargo clippy`. To also run the full test suite before each commit, set the `FORGE_PRECOMMIT_TESTS` environment variable to `1`: -```rust -#[contracterror] -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum CommonError { - // Existing variants... - - /// New error description - NewError = NEXT_AVAILABLE_CODE, -} +```bash +# Run tests on this commit only +FORGE_PRECOMMIT_TESTS=1 git commit -m "your message" ``` -**Process:** -1. Add the variant to `CommonError` with next available error code -2. Update any contracts that should use this new shared variant -3. Add tests to verify the error behavior -4. Update documentation - -## ๐Ÿ— Development Setup +## ๐Ÿ“ฆ Shared Error Crate (forge-errors) -### Prerequisites +When adding new common error variants to `forge-errors`: -- **Rust**: 2021 edition with `wasm32v1-none` target -- **Stellar CLI**: v25.2.0 or higher -- **Make**: (optional) for convenience commands +1. **Consider if the error is truly common** across multiple contracts. +2. **Add descriptive documentation** to the variant in `crates/forge-errors/src/lib.rs`. +3. **Update error codes** to avoid conflicts with existing variants. +4. **Test the change** across all affected contracts. -### Installation +## ๐Ÿ“œ Development Workflow +### Building +Build all contracts in the workspace: ```bash -# Install Rust -rustup target add wasm32v1-none - -# Install Stellar CLI -cargo install --locked stellar-cli - -# Verify installation -stellar --version +make build +# or +cargo build --workspace ``` -## ๐Ÿงช Testing - -### Running Tests - +### Testing +Run the full test suite: ```bash -# Test all contracts make test - -# Test specific contract -cargo test -p forge-governor -cargo test -p forge-multisig -cargo test -p forge-oracle -cargo test -p forge-stream -cargo test -p forge-vesting -cargo test -p forge-vesting-factory +# or +cargo test --workspace ``` -### Test Coverage - -We aim for high test coverage. When adding new features: -- Write unit tests for new functionality -- Test error paths exhaustively -- Include integration tests for contract interactions -- Verify all error variants are tested - -## ๐Ÿ“ Code Style - -Follow these conventions: - -### Rust Style - -- Use `rustfmt` for formatting: `make fmt` -- Use `clippy` for linting: `make lint` -- Follow Rust idioms and Soroban best practices -- Use `#![no_std]` for all contracts -- Prefer `require_auth()` over manual auth checks where possible - -### Contract Patterns - -- **Error Handling**: Use the shared `CommonError` variants when applicable -- **Storage**: Use appropriate storage types (instance vs persistent) -- **Events**: Emit events for all state changes -- **TTL Management**: Extend storage TTLs appropriately -- **Security**: Follow established security patterns from existing contracts - -### Documentation - -- Document all public functions with examples -- Include error conditions in docstrings -- Update README.md for new features -- Keep CHANGELOG.md updated - -## ๐Ÿ› Bug Reports - -When reporting bugs: - -1. **Use the issue template** provided in GitHub Issues -2. **Include reproduction steps** with minimal example -3. **Specify contract name** and affected functions -4. **Include environment details** (OS, Rust version, Stellar CLI version) -5. **Add logs** and error messages when applicable - -## ๐Ÿ’ก Feature Requests - -We welcome feature requests! Please: - -1. **Check existing issues** for similar requests -2. **Describe the use case** clearly -3. **Consider impact** on existing contracts and integrators -4. **Propose implementation approach** if you have ideas - -## ๐Ÿ“„ Pull Request Process - -### Before Submitting - -- [ ] Tests pass: `make test` -- [ ] Code formatted: `make fmt` -- [ ] Linting clean: `make lint` -- [ ] Documentation updated -- [ ] CHANGELOG.md updated +### Linting & Formatting +Ensure your code follows the project's style: +```bash +make check +# which runs: +# cargo fmt --all -- --check +# cargo clippy --all-targets -- -D warnings +``` -### PR Guidelines +## ๐Ÿ—๏ธ Pull Request Process -- **Small, focused PRs** are preferred -- **One feature per PR** when possible -- **Include tests** for new functionality -- **Update documentation** as needed -- **Link to related issues** +1. Create a new branch for your feature or bug fix. +2. Ensure all tests pass and the code is correctly formatted. +3. Update the documentation (`README.md`, `docs/`) if you've changed contract interfaces or added new features. +4. Submit a Pull Request targeting the `main` branch. +5. Use the provided PR template to describe your changes and testing. -### Review Process +## ๐Ÿท๏ธ Issue Labels -Maintainers will review for: -- โœ… Code quality and style -- โœ… Test coverage -- โœ… Security considerations -- โœ… Documentation completeness -- โœ… Breaking changes (if any) +- `good first issue` โ€” Great for newcomers! +- `bug` โ€” Something isn't working correctly. +- `enhancement` โ€” New features or improvements. +- `documentation` โ€” Improvements to the docs. ## ๐Ÿ”’ Security Security is our top priority. If you discover a security vulnerability: -1. **Do NOT open a public issue** +1. **Do NOT open a public issue.** 2. **Email us privately**: security@stellarforge.org -3. **Include details**: Impact, reproduction steps, affected versions -4. **Allow time for response**: We'll acknowledge within 48 hours - -## ๐Ÿ“ง Development Tools - -### Make Commands - -```makefile -build: - cargo build --workspace - -test: - cargo test --workspace - -fmt: - cargo fmt --all - -lint: - cargo clippy --workspace -- -D warnings - -check: - cargo fmt --all && cargo clippy --workspace -- -D warnings && cargo test --workspace - -clean: - cargo clean --workspace -``` - -### Workspace Structure - -``` -stellarforge/ -โ”œโ”€โ”€ crates/ -โ”‚ โ””โ”€โ”€ forge-errors/ # Shared error library -โ”œโ”€โ”€ contracts/ -โ”‚ โ”œโ”€โ”€ forge-governor/ # Governance contract -โ”‚ โ”œโ”€โ”€ forge-multisig/ # Multisig treasury -โ”‚ โ”œโ”€โ”€ forge-oracle/ # Price feed contract -โ”‚ โ”œโ”€โ”€ forge-stream/ # Token streaming -โ”‚ โ”œโ”€โ”€ forge-vesting/ # Token vesting -โ”‚ โ””โ”€โ”€ forge-vesting-factory/ # Multi-beneficiary vesting -โ”œโ”€โ”€ benches/ # Performance benchmarks -โ””โ”€โ”€ scripts/ # Utility scripts -``` +3. **Include details**: Impact, reproduction steps, affected versions. +4. **Allow time for response**: We'll acknowledge within 48 hours. ## ๐Ÿค Community -- **GitHub Discussions**: Use for questions, ideas, and general discussion -- **Issues**: Bug reports and feature requests -- **Discord**: [Join our community](https://discord.gg/stellarforge) for real-time chat +- **GitHub Discussions**: Use for questions, ideas, and general discussion. +- **Issues**: Bug reports and feature requests. +- **Discord**: [Join our community](https://discord.gg/stellarforge) for real-time chat. ## ๐Ÿ“œ License diff --git a/Makefile b/Makefile index fb08fe4..6d71b5b 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,6 @@ clean: cargo clean # Run contract benchmarks (Soroban budget: CPU instructions + memory bytes) -.PHONY: bench -bench: - cargo run -p forge-benches \ No newline at end of file +# .PHONY: bench +# bench: +# cargo run -p forge-benches \ No newline at end of file diff --git a/README.md b/README.md index b4397fe..6b677d5 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,11 @@ Developers evaluating StellarForge can use this table to quickly identify the ri | Contract | Use Case | Admin Required | Events Emitted | Timelock | | :--- | :--- | :--- | :--- | :--- | -| [`forge-governor`](#forge-governor) | Governance | No (Auth-based) | None | Yes (Voting/Execution delay) | -| [`forge-multisig`](#forge-multisig) | Multisig Treasury | Yes (Owners) | None | Yes (Post-approval delay) | -| [`forge-oracle`](#forge-oracle) | Price Feed | Yes (Admin) | `price_updated` | No | +| [`forge-governor`](#forge-governor) | Governance | No (Auth-based) | `proposal_created`, `vote_cast`, `proposal_finalized` | Yes (Voting/Execution delay) | +| [`forge-multisig`](#forge-multisig) | Multisig Treasury | Yes (Owners) | `proposal_created`, `proposal_approved`, `proposal_executed` | Yes (Post-approval delay) | +| [`forge-oracle`](#forge-oracle) | Price Feed | Yes (Admin) | `price_updated`, `admin_transferred` | No | | [`forge-stream`](#forge-stream) | Real-time Payments | No (Stream-specific) | `stream_created`, `withdrawn`, `stream_cancelled`, `stream_paused`, `stream_resumed` | No | -| [`forge-vesting`](#forge-vesting) | Token Vesting | Yes (Admin) | `vesting_initialized`, `claimed`, `vesting_cancelled`, `admin_transferred` | Yes (Cliff period) | +| [`forge-vesting`](#forge-vesting) | Token Vesting | Yes (Admin) | `vesting_initialized`, `claimed`, `vesting_cancelled`, `admin_transferred`, `beneficiary_changed` | Yes (Cliff period) | | [`forge-vesting-factory`](#forge-vesting-factory) | Multi-beneficiary Vesting | Yes (Per-schedule Admin) | `schedule_created`, `claimed`, `schedule_cancelled` | Yes (Cliff period) | --- @@ -144,7 +144,7 @@ A single-deployment factory that manages multiple vesting schedules. Eliminates ### forge-stream Pay-per-second token streams. Ideal for payroll, subscriptions, or real-time contractor payments. -* **Key Function:** `create_stream(sender, token, recipient, rate_per_second, duration_seconds)` +* **Key Function:** `create_stream(sender, token, recipient, rate_per_second, duration_seconds, min_withdrawal_amount)` * **Action:** `withdraw(stream_id)` allows the recipient to pull accrued tokens at any time. * **Pause/Resume:** `pause_stream(stream_id)` and `resume_stream(stream_id)` allow senders to temporarily halt or restart token accrual. * **`is_active` vs `is_claimable`:** `get_stream_status()` returns both fields. A finished stream has `is_active = false` and `is_finished = true`, but may still have `withdrawable > 0`. Always check `is_claimable` (or `withdrawable` directly) to determine whether tokens can be pulled โ€” do not rely on `is_active` alone. @@ -180,14 +180,15 @@ The tables below are verified against the current contract code in `contracts/*/ | :--- | :--- | :--- | | `vesting_initialized` | Emitted by `initialize(...)` after the vesting config and claimed amount are stored. | `total_amount: i128`, `cliff_seconds: u64`, `duration_seconds: u64` | | `claimed` | Emitted by `claim()` after the beneficiary's claimed amount is updated and vested tokens are transferred. | `beneficiary: Address`, `claimable: i128` | -| `vesting_cancelled` | Emitted by `cancel()` after the vesting is marked cancelled and any unvested tokens are returned to the admin. | `admin: Address`, `returnable: i128` | +| `vesting_cancelled` | Emitted by `cancel()` after the vesting is marked cancelled and any unvested tokens are returned to the admin. | `admin: Address`, `to_admin: i128`, `beneficiary: Address`, `to_beneficiary: i128` | | `admin_transferred` | Emitted by `transfer_admin(new_admin)` after admin rights move to the new admin address. | `old_admin: Address`, `new_admin: Address` | +| `beneficiary_changed` | Emitted by `change_beneficiary(new_beneficiary)` after beneficiary rights move to the new address. | `old_beneficiary: Address`, `new_beneficiary: Address` | ### forge-stream | Event Name | Trigger | Fields | | :--- | :--- | :--- | -| `stream_created` | Emitted by `create_stream(...)` after the stream is stored and the active stream count is incremented. | `stream_id: u64`, `recipient: Address`, `rate_per_second: i128`, `duration_seconds: u64` | +| `stream_created` | Emitted by `create_stream(...)` after the stream is stored and the active stream count is incremented. | `stream_id: u64`, `recipient: Address`, `rate_per_second: i128`, `duration_seconds: u64`, `min_withdrawal_amount: i128` | | `withdrawn` | Emitted by `withdraw(stream_id)` after the withdrawn amount is updated and accrued tokens are transferred to the recipient. | `stream_id: u64`, `recipient: Address`, `withdrawable: i128` | | `stream_cancelled` | Emitted by `cancel_stream(stream_id)` after the stream is marked cancelled and funds are paid out/refunded. | `stream_id: u64`, `withdrawable: i128`, `returnable: i128` | | `stream_paused` | Emitted by `pause_stream(stream_id)` after the stream is marked paused. | `stream_id: u64` | @@ -197,19 +198,32 @@ The tables below are verified against the current contract code in `contracts/*/ | Event Name | Trigger | Fields | | :--- | :--- | :--- | -| None | This contract does not currently emit any events. | None | +| `proposal_created` | Emitted by `propose(...)` after the proposal is stored and the proposer is marked as approved. | `proposal_id: u64`, `proposer: Address`, `to: Address`, `token: Address`, `amount: i128` | +| `proposal_approved` | Emitted by `approve(...)` after an owner approves a proposal. | `proposal_id: u64`, `owner: Address`, `approval_count: u32` | +| `proposal_executed` | Emitted by `execute(...)` after a proposal is successfully executed. | `proposal_id: u64`, `executor: Address`, `to: Address`, `amount: i128` | ### forge-governor | Event Name | Trigger | Fields | | :--- | :--- | :--- | -| None | This contract does not currently emit any events. | None | +| `proposal_created` | Emitted by `propose(...)` after the proposal is stored. | `proposal_id: u64`, `proposer: Address`, `vote_end: u64` | +| `vote_cast` | Emitted by `vote(...)` after a vote is successfully cast. | `proposal_id: u64`, `voter: Address`, `direction: VoteDirection`, `weight: i128` | +| `proposal_finalized` | Emitted by `finalize(...)` after a proposal is finalized (Passed or Failed). | `proposal_id: u64`, `votes_for: i128`, `votes_against: i128` | ### forge-oracle | Event Name | Trigger | Fields | | :--- | :--- | :--- | | `price_updated` | Emitted by `submit_price(base, quote, price)` after the submitted price and update timestamp are written to storage. | `base: Symbol`, `quote: Symbol`, `price: i128`, `updated_at: u64` | +| `admin_transferred` | Emitted by `transfer_admin(new_admin)` after admin rights move to the new admin address. | `old_admin: Address`, `new_admin: Address` | + +### forge-vesting-factory + +| Event Name | Trigger | Fields | +| :--- | :--- | :--- | +| `schedule_created` | Emitted by `create_schedule(...)` after a new schedule is created. | `id: u64`, `total_amount: i128` | +| `claimed` | Emitted by `claim(...)` after tokens are claimed for a schedule. | `schedule_id: u64`, `claimable: i128` | +| `schedule_cancelled` | Emitted by `cancel(...)` after a schedule is cancelled. | `schedule_id: u64` | --- diff --git a/contracts/forge-governor/Cargo.toml b/contracts/forge-governor/Cargo.toml deleted file mode 100644 index fb95484..0000000 --- a/contracts/forge-governor/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "forge-governor" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = { workspace = true } -forge-errors = { workspace = true } - -[dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } diff --git a/contracts/forge-governor/README.md b/contracts/forge-governor/README.md deleted file mode 100644 index dfb4666..0000000 --- a/contracts/forge-governor/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# Forge Governor - -## Resource Usage - -> **Note:** Resource usage estimates are approximate and may vary based on contract state and input sizes. Run `stellar contract invoke` with `--cost` flag to measure actual usage for your specific use case. - -### Function Resource Estimates - -| Function | CPU Instructions | Memory (bytes) | Ledger Reads | Ledger Writes | Notes | -| :------------- | :--------------: | :------------: | :----------: | :-----------: | :-------------------------------------------------------------------- | -| `initialize` | ~50,000 | ~2,000 | 0 | 2 | Stores admin and config | -| `propose` | ~80,000 | ~3,000 | 2 | 3 | Validates proposer, creates proposal | -| `vote` | ~60,000 | ~2,500 | 3 | 2 | Updates vote tally, records voter | -| `execute` | ~100,000 | ~3,500 | 4 | 2 | Most expensive - validates quorum, checks timelock, executes proposal | -| `get_proposal` | ~20,000 | ~1,500 | 2 | 0 | Read-only query | -| `get_vote` | ~15,000 | ~1,000 | 1 | 0 | Read-only query | - -### Most Expensive Functions - -1. **`execute`** (~100,000 CPU instructions) - - Why: Validates quorum requirements, checks timelock expiration, and executes the proposal action - - Optimization tip: Ensure proposals are well-formed before submission to avoid failed executions - -2. **`propose`** (~80,000 CPU instructions) - - Why: Validates proposer eligibility, creates new proposal storage, and initializes vote tracking - - Optimization tip: Reuse proposal templates for similar governance actions - -### Cost Estimation - -Soroban charges fees based on: - -- **CPU Instructions:** ~0.0001 XLM per 10,000 instructions -- **Memory:** ~0.00001 XLM per byte -- **Ledger Entries:** ~0.001 XLM per read/write - -**Example:** Executing a proposal costs approximately: - -- CPU: 100,000 instructions ร— 0.0001 XLM / 10,000 = 0.001 XLM -- Memory: 3,500 bytes ร— 0.00001 XLM = 0.035 XLM -- Ledger: 6 operations ร— 0.001 XLM = 0.006 XLM -- **Total:** ~0.042 XLM per execution - ---- - -## Known Limitations - -- No vote delegation -- **Front-running protection:** The `initialize()` function requires authorization from the `admin` address specified in the `GovernorConfig`. This prevents an attacker from monitoring the mempool and front-running the deployer's initialization with a malicious configuration (e.g., quorum = 1, timelock = 0). The admin address must authorize the initialization call via `require_auth()`. - -## Pending Proposal Performance - -- `propose()` appends the new ID to `ActiveProposals` and records its slot in `ActiveProposalIndex`, so activation remains O(1). -- `cancel_proposal()`, `finalize()`, and the `execute()` cleanup path remove active IDs with swap-remove plus the index map, so removal remains O(1) even when many proposals are active. -- `get_pending_proposals()` reads only the current `ActiveProposals` vector and filters out expired or cancelled entries, so enumeration cost is O(n_active), not O(total_proposals_created). -- `get_pending_proposals()` does not guarantee sorted output; callers should treat the returned IDs as an unordered active set. - ---- - -## TTL Strategy - -Stellar persistent storage entries expire when their TTL (time-to-live) reaches zero. Without explicit TTL management, expired entries silently return `false` from `has()`, which would break double-vote protection โ€” an expired `Vote` entry would allow a voter to cast a second vote on the same proposal. - -### Constants - -| Constant | Ledgers | Approx. time | Applied to | -| :--- | :---: | :---: | :--- | -| `INSTANCE_TTL_THRESHOLD` | 17 280 | ~1 day | Contract instance (bump trigger) | -| `INSTANCE_TTL_EXTEND` | 34 560 | ~2 days | Contract instance (bump target) | -| `PROPOSAL_TTL_EXTEND` | 1 036 800 | ~60 days | `DataKey::Proposal` entries | -| `VOTE_TTL_EXTEND` | 1 036 800 | ~60 days | `DataKey::Vote` entries | - -### Rationale - -- `DataKey::Proposal` entries are extended on every write (`propose`, `vote`, `finalize`, `execute`) to ensure the proposal remains readable throughout its full lifecycle. -- `DataKey::Vote` entries are extended immediately after being written in `vote()`. The 60-day ceiling covers any realistic `voting_period + timelock_delay` configuration with a large safety buffer. -- The contract instance TTL is bumped on every mutating call using a threshold/extend pattern so it is only extended when it is actually close to expiry, avoiding unnecessary ledger writes. - ---- - -## Tie-Breaking Behaviour - -When `finalize()` is called after the voting period ends, the contract requires a **strict majority** to pass a proposal: - -``` -votes_for > votes_against -``` - -If `votes_for == votes_against` (a tie), the proposal resolves to **`Failed`**. There is no mechanism that breaks a tie in favour of the proposer, the quorum, or any other party. This behaviour is deterministic and unconditional. - -### Summary table - -| Condition | Outcome | -| :------------------------------------------------------- | :------- | -| `total_votes < quorum` | `Failed` | -| `total_votes >= quorum` and `votes_for > votes_against` | `Passed` | -| `total_votes >= quorum` and `votes_for == votes_against` | `Failed` | -| `total_votes >= quorum` and `votes_for < votes_against` | `Failed` | - -### Rationale - -Requiring a strict majority means the status quo is preserved on a tie. A proposal must actively win โ€” not merely draw โ€” to advance to execution. This is the safest default for an on-chain treasury governor where an ambiguous outcome should never result in a token transfer. diff --git a/contracts/forge-multisig/Cargo.toml b/contracts/forge-multisig/Cargo.toml deleted file mode 100644 index 140eeb1..0000000 --- a/contracts/forge-multisig/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "forge-multisig" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = { workspace = true } -forge-errors = { workspace = true } - -[dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } diff --git a/contracts/forge-multisig/README.md b/contracts/forge-multisig/README.md deleted file mode 100644 index 55ca79f..0000000 --- a/contracts/forge-multisig/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Forge Multisig - -## Resource Usage - -> **Note:** Resource usage estimates are approximate and may vary based on contract state and input sizes. Run `stellar contract invoke` with `--cost` flag to measure actual usage for your specific use case. - -### Function Resource Estimates - -| Function | CPU Instructions | Memory (bytes) | Ledger Reads | Ledger Writes | Notes | -| :------------- | :--------------: | :------------: | :----------: | :-----------: | :--------------------------------------------------------------------- | -| `initialize` | ~60,000 | ~2,500 | 0 | 3 | Stores owners, threshold, and timelock | -| `propose` | ~70,000 | ~3,000 | 2 | 2 | Validates proposer, creates proposal | -| `approve` | ~50,000 | ~2,000 | 3 | 1 | Records approval, checks threshold | -| `execute` | ~90,000 | ~3,500 | 4 | 2 | Most expensive - validates threshold, checks timelock, transfers funds | -| `get_proposal` | ~20,000 | ~1,500 | 2 | 0 | Read-only query | -| `is_approved` | ~15,000 | ~1,000 | 1 | 0 | Read-only query | - -### Most Expensive Functions - -1. **`execute`** (~90,000 CPU instructions) - - Why: Validates approval threshold, checks timelock expiration, and executes token transfer - - Optimization tip: Ensure all required approvals are collected before attempting execution - -2. **`propose`** (~70,000 CPU instructions) - - Why: Validates proposer eligibility and creates new proposal storage - - Optimization tip: Batch similar proposals when possible - -### Cost Estimation - -Soroban charges fees based on: - -- **CPU Instructions:** ~0.0001 XLM per 10,000 instructions -- **Memory:** ~0.00001 XLM per byte -- **Ledger Entries:** ~0.001 XLM per read/write - -**Example:** Executing a multisig proposal costs approximately: - -- CPU: 90,000 instructions ร— 0.0001 XLM / 10,000 = 0.0009 XLM -- Memory: 3,500 bytes ร— 0.00001 XLM = 0.035 XLM -- Ledger: 6 operations ร— 0.001 XLM = 0.006 XLM -- **Total:** ~0.042 XLM per execution - ---- - -## API Design - -All `get_*` query functions that look up a resource by ID return `Result` rather than `Option`. For example, `get_proposal()` returns `Err(MultisigError::ProposalNotFound)` when no proposal exists for the given ID. This is consistent with `forge-governor` and other Forge contracts, so integrators can handle missing resources uniformly across the suite. - ---- - -## Known Limitations - -- Owner list is fixed after initialization -- **Threshold is immutable after initialization.** If a future version adds `update_threshold()`, the `approved_at` logic in `approve()` must be reviewed carefully. Lowering the threshold could cause a proposal to retroactively meet the new threshold at an earlier `approved_at` timestamp, potentially allowing premature execution. Any threshold mutation must preserve the original `approved_at` timestamp or implement additional safeguards. - -## Native XLM Support - -`forge-multisig` supports native XLM transfers alongside regular Soroban token transfers. - -**How it works:** -On Soroban, native XLM is accessed through the Stellar Asset Contract (SAC) for the native asset. The SAC exposes the same `token::Client` interface as any other Soroban token, so execution is handled identically โ€” the only difference is the proposal is flagged with `is_native: true` for clarity. - -**Creating a native XLM proposal:** - -```text -// Transfer 10 XLM (100_000_000 stroops) to a recipient -let id = client.propose_xlm(&owner, &recipient, &xlm_sac_address, &100_000_000); -``` - -**Approval and execution** work exactly the same as token proposals โ€” use `approve()` and `execute()` with the returned proposal ID. - -**Getting the native XLM SAC address:** - -- Testnet/Mainnet: use `stellar contract id asset --asset native --network ` -- In tests: register a SAC with `env.register_stellar_asset_contract_v2(...)` and mint XLM into the contract with `StellarAssetClient::mint()` - -**Identifying proposal type:** - -```text -let proposal = client.get_proposal(&id).unwrap(); -if proposal.is_native { - // native XLM transfer -} else { - // Soroban token transfer -} -``` diff --git a/contracts/forge-oracle/Cargo.toml b/contracts/forge-oracle/Cargo.toml deleted file mode 100644 index c4cedd6..0000000 --- a/contracts/forge-oracle/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "forge-oracle" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = { workspace = true } -forge-errors = { workspace = true } - -[dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } diff --git a/contracts/forge-oracle/README.md b/contracts/forge-oracle/README.md deleted file mode 100644 index 6a209ae..0000000 --- a/contracts/forge-oracle/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Forge Oracle - -## Resource Usage - -> **Note:** Resource usage estimates are approximate and may vary based on contract state and input sizes. Run `stellar contract invoke` with `--cost` flag to measure actual usage for your specific use case. - -### Function Resource Estimates - -| Function | CPU Instructions | Memory (bytes) | Ledger Reads | Ledger Writes | Notes | -| :--- | :---: | :---: | :---: | :---: | :--- | -| `initialize` | ~40,000 | ~1,500 | 0 | 1 | Stores admin address | -| `submit_price` | ~55,000 | ~2,000 | 1 | 1 | Most expensive - validates admin, updates price | -| `get_price` | ~25,000 | ~1,500 | 1 | 0 | Validates staleness, returns price data | -| `get_price_data` | ~20,000 | ~1,200 | 1 | 0 | Read-only query (no staleness check) | -| `transfer_admin` | ~35,000 | ~1,500 | 1 | 1 | Transfers admin rights | - -### Most Expensive Functions - -1. **`submit_price`** (~55,000 CPU instructions) - - Why: Validates admin authorization, encodes price data, and updates ledger storage - - Optimization tip: Batch price updates for multiple pairs when possible - -2. **`get_price`** (~25,000 CPU instructions) - - Why: Performs staleness validation and decodes price data - - Optimization tip: Cache frequently accessed prices in your application - -### Cost Estimation - -Soroban charges fees based on: -- **CPU Instructions:** ~0.0001 XLM per 10,000 instructions -- **Memory:** ~0.00001 XLM per byte -- **Ledger Entries:** ~0.001 XLM per read/write - -**Example:** Submitting a price update costs approximately: -- CPU: 55,000 instructions ร— 0.0001 XLM / 10,000 = 0.00055 XLM -- Memory: 2,000 bytes ร— 0.00001 XLM = 0.02 XLM -- Ledger: 2 operations ร— 0.001 XLM = 0.002 XLM -- **Total:** ~0.023 XLM per price update - ---- - -## Known Limitations - -- Single admin -- No decentralized feed \ No newline at end of file diff --git a/contracts/forge-stream/Cargo.toml b/contracts/forge-stream/Cargo.toml deleted file mode 100644 index 40e6225..0000000 --- a/contracts/forge-stream/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "forge-stream" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = { workspace = true } -forge-errors = { workspace = true } - -[dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } diff --git a/contracts/forge-stream/README.md b/contracts/forge-stream/README.md deleted file mode 100644 index 4f8a531..0000000 --- a/contracts/forge-stream/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Forge Stream - -## Storage Strategy - -`forge-stream` uses two Soroban storage tiers. The rule of thumb: use -**persistent** for data that must survive beyond a single transaction or -contract instance TTL; use **instance** for small, frequently-accessed -scalars that are always read together with the contract instance. - -| `DataKey` variant | Storage type | Rationale | -| :-------------------------- | :----------- | :----------------------------------------------------------------------------------- | -| `Stream(u64)` | `persistent` | Stream data must outlive the instance TTL while tokens remain unclaimed | -| `NextId` | `instance` | Small scalar always read on `create_stream`; co-located with instance for efficiency | -| `ActiveStreamsCount` | `instance` | Updated on every create/cancel/finish; always accessed with other instance data | -| `SenderStreams(Address)` | `persistent` | Grows with each stream; must survive beyond instance TTL for historical lookups | -| `RecipientStreams(Address)` | `persistent` | Same rationale as `SenderStreams` | - -When adding a new `DataKey` variant, choose the storage type using this -checklist: - -- Does the data need to survive after the contract instance TTL expires? โ†’ **persistent** -- Is it a small scalar read on almost every call? โ†’ **instance** -- Is it keyed per-user or per-stream (unbounded growth)? โ†’ **persistent** - ---- - -## Resource Usage - -> **Note:** Resource usage estimates are approximate and may vary based on contract state and input sizes. Run `stellar contract invoke` with `--cost` flag to measure actual usage for your specific use case. - -### Function Resource Estimates - -| Function | CPU Instructions | Memory (bytes) | Ledger Reads | Ledger Writes | Notes | -| :----------------- | :--------------: | :------------: | :----------: | :-----------: | :------------------------------------------------ | -| `create_stream` | ~85,000 | ~3,500 | 2 | 3 | Most expensive - validates inputs, creates stream | -| `withdraw` | ~65,000 | ~2,500 | 3 | 2 | Calculates accrued amount, transfers tokens | -| `cancel_stream` | ~70,000 | ~3,000 | 3 | 2 | Calculates final amounts, refunds/pays out | -| `pause_stream` | ~45,000 | ~2,000 | 2 | 1 | Marks stream as paused | -| `resume_stream` | ~50,000 | ~2,200 | 2 | 1 | Adjusts for paused time, resumes stream | -| `get_stream` | ~20,000 | ~1,500 | 2 | 0 | Read-only query | -| `get_withdrawable` | ~25,000 | ~1,800 | 2 | 0 | Calculates current accrued amount | - -### Most Expensive Functions - -1. **`create_stream`** (~85,000 CPU instructions) - - Why: Validates sender/recipient, encodes stream parameters, creates multiple storage entries - - Optimization tip: Reuse stream configurations for recurring payments - -2. **`cancel_stream`** (~70,000 CPU instructions) - - Why: Calculates final amounts, handles partial refunds, updates multiple storage entries - - Optimization tip: Use pause/resume instead of cancel/recreate for temporary stops - -### Cost Estimation - -Soroban charges fees based on: - -- **CPU Instructions:** ~0.0001 XLM per 10,000 instructions -- **Memory:** ~0.00001 XLM per byte -- **Ledger Entries:** ~0.001 XLM per read/write - -**Example:** Creating a stream costs approximately: - -- CPU: 85,000 instructions ร— 0.0001 XLM / 10,000 = 0.00085 XLM -- Memory: 3,500 bytes ร— 0.00001 XLM = 0.035 XLM -- Ledger: 5 operations ร— 0.001 XLM = 0.005 XLM -- **Total:** ~0.041 XLM per stream creation - ---- - -## Known Limitations - -- No mid-stream rate changes - -## Overflow Protection - -`forge-stream` guards against i128 multiplication overflow in two places: - -- **`create_stream()`** โ€” `rate_per_second * duration_seconds` uses `checked_mul` and returns `InvalidConfig` if the product would exceed `i128::MAX`. This prevents a stream from being created with a total that cannot be represented. -- **`compute_streamed()`** โ€” `rate_per_second * effective_elapsed` uses `checked_mul` and falls back to `total` (the stream's maximum payout) if overflow would occur. The result is also capped at `total`, so callers always receive a value in `[0, total]` regardless of elapsed time or rate magnitude. diff --git a/contracts/forge-vesting-factory/Cargo.toml b/contracts/forge-vesting-factory/Cargo.toml deleted file mode 100644 index 130288f..0000000 --- a/contracts/forge-vesting-factory/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "forge-vesting-factory" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = { workspace = true } -forge-errors = { workspace = true } - -[dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } diff --git a/contracts/forge-vesting/Cargo.toml b/contracts/forge-vesting/Cargo.toml deleted file mode 100644 index 8414198..0000000 --- a/contracts/forge-vesting/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "forge-vesting" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -soroban-sdk = { workspace = true } -forge-errors = { workspace = true } - -[dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } diff --git a/contracts/forge-vesting/README.md b/contracts/forge-vesting/README.md deleted file mode 100644 index 095b1d7..0000000 --- a/contracts/forge-vesting/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Forge Vesting - -## Resource Usage - -> **Note:** Resource usage estimates are approximate and may vary based on contract state and input sizes. Run `stellar contract invoke` with `--cost` flag to measure actual usage for your specific use case. - -### Function Resource Estimates - -| Function | CPU Instructions | Memory (bytes) | Ledger Reads | Ledger Writes | Notes | -| :--- | :---: | :---: | :---: | :---: | :--- | -| `initialize` | ~75,000 | ~3,000 | 1 | 3 | Most expensive - validates inputs, creates vesting schedule | -| `claim` | ~60,000 | ~2,500 | 2 | 2 | Calculates vested amount, transfers tokens | -| `cancel` | ~55,000 | ~2,200 | 2 | 2 | Calculates unvested amount, refunds to admin | -| `transfer_admin` | ~35,000 | ~1,500 | 1 | 1 | Transfers admin rights | -| `get_vesting` | ~20,000 | ~1,500 | 2 | 0 | Read-only query | -| `get_claimable` | ~25,000 | ~1,800 | 2 | 0 | Calculates current claimable amount | - -### Most Expensive Functions - -1. **`initialize`** (~75,000 CPU instructions) - - Why: Validates beneficiary/admin, encodes vesting parameters, creates multiple storage entries - - Optimization tip: Reuse vesting configurations for similar grants - -2. **`claim`** (~60,000 CPU instructions) - - Why: Calculates vested amount based on time, updates claimed amount, transfers tokens - - Optimization tip: Batch claims for multiple vesting schedules when possible - -### Cost Estimation - -Soroban charges fees based on: -- **CPU Instructions:** ~0.0001 XLM per 10,000 instructions -- **Memory:** ~0.00001 XLM per byte -- **Ledger Entries:** ~0.001 XLM per read/write - -**Example:** Initializing a vesting schedule costs approximately: -- CPU: 75,000 instructions ร— 0.0001 XLM / 10,000 = 0.00075 XLM -- Memory: 3,000 bytes ร— 0.00001 XLM = 0.03 XLM -- Ledger: 4 operations ร— 0.001 XLM = 0.004 XLM -- **Total:** ~0.035 XLM per vesting initialization - ---- - -## Known Limitations - -- No partial cancellation -- Single beneficiary only \ No newline at end of file diff --git a/scripts/README.md b/src/scripts/README.md similarity index 100% rename from scripts/README.md rename to src/scripts/README.md diff --git a/scripts/pre-commit b/src/scripts/pre-commit old mode 100755 new mode 100644 similarity index 100% rename from scripts/pre-commit rename to src/scripts/pre-commit diff --git a/scripts/update-wasm-sizes.sh b/src/scripts/update-wasm-sizes.sh similarity index 98% rename from scripts/update-wasm-sizes.sh rename to src/scripts/update-wasm-sizes.sh index 0fddd18..2614546 100644 --- a/scripts/update-wasm-sizes.sh +++ b/src/scripts/update-wasm-sizes.sh @@ -13,7 +13,7 @@ if [[ "${1:-}" == "--dry-run" ]]; then fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" README="$REPO_ROOT/README.md" WASM_DIR="$REPO_ROOT/target/wasm32v1-none/release"