Status |
---|
Accepted |
ICRC-2
is an extension of the base ICRC-1
standard.
ICRC-2
specifies a way for an account owner to delegate token transfers to a third party on the owner's behalf.
The approve and transfer-from flow is a 2-step process.
- Account owner Alice entitles Bob to transfer up to X tokens from her account A by calling the
icrc2_approve
method on the ledger. - Bob can transfer up to X tokens from account A to any account by calling the
icrc2_transfer_from
method on the ledger as if A was Bob's account B. The number of transfers Bob can initiate from account A is not limited as long as the total amount spent is below X.
Approvals are not transitive: if Alice approves transfers from her account A to Bob's account B, and Bob approves transfers from his account B to Eva's account E, Eva cannot withdraw tokens from Alice's account through Bob's approval.
The approve-transfer-from pattern became popular in the Ethereum ecosystem thanks to the ERC-20 token standard. This interface enables new application capabilities:
-
Recurring payments. Alice can approve a large amount to Bob in advance, allowing Bob to make periodic transfers in smaller installments. Real-world examples include subscription services and rents.
-
Uncertain transfer amounts. In some applications, such as automatic trading services, the exact price of goods is unknown in advance. With approve-transfer-from flow, Alice can allow Bob to trade securities on Alice's behalf, buying/selling at yet-unknown price up to a specified limit.
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Canisters implementing the ICRC-2
standard MUST implement all the functions in the ICRC-1
interface
Canisters implementing the ICRC-2
standard MUST include ICRC-2
in the list returned by the icrc1_supported_standards
method
type Account = record {
owner : principal;
subaccount : opt blob;
};
This method entitles the spender
to transfer token amount
on behalf of the caller from account { owner = caller; subaccount = from_subaccount }
.
The number of transfers the spender
can initiate from the caller's account is unlimited as long as the total amounts and fees of these transfers do not exceed the allowance.
The caller does not need to have the full token amount
on the specified account for the approval to succeed, just enough tokens to pay the approval fee.
The call resets the allowance and the expiration date for the spender
account to the given values.
The ledger SHOULD reject the call if the spender account owner is equal to the source account owner.
If the expected_allowance
field is set, the ledger MUST ensure that the current allowance for the spender
from the caller's account is equal to the given value and return the AllowanceChanged
error otherwise.
The ledger MAY cap the allowance if it is too large (for example, larger than the total token supply). For example, if there are only 100 tokens, and the ledger receives an approval for 120 tokens, the ledger may cap the allowance to 100.
icrc2_approve : (ApproveArgs) -> (variant { Ok : nat; Err : ApproveError });
type ApproveArgs = record {
from_subaccount : opt blob;
spender : Account;
amount : nat;
expected_allowance : opt nat;
expires_at : opt nat64;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};
type ApproveError = variant {
BadFee : record { expected_fee : nat };
// The caller does not have enough funds to pay the approval fee.
InsufficientFunds : record { balance : nat };
// The caller specified the [expected_allowance] field, and the current
// allowance did not match the given value.
AllowanceChanged : record { current_allowance : nat };
// The approval request expired before the ledger had a chance to apply it.
Expired : record { ledger_time : nat64; };
TooOld;
CreatedInFuture: record { ledger_time : nat64 };
Duplicate : record { duplicate_of : nat };
TemporarilyUnavailable;
GenericError : record { error_code : nat; message : text };
};
- The caller has enough fees on the
{ owner = caller; subaccount = from_subaccount }
account to pay the approval fee. - If the
expires_at
field is set, it's greater than the current ledger time. - If the
expected_allowance
field is set, it's equal to the current allowance for thespender
.
- The
spender
's allowance for the{ owner = caller; subaccount = from_subaccount }
is equal to the givenamount
.
Transfers a token amount from the from
account to the to
account using the allowance of the spender's account (SpenderAccount = { owner = caller; subaccount = spender_subaccount }
).
The ledger draws the fees from the from
account.
icrc2_transfer_from : (TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError });
type TransferFromError = variant {
BadFee : record { expected_fee : nat };
BadBurn : record { min_burn_amount : nat };
// The [from] account does not hold enough funds for the transfer.
InsufficientFunds : record { balance : nat };
// The caller exceeded its allowance.
InsufficientAllowance : record { allowance : nat };
TooOld;
CreatedInFuture: record { ledger_time : nat64 };
Duplicate : record { duplicate_of : nat };
TemporarilyUnavailable;
GenericError : record { error_code : nat; message : text };
};
type TransferFromArgs = record {
spender_subaccount : opt blob;
from : Account;
to : Account;
amount : nat;
fee : opt nat;
memo : opt blob;
created_at_time : opt nat64;
};
-
The allowance for the
SpenderAccount
from thefrom
account is large enough to cover the transfer amount and the fees (icrc2_allowance({ account = from; spender = SpenderAccount }).allowance >= amount + fee
). Otherwise, the ledger MUST return anInsufficientAllowance
error. -
The
from
account holds enough funds to cover the transfer amount and the fees. (icrc1_balance_of(from) >= amount + fee
). Otherwise, the ledger MUST return anInsufficientFunds
error.
- If the
from
account is not equal to theSpenderAccount
, the(from, SpenderAccount)
allowance decreases by the transfer amount and the fees. - The ledger debited the specified
amount
of tokens and fees from thefrom
account. - The ledger credited the specified
amount
to theto
account.
Returns the token allowance that the spender
account can transfer from the specified account
, and the expiration time for that allowance, if any.
If there is no active approval, the ledger MUST return { allowance = 0; expires_at = null }
.
icrc2_allowance : (AllowanceArgs) -> (Allowance) query;
type AllowanceArgs = record {
account : Account;
spender : Account;
};
type Allowance = record {
allowance : nat;
expires_at : opt nat64;
}
Returns the list of standards this ledger supports.
Any ledger supporting ICRC-2
MUST include a record with the name
field equal to "ICRC-2"
in that list.
icrc1_supported_standards : () -> (vec record { name : text; url : text }) query;
- Alice wants to deposit 100 tokens on an
ICRC-2
ledger to canister C. - Alice calls
icrc2_approve
withspender
set to the canister's default account ({ owner = C; subaccount = null}
) andamount
set to the token amount she wants to deposit (100) plus the transfer fee. - Alice can then call some
deposit
method on the canister, which callsicrc2_transfer_from
withfrom
set to Alice's (the caller) account,to
set to the canister's account, andamount
set to the token amount she wants to deposit (100). - The canister can now determine from the result of the call whether the transfer was successful. If it was successful, the canister can now safely commit the deposit to state and know that the tokens are in its account.
- Canister C wants to transfer 100 tokens on an
ICRC-2
ledger from Alice's account to Bob's account. - Alice previously approved canister C to transfer tokens on her behalf by calling
icrc2_approve
withspender
set to the canister's default account ({ owner = C; subaccount = null }
) andamount
set to the token amount she wants to allow (100) plus the transfer fee. - During some update call, the canister can now call
icrc2_transfer_from
withfrom
set to Alice's account,to
set to Bob's account, andamount
set to the token amount she wants to transfer (100). - Once the call completes successfully, Bob has 100 extra tokens on his account, and Alice has 100 (plus the fee) tokens less in her account.
- Alice wants to remove her allowance of 100 tokens on an
ICRC-2
ledger for canister C. - Alice calls
icrc2_approve
on the ledger withspender
set to the canister's default account ({ owner = C; subaccount = null }
) andamount
set to 0. - The canister can no longer transfer tokens on Alice's behalf.
- Alice wants to remove her allowance of 100 tokens on an
ICRC-2
ledger for canister C. - Alice calls
icrc2_approve
on the ledger withspender
set to the canister's default account ({ owner = C; subaccount = null }
),amount
set to 0, andexpected_allowance
set to 100 tokens. - If the call succeeds, the allowance got removed successfully.
An
AllowanceChanged
error would indicate that canister C used some of the allowance before Alice's call completed.