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
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:
For
transferFrom:ModularCompliance.canSpenderCall(spender, from, to, amount)returns true. EverySPENDERmodule must accept the caller. With noSPENDERmodule, every spender is accepted. Otherwise the call reverts withSpenderNotAllowed, and the allowance is untouched.For
transferFrom: the allowance covers the amount. OtherwiseERC20InsufficientAllowance.The token is not paused. Otherwise
EnforcedPause.Neither the sender's address nor the recipient's address is frozen. Otherwise
FrozenWallet.The amount is within the sender's free balance: its balance less its frozen tokens. Otherwise
ERC20InsufficientBalance.TREXRegistry.isVerified(to)returns true. The registry resolvestoto 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. OtherwiseUnverifiedIdentity.ModularCompliance.canTransfer(from, to, amount)returns true. The compliance resolves the identity of both sides. EachRULEmodule answers the largest amount it allows, andamountmust not exceed the smallest answer. OtherwiseComplianceNotFollowed.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 byamountandToken.balanceOf(to)has increased byamount.Token.totalSupply()is unchanged.ModularCompliance.positionOfhas moved byamountfrom the sender's identity to the recipient's identity.For
transferFrom, the spender's allowance has decreased byamount. An allowance oftype(uint256).maxis not decreased.One
Transferevent was emitted. The transfer is settled: the register has recorded the change of ownership in the same transaction.
Failure modes
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)
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
transferFromit 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)andcanTransfer(from, to, amount)as reads before submitting, and show the holder which gate refuses. Each revert names its cause.Show
freeBalanceOfas the amount a holder can send.balanceOfincludes 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
canSpenderCallbefore atransferFrom. 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.
Related flows
08 Mint a position to an investor : the same gates, without a sender
24 Transfer through the identity : the identity moves tokens from a linked wallet
26 Authorize spenders : the spender policy of an asset
13 Freeze and unfreeze an address or a partial balance : why a transfer can be refused for a freeze
14 Execute a forced transfer : a transfer ordered by an agent
31 Transfer on a satellite : the cross-chain equivalent
Last updated