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

24. Transfer through the identity

An ONCHAINID can move the tokens held in any wallet linked to it, on the reference chain, without a signature from that wallet and without an allowance. The Identity Owner uses this to command a position spread over several wallets from one place, to consolidate it, or to empty a wallet it no longer trusts. The movement is an ordinary transfer: it passes the same gates as any other.

At a glance

Triggered by

Identity Owner

Actors

Identity Owner, Investor, Platform

Contracts

ONCHAINID, Token, TREXRegistry, ModularCompliance

Preconditions

The source wallet resolves to the identity (see 07); the owner controls an ACTION key of the identity; the source wallet holds a free balance on the reference chain; the recipient is eligible for the asset; the token is not paused

Result

The tokens have moved from the linked wallet to the recipient, on the identity's order

Actors and roles

Actor
Role in this flow
What they hold

Identity Owner

Orders the transfer through its identity.

An ACTION key of the identity

Investor

The Identity Owner in its capacity as holder of the position being moved.

A balance in a wallet linked to the identity

Platform

Runs the pre-checks and relays the order.

Whatever keys the owner has authorized

Sequence

Step by step

Step 1. Confirm the transfer can execute

Who: Platform, on behalf of the Identity Owner

Call: Off-chain, using read calls

Before the call: The owner has chosen the source wallet from, the recipient to and the amount.

What happens: The Platform reads TREXRegistry.identity(from), which must return the owner's identity, and Token.freeBalanceOf(from), which must cover the amount. It reads TREXRegistry.isVerified(to) and ModularCompliance.canTransfer(from, to, amount), which must both be true. When to is another address of the same identity, no position changes, and a rule on distribution such as MaxBalancePerIdentityModule does not limit the amount.

What changes: Nothing on-chain.

Step 2. Order the transfer from the identity

Who: Identity Owner (ACTION key), through the identity

Call: Token.identityTransfer(from, to, amount), called by the identity

Before the call: Step 1 passed. An ACTION key instructs the identity to make the call, in a user operation or through the identity's execution queue. The token is an external target, so ACTION is sufficient.

What the contract checks:

  1. from resolves to an identity, and the caller is that identity. Otherwise the call reverts with NotLinkedIdentity.

  2. The token is not paused. Otherwise EnforcedPause.

  3. Neither from nor to is frozen. Otherwise FrozenWallet.

  4. The amount is within the free balance of from. Otherwise ERC20InsufficientBalance.

  5. TREXRegistry.isVerified(to) returns true: to resolves to an identity that holds a valid claim from a trusted Claim Issuer for every required topic. Otherwise UnverifiedIdentity.

  6. ModularCompliance.canTransfer(from, to, amount) returns true. Otherwise ComplianceNotFollowed.

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

These reverts are the token's. When the instruction goes through the identity's execution queue, the queue catches the revert: the owner's transaction succeeds, the queue emits ExecutionFailed, and nothing moves. A successful instruction emits Executed.

What changes: The token debits from and credits to, and emits Transfer(from, to, amount). It calls ModularCompliance.transferred(from, to, amount): the compliance moves the amount from the position of the sender's identity to the position of the recipient's identity, unless they are the same, and tells its tracker modules. The token then emits IdentityTransfer(identity, from, to, amount).

State after the flow

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

  • Token.totalSupply() is unchanged.

  • No allowance was read or written.

  • When to belongs to another identity, ModularCompliance.positionOf has moved by amount between the two identities. When to belongs to the same identity, no position has changed.

  • An indexer has seen Transfer followed by IdentityTransfer.

Failure modes

Symptom
Cause
Fix

Reverts with NotLinkedIdentity

The caller is not the identity from resolves to: the call came from a key directly, from another identity, or from resolves to no identity (step 2, check 1)

Have the identity make the call. Read TREXRegistry.identity(from)

The identity's instruction is refused

The instructing key holds neither ACTION nor MANAGEMENT

Use an ACTION key

The transaction succeeds, ExecutionFailed is emitted and no token moved

The instruction went through the identity's execution queue and the token refused the transfer (step 2, checks 1 to 7)

Run the reads of step 1 again to find the check that fails

Reverts with EnforcedPause

The token is paused (step 2, check 2)

Wait for the Issuer's agents to lift the pause

Reverts with FrozenWallet

from or to is frozen (step 2, check 3)

Ask the asset's agents

Reverts with ERC20InsufficientBalance

The amount exceeds the free balance of from: part of it is frozen, or it is not there (step 2, check 4)

Read freeBalanceOf(from) and reduce the amount

Reverts with UnverifiedIdentity

to 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 5)

Send to an eligible address. For claims, see 05

Reverts with ComplianceNotFollowed

A rule refuses this amount for this recipient (step 2, check 6)

Reduce the amount, or see 11 for where rules come from

Reverts with ZeroValue

The amount is zero (step 2, check 7)

Send a positive amount

The tokens are on a satellite and cannot be moved this way

The identity's control covers the reference chain only

Transfer on the satellite, or recall the position first

Common misunderstandings

Wrong: "The identity has an allowance over its wallets."

Right: No allowance exists. identityTransfer checks that the caller is the identity the wallet resolves to, and moves the tokens. The ERC-20 allowances of the wallet are neither read nor changed, and approve, transferFrom and permit keep their standard meaning.

Wrong: "A key of the identity can call identityTransfer."

Right: The caller must be the identity itself. A key holder that calls the token directly is just another address, and is refused. The key instructs the identity, and the identity calls the token.

Wrong: "A transfer ordered by the identity skips the checks."

Right: It passes every check of a transfer: the pause, freezes, the eligibility of the recipient and the compliance rules. What it skips is the spender policy, because the holder's own identity is not a third party.

Wrong: "Moving tokens between my own wallets is a transfer of ownership."

Right: Ownership belongs to the identity. A movement between two addresses of one identity changes where the tokens sit, not who owns them. The identity's position is unchanged, and a rule on distribution such as MaxBalancePerIdentityModule does not limit it.

Integration notes

  • Run isVerified(to) and canTransfer(from, to, amount) as reads first. They are the checks the token runs.

  • To consolidate, send to the identity's own address. The identity holds tokens itself.

  • Several transfers can go in one user operation as a batch, each from a different linked wallet.

  • Index IdentityTransfer to distinguish a transfer ordered by the identity from one signed by the wallet. It always follows the Transfer it describes.

  • A wallet whose link was revoked still resolves to its identity for this call. The owner can empty it. See 23.

  • For tokens the identity holds at its own address, the identity calls Token.transfer. identityTransfer is for linked wallets.

Last updated