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

10. Transfer between investors

A transfer moves units of the asset from one holder to another on the reference chain. It is the secondary movement of a regulated asset: peer to peer, with no intermediary deciding, and under the rules of the asset. The holder triggers it with a standard ERC-20 call, or a spender the holder approved triggers it on the holder's behalf. The token records the transfer only if the recipient is eligible and the movement complies.

At a glance

Triggered by

Investor (sender), or a spender it approved

Actors

Investor (sender), Investor (recipient), Venue or application (as spender), Platform

Contracts

Token, TREXRegistry, ModularCompliance, compliance modules, the ONCHAINID of the recipient and its Claim Issuers (read only, during verification)

Preconditions

The sender holds a free balance on the reference chain (see 08); the recipient's identity holds a valid claim from a trusted Claim Issuer for every required topic (see 05); the receiving address is the recipient's ONCHAINID, a wallet linked to it, or an address registered to it for the asset (see 06); the token is not paused; for a transfer through a spender, the sender has approved the spender and the asset's spender policy accepts it (see 26)

Result

The sender's balance has decreased and the recipient's has increased by amount, and the position has moved from the sender's identity to the recipient's

Actors and roles

Actor
Role in this flow
What they hold

Investor (sender)

Sends the tokens, or approves a spender to do it.

A free balance at one of its addresses, and the key of that address

Investor (recipient)

Receives the tokens. Does not transact.

An ONCHAINID with valid claims

Venue or application

Executes the transfer for the sender, when the transfer goes through transferFrom.

An allowance from the sender. Acceptance by the asset's spender policy

Platform

Runs the pre-checks and submits.

The key of the party it acts for

Sequence

Step by step

Step 1. Confirm the transfer can execute

Who: Platform, on behalf of the sender

Call: Off-chain, using read calls

Before the call: The sender has chosen the recipient address to and the amount.

What happens: The Platform reads Token.freeBalanceOf(from), which must cover the amount, TREXRegistry.isVerified(to) and ModularCompliance.canTransfer(from, to, amount), which must both be true, and Token.paused(), which must be false. For a transfer through a spender, it also reads Token.allowance(from, spender) and ModularCompliance.canSpenderCall(spender, from, to, amount).

What changes: Nothing on-chain.

Step 2. Approve a spender

Only for a transfer through a spender.

Who: Investor (sender)

Call: Token.approve(spender, amount), or a signature consumed by Token.permit(owner, spender, value, deadline, v, r, s)

Before the call: The sender has decided to let the spender move up to amount of its tokens.

What the contract checks: The standard ERC-20 conditions. For permit: the signature is the owner's, over the current nonce, and the deadline has not passed.

What changes: The allowance of the spender over the sender's address is set. The token emits Approval(owner, spender, value). The approval itself is not subject to the asset's spender policy. The policy applies when the spender uses it.

Step 3. Transfer

Who: Investor (sender), from the address that holds the tokens. Or the spender.

Call: Token.transfer(to, amount) by the holder. Token.transferFrom(from, to, amount) by a spender.

Before the call: Step 1 passed.

What the contract checks:

  1. For transferFrom: ModularCompliance.canSpenderCall(spender, from, to, amount) returns true. Every SPENDER module must accept the caller. With no SPENDER module, every spender is accepted. Otherwise the call reverts with SpenderNotAllowed, and the allowance is untouched.

  2. For transferFrom: the allowance covers the amount. Otherwise ERC20InsufficientAllowance.

  3. The token is not paused. Otherwise EnforcedPause.

  4. Neither the sender's address nor the recipient's address is frozen. Otherwise FrozenWallet.

  5. The amount is within the sender's free balance: its balance less its frozen tokens. Otherwise ERC20InsufficientBalance.

  6. TREXRegistry.isVerified(to) returns true. The registry resolves to to an identity, refuses a wallet whose link was revoked, and requires a valid claim from a trusted Claim Issuer for every topic required of that identity's type. Otherwise UnverifiedIdentity.

  7. ModularCompliance.canTransfer(from, to, amount) returns true. The compliance resolves the identity of both sides. Each RULE module answers the largest amount it allows, and amount must not exceed the smallest answer. Otherwise ComplianceNotFollowed.

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

What changes: The token debits the sender and credits the recipient, and emits Transfer(from, to, amount). It calls ModularCompliance.transferred(from, to, amount). The compliance moves amount from the position of the sender's identity to the position of the recipient's identity and calls afterTransfer on every TRACKER module. When both addresses belong to the same identity, no position changes.

State after the flow

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

  • Token.totalSupply() is unchanged.

  • ModularCompliance.positionOf has moved by amount from the sender's identity to the recipient's identity.

  • For transferFrom, the spender's allowance has decreased by amount. An allowance of type(uint256).max is not decreased.

  • One Transfer event was emitted. The transfer is settled: the register has recorded the change of ownership in the same transaction.

Failure modes

Symptom
Cause
Fix

Reverts with SpenderNotAllowed

The asset's spender policy refuses the caller of transferFrom (step 3, check 1)

The Issuer authorizes the spender: 26. Or the holder transfers directly

Reverts with ERC20InsufficientAllowance

The spender has no allowance, or not enough (step 3, check 2)

The holder approves the spender (step 2)

Reverts with EnforcedPause

The token is paused (step 3, check 3)

Wait for the Issuer's agents to lift the pause

Reverts with FrozenWallet

The sender's or the recipient's address is frozen (step 3, check 4)

The error names the address. Ask the asset's agents: 13

Reverts with ERC20InsufficientBalance

The amount exceeds the free balance: part of the balance is frozen, or the tokens are on a satellite (step 3, check 5)

Read freeBalanceOf(from). Reduce the amount

Reverts with UnverifiedIdentity

The recipient is not eligible: no identity, a revoked link, a missing, expired or revoked claim, or an issuer not trusted for the topic (step 3, check 6)

The recipient completes its onboarding: 05, 07

Reverts with ComplianceNotFollowed

A rule refuses this amount for this pair of holders, or one side resolves to no identity while a rule is bound (step 3, check 7)

Reduce the amount. See 11 for where rules come from

Reverts with ZeroValue

The amount is zero. Unlike a plain ERC-20, the token refuses a transfer of zero (step 3, check 8)

Send a positive amount

A wallet shows a balance and cannot send it

The balance includes frozen tokens. balanceOf is free plus frozen

Show freeBalanceOf as the transferable amount

Common misunderstandings

Wrong: "A transfer is approved by the Issuer or by an agent."

Right: Nobody approves a transfer. The holder sends, and the token evaluates the rules of the asset in the same transaction. The transfer settles if the recipient is eligible and the movement complies, and reverts otherwise.

Wrong: "Both parties must be whitelisted."

Right: There is no list of approved addresses. The token checks the eligibility of the recipient, from the claims on its identity, at the moment of the transfer. The sender is not re-verified, by design: a holder whose claim expired was eligible when it received the tokens and is still identified, so it keeps its position and can still send to an eligible recipient. An Issuer that wants to block it freezes its tokens or its address.

Wrong: "The compliance check looks at the sender and the receiver."

Right: It looks at their identities, at the amount, and at the distribution of the asset: a rule can count everything an identity owns across its addresses and chains. On transferFrom it also looks at the spender.

Wrong: "A non-compliant transfer is flagged and reversed."

Right: It is not recorded. The transaction reverts before any balance moves. There is nothing to reverse.

Integration notes

  • Run isVerified(to) and canTransfer(from, to, amount) as reads before submitting, and show the holder which gate refuses. Each revert names its cause.

  • Show freeBalanceOf as the amount a holder can send. balanceOf includes frozen tokens, and does not include tokens active on satellites.

  • A recipient can be an ONCHAINID. Do not assume that a holding address is a wallet.

  • A transfer of zero reverts. An application that sends zero-value transfers to probe a token, as some do with ERC-20 tokens, uses the reads instead.

  • Token.batchTransfer(toList, amounts) sends to several recipients in one transaction. Each entry passes every check. The batch reverts as a whole if one fails.

  • A venue reads canSpenderCall before a transferFrom. Being approved by the holder is not sufficient when the asset has a spender policy.

  • The transfer is final as a change of ownership when the transaction is. There is no pending state on the reference chain.

  • A transfer involving a wallet on a satellite is a different flow, with an authorization issued first.

Last updated