Skip to content
Open
4 changes: 4 additions & 0 deletions dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"type": "motoko",
"main": "src/ICRC1/Canisters/Token.mo"
},
"icrc2": {
"type": "motoko",
"main": "src/ICRC2/Canisters/Token.mo"
},
"test": {
"type": "motoko",
"main": "tests/ActorTest.mo",
Expand Down
12 changes: 2 additions & 10 deletions example/dfx.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
{
"version": 1,
"dfx": "0.11.2",
"canisters": {
"icrc1_example": {
"type": "motoko",
"main": "src/icrc1/main.mo"
"main": "icrc1/main.mo"
}
},
"defaults": {
"build": {
"packtool": "",
"packtool": "mops sources",
"args": ""
}
},

"networks": {
"local": {
"bind": "127.0.0.1:8000",
"type": "ephemeral"
}
}
}
8 changes: 4 additions & 4 deletions example/icrc1/main.mo
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ shared ({ caller = _owner }) actor class Token(
};

public shared ({ caller }) func icrc1_transfer(args : ICRC1.TransferArgs) : async ICRC1.TransferResult {
await ICRC1.transfer(token, args, caller);
await* ICRC1.transfer(token, args, caller);
};

public shared ({ caller }) func mint(args : ICRC1.Mint) : async ICRC1.TransferResult {
await ICRC1.mint(token, args, caller);
await* ICRC1.mint(token, args, caller);
};

public shared ({ caller }) func burn(args : ICRC1.BurnArgs) : async ICRC1.TransferResult {
await ICRC1.burn(token, args, caller);
await* ICRC1.burn(token, args, caller);
};

// Functions from the rosetta icrc1 ledger
Expand All @@ -77,7 +77,7 @@ shared ({ caller = _owner }) actor class Token(

// Additional functions not included in the ICRC1 standard
public shared func get_transaction(i : ICRC1.TxIndex) : async ?ICRC1.Transaction {
await ICRC1.get_transaction(token, i);
await* ICRC1.get_transaction(token, i);
};

// Deposit cycles into this archive canister.
Expand Down
3 changes: 3 additions & 0 deletions example/mops.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[dependencies]
base = "0.8.7"
icrc1 = "0.0.1"
30 changes: 15 additions & 15 deletions icrc1-default-args.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
( record {
name = "<Insert Token Name>";
symbol = "<Insert Symbol>";
decimals = 6;
fee = 1_000_000;
max_supply = 1_000_000_000_000;
initial_balances = vec {
record {
record {
owner = principal "r7inp-6aaaa-aaaaa-aaabq-cai";
subaccount = null;
};
100_000_000
}
};
( record {
name = "<Insert Token Name>";
symbol = "<Insert Symbol>";
decimals = 6;
fee = 1_000_000;
max_supply = 1_000_000_000_000;
initial_balances = vec {
record {
record {
owner = principal "r7inp-6aaaa-aaaaa-aaabq-cai";
subaccount = null;
};
100_000_000
}
};
min_burn_amount = 10_000;
minting_account = null;
advanced_settings = null;
Expand Down
1 change: 0 additions & 1 deletion src/ICRC1/Canisters/Token.mo
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ExperimentalCycles "mo:base/ExperimentalCycles";
import SB "mo:StableBuffer/StableBuffer";

import ICRC1 "..";
import Archive "Archive";

shared ({ caller = _owner }) actor class Token(
init_args : ICRC1.TokenInitArgs,
Expand Down
7 changes: 5 additions & 2 deletions src/ICRC1/Types.mo
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,17 @@ module {
#CreatedInFuture : { ledger_time : Timestamp };
};

public type TransferError = TimeError or {
public type OperationError = TimeError or {
#BadFee : { expected_fee : Balance };
#BadBurn : { min_burn_amount : Balance };
#InsufficientFunds : { balance : Balance };
#Duplicate : { duplicate_of : TxIndex };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};

public type TransferError = OperationError or {
#BadBurn : { min_burn_amount : Balance };
};

public type TransferResult = {
#Ok : TxIndex;
Expand Down
2 changes: 1 addition & 1 deletion src/ICRC1/Utils.mo
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module {

public let default_standard : T.SupportedStandard = {
name = "ICRC-1";
url = "https://github.com/dfinity/ICRC-1";
url = "https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-1";
};

// Creates a Stable Buffer with the default supported standards and returns it.
Expand Down
162 changes: 162 additions & 0 deletions src/ICRC2/Approve.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import Blob "mo:base/Blob";
import Debug "mo:base/Debug";
import Int "mo:base/Int";
import Nat "mo:base/Nat";
import Nat64 "mo:base/Nat64";
import Option "mo:base/Option";
import Result "mo:base/Result";
import Time "mo:base/Time";

import STMap "mo:StableTrieMap";

import T "Types";
import Utils "Utils";
import Account "../ICRC1/Account";
import Transfer "../ICRC1/Transfer";

module {

// Checks if an approval expiration is greater than the current ledger time
public func validate_expiration(token : T.TokenData, expires_at : ?Nat64) : Bool {
switch (expires_at) {
case null { return true };
case (?expiration) {
return Transfer.is_in_future(token, expiration);
};
};
};

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

Copy link
Copy Markdown
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
Copy Markdown
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.

token : T.TokenData,
app_req : T.ApproveRequest,
) : Result.Result<(), T.ApproveError> {

if (app_req.from.owner == app_req.spender.owner) {
return #err(
#GenericError({
error_code = 0;
message = "The spender account owner cannot be equal to the source account owner.";
})
);
};

if (not Account.validate(app_req.from)) {
return #err(
#GenericError({
error_code = 0;
message = "Invalid account entered for approval source. " # debug_show (app_req.from);
})
);
};

if (not Account.validate(app_req.spender)) {
return #err(
#GenericError({
error_code = 0;
message = "Invalid account entered for approval spender. " # debug_show (app_req.spender);
})
);
};

// 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";
})
);
};
Comment on lines +62 to +70

Copy link
Copy Markdown
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.


// 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;
}
);
};
Comment on lines +72 to +79

Copy link
Copy Markdown
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
Copy Markdown
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.


let balance : T.Balance = Utils.get_balance(
token.accounts,
app_req.encoded.from,
);

// If no approval fee provided, validates against transaction fee.
if(Option.get(app_req.fee, token._fee) > balance){
return #err(#InsufficientFunds { balance });
};

// Validates that the approval contains the expected allowance
switch (app_req.expected_allowance) {
case null {};
case (?expected) {
let allowance_record = Utils.get_allowance(token.approvals, app_req.encoded);
if (expected != allowance_record.allowance) {
return #err(
#AllowanceChanged {
current_allowance = allowance_record.allowance;
}
);
};
};
};

if (not validate_expiration(token, app_req.expires_at)) {
return #err(
#Expired {
ledger_time = Nat64.fromNat(Int.abs(Time.now()));
}
);
};

switch (app_req.created_at_time) {
case (null) {};
case (?created_at_time) {

if (Transfer.is_too_old(token, created_at_time)) {
return #err(#TooOld);
};

if (Transfer.is_in_future(token, created_at_time)) {
return #err(
#CreatedInFuture {
ledger_time = Nat64.fromNat(Int.abs(Time.now()));
}
);
};
};
};

#ok();
};

/// Writes/overwrites the allowance of an approval
public func write_approval(
token : T.TokenData,
app_req : T.WriteApproveRequest,
) : async* T.ApproveResult {

let { amount = allowance; expires_at; encoded } = app_req;
let prev_allowance = Utils.get_allowance(token.approvals, encoded);
let new_allowance : T.Allowance = { allowance; expires_at };

if (new_allowance != prev_allowance) {
let accont_approvals = Utils.get_account_approvals(token.approvals, encoded.from);
switch (accont_approvals) {
case (?approvals) {
let spenders = STMap.get(approvals, Blob.equal, Blob.hash, encoded.spender);
STMap.put(approvals, Blob.equal, Blob.hash, encoded.spender, new_allowance);
};
case null {
let approvals : T.Approvals = STMap.new();
STMap.put(approvals, Blob.equal, Blob.hash, encoded.spender, new_allowance);
STMap.put(token.approvals, Blob.equal, Blob.hash, encoded.from, approvals);
};
};
};

#Ok(allowance);
};
};
Loading