Skip to content

Title: fix(factory): atomically deploy and initialize pools to close admin-hijack front-running window - #105

Merged
prodbycorne merged 3 commits into
SmartDropLabs:mainfrom
Cyber-Mitch:fix/79-atomic-pool-initialize
Jul 26, 2026
Merged

Title: fix(factory): atomically deploy and initialize pools to close admin-hijack front-running window#105
prodbycorne merged 3 commits into
SmartDropLabs:mainfrom
Cyber-Mitch:fix/79-atomic-pool-initialize

Conversation

@Cyber-Mitch

@Cyber-Mitch Cyber-Mitch commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Closes #79

create_pool now deploys and initializes the pool atomically — deploy_v2 and a call to the pool's initialize happen inside the same invocation, so there's no externally-observable window where a deployed pool exists but is uninitialized for an attacker to race.

Why this uses env.invoke_contract with raw args, not a typed FarmingPoolClient

An earlier version of this fix used FarmingPoolClient::new(&env, &pool_address).initialize(...). That fails to link: farming-pool's #[contractimpl] macro exports admin and transfer_admin as top-level contract functions, and linking that crate into factory's own cdylib (which exports functions of the same names) collides at link time:

rust-lld: error: duplicate symbol: admin
rust-lld: error: duplicate symbol: transfer_admin

cargo build/cargo test never catch this — they only compile the rlib crate type, never cdylib. The actual deployable target has to be built explicitly to catch it:

cargo build -p factory --target wasm32v1-none --release

This PR builds initialize's call via env.invoke_contract with raw Val arguments instead, avoiding the typed client and the symbol collision entirely. Verified by running the real WASM build target, not just cargo build/cargo test — confirmed clean both before and after this change.

Since env.invoke_contract doesn't need FarmingPoolClient, farming-pool moved back to [dev-dependencies] in factory/Cargo.toml (used only by tests, not at runtime) — this closes the collision risk for good, not just for admin/transfer_admin specifically.

Design decisions

  • Admin: the factory's own admin (load_admin), not a caller-supplied delegate. Closes the front-running window, which is this issue's scope; per-pool admin delegation is #80's separate concern, not addressed here.
  • stake_token reconciliation: asset is the same variable passed to both PoolRecord.asset and the pool's initialize call — they cannot diverge, closing the stake_token-hijack variant the issue also flags.
  • Deliberately did not adopt #80-scoped additions from an existing unmerged commit that solves this same collision (a new global_multiplier parameter, daily_ratecredit_rate conversion, Result return type) — those are out of scope for #79.

Tests

  • test_create_pool_pool_admin_is_set_atomicallyadmin() on the deployed pool equals the intended admin immediately after create_pool returns.
  • test_third_party_cannot_reinitialize_deployed_pool — an unrelated address's initialize attempt on the deployed pool returns PoolError::AlreadyInitialized as a typed error, not a panic; confirms admin is unchanged.
  • Both use #87's real-WASM integration test infrastructure.

Verification

Check Result
cargo build --workspace
cargo build -p factory --target wasm32v1-none --release (the real gate) ✅ links cleanly, no duplicate symbols
cargo test --workspace ✅ factory: 29 unit + 5 integration; farming-pool: 74 unit, all pass
cargo clippy -p factory --all-targets ✅ no new warnings

Cross-reference

Same root cause as #78 — resolving together as the issue requests.

Task completed
@prodbycorne Please Review

@netlify

netlify Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy Preview for sdcontracts ready!

Name Link
🔨 Latest commit 60287a6
🔍 Latest deploy log https://app.netlify.com/projects/sdcontracts/deploys/6a60ab0dc14bc40008cc1dbc
😎 Deploy Preview https://deploy-preview-105--sdcontracts.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

…initialize

# Conflicts:
#	soroban/contracts/factory/Cargo.toml
#	soroban/contracts/factory/src/lib.rs
#	soroban/contracts/factory/src/test.rs
#	soroban/contracts/factory/test_snapshots/test/test_admin_getter_returns_stored_address.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_emits_pool_crtd_event.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_emits_pool_crtd_event_with_payload.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_increments_count_after_each_pool.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_records_zero_daily_rate.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_rejects_unmatched_non_admin_auth.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_returns_incrementing_ids.1.json
#	soroban/contracts/factory/test_snapshots/test/test_create_pool_uses_deterministic_pool_addresses.1.json
#	soroban/contracts/factory/test_snapshots/test/test_double_initialize_returns_error.1.json
#	soroban/contracts/factory/test_snapshots/test/test_get_pool_bumps_pool_record_ttl.1.json
#	soroban/contracts/factory/test_snapshots/test/test_get_pool_returns_correct_record.1.json
#	soroban/contracts/factory/test_snapshots/test/test_get_pool_returns_error_on_missing_id.1.json
#	soroban/contracts/factory/test_snapshots/test/test_get_pools_by_asset_returns_empty_when_no_pools.1.json
#	soroban/contracts/factory/test_snapshots/test/test_get_pools_by_asset_returns_matching_ids.1.json
#	soroban/contracts/factory/test_snapshots/test/test_get_pools_by_asset_unknown_asset_returns_empty.1.json
#	soroban/contracts/factory/test_snapshots/test/test_initialize_sets_admin.1.json
#	soroban/contracts/factory/test_snapshots/test/test_list_pools_caps_limit_at_twenty.1.json
#	soroban/contracts/factory/test_snapshots/test/test_list_pools_returns_empty_when_start_is_beyond_count.1.json
#	soroban/contracts/factory/test_snapshots/test/test_list_pools_returns_first_page.1.json
#	soroban/contracts/factory/test_snapshots/test/test_list_pools_returns_partial_last_page.1.json
#	soroban/contracts/factory/test_snapshots/test/test_list_pools_returns_second_page.1.json
#	soroban/contracts/factory/test_snapshots/test/test_multiple_pools_stored_independently.1.json
#	soroban/contracts/factory/test_snapshots/test/test_old_admin_cannot_create_pool_after_transfer_but_new_admin_can.1.json
#	soroban/contracts/factory/test_snapshots/test/test_pool_count_zero_after_initialize.1.json
#	soroban/contracts/factory/test_snapshots/test/test_pool_wasm_hash_returns_stored_hash.1.json
#	soroban/contracts/factory/test_snapshots/test/test_transfer_admin_changes_admin.1.json
#	soroban/contracts/factory/test_snapshots/test/test_transfer_admin_emits_event_with_old_and_new_admin.1.json
#	soroban/contracts/factory/test_snapshots/test/test_transfer_admin_non_admin_rejected.1.json
#	soroban/contracts/factory/tests/factory_pool_integration.rs
@Cyber-Mitch

Copy link
Copy Markdown
Contributor Author

@prodbycorne Please Review

@prodbycorne
prodbycorne merged commit b46e857 into SmartDropLabs:main Jul 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

factory: a newly deployed, uninitialized farming-pool contract has no admin — anyone can front-run initialize() to seize control

2 participants