Skip to content

fix(sweep_controller): remove token transfer from sweep_account to match the SDK's custodial payout model - #760

Merged
phertyameen merged 1 commit into
mainfrom
fix/remove-controller-token-transfer-758
Oct 9, 2026
Merged

phertyameen merged 1 commit into
mainfrom
fix/remove-controller-token-transfer-758

Conversation

@phertyameen

Copy link
Copy Markdown
Contributor

Summary

sweep_account finished by calling transfers::execute_transfers, which did TokenClient::transfer(from = <ephemeral contract>, to = destination, amount). That can never succeed on-chain:

  1. Authorization - a contract must authorize its own outgoing transfer (authorize_as_current_contract) and EphemeralAccount::sweep() never does, so the host rejects it (Error(Auth, InvalidAction)).
  2. Funds aren't on the contract - the sender pays the ephemeral classic G... account via Horizon; the contract only records the payment (record_payment). Even with the auth fixed there is no balance to move.
  3. It would also double-pay the SDK: SweepsService already performs the classic Horizon payout.

The transfer was added in b21f6bc and the integration tests missed it because they used mock_all_auths(). This PR removes the transfer so the controller matches the SDK's custodial payout model: it verifies the signature/nonce, authorizes and marks the account swept, and emits SweepCompleted. It moves no tokens.

Changes

  • contracts/sweep_controller/src/lib.rs - drop the execute_transfers call and the Payment conversion in sweep_account; remove mod transfers; and the now-unused Payment import; fix the execute_sweep doc comment.
  • contracts/sweep_controller/src/transfers.rs - deleted (only used by sweep_account).
  • contracts/sweep_controller/tests/integration.rs - exercise the sweep path with explicit mock_auths only, no blanket mock_all_auths; add a test asserting a missing controller authorization fails.
  • docs/api-reference.md, docs/architecture.md, README.md - document that the controller does not move funds and the SDK performs the custodial payout.

Tests

Validated locally that a successful execute_sweep (with a real Ed25519 signature over SHA256(destination || nonce || controller)) now passes, and that the same path fails at transfer on the pre-change code. No mock_all_auths() was added; the suite does not rely on one.

  • cargo fmt --all -- --check - clean
  • cargo clippy -p sweep_controller --lib -- -D warnings - clean
  • cargo test - all pass (ephemeral_account 32, reserve_contract 20, sweep_controller integration 9)
  • cargo build --release --target wasm32v1-none - all four WASMs build

No new dependencies

The success-path test that requires signing was run during development and then removed so the PR introduces no dependency. The committed tests cover the sweep path with explicit auths only.

Redeploy steps (new controller ID pending)

The live controller CBEU4X5M...Z5KWE still contains the transfer call. Redeploy is required and must be run by a key-holder (no deployer secret available here):

  1. stellar contract build
  2. stellar contract deploy --wasm target/wasmv1-none/release/sweep_controller.wasm --network testnet --source <DEPLOYER_SECRET>
  3. stellar contract invoke --id <NEW_CONTROLLER_ID> --network testnet --source <DEPLOYER_SECRET> -- initialize --creator <CREATOR> --authorized_signer 16ac79d642e33ac696e822cc7175ad7ddd5587a9dd8b6942640cbb832a1ab4bf --authorized_destination <none|DEST>
  4. Record the new ID in deployments/testnet.json as sweepController.

Existing test accounts bound to the old controller keep pointing at the old authorized_controller; they need fresh sends against accounts initialized with the new controller ID.

Refs #758

…DK custodial payout

sweep_account called transfers::execute_transfers after EphemeralAccount::sweep,
which invoked TokenClient::transfer(from = <ephemeral contract>, ...). That can
never succeed: a contract must authorize its own outgoing transfer (sweep() never
does) and the funds sit on the ephemeral account's classic G... address, not on
the contract, so a fixed authorization would still leave no balance to move. It
would also double-pay the SDK's classic Horizon payout.

Remove the transfer call and the now-unused transfers module; the controller
verifies the signature/nonce, authorizes and marks the account swept, and emits
SweepCompleted. Update integration tests to exercise the sweep path with explicit
mock_auths only (no blanket mock_all_auths) and assert a missing controller
authorization fails. Align docs/api-reference.md, docs/architecture.md and the
README with the custodial payout model.

Refs #758
@phertyameen
phertyameen merged commit b4348bd into main Oct 9, 2026
1 check 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.

1 participant