From 83b303625854944b99cc5d6a6760fdf3c350b2c0 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 10 Jul 2025 17:11:13 +0200 Subject: [PATCH 01/10] feat: delegation logic wip --- src/controller/config.lua | 6 ++++++ src/supply/delegation.lua | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/supply/delegation.lua diff --git a/src/controller/config.lua b/src/controller/config.lua index a3b0f1a..dff5902 100644 --- a/src/controller/config.lua +++ b/src/controller/config.lua @@ -20,6 +20,7 @@ function mod.update(msg) local newJumpRate = tonumber(msg.Tags["Jump-Rate"]) local newInitRate = tonumber(msg.Tags["Init-Rate"]) local newCooldownPeriod = tonumber(msg.Tags["Cooldown-Period"]) + local newWrappedAO = msg.Tags["WAO-Process"] -- validate new config values, update assert( @@ -62,6 +63,10 @@ function mod.update(msg) not newCooldownPeriod or assertions.isValidInteger(newCooldownPeriod), "Invalid cooldown period" ) + assert( + not newWrappedAO or assertions.isAddress(newWrappedAO), + "Invalid Wrapped AO process ID" + ) if newValueLimit then assert( @@ -96,6 +101,7 @@ function mod.update(msg) if newJumpRate then JumpRate = newJumpRate end if newInitRate then InitRate = newInitRate end if newCooldownPeriod then CooldownPeriod = newCooldownPeriod end + if newWrappedAO then WrappedAO = newWrappedAO end msg.reply({ Oracle = Oracle, diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua new file mode 100644 index 0000000..0fbf939 --- /dev/null +++ b/src/supply/delegation.lua @@ -0,0 +1,32 @@ +local assertions = require ".utils.assertions" + +local mod = {} + +---@type HandlerFunction +function mod.setup() + -- validate wAO address + local wAOProcess = ao.env.Process.Tags["WAO-Process"] + + if not wAOProcess then return end + assert(assertions.isAddress(wAOProcess), "Invalid wAO process id") + + WrappedAO = wAOProcess +end + +-- Claims and distributes accrued AO yield for owAR +---@type HandlerFunction +function mod.delegate() + -- only run if defined + if not WrappedAO then return end + + -- record oToken balances, so the correct quantities are used + -- after the handler below is triggered later + local balances = Balances -- TODO: fix this to clone the balances + + -- claim accrued AO, but do not stop execution with .receive() + -- + -- this is necessary, because this handler runs before interactions + -- (supply/redeem/liquidate position) that should not be delayed +end + +return mod From 98e0ba692f3bb4d4e7706b2f4b393ebba74eb44e Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 10 Jul 2025 17:17:25 +0200 Subject: [PATCH 02/10] feat: delegation handler --- src/process.lua | 20 ++++++++++++++++++++ src/supply/delegation.lua | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/process.lua b/src/process.lua index 04e77ef..2c513a0 100644 --- a/src/process.lua +++ b/src/process.lua @@ -31,6 +31,7 @@ local liquidate = require ".liquidations.liquidate" local mint = require ".supply.mint" local rate = require ".supply.rate" local redeem = require ".supply.redeem" +local delegation = require ".supply.delegation" local utils = require ".utils.utils" local precision = require ".utils.precision" @@ -119,6 +120,25 @@ local function setup_handlers() errorHandler = cooldown.refund }) + -- accrued AO distribution for actions that update oToken balances + Handlers.add( + "supply-delegate-ao", + function (msg) + local action = msg.Tags["X-Action"] or msg.Tags.Action + + if action == "Delegate" then return true end + if + action == "Mint" or + action == "Redeem" or + action == "Liquidate-Position" or + action == "Transfer" + then return "continue" end + + return false + end, + delegation.delegate + ) + -- communication with the controller Handlers.add( "controller-updater", diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index 0fbf939..1dc4267 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -26,7 +26,7 @@ function mod.delegate() -- claim accrued AO, but do not stop execution with .receive() -- -- this is necessary, because this handler runs before interactions - -- (supply/redeem/liquidate position) that should not be delayed + -- (mint/redeem/liquidate position/transfer) that should not be delayed end return mod From 002c3267ea118f214bc084582aa1b71e1f8ca38d Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 10 Jul 2025 18:03:16 +0200 Subject: [PATCH 03/10] feat: distribution wip --- src/supply/delegation.lua | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index 1dc4267..da6dfdf 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -1,6 +1,9 @@ local assertions = require ".utils.assertions" +local utils = require ".utils.utils" -local mod = {} +local mod = { + aoToken = "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc" +} ---@type HandlerFunction function mod.setup() @@ -15,7 +18,7 @@ end -- Claims and distributes accrued AO yield for owAR ---@type HandlerFunction -function mod.delegate() +function mod.delegate(msg) -- only run if defined if not WrappedAO then return end @@ -27,6 +30,32 @@ function mod.delegate() -- -- this is necessary, because this handler runs before interactions -- (mint/redeem/liquidate position/transfer) that should not be delayed + local claimMsg = ao.send({ + Target = WrappedAO, + Action = "Claim" + }) + local claimRef = utils.find( + function (tag) return tag.name == "Reference" end, + claimMsg.Tags + ) + + -- add handler that handles a potential claim error/credit-notice + Handlers.once( + function (msg) + local action = msg.Tags.Action + + -- claim error + if action == "Claim-Error" and msg.From == WrappedAO and msg.Tags["X-Reference"] == claimRef then + return true + end + + if action == "Credit-Notice" and msg.From == + function () + + end + ) + + -- TODO: save remainder AO with extra (internal) precision and redistribute it later end return mod From ca503afd443e1afd9993bd894ca41678249bc945 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Thu, 10 Jul 2025 18:20:52 +0200 Subject: [PATCH 04/10] feat: catch claim credit-notices and let them through --- src/borrow/pool.lua | 3 +++ src/process.lua | 6 +++++- src/supply/delegation.lua | 20 +++++++++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/borrow/pool.lua b/src/borrow/pool.lua index 54505a6..a8193c5 100644 --- a/src/borrow/pool.lua +++ b/src/borrow/pool.lua @@ -11,6 +11,9 @@ function mod.setup(msg) "Invalid collateral id" ) + -- AO token process + AOToken = AOToken or "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc" + -- token that can be lent/borrowed CollateralID = CollateralID or ao.env.Process.Tags["Collateral-Id"] diff --git a/src/process.lua b/src/process.lua index 2c513a0..33dea04 100644 --- a/src/process.lua +++ b/src/process.lua @@ -52,6 +52,7 @@ local function setup_handlers() pool.setup(msg, env) oracle.setup(msg, env) cooldown.setup(msg, env) + delegation.setup(msg, env) end ) @@ -86,8 +87,11 @@ local function setup_handlers() if msg.Tags.Action ~= "Credit-Notice" then return false -- not a token transfer end + if WrappedAO ~= nil and msg.From == AOToken and msg.Tags.Sender == WrappedAO then + return false -- this oToken process accrues AO and the message is a wAO claim response + end if msg.From ~= CollateralID then - return true -- unknown token + return true -- unknown token end if utils.includes(msg.Tags["X-Action"], { "Repay", diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index da6dfdf..ed2eb70 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -1,9 +1,7 @@ local assertions = require ".utils.assertions" local utils = require ".utils.utils" -local mod = { - aoToken = "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc" -} +local mod = {} ---@type HandlerFunction function mod.setup() @@ -22,6 +20,9 @@ function mod.delegate(msg) -- only run if defined if not WrappedAO then return end + -- the original message this message was pushed for + local pushedFor = msg.Tags["Pushed-For"] or msg.Id + -- record oToken balances, so the correct quantities are used -- after the handler below is triggered later local balances = Balances -- TODO: fix this to clone the balances @@ -49,13 +50,18 @@ function mod.delegate(msg) return true end - if action == "Credit-Notice" and msg.From == - function () + -- credit notice for the claimed AO + if action == "Credit-Notice" and msg.From == AOToken and msg.Tags["Pushed-For"] == pushedFor then + return true + end + return false + end, + function (msg) + -- TODO: distribute + -- TODO: save remainder AO with extra (internal) precision and redistribute it later end ) - - -- TODO: save remainder AO with extra (internal) precision and redistribute it later end return mod From a9d7f07923f5bd5c78ce38594347cf458fa1a357 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Fri, 11 Jul 2025 18:51:22 +0200 Subject: [PATCH 05/10] fix: balances clone --- src/supply/delegation.lua | 76 +++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index ed2eb70..1392509 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -1,4 +1,5 @@ local assertions = require ".utils.assertions" +local bint = require ".utils.bint"(1024) local utils = require ".utils.utils" local mod = {} @@ -11,7 +12,11 @@ function mod.setup() if not wAOProcess then return end assert(assertions.isAddress(wAOProcess), "Invalid wAO process id") - WrappedAO = wAOProcess + -- wrapped arweave process id + WrappedAO = WrappedAO or wAOProcess + + -- remaining quantity to distribute + RemainingDelegateQuantity = RemainingDelegateQuantity or "0" end -- Claims and distributes accrued AO yield for owAR @@ -23,9 +28,13 @@ function mod.delegate(msg) -- the original message this message was pushed for local pushedFor = msg.Tags["Pushed-For"] or msg.Id - -- record oToken balances, so the correct quantities are used - -- after the handler below is triggered later - local balances = Balances -- TODO: fix this to clone the balances + -- record oToken balances before the current interaction, so the + -- correct quantities are used after the handler below is triggered + local balancesRecord = {} + + for addr, balance in pairs(Balances) do + balancesRecord[addr] = balance + end -- claim accrued AO, but do not stop execution with .receive() -- @@ -58,8 +67,63 @@ function mod.delegate(msg) return false end, function (msg) - -- TODO: distribute - -- TODO: save remainder AO with extra (internal) precision and redistribute it later + -- do not distribute if there was an error + if msg.Tags.Action == "Claim-Error" then return end + + -- validate quantity + assert( + assertions.isTokenQuantity(msg.Tags.Quantity), + "Invalid claimed quantity" + ) + + -- quantity to distribute (the incoming + the remainder) + local quantity = bint(msg.Tags.Quantity) + bint(RemainingDelegateQuantity or "0") + + -- number of token holders + local holdersCount = #utils.keys(balancesRecord) + + -- if the claimed quantity is less than the amount of oToken + -- holders, do not distribute, but save the amount for future + -- distribution + if bint.ult(quantity, holdersCount) then + RemainingDelegateQuantity = tostring(quantity) + return + end + + -- distribute claimed AO + local remaining = bint(quantity) + local totalSupply = bint(TotalSupply) + local zero = bint.zero() + + for addr, rawBalance in pairs(balancesRecord) do + -- parsed wallet balance + local balance = bint(rawBalance) + + -- amount to distribute to this wallet + local distributeQty = quantity.udiv( + balance * quantity, + totalSupply + ) + + -- distribute if more than 0 + if bint.ult(zero, distributeQty) then + ao.send({ + Target = AOToken, + Action = "Transfer", + Quantity = tostring(distributeQty), + Recipient = addr + }) + remaining = remaining - distributeQty + end + end + + -- make sure that the remainder is at least zero, otherwise + -- something went wrong with the calculations (this should not + -- happen) + assert(bint.ult(zero, remaining), "The distribution remainder cannot be less than zero") + + -- update the remaining amount + RemainingDelegateQuantity = tostring(remaining) end ) end From daae1deda94622b6081b816083eaff57b62c6c84 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Sat, 12 Jul 2025 12:12:09 +0200 Subject: [PATCH 06/10] feat: config for ao and wrapped ao tokens --- src/borrow/pool.lua | 2 +- src/controller/config.lua | 18 +++++++++++++----- src/supply/delegation.lua | 10 +++++----- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/borrow/pool.lua b/src/borrow/pool.lua index a8193c5..1595ec2 100644 --- a/src/borrow/pool.lua +++ b/src/borrow/pool.lua @@ -12,7 +12,7 @@ function mod.setup(msg) ) -- AO token process - AOToken = AOToken or "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc" + AOToken = AOToken or ao.env.Process.Tags["AO-Token"] or "0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc" -- token that can be lent/borrowed CollateralID = CollateralID or ao.env.Process.Tags["Collateral-Id"] diff --git a/src/controller/config.lua b/src/controller/config.lua index dff5902..b02406f 100644 --- a/src/controller/config.lua +++ b/src/controller/config.lua @@ -20,7 +20,8 @@ function mod.update(msg) local newJumpRate = tonumber(msg.Tags["Jump-Rate"]) local newInitRate = tonumber(msg.Tags["Init-Rate"]) local newCooldownPeriod = tonumber(msg.Tags["Cooldown-Period"]) - local newWrappedAO = msg.Tags["WAO-Process"] + local newWrappedAOToken = msg.Tags["Wrapped-AO-Token"] + local newAOToken = msg.Tags["AO-Token"] -- validate new config values, update assert( @@ -64,8 +65,12 @@ function mod.update(msg) "Invalid cooldown period" ) assert( - not newWrappedAO or assertions.isAddress(newWrappedAO), - "Invalid Wrapped AO process ID" + not newWrappedAOToken or assertions.isAddress(newWrappedAOToken), + "Invalid Wrapped AO token process ID" + ) + assert( + not newAOToken or assertions.isAddress(newAOToken), + "Invalid AO token process ID" ) if newValueLimit then @@ -101,7 +106,8 @@ function mod.update(msg) if newJumpRate then JumpRate = newJumpRate end if newInitRate then InitRate = newInitRate end if newCooldownPeriod then CooldownPeriod = newCooldownPeriod end - if newWrappedAO then WrappedAO = newWrappedAO end + if newWrappedAOToken then WrappedAOToken = newWrappedAOToken end + if newAOToken then AOToken = newAOToken end msg.reply({ Oracle = Oracle, @@ -113,7 +119,9 @@ function mod.update(msg) ["Kink-Param"] = tostring(KinkParam), ["Base-Rate"] = tostring(BaseRate), ["Jump-Rate"] = tostring(JumpRate), - ["Init-Rate"] = tostring(InitRate) + ["Init-Rate"] = tostring(InitRate), + ["AO-Token"] = tostring(AOToken), + ["Wrapped-AO-Token"] = tostring(WrappedAOToken) }) end diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index 1392509..9a10433 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -7,13 +7,13 @@ local mod = {} ---@type HandlerFunction function mod.setup() -- validate wAO address - local wAOProcess = ao.env.Process.Tags["WAO-Process"] + local wAOProcess = ao.env.Process.Tags["Wrapped-AO-Token"] if not wAOProcess then return end assert(assertions.isAddress(wAOProcess), "Invalid wAO process id") -- wrapped arweave process id - WrappedAO = WrappedAO or wAOProcess + WrappedAOToken = WrappedAOToken or wAOProcess -- remaining quantity to distribute RemainingDelegateQuantity = RemainingDelegateQuantity or "0" @@ -23,7 +23,7 @@ end ---@type HandlerFunction function mod.delegate(msg) -- only run if defined - if not WrappedAO then return end + if not WrappedAOToken then return end -- the original message this message was pushed for local pushedFor = msg.Tags["Pushed-For"] or msg.Id @@ -41,7 +41,7 @@ function mod.delegate(msg) -- this is necessary, because this handler runs before interactions -- (mint/redeem/liquidate position/transfer) that should not be delayed local claimMsg = ao.send({ - Target = WrappedAO, + Target = WrappedAOToken, Action = "Claim" }) local claimRef = utils.find( @@ -55,7 +55,7 @@ function mod.delegate(msg) local action = msg.Tags.Action -- claim error - if action == "Claim-Error" and msg.From == WrappedAO and msg.Tags["X-Reference"] == claimRef then + if action == "Claim-Error" and msg.From == WrappedAOToken and msg.Tags["X-Reference"] == claimRef then return true end From f2f0891426c227628067120455bbb29e0e6e3d2d Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Mon, 14 Jul 2025 13:48:10 +0200 Subject: [PATCH 07/10] feat: remove delegation check for holder count --- src/supply/delegation.lua | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index 9a10433..06ad864 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -79,17 +79,6 @@ function mod.delegate(msg) -- quantity to distribute (the incoming + the remainder) local quantity = bint(msg.Tags.Quantity) + bint(RemainingDelegateQuantity or "0") - -- number of token holders - local holdersCount = #utils.keys(balancesRecord) - - -- if the claimed quantity is less than the amount of oToken - -- holders, do not distribute, but save the amount for future - -- distribution - if bint.ult(quantity, holdersCount) then - RemainingDelegateQuantity = tostring(quantity) - return - end - -- distribute claimed AO local remaining = bint(quantity) local totalSupply = bint(TotalSupply) From c0a5690941afd704305790dc00ad0ff95243331b Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Mon, 14 Jul 2025 14:04:21 +0200 Subject: [PATCH 08/10] fix: claim ref value --- src/supply/delegation.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index 06ad864..8e58ac0 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -44,10 +44,10 @@ function mod.delegate(msg) Target = WrappedAOToken, Action = "Claim" }) - local claimRef = utils.find( + local claimRef = (utils.find( function (tag) return tag.name == "Reference" end, claimMsg.Tags - ) + ) or {}).value -- add handler that handles a potential claim error/credit-notice Handlers.once( From 223b6bbd95378158747e46da50616646ac053f34 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Mon, 14 Jul 2025 14:25:47 +0200 Subject: [PATCH 09/10] feat: tests for delegation --- __tests__/supply.test.ts | 290 ++++++++++++++++++++++++++++++++++++++ src/supply/delegation.lua | 2 +- 2 files changed, 291 insertions(+), 1 deletion(-) diff --git a/__tests__/supply.test.ts b/__tests__/supply.test.ts index cbcd608..46cece0 100644 --- a/__tests__/supply.test.ts +++ b/__tests__/supply.test.ts @@ -876,3 +876,293 @@ describe("Price and underlying asset value, supplies after initial provide", () it.todo("Price input quantity is 1 by default when there is no quantity provided"); }); + +describe("AO delegation", () => { + let handle: HandleFunction; + let tags: Record; + + beforeEach(async () => { + const envWithWAO = { + Process: { + ...env.Process, + Tags: [ + ...env.Process.Tags, + { name: "AO-Token", value: generateArweaveAddress() }, + { name: "Wrapped-AO-Token", value: generateArweaveAddress() }, + ] + } + }; + + handle = await setupProcess(envWithWAO); + tags = normalizeTags(envWithWAO.Process.Tags); + }); + + it("Does not run delegate if there is no wAO token process defined", async () => { + const handle = await setupProcess(env); + const res = await handle(createMessage({ Action: "Delegate" })); + + expect(res.Messages).toHaveLength(0); + }); + + it("Does not delegate any tokens if the claim failed", async () => { + const delegateRes = await handle(createMessage({ Action: "Delegate" })); + + expect(delegateRes.Messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Target: tags["Wrapped-AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Claim" + }) + ]) + }) + ]) + ); + + const claimRes = await handle(createMessage({ + Owner: tags["Wrapped-AO-Token"], + From: tags["Wrapped-AO-Token"], + Action: "Claim-Error", + Error: "No balance", + "X-Reference": normalizeTags( + getMessageByAction("Claim", delegateRes.Messages)?.Tags || [] + )["Reference"] + })); + + expect(claimRes.Messages).toHaveLength(0); + }); + + it("Does not distribute an invalid quantity", async () => { + const id = generateArweaveAddress(); + const delegateRes = await handle(createMessage({ + Id: id, + Action: "Delegate" + })); + + expect(delegateRes.Messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Target: tags["Wrapped-AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Claim" + }) + ]) + }) + ]) + ); + + const claimRes = await handle(createMessage({ + Owner: tags["AO-Token"], + From: tags["AO-Token"], + Action: "Credit-Notice", + Quantity: "invalid", + Sender: tags["Wrapped-AO-Token"], + ["Pushed-For"]: id + })); + + expect(claimRes.Messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Credit-Notice-Error" + }), + expect.objectContaining({ + name: "Error", + value: expect.stringContaining( + "Invalid claimed quantity" + ) + }) + ]) + }) + ]) + ); + }); + + it("Distributes the claimed quantity evenly", async () => { + // prepare by adding holders + const balances = [ + { addr: generateArweaveAddress(), qty: "2" }, + { addr: generateArweaveAddress(), qty: "1" } + ]; + await handle(createMessage({ + Action: "Update", + Data: `Balances = { ["${balances[0].addr}"] = "${balances[0].qty}", ["${balances[1].addr}"] = "${balances[1].qty}" } + TotalSupply = "3"` + })); + + // distribute + const id = generateArweaveAddress(); + const delegateRes = await handle(createMessage({ + Id: id, + Action: "Delegate" + })); + + expect(delegateRes.Messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Target: tags["Wrapped-AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Claim" + }) + ]) + }) + ]) + ); + + const claimRes = await handle(createMessage({ + Owner: tags["AO-Token"], + From: tags["AO-Token"], + Action: "Credit-Notice", + Quantity: "6", + Sender: tags["Wrapped-AO-Token"], + ["Pushed-For"]: id + })); + + expect(claimRes.Messages).toEqual( + expect.arrayContaining(balances.map((balance) => ( + expect.objectContaining({ + Target: tags["AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Transfer" + }), + expect.objectContaining({ + name: "Recipient", + value: balance.addr + }), + expect.objectContaining({ + name: "Quantity", + value: (parseInt(balance.qty) * 2).toString() + }), + ]) + }) + ))) + ); + }); + + it("Distributes the claimed quantity with a remainder", async () => { + // prepare by adding holders + const balances = [ + { addr: generateArweaveAddress(), qty: "2" }, + { addr: generateArweaveAddress(), qty: "1" } + ]; + await handle(createMessage({ + Action: "Update", + Data: `Balances = { ["${balances[0].addr}"] = "${balances[0].qty}", ["${balances[1].addr}"] = "${balances[1].qty}" } + TotalSupply = "3"` + })); + + // distribute + const id = generateArweaveAddress(); + const delegateRes = await handle(createMessage({ + Id: id, + Action: "Delegate" + })); + + expect(delegateRes.Messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Target: tags["Wrapped-AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Claim" + }) + ]) + }) + ]) + ); + + const claimRes = await handle(createMessage({ + Owner: tags["AO-Token"], + From: tags["AO-Token"], + Action: "Credit-Notice", + Quantity: "7", + Sender: tags["Wrapped-AO-Token"], + ["Pushed-For"]: id + })); + + expect(claimRes.Messages).toEqual( + expect.arrayContaining(balances.map((balance) => ( + expect.objectContaining({ + Target: tags["AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Transfer" + }), + expect.objectContaining({ + name: "Recipient", + value: balance.addr + }), + expect.objectContaining({ + name: "Quantity", + value: (parseInt(balance.qty) * 2).toString() + }), + ]) + }) + ))) + ); + + // now check redistribution of the remainder + const id2 = generateArweaveAddress(); + const delegateRes2 = await handle(createMessage({ + Id: id2, + Action: "Delegate" + })); + + expect(delegateRes2.Messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + Target: tags["Wrapped-AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Claim" + }) + ]) + }) + ]) + ); + + const claimRes2 = await handle(createMessage({ + Owner: tags["AO-Token"], + From: tags["AO-Token"], + Action: "Credit-Notice", + Quantity: "2", + Sender: tags["Wrapped-AO-Token"], + ["Pushed-For"]: id2 + })); + + expect(claimRes2.Messages).toEqual( + expect.arrayContaining(balances.map((balance) => ( + expect.objectContaining({ + Target: tags["AO-Token"], + Tags: expect.arrayContaining([ + expect.objectContaining({ + name: "Action", + value: "Transfer" + }), + expect.objectContaining({ + name: "Recipient", + value: balance.addr + }), + expect.objectContaining({ + name: "Quantity", + value: balance.qty + }), + ]) + }) + ))) + ); + }); +}); diff --git a/src/supply/delegation.lua b/src/supply/delegation.lua index 8e58ac0..41c11b3 100644 --- a/src/supply/delegation.lua +++ b/src/supply/delegation.lua @@ -109,7 +109,7 @@ function mod.delegate(msg) -- make sure that the remainder is at least zero, otherwise -- something went wrong with the calculations (this should not -- happen) - assert(bint.ult(zero, remaining), "The distribution remainder cannot be less than zero") + assert(bint.ule(zero, remaining), "The distribution remainder cannot be less than zero") -- update the remaining amount RemainingDelegateQuantity = tostring(remaining) From 036b0e73e78c66c4a4cc681305c1dbe8848e9b46 Mon Sep 17 00:00:00 2001 From: Marton Lederer Date: Mon, 14 Jul 2025 14:30:21 +0200 Subject: [PATCH 10/10] chore: update install url --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d209822..f5e0689 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v2 - name: Install ao - run: curl -L https://install_ao.g8way.io | bash + run: curl -L https://install_ao.arweave.net | bash - name: Build module run: |