从概念到实施:在Sui上实现共享托管

92 篇文章 0 订阅
18 篇文章 0 订阅

本文作者:Kevin | Aftermath

定期定额投资(Dollar-cost averaging,DCA)是一种投资策略,你可以设置定期将资金交易成所需资产。这可能听起来有些复杂,但关键在于“频率”。

将这些交易自动化是至关重要的,手动进行交易会削弱策略的有效性,因为它要求在规定的时间间隔内始终保持交易的可用性。这导致了一个显而易见的结论:

DCA从根本上受益于多方共享访问同一状态的能力。

这一问题不仅仅存在于DCA中,大多数DeFi应用程序本质上都需要多个参与方共享对链上状态的访问。例如,如果借贷协议中的用户头寸仅在清算时用户自己才能访问,如何能够及时进行清算呢?而在DCA中,你需要一组实体能够写入共享状态,具体来说是用户和代表用户进行交易的次要地址。

共享托管

在基于账户的数据模型中,解决方案很简单:你只需授予另一个合约或地址访问你资金的权限(即Token Allowance)。然而,在Sui的基于对象的数据模型中,过程就没那么直接了,从而导致多种潜在的解决方案,每种解决方案都有其优缺点。

共享即关爱

在Sui上实施共享托管关系的最直接方式是通过对象所有权模型:使用共享对象。自然地,如果多个写入者需要访问某个对象,那么它就不能仅由一个地址拥有。如果只有一个地址拥有对象并能够用它发起交易,其他地址如何与其交互呢?显而易见的解决方案是共享对象,以便双方都能访问它。

虽然这解决了我们主要的访问问题,但它引入了两个全新的问题:

  • 任何人现在都可以与对象交互,
  • 他们可以在没有限制的情况下进行交互。

这显然不是我们想要的。通过实施直接的解决方案,你可以通过两个步骤来解决:

1. 将共享状态(例如用户希望从中进行DCA的资金)封装到一个自定义对象中,并共享该自定义对象。这种方法允许你定义一组严格的规则来规定如何与自定义对象交互,例如可以提取多少资金及其频率。在DCA的语境中,我们将这种自定义对象称为订单(Order) — — 我将从这里开始使用这个术语。

/// Encapsulates a user's DCA order; holds the user's `Balance<CI>` until it is 
/// time to trade with it.
public struct Order<phantom CI, phantom CO> has key {
    id: UID,

    /// The user's coin that is still remaining to be swapped into `Coin<CO>`.
    balance: Balance<CI>,
  
    /// Other relevant fields.
    ...
}

/// Creates, and subsequently shares, a DCA `Order` object to hold a user's funds.
public fun new<CI, CO>(
 coin_in: Coin<CI>,
    ...
   ctx: &mut TxContext,
) {
 // i. Wrap the user's funds into the `Order` object.
   let order = Order<CI, CO>{
     id: 0x2::object::new(ctx),
       balance: coin_in.into_balance(),
     ...
   };
  
    // ii. Share the `Order` object to enable multiple writers to access it. 
    0x2::transfer::share_object(order);
}

/// Withdraw funds from the `Order`. Can only be called on a Sunday.
public fun withdraw<CI, CO>(
 order: &mut Order<CI, CO>,
   clock: &Clock,
   amount: u64,
   ...
   ctx: &mut TxContext,
): Coin<CI> {
   let days_since_unix_epoch = clock.timestamp_ms() / 86400;
}

片段 1:自定义订单对象和相关的构造函数。

片段1展示了 (1) 的一个示例实现,其中包含了一个自定义的 withdraw 函数,用于限制该函数的调用时间。要创建一个 Order 对象,用户需要传入他们想要交易的资金。提供的 Coin<CI> 将被转换为 Balance<CI> ,然后由 Order 对象包装后再共享。

尽管 withdraw 现在确保在有效的情况下被调用,但我们仍然需要解决谁有权调用它的问题。

2. 创建自定义权限对象,赋予其所有者与特定 Order 交互的权限(例如,在需要交易时提取资金)。然后双方将分别收到一个权限对象,从而赋予他们对 Order 的独占许可访问权。

/// Capability object that grants the owner the ability to withdraw funds from a
/// corresponding `Order` object.
public struct OwnerCap has key {
    id: UID,
  
   /// The ID of the associated `Order` object. Allows validating that an
   /// `OwnerCap` is interacting with the correct `Owner` object.
    order_id: ID,
}

/// Creates a DCA `Order` object to hold a user's funds and subsequently shares
/// it. Also creates two `OwnerCap` objects for the two entities that need 
/// permissioned access to the created `Object`.
public fun new<CI, CO>(
 coin_in: Coin<CI>,
   other: address,
    ...,
   ctx: &mut TxContext,
) {
 // i. Wrap the user's funds into the `Order` object.
   let order = Order<CI, CO>{
     id: 0x2::object::new(ctx),
       balance: coin_in.into_balance(),
     ...
   };

   let order_id = order.id.to_inner();
    // ii. Share the `Order` object to enable multiple writers to access it. 
    0x2::transfer::share_object(order);
  
   // iiia. Grant the user permission to interact with the `Order`.
   let owner_cap = OwnerCap {
       id: 0x2::object::new(ctx),
       order_id,
    };
  
   0x2::transfer::transfer(owner_cap, ctx.sender());
  
    // iiib. Grant the `other` address permission to interact with the `Order`.
   let owner_cap = OwnerCap {
       id: 0x2::object::new(ctx),
       order_id,
    };
  
   0x2::transfer::transfer(owner_cap, other);
}

/// Withdraw funds from the `Order`. Can only be called on a Sunday.
public fun withdraw<CI, CO>(
 order: &mut Order<CI, CO>,
   owner_cap: &OwnerCap,
   clock: &Clock,
   amount: u64,
   ...,
}

片段2:扩展Order对象并添加关联的OwnerCap能力对象。

片段2在片段1的基础上进行了扩展,在创建 Order 对象时生成了两个 OwnerCap 能力对象,并将它们转移给将共享该 Order 监护权的实体。现在,(1) 和 (2) 所需的对象已经定义,您可以通过接受Order及其关联的 OwnerCap 作为输入,来创建任何所需的权限函数。 withdraw 也已按照这种方式进行了更新。

就这样,您现在有了一个理想的设置,只有这两个实体可以访问共享的状态。这个方案简单、直接且易于实现。那么……

如何实施?

尽管上面的设计看似简单,但它有两个显著的缺点,涉及gas费网络拥堵

Gas: 每个创建的DCA订单需要三个对象来指定一个简单的两人访问控制列表。¹ 仅仅管理这些对象就增加了复杂性,更不用说在所有DCA函数中引入另一个对象作为输入所增加的gas成本了。² 直观地说,这些对象只需在DCA订单的生命周期内存在,并且应在之后销毁以回收其存储补贴。如果任何一个对象未被销毁,很容易导致SUI的损失。DCA的全部意义在于设置后无需再操心。作为用户,我不希望在DCA订单结束后还需要执行一个后续交易来回收存储补贴。

¹ 针对这一特定问题,一个简单的反驳可能会建议将所有需要与对象交互的地址列入白名单,从而消除对两个能力对象的需求。然而,这种方法不够灵活,仍然需要额外的计算来验证正确的地址,并且与Sui的对象中心性质有些背离。此外,它并没有解决下一个问题。

² 你可能会认为这只是gas成本的边际增加。对于一次交易来说,你的想法没错。但是请记住,我们不是为少数用户使用几次产品,而是为零售用户和机构多年来的使用而设计产品。减少所有DCA交易的gas成本随着时间的推移会累积成可观的节省。

网络拥堵:使用共享对象引入了另一个挑战:共识。Sui的共识机制从根本上驱动了涉及共享对象的交易调度;当两个影响同一个共享对象的交易被提交执行时,共识机制将决定它们的执行顺序。在步骤 (1) 中,我们创建了一个自定义的 Order 对象来持有我们想要交易的资金,在步骤 (2) 中,我们限制了该对象的使用方式。这确保了对象资金的安全性,但并未限制该对象作为输入的使用方式或地点。任何人都可以创建另一个move函数,该函数接受对自定义对象的可变引用,并反复调用这个函数,导致对热门对象的拥堵增加。虽然资金不会面临风险,但这可能被用作一种潜在的DDoS攻击向量,通过延迟共识过程中DCA交易的排序来实现攻击。³

值得注意的是,这并不是Sui独有的问题;在任何需要多个参与方对共享状态进行写入访问的去中心化、分布式环境(如区块链)中,这都是一个存在的问题。至少从应用开发者的角度来看,这没有简单的解决方法。

³ 这种攻击的影响看起来可能微不足道,但在市场高度波动期间以及交易资金量增加时,它的影响会显著增长。想象一个场景:一个巨鲸试图将大量的meme币DCA转换为更稳定的资产,而此时meme币的价格开始下跌。一个恶意方可以在链上查看DCA订单的参数后,利用这个攻击向量来延迟DCA交易的执行时间,从而先行卖出他们的meme币。无论如何,当用户决定进行DCA时,他们信任其订单会按指定的频率执行;任何偏离这种频率的情况都会削弱用户与协议之间的信任。

我们能做得更好吗?

这是我们经常问自己的一个问题,在这种情况下,我们确实可以做得更好。Sui的众多独特功能之一是它为涉及独享对象的交易提供的快速路径,目前正在通过新的共识引擎Mysticeti进行增强。如果你的交易只涉及独享对象,你可以使用可靠广播执行来绕过共识。其背后的逻辑是,由于状态仅由单一实体控制和访问,因此不再需要共识来排序触及该独享对象的交易;实体可以自行建议交易顺序。这赋予了Sui一个非常独特的特性:

独享对象可以避免因拥堵队列而导致的执行延迟。

如果我们能够在保留独享对象优势的同时,仍然允许多个实体对一个对象进行权限访问,那不是很棒吗?

Aftermath的解决方案

让我们重新审视我之前提到的一句话。

“自然地,如果多个写入者需要访问一个对象,那么它不能由单一地址拥有。”

从技术上讲,这个说法是正确的。验证节点对传入的交易进行一系列有效性检查,包括验证独享对象是否确实由发送者拥有。如果你尝试执行涉及由另一个地址独享对象的交易,它将无法通过验证节点的检查。因此,当多个唯一地址需要与一个对象交互时,该对象不能由单一实体拥有。

但请听我说:如果多个实体可以访问独享该对象的地址呢?通过这种方式,你可以保留独享对象的优势,同时消除上面提到的与gas和拥堵相关的低效问题,并实现共享托管模型。这听起来可能好得令人难以置信。幸运的是Sui提供了一种原生机制,正是为此设计的:k-of-n多签(multisig)交易

多重签名:我们的共享托管问题解决方案变得非常简单:没有权限对象,没有共享对象,只有我们在 (1) 中指定的独享对象。创建该对象后,我们将其转移到一个唯一可派生的每地址1-of-2多签(multisig)中,两个所需的实体 — — 终端用户和次要地址 — — 是多签背后的同等权重签名者。

可视化1-of-2多签模型

于是,你现在拥有了理想的设置,只有两个参与方可以访问共享状态。这个设计简单、直接,而且相对容易实现。那么,问题出在哪儿呢?

从用户的角度来看,几乎没有任何缺点;你可以享受到我上面提到的所有好处,而没有任何不利之处。当然,这样说有些天真,因为没有任何设计是没有局限的。不过,重要的是,我们的设计并没有影响或恶化用户体验。

从我们的角度来看,还有一些复杂性需要我们去解决,稍后我会详细说明。但首先……

是否安全?

Aftermath的所有产品一样,我们优先考虑用户的安全性和透明度。无论我们选择哪种共享保管设计,我们的目标都是确保用户的资金不会遭受任何恶意行为。我们的实现方案 (2) 以及许多其他设计,将写入权限实体的范围缩小到仅需的两个。然而,从用户的角度来看,对于某些操作来说,这可能仍然多了一个。

虽然熟悉我的人可以相信我不会做出恶意行为,但我不希望我们的用户必须依赖这种假设。这引导我们遵循一个核心原则,并将其嵌入到所有产品中:

在与协议互动时,用户不应假设信任;相反,功能应该在链上被非常清晰透明地执行。

因此,我们严格限制与自定义 Order 对象(即实现方案 (1))的互动。DCA交易只能在用户定义的时间间隔后执行,资金始终发送到用户定义的地址,而 Order 对象与多签地址绑定,从而消除了其被转移到用户控制范围之外的风险。即使在像DCA这样需要大量链下实现的领域,我们也可以通过在链上建立非常透明的访问控制列表,为用户资金的安全提供严格的保障。

设计考量

作为实现者,我们在设计中需要解决哪些复杂性问题?

可访问性

确保 Order 对象始终由多签地址拥有变得至关重要。将其转移到其他地址可能会阻止用户和/或第二个地址与该对象进行交互,进而可能导致资金的完全丧失。我们通过两种不同的方式提供这一保障:

  • 首先,我们在链上推导多签地址,而不是将其作为从链下传入的输入,以验证 Order 对象始终直接转移到与用户关联的多签地址。
  • 一旦多签地址拥有了该对象,我们需要确保它始终保留在这个地址内。在Sui上,这很容易实现。与实现方案 (1) 类似,我们的自定义对象没有存储能力,因此除非提供自定义转移函数,否则它无法被转移。我们并不提供转移该对象的方法;相反,必须通过取消DCA订单、将底层资金转移到新地址并重新创建订单来模仿转移。不过值得注意的是,只要链上推导出多签地址,确保该对象始终留在1-of-2多重签名生态内,实际上可以提供转移功能(我们有一个,但它不是公开的⁴)。

满足这两个保障后, Order 对象在创建后将绑定到多签地址,无法再被转移。

⁴ 我们决定将转移功能保持私密有重要原因。转移 Order 对象属于终端用户应独占权限的功能类别。由于我们无法区分哪一个多签者签署了转移,因此无法限制该功能只能由终端用户发起。仅因为这个原因,我们不允许转移。

Gas管理

鉴于每个用户都有唯一关联的多签地址,我们需要在潜在无限多的地址上维持SUI的余额以支付Gas费。虽然这并非完全准确,但你可以看到这样很快变得难以管理。我们的解决方案是引入第三个Sui原语:赞助交易

通过使用赞助交易,我们只需在少数地址上维持Gas,前提是我们使用这些地址来赞助每一笔执行的DCA交易。这显著减少了我们需要积极管理的账户范围到一个有限的集合,并且减少了我们在任何时候为Gas保留的SUI数量。尽管我在这里简化了我们的解决方案,但理解赞助交易的角色是最重要的。

解决模棱两可

Sui的无共识快速路径引入了一个独特的问题:当一个账户发起两个相互冲突的独享对象交易时会发生什么。在共识支持的方法中,一个交易将排在另一个交易之前。

由于快速路径缺乏共识,最终可能会有一半的验证节点集知道并准备两个交易中的一个,而另一半则为第二个交易做同样的事情。这会导致所涉及的自有对象被锁定,直到在epoch改变协议期间解决模棱两可的问题。

由于从共享对象迁移到独享对象,这种情况在我们的DCA实现中是可能的。实际上,这种情况只会在用户试图在调用DCA交易的同一时间取消 Order 时发生。虽然发生这种情况的可能性很小,但这是我们需要考虑和缓解的问题。

有很多不同的方法可以解决模棱两可的风险,每种方法都会产生不同程度的缓解。我们的解决方案旨在彻底消除常见情况下的这一问题:当用户想要取消DCA Order 时,我们会调用后端。这样我们就可以检查 Order 是否正在执行,如果正在执行,则延迟提交取消交易,直到 Order 解锁。如果 Order 当前处于空闲状态,我们会将其锁定并继续取消。

我们解决这三个问题的方式确保了我们的用户体验保持安全、透明和高效;这正是我们部署这种共享托管模式所要实现的目标。

结论

简而言之,通过利用Sui上的几个原生功能 — — 多签交易、可编程交易区块、赞助交易和Sui的快速路径 — — 我们能够提供更安全、更高效的用户体验。我们并没有重新发明轮子,只是将我们拥有的原生功能拼凑在一起。

这是我特别引以为豪的一个实现细节,因为我一直认为,DeFi应用程序自然需要共享访问链上状态,因此无法从Sui的独享对象中获益太多。⁵ 在我们的案例中,持有共享资金的核心对象仍然完全被拥有,并保留了被拥有的所有好处。虽然你不会看到这种设计扩展到在Sui上创建下一个AMM,但它是Sui构建者在面临共享托管困境时可以利用的另一种设计模式。

⁵ 至少对于需要多个写入器的应用程序中心对象而言;例如池、订单簿、金库等。

DCA只是我们将推出的众多产品中的第一个,我们将在其中应用这种共享托管解决方案。我们期待将这种模式扩展到其他领域,以提供更高级别的用户安全性和Web3中最透明的用户体验。


关于Sui Network

Sui是基于第一原理重新设计和构建而成的L1公有链,旨在为创作者和开发者提供能够承载Web3中下一个十亿用户的开发平台。Sui上的应用基于Move智能合约语言,并具有水平可扩展性,让开发者能够快速且低成本支持广泛的应用开发。获取更多信息:https://linktr.ee/sui_apac

官网英文Twitter中文TwitterDiscord英文电报群中文电报群

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值