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
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:
fromresolves to an identity, and the caller is that identity. Otherwise the call reverts withNotLinkedIdentity.The token is not paused. Otherwise
EnforcedPause.Neither
fromnortois frozen. OtherwiseFrozenWallet.The amount is within the free balance of
from. OtherwiseERC20InsufficientBalance.TREXRegistry.isVerified(to)returns true:toresolves to an identity that holds a valid claim from a trusted Claim Issuer for every required topic. OtherwiseUnverifiedIdentity.ModularCompliance.canTransfer(from, to, amount)returns true. OtherwiseComplianceNotFollowed.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 byamount, andToken.balanceOf(to)has increased byamount.Token.totalSupply()is unchanged.No allowance was read or written.
When
tobelongs to another identity,ModularCompliance.positionOfhas moved byamountbetween the two identities. Whentobelongs to the same identity, no position has changed.An indexer has seen
Transferfollowed byIdentityTransfer.
Failure modes
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.
identityTransferchecks 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, andapprove,transferFromandpermitkeep 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
MaxBalancePerIdentityModuledoes not limit it.
Integration notes
Run
isVerified(to)andcanTransfer(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
IdentityTransferto distinguish a transfer ordered by the identity from one signed by the wallet. It always follows theTransferit 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.identityTransferis for linked wallets.
Related flows
07 Link a wallet to an ONCHAINID : what gives the identity this authority
23 Revoke a wallet : emptying a wallet that is no longer trusted
10 Transfer between investors : the same gates, signed by the holding address
26 Authorize spenders : third parties, which this flow does not involve
32 Recall a position to the reference chain : bringing a satellite position within the identity's reach
Last updated