Purpose: Fast lookup for developers working with token transfers in the Fluxora streaming contract
fn pull_token(env: &Env, from: &Address, amount: i128) -> Result<(), ContractError>What it does: Transfers tokens FROM an external address TO the contract
When to use: Stream creation, top-ups, any operation that deposits tokens
Authorization: Caller must have from.require_auth() before calling
Failure: Panics if insufficient balance/allowance, reverts transaction
Usage Pattern:
sender.require_auth(); // 1. Authorize first
validate_params(...)?; // 2. Validate inputs
pull_token(&env, &sender, amount)?; // 3. Pull tokens
persist_state(&env, ...); // 4. Save state afterfn push_token(env: &Env, to: &Address, amount: i128) -> Result<(), ContractError>What it does: Transfers tokens FROM the contract TO an external address
When to use: Withdrawals, refunds, any operation that sends tokens out
Authorization: Caller must be authorized for the operation (recipient/sender/admin)
Failure: Panics if insufficient contract balance, reverts transaction
Usage Pattern:
recipient.require_auth(); // 1. Authorize first
let amount = calculate_amount(...)?; // 2. Calculate amount
save_state(&env, ...); // 3. Save state BEFORE transfer (CEI!)
push_token(&env, &recipient, amount)?; // 4. Push tokens last
emit_event(&env, ...); // 5. Emit event after successChecks → Effects → Interactions
Always follow this order:
// ✅ CORRECT
fn withdraw(env: Env, stream_id: u64) -> Result<i128, ContractError> {
// CHECKS
let stream = load_stream(&env, stream_id)?;
stream.recipient.require_auth();
if stream.status != Active { return Err(...); }
// EFFECTS (update state FIRST)
stream.withdrawn_amount += amount;
save_stream(&env, &stream);
// INTERACTIONS (external calls LAST)
push_token(&env, &stream.recipient, amount)?;
env.events().publish(...);
Ok(amount)
}
// ❌ WRONG - Don't do this!
fn withdraw_wrong(env: Env, stream_id: u64) -> Result<i128, ContractError> {
let stream = load_stream(&env, stream_id)?;
// ❌ External call before state update
push_token(&env, &stream.recipient, amount)?;
// ❌ State update after external call (reentrancy risk!)
stream.withdrawn_amount += amount;
save_stream(&env, &stream);
Ok(amount)
}Why CEI matters:
- Prevents reentrancy attacks
- Ensures consistent state on failure
- Makes code easier to audit
pub fn create_stream(env: Env, sender: Address, ...) -> Result<u64, ContractError> {
sender.require_auth(); // Sender must authorize
// ... validation ...
pull_token(&env, &sender, deposit_amount)?;
// ... persist state ...
}pub fn withdraw(env: Env, stream_id: u64) -> Result<i128, ContractError> {
let stream = load_stream(&env, stream_id)?;
stream.recipient.require_auth(); // Recipient must authorize
// ... calculate amount ...
push_token(&env, &stream.recipient, amount)?;
// ... emit events ...
}pub fn cancel_stream_as_admin(env: Env, stream_id: u64) -> Result<(), ContractError> {
let admin = get_admin(&env)?;
admin.require_auth(); // Admin must authorize
// ... cancel logic ...
push_token(&env, &stream.sender, refund_amount)?;
}pub fn top_up_stream(env: Env, stream_id: u64, funder: Address, amount: i128) -> Result<(), ContractError> {
funder.require_auth(); // Any funder can authorize
// ... validation ...
pull_token(&env, &funder, amount)?;
}// ✅ GOOD: Check before push_token to avoid unnecessary calls
if refund_amount > 0 {
push_token(&env, &sender, refund_amount)?;
}
// ✅ GOOD: Early return for zero withdrawable
let withdrawable = accrued - withdrawn;
if withdrawable == 0 {
return Ok(0); // No transfer, no state change
}
// ✅ GOOD: Check before batch pull
if total_deposit > 0 {
pull_token(&env, &sender, total_deposit)?;
}- Avoids unnecessary token client invocations (gas savings)
- Prevents confusing events (no event if no transfer)
- Makes code more efficient and clearer
// ❌ WRONG
pub fn withdraw(env: Env, stream_id: u64) -> Result<i128, ContractError> {
let stream = load_stream(&env, stream_id)?;
// Missing: stream.recipient.require_auth();
push_token(&env, &stream.recipient, amount)?;
}// ❌ WRONG
pub fn cancel_stream(env: Env, stream_id: u64) -> Result<(), ContractError> {
let stream = load_stream(&env, stream_id)?;
push_token(&env, &stream.sender, refund)?; // ❌ Transfer before state update
stream.status = Cancelled;
save_stream(&env, &stream);
}// ❌ WRONG - Don't bypass helpers!
pub fn withdraw(env: Env, stream_id: u64) -> Result<i128, ContractError> {
let token_client = token::Client::new(&env, &token_address);
token_client.transfer(&env.current_contract_address(), &recipient, &amount); // ❌
}
// ✅ CORRECT - Use helper
pub fn withdraw(env: Env, stream_id: u64) -> Result<i128, ContractError> {
push_token(&env, &recipient, amount)?; // ✅
}// ❌ WRONG
pub fn withdraw(env: Env, stream_id: u64) -> Result<i128, ContractError> {
let stream = load_stream(&env, stream_id)?;
// Missing: status checks (Paused? Completed?)
push_token(&env, &stream.recipient, amount)?;
}When adding new token transfer code, test:
- Authorization: Unauthorized calls fail
- Insufficient Balance: Fails gracefully, no state change
- Zero Amount: Handles correctly (early return or skip)
- State Consistency: State updated before transfer
- Event Emission: Events only emitted on success
- Balance Tracking: Contract balance matches expected
- Atomicity: Failed transfer reverts all changes
Check:
- Does sender have enough tokens?
token.balance(&sender) - Has sender approved contract?
token.allowance(&sender, &contract) - Is amount calculation correct? Add debug logs
Check:
- Is
require_auth()called on correct address? - Is authorization called before helper?
- Is test using
mock_all_auths()or specific auth?
Check:
- Is CEI pattern followed? (state before transfer)
- Are you using
save_stream()beforepush_token()? - Are you testing with transaction rollback?
Check:
- Are events emitted AFTER successful transfer?
- Is transaction succeeding? (check for panics)
- Are you checking the right event topic?
When reviewing token transfer code:
- Uses
pull_tokenorpush_token(no direct token client) - Authorization before helper call
- CEI pattern followed (state before transfer)
- Zero-amount check (if applicable)
- Events emitted after success
- Error handling correct
- Tests cover failure cases
- Documentation updated
For questions about:
- Token transfer security → [security-team]
- CEI pattern → [architecture-team]
- Test failures → [qa-team]
- Production issues → [on-call-engineer]
- Full audit report:
TOKEN_HELPERS_AUDIT.md - Implementation checklist:
TOKEN_HELPERS_IMPLEMENTATION_CHECKLIST.md - Contract documentation:
docs/streaming.md - Security guidelines:
docs/security.md
Last Updated: 2026-03-26
Version: 1.0