From 9794bc6833fe7952832968a1cad77ba593827706 Mon Sep 17 00:00:00 2001 From: patnorris Date: Thu, 9 Apr 2026 11:31:40 +0200 Subject: [PATCH 1/5] Add functionality to use cycles balance instead of CMC for topups --- src/GameState/src/Main.mo | 138 ++++++++++++++++++++++++++++++++++++-- src/common/Types.mo | 5 ++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/src/GameState/src/Main.mo b/src/GameState/src/Main.mo index 2ba68d3..148e69e 100644 --- a/src/GameState/src/Main.mo +++ b/src/GameState/src/Main.mo @@ -330,6 +330,31 @@ persistent actor class GameStateCanister() = this { return #Ok({ flag = DISBURSE_FUNDS_TO_TREASURY }); }; + // Flag to disable disbursements to team + var DISBURSE_FUNDS_TO_TEAM : Bool = false; + + public shared (msg) func toggleDisburseFundsToTeamFlagAdmin() : async Types.AuthRecordResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + DISBURSE_FUNDS_TO_TEAM := not DISBURSE_FUNDS_TO_TEAM; + let authRecord = { auth = "You set the flag to " # debug_show(DISBURSE_FUNDS_TO_TEAM) }; + return #Ok(authRecord); + }; + + public query (msg) func getDisburseFundsToTeamFlag() : async Types.FlagResult { + if (Principal.isAnonymous(msg.caller)) { + return #Err(#Unauthorized); + }; + if (not Principal.isController(msg.caller)) { + return #Err(#Unauthorized); + }; + return #Ok({ flag = DISBURSE_FUNDS_TO_TEAM }); + }; + // Threshold of minimum ICP balance Game State should keep var MINIMUM_ICP_BALANCE : Nat = 30; // in full ICP @@ -441,7 +466,7 @@ persistent actor class GameStateCanister() = this { let transferResult : Types.IcpTransferResult = await transfer(transferArgs); D.print("GameState: disburseIncomingFundsToTreasury - transferResult: " # debug_show(transferResult)); - // check if the transfer was successfull + // Check if the transfer was successful switch (transferResult) { case (#Err(transferError)) { D.print("GameState: disburseIncomingFundsToTreasury - transferError: " # debug_show(transferError)); @@ -468,6 +493,45 @@ persistent actor class GameStateCanister() = this { }; }; + private func disburseIncomingFundsToTeam(amountToDisburse : Nat) : async Types.AuthRecordResult { + D.print("GameState: disburseIncomingFundsToTeam - DISBURSE_FUNDS_TO_TEAM: " # debug_show(DISBURSE_FUNDS_TO_TEAM)); + if (not DISBURSE_FUNDS_TO_TEAM) { + return #Err(#Unauthorized); + }; + D.print("GameState: disburseIncomingFundsToTeam - amountToDisburse: " # debug_show(amountToDisburse)); + // amountToDisburse is in e8s + let E8S_PER_ICP : Nat = 100_000_000; // 10^8 e8s per ICP + if (amountToDisburse > 100 * E8S_PER_ICP) { + // Block big disbursements as a security measurement + return #Err(#Unauthorized); + }; + + let amountForTransfer : TokenLedger.Tokens = { e8s : Nat64 = Nat64.fromNat(amountToDisburse); }; + D.print("GameState: disburseIncomingFundsToTeam - amountForTransfer: " # debug_show(amountForTransfer)); + let TEAM_WALLET_ADDRESS : Blob = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783"; + let transferArgs : Types.IcpTransferArgsAccount = { + amount : TokenLedger.Tokens = amountForTransfer; + to = TEAM_WALLET_ADDRESS; + }; + D.print("GameState: disburseIncomingFundsToTeam - transferArgs: " # debug_show(transferArgs)); + + let transferResult : Types.IcpTransferResult = await transferToTeam(transferArgs); + D.print("GameState: disburseIncomingFundsToTeam - transferResult: " # debug_show(transferResult)); + + // Check if the transfer was successful + switch (transferResult) { + case (#Err(transferError)) { + D.print("GameState: disburseIncomingFundsToTeam - transferError: " # debug_show(transferError)); + return #Err(#Other("Couldn't transfer funds:\n" # debug_show (transferError))); + }; + case (#Ok(blockIndex)) { + D.print("GameState: disburseIncomingFundsToTeam - blockIndex: " # debug_show(blockIndex)); + let authRecord = { auth = "You disbursed ICP with this block index: " # debug_show (blockIndex) }; + return #Ok(authRecord); + }; + }; + }; + // ICP Ledger private func transfer(args : Types.IcpTransferArgs) : async Types.IcpTransferResult { D.print( @@ -498,7 +562,7 @@ persistent actor class GameStateCanister() = this { try { let transferResult : TokenLedger.Result = await ICP_LEDGER_ACTOR.icrc1_transfer(transferArg); - // check if the transfer was successfull + // Check if the transfer was successful switch (transferResult) { case (#Err(transferError)) { return #Err(#Other("Couldn't transfer funds:\n" # debug_show (transferError))); @@ -511,6 +575,40 @@ persistent actor class GameStateCanister() = this { }; }; + private func transferToTeam(args : Types.IcpTransferArgsAccount) : async Types.IcpTransferResult { + let TEAM_WALLET_ADDRESS : Blob = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783"; + D.print( + "Transferring " + # debug_show (args.amount) + # " tokens to TEAM_WALLET_ADDRESS " + # debug_show (TEAM_WALLET_ADDRESS) + ); + + let transferArgs : TokenLedger.TransferArgs = { + to : Blob = TEAM_WALLET_ADDRESS; + fee : TokenLedger.Tokens = { e8s : Nat64 = 10_000; }; + memo : Nat64 = 42; + from_subaccount : ?Blob = null; + created_at_time = null; + amount : TokenLedger.Tokens = args.amount; + }; + + try { + let transferResult : TokenLedger.Result_6 = await ICP_LEDGER_ACTOR.transfer(transferArgs); + + // Check if the transfer was successful + switch (transferResult) { + case (#Err(transferError)) { + return #Err(#Other("Couldn't transfer funds:\n" # debug_show (transferError))); + }; + case (#Ok(blockIndex)) { return #Ok(blockIndex) }; + }; + } catch (error : Error) { + // catch any errors that might occur during the transfer + return #Err(#Other("Reject message: " # Error.message(error))); + }; + }; + // Code Verification for all mAIner agents // Users should not be able to tamper with the mAIner code @@ -4717,8 +4815,16 @@ persistent actor class GameStateCanister() = this { }; }; case (#MainerTopUp(mainerCanisterAddress)) { - D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress): "# debug_show(mainerCanisterAddress)); - amountToConvert := amountForMainer; // Always convert mAIner's share of payment into cycles; TODO: rethink this (if cycle balance is high enough, can the existing cycles be used and thus no conversion) + D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress): "# debug_show(mainerCanisterAddress)); + D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress) PROTOCOL_CYCLES_BALANCE_BUFFER: "# debug_show(PROTOCOL_CYCLES_BALANCE_BUFFER)); + D.print("GameState: handleIncomingFunds - #MainerTopUp(mainerCanisterAddress) Cycles.balance(): "# debug_show(Cycles.balance())); + if (PROTOCOL_CYCLES_BALANCE_BUFFER > Cycles.balance()) { + // Cycles balance is lower than security threshold, so convert the payment's share for the mAIner to cycles + amountToConvert := amountForMainer; + } else { + // No need to convert to cycles as cycle balance is high enough + amountToConvert := 0; + }; }; case (_) { return #Err(#Other("Unsupported")); } }; @@ -4815,11 +4921,33 @@ persistent actor class GameStateCanister() = this { // Calculate cycles let cycles : Nat = (icpAmount * Nat64.toNat(xdrPermyriadPerIcp) * CYCLES_PER_XDR) / (10_000 * E8S_PER_ICP); // Where 10_000 is to convert from permyriad (1/10000 of a unit) D.print("GameState: handleIncomingFunds - no conversion necessary, cycles: "# debug_show(cycles)); + + // Bonus for mAIner as existing cycles balance can be used + let bonusCycles : Nat = cycles / 10; // 10% bonus for mAIner + D.print("GameState: handleIncomingFunds - no conversion necessary, bonusCycles: "# debug_show(bonusCycles)); - let cyclesForMainer : Nat = cycles; + let cyclesForMainer : Nat = cycles + bonusCycles; let cyclesForProtocol : Nat = 0; // Protocol already took its cut in ICP D.print("GameState: handleIncomingFunds - no conversion necessary, cyclesForMainer: "# debug_show(cyclesForMainer)); + + // Disburse incoming ICP to treasury if applicable + try { + if (DISBURSE_FUNDS_TO_TREASURY and amountToKeep > 0) { + ignore disburseIncomingFundsToTreasury(amountToKeep); + }; + } catch (error : Error) { + D.print("GameState: handleIncomingFunds - no conversion necessary, disburse to treasury error: "# Error.message(error)); + }; + // Disburse incoming ICP to team (instead of CMC as no conversion was needed) if applicable + try { + if (DISBURSE_FUNDS_TO_TEAM and amountForMainer > 0) { + ignore disburseIncomingFundsToTeam(amountForMainer); + }; + } catch (error : Error) { + D.print("GameState: handleIncomingFunds - no conversion necessary, disburse to team error: "# Error.message(error)); + }; + let response : Types.HandleIncomingFundsRecord = { cyclesForProtocol: Nat = cyclesForProtocol; cyclesForMainer : Nat = cyclesForMainer; diff --git a/src/common/Types.mo b/src/common/Types.mo index 538cb12..cefbe8d 100644 --- a/src/common/Types.mo +++ b/src/common/Types.mo @@ -561,6 +561,11 @@ module Types { toSubaccount : ?Blob; }; + public type IcpTransferArgsAccount = { + amount : TokenLedger.Tokens; + to : Blob; + }; + public type IcpTransferResult = Result; public type MainerCreationInput = PaymentTransactionBlockId and { From 1a7eebb69b3cd59f4ff035a3ca88d61e5b0141be Mon Sep 17 00:00:00 2001 From: patnorris Date: Thu, 23 Apr 2026 10:39:53 +0200 Subject: [PATCH 2/5] Update src/GameState/src/Main.mo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/GameState/src/Main.mo | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/GameState/src/Main.mo b/src/GameState/src/Main.mo index 148e69e..35fd4f8 100644 --- a/src/GameState/src/Main.mo +++ b/src/GameState/src/Main.mo @@ -584,8 +584,12 @@ persistent actor class GameStateCanister() = this { # debug_show (TEAM_WALLET_ADDRESS) ); + if (args.to != TEAM_WALLET_ADDRESS) { + return #Err(#Other("Invalid transfer destination for transferToTeam")); + }; + let transferArgs : TokenLedger.TransferArgs = { - to : Blob = TEAM_WALLET_ADDRESS; + to : Blob = args.to; fee : TokenLedger.Tokens = { e8s : Nat64 = 10_000; }; memo : Nat64 = 42; from_subaccount : ?Blob = null; From 2f28a5e039cbb5b229602339ca7e198f601f379d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:46:51 +0000 Subject: [PATCH 3/5] Fix team wallet account derivation for ICP transfers Agent-Logs-Url: https://github.com/onicai/PoAIW/sessions/b2ca583e-6016-4447-85f9-b88632c99db5 Co-authored-by: patnorris <23037714+patnorris@users.noreply.github.com> --- src/GameState/src/Main.mo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GameState/src/Main.mo b/src/GameState/src/Main.mo index 35fd4f8..d59b445 100644 --- a/src/GameState/src/Main.mo +++ b/src/GameState/src/Main.mo @@ -508,7 +508,6 @@ persistent actor class GameStateCanister() = this { let amountForTransfer : TokenLedger.Tokens = { e8s : Nat64 = Nat64.fromNat(amountToDisburse); }; D.print("GameState: disburseIncomingFundsToTeam - amountForTransfer: " # debug_show(amountForTransfer)); - let TEAM_WALLET_ADDRESS : Blob = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783"; let transferArgs : Types.IcpTransferArgsAccount = { amount : TokenLedger.Tokens = amountForTransfer; to = TEAM_WALLET_ADDRESS; @@ -576,7 +575,6 @@ persistent actor class GameStateCanister() = this { }; private func transferToTeam(args : Types.IcpTransferArgsAccount) : async Types.IcpTransferResult { - let TEAM_WALLET_ADDRESS : Blob = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783"; D.print( "Transferring " # debug_show (args.amount) @@ -4725,6 +4723,8 @@ persistent actor class GameStateCanister() = this { // Payment memo to specify in transaction to Protocol let MEMO_PAYMENT : Nat64 = 173; transient let PROTOCOL_PRINCIPAL_BLOB : Blob = Principal.toLedgerAccount(Principal.fromActor(this), null); + transient let TEAM_WALLET_PRINCIPAL : Principal = Principal.fromText("k3pwi-qyaaa-aaaab-acbrq-cai"); + transient let TEAM_WALLET_ADDRESS : Blob = Principal.toLedgerAccount(TEAM_WALLET_PRINCIPAL, null); // Construct subaccount for the canister principal private func principalToSubaccount(principal : Principal) : Blob { let sub = Buffer.Buffer(32); From 61ad81e1632f812b41fab34844809d14a55eff1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:50:18 +0000 Subject: [PATCH 4/5] Derive and validate team ICP account from principal Agent-Logs-Url: https://github.com/onicai/PoAIW/sessions/b2ca583e-6016-4447-85f9-b88632c99db5 Co-authored-by: patnorris <23037714+patnorris@users.noreply.github.com> --- src/GameState/src/Main.mo | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/GameState/src/Main.mo b/src/GameState/src/Main.mo index d59b445..154341a 100644 --- a/src/GameState/src/Main.mo +++ b/src/GameState/src/Main.mo @@ -575,6 +575,11 @@ persistent actor class GameStateCanister() = this { }; private func transferToTeam(args : Types.IcpTransferArgsAccount) : async Types.IcpTransferResult { + let derivedTeamWalletAccount : Text = Utils.bytesToText(Blob.toArray(TEAM_WALLET_ADDRESS)); + if (derivedTeamWalletAccount != TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER) { + return #Err(#Other("Team wallet account derivation mismatch")); + }; + D.print( "Transferring " # debug_show (args.amount) @@ -583,7 +588,7 @@ persistent actor class GameStateCanister() = this { ); if (args.to != TEAM_WALLET_ADDRESS) { - return #Err(#Other("Invalid transfer destination for transferToTeam")); + return #Err(#Other("Invalid transfer destination for transferToTeam. Expected ICP native account id: " # TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER)); }; let transferArgs : TokenLedger.TransferArgs = { @@ -4723,8 +4728,9 @@ persistent actor class GameStateCanister() = this { // Payment memo to specify in transaction to Protocol let MEMO_PAYMENT : Nat64 = 173; transient let PROTOCOL_PRINCIPAL_BLOB : Blob = Principal.toLedgerAccount(Principal.fromActor(this), null); - transient let TEAM_WALLET_PRINCIPAL : Principal = Principal.fromText("k3pwi-qyaaa-aaaab-acbrq-cai"); - transient let TEAM_WALLET_ADDRESS : Blob = Principal.toLedgerAccount(TEAM_WALLET_PRINCIPAL, null); + let TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER : Text = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783"; + let TEAM_WALLET_PRINCIPAL : Principal = Principal.fromText("k3pwi-qyaaa-aaaab-acbrq-cai"); + let TEAM_WALLET_ADDRESS : Blob = Principal.toLedgerAccount(TEAM_WALLET_PRINCIPAL, null); // Construct subaccount for the canister principal private func principalToSubaccount(principal : Principal) : Blob { let sub = Buffer.Buffer(32); From 6d4678ae81c38b2d0e5a33475392934f58ed3b82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:15:15 +0000 Subject: [PATCH 5/5] Use ICP native account-id bytes for team wallet transfers Agent-Logs-Url: https://github.com/onicai/PoAIW/sessions/f19ebc12-d53d-482f-833d-747ebb47ca1f Co-authored-by: patnorris <23037714+patnorris@users.noreply.github.com> --- src/GameState/src/Main.mo | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/GameState/src/Main.mo b/src/GameState/src/Main.mo index 154341a..424fc89 100644 --- a/src/GameState/src/Main.mo +++ b/src/GameState/src/Main.mo @@ -4729,8 +4729,7 @@ persistent actor class GameStateCanister() = this { let MEMO_PAYMENT : Nat64 = 173; transient let PROTOCOL_PRINCIPAL_BLOB : Blob = Principal.toLedgerAccount(Principal.fromActor(this), null); let TEAM_WALLET_ICP_NATIVE_ACCOUNT_IDENTIFIER : Text = "5d9bb4f164022de0933d3b45eaf33f1902e9578a2f330a1301d531c42bebf783"; - let TEAM_WALLET_PRINCIPAL : Principal = Principal.fromText("k3pwi-qyaaa-aaaab-acbrq-cai"); - let TEAM_WALLET_ADDRESS : Blob = Principal.toLedgerAccount(TEAM_WALLET_PRINCIPAL, null); + let TEAM_WALLET_ADDRESS : Blob = "\5D\9B\B4\F1\64\02\2D\E0\93\3D\3B\45\EA\F3\3F\19\02\E9\57\8A\2F\33\0A\13\01\D5\31\C4\2B\EB\F7\83"; // Construct subaccount for the canister principal private func principalToSubaccount(principal : Principal) : Blob { let sub = Buffer.Buffer(32);