Can someone help checking my smart contract?

import Trie "mo:base/Trie";
import Principal "mo:base/Principal";
import Nat "mo:base/Nat";
import Option "mo:base/Option";
import Bool "mo:base/Bool";
import Error "mo:base/Error";
import Float "mo:base/Float";
import Int "mo:base/Int";


actor AppCoin {
  type Trie<K, V> = Trie.Trie<K, V>;
  type Key<K> = Trie.Key<K>;

  stable var balances: Trie<Principal, Nat> = Trie.empty();
  stable var allowances: Trie<Principal, Trie<Principal, Nat>> = Trie.empty();
  stable var minter: Principal = Principal.fromText("vnu7n-jfcje-r3hyn-e4trh-3elfl-quzyo-fzhbo-tlly7-mp65l-3tca4-uae");
  
  stable var distributeWallet: Principal = Principal.fromText("czt32-ay6h4-56cvi-m5vl3-oigsd-zwseg-byrb3-bxeg4-bfz5w-dyigi-wae");
  stable var owner: Principal = Principal.fromText("atewe-etgil-mre73-twlmx-z2o2f-goui6-glmiq-qulqx-osud5-xdxsn-aqe");  
  stable var admin: Principal = Principal.fromText("atewe-etgil-mre73-twlmx-z2o2f-goui6-glmiq-qulqx-osud5-xdxsn-aqe");  // Admin is owner for now

  stable var consumerCanister: Principal = Principal.fromText("zrakb-eaaaa-aaaab-qacaq-cai");  

  

  let DECIMALS: Nat = 9;                             // 0.000000001
  let UNIT: Nat = Nat.pow(10, DECIMALS);             // 10^9  
  let MAX_SUPPLY: Nat = 1_000_000_000 * UNIT;        // 1 billion tokens
  let MIN_BALANCE_TODISTRIBUTE: Nat = 10000 * UNIT;
  
  // Test
  // let DECIMALS: Nat = 3;                        
  // let UNIT: Nat = Nat.pow(10, DECIMALS);        
  // let MAX_SUPPLY: Nat = 100_000 * UNIT;  
  // let MIN_BALANCE_TODISTRIBUTE: Nat = 5000 * UNIT;  


  func key(p: Principal): Key<Principal> {
    {hash = Principal.hash p; key = p}
  };

  //------------------------------------------------------------------------------------------//
  // Minter
  //------------------------------------------------------------------------------------------//
  public shared ({caller}) func setMinter(newMinter: Principal): async () {
    if (Principal.notEqual(caller, minter) and Principal.notEqual(caller, owner) ) {
      throw Error.reject("Only the current minter or owner can set a new minter.");
    };
    // Move the assets to the new minter
    let balance = Option.get(Trie.get(balances, key(minter), Principal.equal), 0);     
    if (balance > 0) {
      await transferInternal(minter, newMinter, balance);
    }; 
    minter := newMinter;
  };
  
  // Minter, repeated code
  public shared ({caller}) func setOwner(newOwner: Principal): async () {
    if ( Principal.notEqual(caller, owner)) {
      throw Error.reject("Only the current owner can set a new owner.");
    };
    // Move the assets to the new minter
    let balance = Option.get(Trie.get(balances, key(owner), Principal.equal), 0);     
    if (balance > 0) {
      await transferInternal(owner, newOwner, balance);
    }; 
    owner := newOwner;
  };

   public shared ({caller}) func mint(amount: Nat): async () {    
    if (Principal.notEqual( caller,  minter) and Principal.notEqual( caller,  owner)) {
      throw Error.reject("Caller: " # Principal.toText(caller) # " is not a minter or owner");
    };
    let balance = Option.get(Trie.get(balances, key(minter), Principal.equal), 0);    
    let  sumTotalBalanc: Nat = await totalSupply();
    if (amount + sumTotalBalanc > MAX_SUPPLY) {
      throw Error.reject("Cannot mint more than Max supply: " # Nat.toText(MAX_SUPPLY / UNIT));
    };
    balances := Trie.put(balances, key(minter), Principal.equal, amount + balance).0;
  };
  
  public shared ({caller}) func burn(burnFrom: Principal, amount: Nat): async () {
	// Explicitly ask for the burnFrom which is the same as caller to avoid accedently burn coins
	if(caller != burnFrom) {
		throw Error.reject("You can only burn from your own wallet");
	};
	await transferInternal(burnFrom, Principal.fromText("aaaaa-aa"), amount);
  };

  public query func getCurrentMinter(): async Text {
    return Principal.toText(minter);
  };

  public query func getCurrentOwner(): async Text {
    return Principal.toText(owner);
  };

  //------------------------------------------------------------------------------------------//
  // Balances and Token share distribution
  //------------------------------------------------------------------------------------------//
   public shared func totalSupply() : async Nat {
    var sumTotalNat: Nat = 0;
    let iter = Trie.iter(balances);
    for ((k, v) in iter) {
      sumTotalNat := sumTotalNat+v; 
    }; 
    return sumTotalNat;
  };
  
  public shared func supplyUnminted(): async Nat {
    return  MAX_SUPPLY - (await totalSupply()); 
  };

  public query func maxSupply(): async Nat { return  MAX_SUPPLY; };
  public query func balanceOfOwner(): async ?Nat {
      return Trie.find<Principal, Nat>(balances, key(owner), Principal.equal);
  };
  public query func balanceOfMinter(): async ?Nat { 
    return Trie.find<Principal, Nat>(balances, key(minter), Principal.equal);
  };
  public query func balanceOfDistributable(): async ?Nat {
      return Trie.find<Principal, Nat>(balances, key(distributeWallet), Principal.equal);
  };

  // Anyone can make distribute any time to maintain transparency
  public shared  func distribute() : async () {    
    let balance = await balanceOf(distributeWallet);
    if (balance < MIN_BALANCE_TODISTRIBUTE) {
      throw Error.reject("Cannot distribute from " # Principal.toText(distributeWallet) # " yet, please try later current amount: " # Nat.toText(balance / UNIT) # ". Distributable amount is " # Nat.toText(MIN_BALANCE_TODISTRIBUTE / UNIT) # " tokens at minimum");
    };
    let  sumTotalNat: Nat = (await totalSupply()) - balance; // Remove the balance of dist wallet from calculation    
    var currentFactor: Float = 0.0;
    var addedBalance: Float = 0;
    var newCalculatedBalanceI: Int = 0;  
    var newCalculatedBalanceN: Nat = 0;
    let iter = Trie.iter(balances);
    for ((k, v) in iter) {       
      if (Principal.notEqual(k, distributeWallet) and v > 0) {        
        currentFactor := Float.fromInt(v) / Float.fromInt(sumTotalNat);
        addedBalance := currentFactor * Float.fromInt(balance);
        newCalculatedBalanceI :=  Float.toInt(addedBalance);  
        newCalculatedBalanceN := Option.get(Nat.fromText (Int.toText(newCalculatedBalanceI)), 0);
        await transferInternal(distributeWallet, k, newCalculatedBalanceN);
      };
    };
    return;
  };

  //------------------------------------------------------------------------------------------//
  // Adminitration called by consumer of this token, or any one who want to use
  //------------------------------------------------------------------------------------------//
  public shared ({caller}) func approve(spender: Principal, maxAmount: Nat): async Bool {
    var ownerAllowances = Option.get(Trie.get(allowances, key(caller), Principal.equal), Trie.empty());
    let updatedAllowances = Trie.put(ownerAllowances, key(spender), Principal.equal, maxAmount).0;
    allowances := Trie.put(allowances, key(caller), Principal.equal, updatedAllowances).0;
    return true;
  };

  public shared ({caller}) func revoke(spender: Principal): async Bool {
    var ownerAllowances = Option.get(Trie.get(allowances, key(caller), Principal.equal), Trie.empty());
    let updatedAllowances = Trie.remove(ownerAllowances, key(spender), Principal.equal).0; 
    allowances := Trie.put(allowances, key(caller), Principal.equal, updatedAllowances).0;
    return true;
  };

  // Only Payment canister can call this function
  public shared ({caller}) func useCredit(payer: Principal, amount: Nat, seller: Principal): async Text {
    if (caller != consumerCanister) {
      throw Error.reject("Not allowed to call. Administration function cannot be called by end user");
    };
    if (amount == 0) {
      throw Error.reject("Amount cannot be zero");
    };
    //transferInternal(from: Principal, to: Principal, amount: Nat)
    
    // Make the calculation
    let toAdminFactor: Float = 0.4;
    let toSellerFactor: Float = 0.3;
    let toOwnerFactor: Float = 0.01;
    var toStakeHoldersFact: Float = 0.29;
    
    if (seller == owner or Principal.isAnonymous(seller)) {
      toStakeHoldersFact := toStakeHoldersFact + toSellerFactor;
    };

    let toAdmin: Nat = Option.get(Nat.fromText (Int.toText( Float.toInt( toAdminFactor * Float.fromInt(amount)))), 0);
    let toSeller: Nat = Option.get(Nat.fromText (Int.toText( Float.toInt( toSellerFactor * Float.fromInt(amount)))), 0);
    let toOwner: Nat = Option.get(Nat.fromText (Int.toText( Float.toInt( toOwnerFactor * Float.fromInt(amount)))), 0);
    var toStakeHolders: Nat = Option.get(Nat.fromText (Int.toText( Float.toInt( toStakeHoldersFact * Float.fromInt(amount)))), 0);

    // Performance issue
    await transferInternal(payer, admin, toAdmin);  
    await transferInternal(payer, seller, toSeller);  
    await transferInternal(payer, owner, toOwner);  
    await transferInternal(payer, distributeWallet, toStakeHolders);  

    return "OK";

  };

  //------------------------------------------------------------------------------------------//
  // Token core 
  //------------------------------------------------------------------------------------------//
  public shared ({caller}) func transfer(to: Principal, amount: Nat): async () {
    return await transferInternal(caller, to, amount);
  };

  public shared ({caller}) func transferFrom(from: Principal, to: Principal, amount: Nat): async () {
    let ownerAllowances = Option.get(Trie.get(allowances, key(from), Principal.equal), Trie.empty());
    let allowanceAmount = Option.get(Trie.get(ownerAllowances, key(caller), Principal.equal), 0);

    if (allowanceAmount == 0 or amount > allowanceAmount) {
      throw Error.reject("Insufficient allowance: " # Nat.toText(allowanceAmount));
    };
    return await transferInternal(from, to, amount);
  };

  private func transferInternal(from: Principal, to: Principal, amount: Nat): async () {

    let balance = Option.get(Trie.get(balances, key(from), Principal.equal), 0);
    let balanceTo = Option.get(Trie.get(balances, key(to), Principal.equal), 0);

    var newBalanceDecrease = 0;    
    if (amount<=balance) {
      newBalanceDecrease := balance - amount;
    } else {
      throw Error.reject("Withdraw amount :" # Nat.toText(amount) # "cannot be more than the balance: " # Nat.toText(balance));
    };    
    let newBalanceIncrease = balanceTo + amount; // Increase for receiver
    balances := Trie.put(balances, key(to), Principal.equal, newBalanceIncrease).0;
    balances := Trie.put(balances, key(from), Principal.equal, newBalanceDecrease).0;
  };

  public query func balanceOf(principal: Principal): async Nat {
    return Option.get(Trie.get(balances, key(principal), Principal.equal), 0);
  };

  public query ({caller}) func getAllowedSpenders(): async Trie<Principal, Nat> {
    let ownerAllowances = Option.get(Trie.get(allowances, key(caller), Principal.equal), Trie.empty());
    return ownerAllowances;
  };

  //------------------------------------------------------------------------------------------//
  // Misc
  //------------------------------------------------------------------------------------------//
  public query ({caller}) func whoAmI(): async Text {
    return Principal.toText(caller);
  };


  //------------------------------------------------------------------------------------------//
  // --> Test , Do not deploy
  //------------------------------------------------------------------------------------------//
 public shared  func test_show_distribute_calculation() : async Text {       
    let balance = await balanceOf(distributeWallet);
    if (balance < MIN_BALANCE_TODISTRIBUTE) {
      throw Error.reject("Cannot distribute from " # Principal.toText(distributeWallet) # " yet, please try later current amount: " # Nat.toText(balance / UNIT) # ". Distributable amount is " # Nat.toText(MIN_BALANCE_TODISTRIBUTE / UNIT) # " tokens at minimum");
    };

    let  sumTotalNat: Nat = (await totalSupply()) - balance;
    
    var trace_distribute: Text = " {\"total_dist_balance\": " #  Nat.toText(balance);
    trace_distribute := trace_distribute # ", \"total_sub_balance\": " # Nat.toText(sumTotalNat) # ", \"amounts\": [";

    var currentFactor: Float = 0.0;
    var addedBalance: Float = 0;
    var newCalculatedBalanceI: Int = 0;  
    var newCalculatedBalanceN: Nat = 0;

    let iter2 = Trie.iter(balances);
    for ((k, v) in iter2) { 
      if (k != distributeWallet and v > 0) {
        trace_distribute := trace_distribute # " {\"key\": \"" # Principal.toText(k) # "\", ";
        trace_distribute := trace_distribute # "\"value\": " # Nat.toText(v) # ", ";
        currentFactor := Float.fromInt(v) / Float.fromInt(sumTotalNat);
        trace_distribute := trace_distribute # "\"factor\":" # Float.toText(currentFactor) # ", ";
        addedBalance := currentFactor * Float.fromInt(balance);
        trace_distribute := trace_distribute # "\"added_balance\":" # Float.toText(addedBalance)# ", ";
        newCalculatedBalanceI :=  Float.toInt(addedBalance) +  v;  
        newCalculatedBalanceN := Option.get(Nat.fromText (Int.toText(newCalculatedBalanceI)), 0);
        trace_distribute := trace_distribute # "\"assigned_balance\":" # Nat.toText(newCalculatedBalanceN) # " },";
      };
    };
    trace_distribute := trace_distribute # "]}" ;
    return trace_distribute;
  };


}

Checking for what?
You might want to just link this file or put it in a code block. Hard to read

Thanks for the answer. I fixed in a code block.
I need to se if there is any vulnerability or wrong calculation any where, you can choose any place :slight_smile: It is under development.
Again thanks for helping!

I would that dropping your code into the forums and asking for an audit/review might not be the most successful approach, but I’ll look over it a bit

Couple things
I don’t think transferInternal needs to be async, so maybe try to remove that. You’ll have a slow down but more importantly,if you use async, you don’t auto rollback. So if you get through some but not all, you’re stuck in a weird state

Also i see a lot of throwing of errors. Might want to look into returning an #ok/#err with Result.Result<TOk, TErr>. Will allow the ability for the consumer of the api to programmatically handle errors vs throwing and error message

Thanks alot for your help, really appreciated. Some notes about your comments.
1 - About the awaitable transferinternal, It is intended from me that when the call fails it should continue without recovery, because it would not effect the total supply, and when retried again it would correlate if needed. To make it as a database transaction alike way would of course be a good practice, but you do not know the error, so recovery would not help, it will fail in the same issue again.
2- About throwing errors, it is also intentional, because I want the process to trap.

I have backend canisters and most of them returns Result<Object, Text>, and that is because I want the end-user to be informed without crasching the frontend.

Again, thanks alot for your answer

1 Like

I have to admit here, I may totally missed it about the token :rofl: . My aim was to create a canister which act as a token:ish which ease the functionality of the main application by consuming tokens. Now I found a video in ICDevs which explains exactly how to create a token ( real one :grinning: ) but I still need answer for the simple question below:
Can I add some of the functionalities I have already developed in my contract above.
I just want to remove complications from the users of the application, by just buying the token and start using the application, and those who have the token would gain some profit.