-
Notifications
You must be signed in to change notification settings - Fork 23
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
base: main
Are you sure you want to change the base?
Conversation
Closing this PR in favor of #18 |
Hey @receronp, |
Hi @tomijaga, |
Hey @receronp, Your implementation looks really good and it accurately implements the Here's a link to the reference implementation for both the icrc1 and icrc2 standards. Let me know if you have any questions |
Hi @tomijaga, |
tests/ICRC2/Archive.ActorTest.mo
Outdated
There was a problem hiding this comment.
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.
// 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; | ||
} | ||
); | ||
}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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; | ||
}; | ||
}; | ||
}; |
There was a problem hiding this comment.
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
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 } | |
}; |
// 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"; | ||
}) | ||
); | ||
}; |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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;
};
Hey @receronp, |
@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. |
@skilesare I am still working on the review provided by @tomijaga. Expect to have this completed by the end of the week. |
I'll be reassigning it to you |
Hey @receronp, Let me know if I can help with anything that's unclear. |
Sorry guys, been doing changes in my local repo, and because some of them are breaking changes I've not pushed them here. |
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. |
Hey @receronp, |
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.