For the complete documentation index, see llms.txt. This page is also available as Markdown.

14. Execute a forced transfer

A forced transfer moves tokens from one address to another without the holder. An agent executes it when a decision overrides the holder: a court order, a succession, the correction of an error, the enforcement of a pledge. It goes through freezes and does not ask the compliance rules, and it keeps one gate: the recipient must be eligible.

At a glance

Triggered by

Token Agent

Actors

Issuer, Token Agent, Investor (current holder), Investor (recipient), Platform

Contracts

Token, TREXRegistry, ModularCompliance, compliance modules

Preconditions

The caller holds AGENT_FORCED_TRANSFER in the asset's domain (see 25); the source address holds at least amount on the reference chain; the recipient's identity holds a valid claim from a trusted Claim Issuer for every required topic (see 05); the token is not paused; the decision is taken and documented under the Issuer's process

Result

The tokens have moved from the source address to the recipient, and the position has moved between their identities

Actors and roles

Actor
Role in this flow
What they hold

Issuer

Decides the forced transfer and keeps the evidence that justifies it. Does not transact in this flow.

The roles it delegated to its agents

Token Agent

Submits the forced transfer.

AGENT_FORCED_TRANSFER on the Token

Investor (current holder)

Holds the tokens that are moved. Does not transact and does not consent.

A balance at the source address

Investor (recipient)

Receives the tokens. Does not transact.

An ONCHAINID with valid claims

Platform

Runs the reads and submits with the Token Agent's key.

The Token Agent's signing key, or a key authorized for it

Sequence

Step by step

Step 1. Assess the transfer

Who: Platform, on behalf of the Token Agent

Call: Off-chain, using read calls

Before the call: The Issuer has decided the transfer and holds the document that justifies it. The Platform knows the source address from, the recipient address to and the amount.

What happens: The Platform reads Token.balanceOf(from), which must cover the amount, and Token.getFrozenTokens(from), to know whether the transfer will release frozen tokens. It reads Token.paused(), which must be false, and TREXRegistry.isVerified(to), which must be true. It reads ModularCompliance.canTransfer(from, to, amount) for information: the contract does not ask the rules, so a false answer tells the Issuer that the transfer will leave the asset in a state its rules would have refused. If part of the holder's position is active on a satellite, that part is not in balanceOf and is recalled first.

What changes: Nothing on-chain.

Step 2. Force the transfer

Who: Token Agent (AGENT_FORCED_TRANSFER)

Call: Token.forcedTransfer(from, to, amount)

Before the call: Step 1 is done.

What the contract checks:

  1. The caller holds AGENT_FORCED_TRANSFER in the asset's domain. Otherwise the call reverts with AccessManagedUnauthorized.

  2. The token is not paused. Otherwise EnforcedPause.

  3. TREXRegistry.isVerified(to) returns true: to resolves to an identity that holds a valid claim from a trusted Claim Issuer for every required topic, and its link is not revoked. Otherwise UnverifiedIdentity.

  4. The amount is within the balance of from, frozen tokens included. Otherwise ERC20InsufficientBalance.

  5. The amount is not zero. The compliance does not record a movement of zero. Otherwise ZeroValue.

A freeze on from or on to is not checked. The compliance rules are not asked. The eligibility of the current holder is not evaluated.

What changes: If the amount exceeds the free balance of from, the token releases the frozen tokens it needs and emits TokensUnfrozen(from, releasedAmount). It debits from, credits to, and emits Transfer(from, to, amount), then ForcedTransfer(agent). It calls ModularCompliance.transferred(from, to, amount). The compliance moves amount from the position of the holder's identity to the position of the recipient's identity and calls afterTransfer on every TRACKER module.

State after the flow

  • Token.balanceOf(from) has decreased by amount and Token.balanceOf(to) has increased by amount.

  • Token.totalSupply() is unchanged.

  • Token.getFrozenTokens(from) has decreased by what the transfer had to release. The tokens arrive free at to.

  • ModularCompliance.positionOf has moved by amount from the holder's identity to the recipient's identity. When both addresses belong to one identity, no position has changed.

  • No allowance was read or written.

  • An indexer has seen Transfer followed by ForcedTransfer, which names the agent.

Failure modes

Symptom
Cause
Fix

Reverts with AccessManagedUnauthorized

The caller does not hold AGENT_FORCED_TRANSFER in the asset's domain (step 2, check 1)

Grant the role: 25. Holding another agent role is not enough

Reverts with EnforcedPause

The token is paused. A forced transfer does not go through the pause (step 2, check 2)

Lift the pause first: 16

Reverts with UnverifiedIdentity

The recipient is not eligible: it resolves to no identity, its link was revoked, or its identity lacks a valid claim for a required topic (step 2, check 3)

Deliver to an eligible address. For claims, see 05

Reverts with ERC20InsufficientBalance

from holds less than amount on the reference chain (step 2, check 4)

Read balanceOf(from). For a position on a satellite, recall it first

Reverts with ZeroValue

The amount is zero (step 2, check 5)

Pass a positive amount

The transfer succeeded and a rule of the asset is now breached

A forced transfer does not ask the rules

Anticipate it in step 1 with canTransfer. The Issuer regularizes, or accepts the exception and documents it

The transfer succeeded to a frozen address

A freeze on the recipient does not block a forced transfer

Intended. The tokens stay immobilized there for the holder

An unexpected TokensUnfrozen event appears

The transfer exceeded the free balance and released frozen tokens

Intended. To keep them frozen at the destination, freeze the amount there: 13

Common misunderstandings

Wrong: "A forced transfer bypasses everything."

Right: It bypasses the holder's consent, the freezes and the compliance rules. It does not bypass eligibility: the recipient must be verified, and the call reverts otherwise. It does not bypass the pause either. The register never records a holding that should not exist, even on an agent's instruction.

Wrong: "Forced transfers are invisible to the compliance."

Right: The token tells the compliance after the move. Positions follow the tokens and tracker modules record the movement. What is skipped is the question, not the bookkeeping.

Wrong: "The agent has to unfreeze before forcing a transfer."

Right: The transfer releases exactly the frozen amount it lacks, and emits TokensUnfrozen for it. The rest of the freeze stays in place on the source address.

Wrong: "Any agent can force a transfer."

Right: The function is gated by AGENT_FORCED_TRANSFER, a role of its own. An agent that mints, freezes or pauses cannot move a holder's tokens unless this role is granted to it.

Integration notes

  • Read isVerified(to) before submitting. It is the one gate the contract applies.

  • Read canTransfer(from, to, amount) and record the answer with the decision. The contract will not stop a transfer the rules refuse, so the assessment belongs to the Issuer's file.

  • Use Token.batchForcedTransfer(fromList, toList, amounts) for several transfers. It is gated by the same role, the three lists have the same length, and the batch reverts as a whole if one entry fails.

  • Index ForcedTransfer(agent). It is emitted in the same transaction as the Transfer it qualifies, right after it, and it is what distinguishes a forced transfer from a holder's transfer.

  • Store the legal basis of each forced transfer off-chain, keyed by the transaction hash. The chain records who moved what and when, not why.

  • To move a whole position from a wallet the holder lost to another address of the same holder, use a recovery. It carries the freezes over, which a forced transfer does not.

Last updated