Autonomous Finance 在 AO 上推出第一个去中心化自主投资代理(Autonomous Investment Agent)应用,可以使用美元平均成本(DCA)策略在 AO 生态的各种流动性池中执行交易。
作者: Autonomous Finan
翻译: Jomosis
审阅: Tina
导读
Autonomous Finance 在 AO 上推出第一个去中心化自主投资代理(Autonomous Investment Agent)应用,可以使用美元平均成本(DCA)策略在 AO 生态的各种流动性池中执行交易。 自主投资代理拥有完全的自主性。一旦启动,其将会在 AO 生态上自主运行,无需链外信号或人为干预。用户可以通过前端界面配置代理的参数,包括定投资产种类、滑点范围、DEX 的 AMM 流动池等。存入资金后,启动即可自动运行,用户也可以随时修改参数或停止运行。本文除了介绍自主投资代理的概念以及使用方法,还为开发者提供了详细的代码示例,敬请阅读!
这个软件是一个自主投资代理(AIA),它可以使用动态的定投(DCA)投资策略,在 AO 生态系统内的各种流动性池中执行交易。该代理会根据预设的、可由用户配置的间隔,使用一定数量的报价代币为每笔交易自动购买基础代币。
它最大的特点是功能自主性,这是 AO 网络独有的,对于自动化功能至关重要。一旦启动,它将在AO 平台上自主运行,无需离线信号或人为干预。
DCA 代理的管理界面通过托管在 Arweave 的 Permaweb 上的前端实现,并且无需信任的中间人。
DCA代理凭借其设计和技术堆栈,成为了第一个完全去中心化和抗审查的 AgentFi 应用程序。一旦部署,代理将按照编程方式运行,并且只能由其创建者停用。
可以在 Permaweb 上访问最新的 DCA代理,或在 GitHub 上复刻代码仓库。
在创建时,首先配置您的代理:
配置包括设置DCA参数、定义可接受的滑点范围,并选择DEX(AMM流动性池)进行交换。
只要有足够的报价存入代币,您的代理将执行 DCA 买入的操作:
仪表板为代理所有者提供以下功能:
DCA 代理充当更进一步发展的基础蓝图,旨在促进更复杂的多代理配置。虽然处于初始阶段并不断改进,但它挑起了执行大部分特定应用的重担,为有远见的构建者提供了重要的先机。
随着 AO 自主金融生态系统的发展,我们预期会为 DCA 代理和 AIAs 开发更先进和复杂的功能。由于其高度可配置性和可组合性,DCA 代理可以轻松地定制并集成到多代理配置中。想象一下,通过简单地集成 DCA 代理,作为策略的一部分,即可将 AIA 包括在其中。
以下是关于 DCA 代理未来潜力和可组合性的一些思考。
DCA 代理已经具有高度可组合性,为了获得更大的灵活性,还可以探索一些替代方案。其中一种方法涉及到永不托管用户资金的代理。在这种模型中,多个专门的代理只有在出现盈利机会时会被授予访问用户资金的权限。例如,DCA 代理可能访问获得许可的资金,执行盈利交易,然后将资金返回给用户的自托管钱包。
这种方法显著提高了资本效率。用户不需要将资本锁定在特定的策略或池中;相反,他们授权代理使用预定义金额的资金,在任何代理执行盈利行动之前,这些资金仍然可供用户使用。
通过对 DCA 代理的初步开发,我们对 AO 平台有了更深入的了解,包括其能力和限制。这个过程也让我们对适用于 DCA 代理开发的有效设计模式有了重要的见解。
我们将在即将发布的文章中详细阐述这些见解。
这个项目作为新兴的 AgentFI 领域中的基础示例,展示了如何通过一个无需第三方工具或基础设施的无需信任、完全链上的过程来管理自主投资。它旨在为社区搭建更复杂代理的起点。
我们的意图是以更健壮的设计迭代这个基础,使用高级模式,这些模式将在更多的文章和蓝图中进行阐述。
目前项目的版本包括:
未来,我们计划扩展功能,包括与 DEXI 更紧密集成 - 这是我们的应用程序,允许 DeFi 用户浏览和发现 AO 生态系统。
未来版本的代理可能包括以下功能:
提供 DCA 代理活动历史的可视化,包括每次与 AOLINK 相关的链接,将允许所有者通过其唯一 ID 检查每次交换。
AO 平台通过原生的任务支持自动化,在 dApp 开发领域有着独特的优势。开发者可以设置完全基于链上运行的计划任务,这对于实现金融代理的完全自主性至关重要。
鉴于仍在致力于不断增强 cron 计划任务能力和可靠性,现在确定最佳实践还为时过早。但是,在当前阶段,我们可以分享一个在当前阶段相关的见解。
我们设计了一种称为直接触发的模式,用于在简单情况下利用 cron计划任务,这种情况仅需要定期执行一次操作。在这种情况下,进程在创建(或生成)时被标记以接收 cron 时钟信号。而触发的信号频率由一个参数确定。
直接触发
// spawning cron process from @permaweb/ao-connect
const signer = createDataItemSigner(window.arweaveWallet)
const process = await ao.spawn({
module: "SBNb1qPQ1TDwpD_mboxm2YllmMLXpWw4U8P9Ff8W9vk",
scheduler: "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA",
signer,
// -- configure cron
tags: [
{ name: "Cron-Interval", value: "1-minute" },
{ name: "Cron-Tag-Action", value: "TriggerSwap" },
],
})
// Turn on monitoring to activate cron triggering
await monitor({
process,
signer,
})
/*
* As a result, our process starts receiving cron ticks once per minute.
* If it has a handler that matches { "Action" : "TriggerSwap" }
* these ticks will execute said handler.
*/
-- process.lua
Handlers.add(
"triggerSwap",
function(msg)-- ensure only authentic cron ticks are matched
return Handlers.utils.hasMatchingTag("Action", "TriggerSwap")(msg)
and msg.Cron
end,
function(msg)-- ... perform swap
end
)
在需要定期执行多种类型动作的场景中,我们采用一种间接触发模式。这种方法使用独立的过程作为触发-通知器。这些是简单的 cron 驱动的代理,它们会提醒主要过程执行其任务。
这种模式的一个实际用例,它用于管理重新尝试失败消息传递的 DCA 代理。该代理:
间接触发
-- swap_notifier.lua
Agent = Agent or nil -- set after creation via dedicated handler
Handlers.add(
"cron",
function(msg)return Handlers.utils.hasMatchingTag("Action", "Cron")(msg)
and msg.Cron
end,
function(msg)
ao.send({Target = Agent})
end
)
-- retry_notifier.lua
-- analogous to swap_notifier.lua, but handler execution is
-- ...
ao.send({Target = Agent})
-- ...
-- process.lua
SwapNotifier = SwapNotifier or nil -- set via handler after notifier is spawned
RetryNotifier = SwapNotifier or nil -- set via handler after notifier is spawned
Handlers.add(
"triggerSwap",
function(msg)return msg.From == SwapNotifier
end,
function(msg)-- ... perform swap
end
)
Handlers.add(
"checkQueue",
function(msg)return msg.From == RetryNotifier
end,
function(msg)-- ... perform check on retry queue
end
)
在实施间接触发模式时,对于主要过程来说,识别其触发-通知器的身份变得至关重要,以确保它不会被冒名顶替者激活。
此外,这种模式在更新 cron 时钟间隔方面具有战略优势,这在单个 cron 操作情景下是无法实现的。虽然 cron 间隔在创建时被设置为进程标记,并且之后无法更改,但使用间接触发允许灵活性。通过简单地生成一个具有不同间隔的新触发-通知器,就可以轻松调整任务的时间安排。
在接下来的部分中,我们将主要探讨对我们项目至关重要的 AO 进程。我们可能还会重点介绍前端功能,或深入探讨体现技术价值的实施细节。
我们的项目围绕两个对 DCA 代理应用至关重要的关键 AO 进程构建:
这些进程与以下组件交互:
DCA 代理,后端以及相关的进程
下面的图表展示了 DCA 购买的架构流程,其中发生了从报价代币到基础代币的交换。这种可视化有助于详细描述此类交易中的步骤和交互。
DCA 购买的步骤
在相反方向的代币(从基础到报价两方)进行兑换时,涉及到的消息序列与 DCA 购买类似,这在代理资产清算中起着关键作用。
代理进程安全地持有资金,需要存款来促进 DCA 购买。在执行交换后,它保留了所得到的资产,确保所有资产保持流动性(即,用户随时可以提取)。
目前,ao 后端进程与代理紧密耦合。我们设想标准化这两个组件,使它们可以轻松地与其他组件进行交换,同时保持系统兼容性。然而,鉴于我们特定的目标是将这些代理与 DEXI 集成,我们决定将DCA 代理和后端的标准化推迟到未来的迭代中。
前端利用 Arweave 网关与 AO 进程进行交互。它与以下组件进行交互:
前端架构概述
Web 前端支持基本的代理管理和交互,每个用户可以支持多个代理。为了保的独立性并增强去中心化,UI 设计为部署在永久网络上的静态网站。所有持久化数据存储在 AO 上,在AO 进程的状态中维护。
为了简化操作,代理是独立生成的,并且它们的所有者可以在没有任何访问控制的情况下将它们注册到后端。这种设置是安全的,因为后端在其当前配置中不需要访问控制;也意味着它不能被滥用。注册的代理与其所有者的帐户关联,防止未经授权的注册污染其他用户的数据。
另一种方法可能涉及一个工厂设置,其中代理直接从后端生成。有关我们不采用此方法的更多详细信息,请参阅这里的讨论。
-- backend.lua
Handlers.add(
'registerAgent',
Handlers.utils.hasMatchingTag('Action', 'RegisterAgent'),
registration.registerAgent
)
-- registration.lua
mod.registerAgent = function(msg)local agent = msg.Tags.Agent
-- ...
local sender = msg.From
-- 👇 registration only affects sender-related data
-- => ok to do without access control
RegisteredAgents[agent] = msg.From
AgentsPerUser[sender] = AgentsPerUser[sender] or {}
AgentInfosPerUser[sender] = AgentInfosPerUser[sender] or {}
table.insert(AgentsPerUser[sender], agent)
table.insert(AgentInfosPerUser[sender], {
-- ...
})
response.success("RegisterAgent")(msg)
end
尽管将后端设计为生成代理的工厂是可行的,但我们发现这种方法对用户来说更加繁琐。因此,我们选择了非工厂设置。然而,如果未来需要访问受控制的注册,返回到基于工厂的方法可能是一个可行的解决方案。
以下是在工厂版本中代理创建和注册将如何运作:
前端的变化:
这种方法确保了一个简化的过程,集成了安全性和用户便利性,如果未来需要转向工厂设置的话,则会更加方便。
在我们的实现中,用户通常作为代理的所有者开始,但所有权可以在初始化阶段转移,允许所有者是另一个进程。这种灵活性支持各种操作场景,例如:
备选所有权模型: 一个有趣的备选所有权模型涉及一个不持有资金本身的代理,但被授权从另一个来源访问资金,比如一个负责维护这些资金的不同进程。
用例示例: 作为所有者,我可以利用多个代理来管理投资,而无需在它们之间分配资金。如果代理不是按固定间隔投资而是响应不可预测的信号,则这特别有用。这种设置不仅减少了操作开销,还提高了可用资金的有效利用率,提高了整体资本效率。
在 AO 中,每个进程都有一个全局变量 Owner
,在进程创建时自动分配。进程的所有者可以是钱包或另一个进程。
所有权可以通过更新 Owner
的值来转移。我们通过增强我们的处理程序功能,类似于 Solidity 智能合约中常用的 onlyOwner
修饰符,将此机制纳入我们的应用程序中,以确保操作仅限于进程所有者。
-- process.lua
Handlers.add(
"retire",
Handlers.utils.hasMatchingTag("Action", "Retire"),
function(msg)
permissions.onlyOwner(msg)
lifeCycle.retire(msg)
end
)
...
-- permissions.lua
mod.onlyOwner = function(msg)assert(msg.From == Owner, "Only the owner is allowed")
end
与以太坊智能合约不同,AO 进程没有构造函数。因此,为了使代理正常运行所需的任何比哟啊的初始化都必须通过后续消息执行。这个消息类似于常规消息,但是专门设计用于更新代理的状态。
为了管理初始化状态,我们采用一个IsInitialized
标志。尚未初始化的进程将拒绝处理(几乎)任何消息。它们只允许:
这种有选择性的消息处理是通过在 Handlers 列表的顶部放置一个 “catch-all” 消息处理程序来实现的。这种设置充当了门卫,确保进程在正确初始化之前不能执行或响应任何操作。
-- msg to be sent by end user or another process
Handlers.add(
"initialize",
Handlers.utils.hasMatchingTag("Action", "Initialize"),
lifeCycle.initialize
)
-- ! every handler below is gated on Initialized == true
Handlers.add(
"checkInit",
function(msg)return not Initialized -- the match is positive if we're not initialized yet
end,
response.errorMessage("error - process is not initialized")
)
在我们的 DCA 代理应用中,虽然创建代理的用户通常应该是所有者,但我们的目标是最大限度地提高可组合性和设置灵活性。这种方法允许将其他进程指定为 DCA 代理的所有者。此外,我们正在探索与 DEXI 的未来集成,使用户可以通过 DEXI UI 创建代理,使用潜在可信的代理工厂。这种设置理想情况下会分离生成进程的角色、成为真正所有者的角色以及执行初始化的角色。
在智能合约开发中,对初始化进行严格的访问控制通常是至关重要的,以确保只有指定的所有者可以执行初始化。然而,为了保持简单,我们选择了一种更加宽松的方法。
为了简化我们的设计,我们将“真正所有者初始化”和“DCA配置初始化”合并为单个“初始化”操作。在执行此初始化之前,代理保持“无用”。发送“初始化”消息的人将自动成为进程的真正所有者。
理论上,这种设计选择确实开启了恶意破坏的可能性。如果其他人在预设的所有者之前初始化进程,他们可能会控制它。然而,我们评估认为在 AO 上,与其他全局状态虚拟机平台相比,这种前置运行的风险要低得多。有关安全方面的更多信息以及为什么这个问题在 AO 上不那么突出,请参阅我们详细的安全讨论。
在我们的系统中,所有关键数据都直接存储在进程状态中,也就消除了通过 Arweave 网关进行查询的需要。
通过后端进程,我们可以轻松地检索每个用户的所有可用代理列表,包括关键信息,如购买和出售的历史记录。
为了确保在资产上执行的多步操作(如DCA购买、取款和清算)的原子性和隔离性,我们的代理松散地采用了状态机模型,类似于互斥锁。这个模型也跟踪相关的成功和失败。
IsSwapping = IsSwapping or false
IsWithdrawing = IsWithdrawing or false
IsDepositing = IsDepositing or false
IsLiquidating = IsLiquidating or false
LastWithdrawalNoticeId = LastWithdrawalNoticeId or nil
LastDepositNoticeId = LastDepositNoticeId or nil
LastLiquidationNoticeId = LastLiquidationNoticeId or nil
LastSwapNoticeId = LastSwapNoticeId or nil
LastWithdrawalError = LastWithdrawalError or nil
LastLiquidationError = LastLiquidationError or nil
LastSwapError = LastSwapError or nil
一个更高级的状态机将有助于跟踪每个特定的多步操作的进度。这种改进可以帮助开发开发一个 UI,为用户提供有关任何特定操作的准确进度。
目前,我们的代理提供的隔离性相当简单。如果在进行另一个操作时尝试执行资产操作,代理只会返回错误信息。
mod.checkNotBusy = function()local flags = json.encode({
IsSwapping = IsSwapping,
IsDepositing = IsDepositing,
IsWithdrawing = IsWithdrawing,
IsLiquidating = IsLiquidating
})
if IsDepositing or IsWithdrawing or IsLiquidating or IsSwapping then
response.errorMessage(
"error - process is busy with another action on funds" .. flags
)()
end
end
这里的示例展示了如何使用检查来实现类似互斥锁的行为:
Handlers.add(
"withdrawQuoteToken",
Handlers.utils.hasMatchingTag("Action", "WithdrawQuoteToken"),
function(msg)
permissions.onlyOwner(msg)
status.checkNotBusy()
progress.startWithdrawal(msg)
withdrawals.withdrawQuoteToken(msg)
end
)
更优化的行为是对触发资产操作的传入消息进行排队,而不是拒绝它们。这种变化将减轻触发实体重新尝试操作的频率,提升用户体验和系统效率。
在为 AO 平台开发 JavaScript 时,需要注意没有直接订阅 AO 事件的方法——这是以太坊虚拟机开发中才有得常用功能,通过智能合约事件和 JavaScript 库(如 web3 和 ethers)实现。
对于我们的 DCA 代理 UI,一种方法涉及轮询 Arweave 网关端点,以获取带有特定标签的 AO 消息。然而,考虑到我们的代理在其进程内存中有大量重要的状态管理,我们选择直接从代理进程通过dryRun
调用轮询更新。这个选择使我们的 UI 能够更准确地反映代理的实时状态,尽管需要额外的开销来管理 React Hook 以检测状态变化。目前,鉴于代理的复杂程度,这种复杂性是可管理的。
对于 DCA 代理,特别是对于更复杂的代理,保持对其操作相关数据的实时感知至关重要。例如,一个代理需要知道特定代币的余额,以便根据传入信号做出及时决策。
虽然代理可能会本地跟踪这种状态,但它也必须警惕由于分布式计算平台固有的无序性可能导致的潜在不一致。让数据随时可用有助于做出有效的实时决策,尽管代理必须不断检查和协调任何不一致之处。
考虑一个例子,其中一个代理被编程来响应两种类型的信号,A 和 B。信号A 表示异常的、高利润的机会,而 信号B 则定期发生。当检测到 信号A 时,代理应该使用其资金执行特定的交换。然而,这些资金也被用于由 信号B 触发的活动。挑战在于在为 信号A 最大化资金使用的同时,确保为 信号B 留有足够的储备。
这种情况强调了代理需要准确了解其代币余额的必要性,这可能因未记录的存款或第三方可能的取款而波动,如果 AO 代币进程曾支持类似于 ERC20 代币的批准。
尽管这个具体的挑战只出现过一次,但它代表了一个真实的机会损失风险,可能影响代理的竞争优势。因此,解决这个问题至关重要。
Credit-Notice
或 Debit-Notice
,并回应一个 {"Action" : "Balance"}
消息来更新其余额。然后,它根据带有“Balance”标签的响应来更新其内部余额。然而,由于缺乏有序消息保证,存在响应可能不对应最近的余额查询的风向。这可能错误地影响到本地镜像的余额。
我们的方法通过跟踪每次余额更新的时间戳来缓解这个问题。如果新的更新时间戳不晚于上次的更新时间戳,则代理会忽略这个更新,确保余额信息的完整性和准确性。
mod.latestBalanceUpdateBaseToken = function(msg)-- balance responses may come in any order, so we disregard delayed ones (possibly stale values)
if (msg.Timestamp > LatestBaseTokenBalTimestamp) then
LatestBaseTokenBal = msg.Balance
LatestBaseTokenBalTimestamp = msg.Timestamp
ao.send({ Target = Backend, Action = "UpdateBaseTokenBalance", Balance = msg.Balance })
end
end
在典型的去中心化金融平台中,DCA 代理对交换时间和金额的可预测性可能会引发前置运行的担忧,特别是在全局状态虚拟机中,交易可以被精确排序以利于攻击者,从而实施诸如夹击攻击之类的战术。然而,在 AO 上,情况则完全不同。
由于缺乏关于交易排序的保证,因此在 AO 上认为前置运行不太可能发生。尽管攻击者可能会尝试通过在 DCA 代理预定的 swap 交易周围安排其交易,来执行夹击攻击,但这些操作对其他人是可见的,后者可能会自行进行前置运行攻击,从而抵消攻击的盈利性。与传统 DeFi 设置相比,这种固有风险使得在 AO 上前置运行不是一种有效的策略。
在确定交换的预期输出时,AMM 池本身可以作为 AO 上定价的可靠来源。这与基于EVM的系统形成对比,后者在交换执行时依赖于池的价格,可能会让用户更容易被前置运行者利用。
在具有全局状态虚拟机的平台上,DeFi 应用程序通常依赖于外部预言机来减轻与操纵定价相关的风险。这些预言机本身则成为潜在的攻击向量。考虑到在 AO 上前置运行不太可能发生,因此对外部价格预言机的需求降低了,使我们能够直接依赖 AMM 池价格,而无需采纳其他 DeFi 环境中常见的外部预言机最佳实践。
设计代理进程的一个关键挑战围绕着对传入消息的编码和处理。准确识别每条消息的目的对于适当的响应行动至关重要。
来自Quote Token进程的Credit-Notice
可能表示几种情况:
为了解决这种复杂性,我们选择了增强处理程序内部匹配函数的粒度。这种方法使我们能够:
消息处理的区分确保了每个动作都被准确处理,提升了代理操作的整体稳健性和清晰度。
-- process.lua
--[[
This file defines all the handlers.
It should give a clear overview for understanding
everything the process can do in terms of handling messages
--]]
--[[
The handler below matches Credit-Notice messages from the BaseToken,
but is not the only one doing so ==>> we use the patterns.continue()
wrapper to allow for matching of other handlers, too
]]
Handlers.add(
"balanceUpdateCreditBaseToken",
patterns.continue(function(msg)return Handlers.utils.hasMatchingTag("Action", "Credit-Notice")(msg)
and msg.From == BaseToken
end),
balances.balanceUpdateCreditBaseToken
)
--[[
The handler below is another Credit-Notice matcher for messages from
BaseToken. It uses an imported function to perform a match,
but keeps the matching of the 'Action' Tag right here in explicit form,
so that we can rapidly find all the 'Credit-Notice' handlers of the process.
Furthermore, using the patterns.continue() wrapper has the effect that this
--]]
Handlers.add(
'swapBackErrorByPool',
function(msg)return Handlers.utils.hasMatchingTag('Action', 'Credit-Notice')(msg)
and liquidation.isSwapBackErrorByRefundCreditNotice(msg)
end,
progress.concludeLiquidationOnErrorByRefundCreditNotice
)
-- liquidation.lua
mod.isSwapBackErrorByRefundCreditNotice = function(msg)return msg.From == BaseToken
and msg.Sender == Pool
and msg.Tags["X-Refunded-Transfer"] ~= nil
end
在开发我们的代理时,我们策略性地决定省略一些通常用于在 AO 上创建生产级代理的最佳实践。这样做是为了集中我们的精力有效解决更具挑战性的问题。
msg.From
,PatternFunction
等)。这些省略是为了专注于提供一个功能原型,以解决核心功能,同时承认未来可以进行改进的领域。
感谢您抽出时间来探索我们在 AO 上开发 DCA 代理的复杂性。本文旨在公示我们的开发过程,分享我们当前的成就和我们计划改进的领域。我们希望这些见解能够增进对去中心化代理开发中挑战和潜在解决方案的深入理解,推动 AgentFi 领域的发展。
请继续关注我们随着技术的不断完善和新可能性的探索。我们致力于推进这一领域并分享我们的经验教训。
要获取更多信息,贡献意见或了解我们的进展,请访问以下资源:
我们对未来充满期待。请继续期待我们有关推动去中心化金融的边界更详细的文章和更新!
关于 PermaDAO:Website | Twitter | Telegram | Discord| Medium | Youtube
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!