Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICRC-2: Approve and Transfer From #12

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

receronp
Copy link

This PR adds the ICRC-2 Standard Approve and Transfer From methods (icrc2_approve, icrc2_transfer_from, icrc2_allowance) as specified in the DFINITY ICRC-2 Standard Methods. Methods from ICRC-1 are supported by reusing code from the ICRC-1 codebase, with icrc1_supported_standards modified to report name = "ICRC-2" and corresponding url.
Actor tests for the new standard methods have also been created, covering ApproveError and TransferFromError cases, as well as the examples defined in the DFINITY ICRC-2 Standard Examples.

@receronp
Copy link
Author

Closing this PR in favor of #18

@receronp receronp closed this May 29, 2023
@tomijaga tomijaga reopened this Jun 6, 2023
@tomijaga
Copy link
Member

tomijaga commented Jun 6, 2023

Hey @receronp,
I'd like to review the ICRC-2 before moving over to the ICRC-3 implementation.

@receronp
Copy link
Author

Hi @tomijaga,
Sounds good, let me know if you have any suggestions.

@tomijaga tomijaga requested a review from skilesare July 1, 2023 21:08
@tomijaga
Copy link
Member

tomijaga commented Jul 2, 2023

Hey @receronp,
I've completed the review and added a few more comments.

Your implementation looks really good and it accurately implements the ICRC-2 standard.
However, there are some updates needed for storing permitted transfers and approvals.
Currently, permitted transfers are stored as icrc1 transfers, which means the spender field is not included, and successful approval requests are not being stored. I have provided the necessary changes to the Transaction type in a review comment so that approved requests can be stored.

Here's a link to the reference implementation for both the icrc1 and icrc2 standards. Let me know if you have any questions

@receronp
Copy link
Author

receronp commented Jul 6, 2023

Hi @tomijaga,
I've looked at the provided reference implementation and am working on the updates for the Transaction type to enable the spender field and successful approvals. I however cannot seem to find the review comments that you mention.
Could you share those once more?
Thanks for the help :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Archive test doesn't need to be duplicated for ICRC-2 since it wasn't updated in the implementation, and no new test cases were added.

Comment on lines +72 to +79
// TODO: Verify if approval fee should be validated as a transfer fee.
if (not Transfer.validate_fee(token, app_req.fee)) {
return #err(
#BadFee {
expected_fee = token._fee;
}
);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the approval fee, we can add a specific field in the TokenData that the user can set when the ICRC2.init() function is called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the ICRC-2 standard again and it doesn't have a icrc2_approval_fee function. Adding a new fee might deviate from the standard so let's stick with your original decision to use the transfer fee.

Comment on lines +75 to +153
public func init(args : T.InitArgs) : T.TokenData {
let {
name;
symbol;
decimals;
fee;
minting_account;
max_supply;
initial_balances;
min_burn_amount;
advanced_settings;
} = args;

var _burned_tokens = 0;
var permitted_drift = 60_000_000_000;
var transaction_window = 86_400_000_000_000;

switch (advanced_settings) {
case (?options) {
_burned_tokens := options.burned_tokens;
permitted_drift := Nat64.toNat(options.permitted_drift);
transaction_window := Nat64.toNat(options.transaction_window);
};
case (null) {};
};

if (not Account.validate(minting_account)) {
Debug.trap("minting_account is invalid");
};

let accounts : T.AccountBalances = StableTrieMap.new();
let approvals : T.ApprovalAllowances = StableTrieMap.new();

var _minted_tokens = _burned_tokens;

for ((i, (account, balance)) in Itertools.enumerate(initial_balances.vals())) {

if (not Account.validate(account)) {
Debug.trap(
"Invalid Account: Account at index " # debug_show i # " is invalid in 'initial_balances'"
);
};

let encoded_account = Account.encode(account);

StableTrieMap.put(
accounts,
Blob.equal,
Blob.hash,
encoded_account,
balance,
);

_minted_tokens += balance;
};

{
name = name;
symbol = symbol;
decimals;
var _fee = fee;
max_supply;
var _minted_tokens = _minted_tokens;
var _burned_tokens = _burned_tokens;
min_burn_amount;
minting_account;
accounts;
approvals;
metadata = Utils.init_metadata(args);
supported_standards = Utils.init_standards();
transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER);
permitted_drift;
transaction_window;
archive = {
var canister = actor ("aaaaa-aa");
var stored_txs = 0;
};
};
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can reduce the code here by calling the init function for ICRC1 and updating the result as needed

Suggested change
public func init(args : T.InitArgs) : T.TokenData {
let {
name;
symbol;
decimals;
fee;
minting_account;
max_supply;
initial_balances;
min_burn_amount;
advanced_settings;
} = args;
var _burned_tokens = 0;
var permitted_drift = 60_000_000_000;
var transaction_window = 86_400_000_000_000;
switch (advanced_settings) {
case (?options) {
_burned_tokens := options.burned_tokens;
permitted_drift := Nat64.toNat(options.permitted_drift);
transaction_window := Nat64.toNat(options.transaction_window);
};
case (null) {};
};
if (not Account.validate(minting_account)) {
Debug.trap("minting_account is invalid");
};
let accounts : T.AccountBalances = StableTrieMap.new();
let approvals : T.ApprovalAllowances = StableTrieMap.new();
var _minted_tokens = _burned_tokens;
for ((i, (account, balance)) in Itertools.enumerate(initial_balances.vals())) {
if (not Account.validate(account)) {
Debug.trap(
"Invalid Account: Account at index " # debug_show i # " is invalid in 'initial_balances'"
);
};
let encoded_account = Account.encode(account);
StableTrieMap.put(
accounts,
Blob.equal,
Blob.hash,
encoded_account,
balance,
);
_minted_tokens += balance;
};
{
name = name;
symbol = symbol;
decimals;
var _fee = fee;
max_supply;
var _minted_tokens = _minted_tokens;
var _burned_tokens = _burned_tokens;
min_burn_amount;
minting_account;
accounts;
approvals;
metadata = Utils.init_metadata(args);
supported_standards = Utils.init_standards();
transactions = SB.initPresized(MAX_TRANSACTIONS_IN_LEDGER);
permitted_drift;
transaction_window;
archive = {
var canister = actor ("aaaaa-aa");
var stored_txs = 0;
};
};
};
public func init(args : T.InitArgs) : T.TokenData {
let icrc1_token = ICRC1.init(args);
let approvals : T.ApprovalAllowances = StableTrieMap.new();
return { icrc1_token with approvals }
};

Comment on lines +62 to +70
// TODO: Verify if approval memo should be validated for approvals.
if (not Transfer.validate_memo(app_req.memo)) {
return #err(
#GenericError({
error_code = 0;
message = "Memo must not be more than 32 bytes";
})
);
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yh, validating the memo here is fine. Ideally, an approval request should function under the same conditions as a transfer request.

};

/// Checks if an approve request is valid
public func validate_request(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can update this to prevent the minting account from making approval requests or being included as the spender in approval requests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After giving it more thought, I think it would be best to leave this decision to the token owner. I currently can't think of a use case where the minting_account permits other accounts to mint tokens but some may come up in the future.


public type TransferArgs = Types1.TransferArgs;

public type Transfer = Types1.Transfer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update the Transfer type so that it stores the additional spender field from a transfer_from request. I think the spender field should be optional to differentiate between normal and permitted transfers.

/// Internal representation of a transaction request
public type TransactionRequest = Types1.TransactionRequest;

public type Transaction = Types1.Transaction;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's update the Transaction type with an approve field so the canister can store successful approval requests

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public type Transaction = Types1.Transaction and {
    approve : ?Approve
};

public type Approve = {
    from : Account;
    spender : Account;
    amount : Balance;
    expected_allowance : ?Nat;
    expires_at : ?Nat64;
    fee : ?Balance;
    memo : ?Memo;
    created_at_time : ?Nat64;
};

@tomijaga
Copy link
Member

tomijaga commented Jul 8, 2023

Hey @receronp,
My bad, I forgot to publish the review.
I've published it now so you should be able to see the comments

@skilesare
Copy link
Collaborator

@raceronp. Have you been able to address these issue? If so, I'll suggest that the bounty be awarded to you for the ICRC2 part and we can split out ICRC3...they have just released some updates to the format for ICRC3 last week so there may need to be some adjustments.

@receronp
Copy link
Author

@skilesare I am still working on the review provided by @tomijaga. Expect to have this completed by the end of the week.
I am however still not assigned to the bounty, would that be required before completion and award?

@skilesare
Copy link
Collaborator

I'll be reassigning it to you

@tomijaga
Copy link
Member

tomijaga commented Aug 2, 2023

Hey @receronp,
Have you made progress on the discussed changes?

Let me know if I can help with anything that's unclear.

@receronp
Copy link
Author

receronp commented Aug 3, 2023

Sorry guys, been doing changes in my local repo, and because some of them are breaking changes I've not pushed them here.
I expect to have a working solution in the coming week.

@sardariuss
Copy link

sardariuss commented Aug 25, 2023

Just came to say that there is a spender_subaccount field in the TransferFromArgs type that is not present in the ICRC2 standard and seems to not be used in the code.

@tomijaga
Copy link
Member

Hey @receronp,
I wanted to check in and see if you had a chance to make the changes for the implementation yet.
If you haven't, please push any changes you have to the repo. I'll take a look and finish up the rest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants