Thanos开发故事 — 4

  • Xeoyoung
  • 发布于 2024-08-30 19:17
  • 阅读 1194

该文章介绍了Thanos网络主网启动前,预先部署USDC Bridge合约的过程,以支持Layer 1和Layer 2之间无缝的资产转移。文章详细描述了每个合约的功能,以及开发过程中使用的相关软件包和代码仓库,包括op-bindings和op-chain-ops等。

Thanos 开发故事 — 4

预部署合约:USDC Bridge

在 Thanos 网络主网启动之前,TOP 项目团队正在努力为用户提供改进的和更可靠的服务。

为此,我们正在预部署 USDC Bridge 合约,以支持 Layer 1 和 Layer 2 之间的无缝资产转移。这种预部署确保了在网络上线之前,必要的功能已经到位并保持稳定,从而使我们能够立即为用户提供服务。

这个开发故事涵盖了在 Thanos 网络上预部署合约以支持 USDC Bridge 的过程、每个合约的描述,以及关于开发这些合约的软件包和仓库的信息。

Thanos 开发故事系列

  1. Thanos 基础设施优化
  2. L2 原生 Token 功能
  3. Thanos SDK
  4. 预部署合约:USDC Bridge
  5. 预部署合约:UniswapV3
  6. Thanos 网络的架构
  7. 网络升级:Span batch 和 Proto-danksharding

开发背景

关于 Thanos 网络的详细信息和源代码可以在这里找到,USDC Bridge 预部署的源代码也可以找到。用于预部署的合约位于 tokamak-thanos/packages/tokamak/contracts-bedrock/src/tokamak-contracts/USDC

为 USDC Bridge 预部署开发的代码使用了 op-bindingsop-chain-ops 软件包。

  • op-bindings:此软件包包含合约构建的 GO 绑定,允许基于绑定中包含的字节码在 Go 环境中部署合约。此绑定文件用于动态设置状态中的字节码和存储槽。op-bindings 软件包还包括一个 addresses.go 文件,该文件定义了所有地址以支持鲁棒性和可维护性。
  • op-chains-ops:此软件包负责配置网络运行所需的所有状态,并为 Layer 1 和 Layer 2 创建创世区块。它通过在 Layer 1 部署合约、计算 Layer 2 预部署的实现地址以及创建不可变的值和存储来设置网络中的创世状态。op-chains-ops 软件包包含诸如 config.goimmutables.gosetters.go 之类的文件,这些文件在预部署期间与位于 op-bindings 软件包中的绑定和 addresses.go 结合使用。

预部署的智能合约

为了支持 USDC Bridge,需要各种合约。我们部署了 USDC Bridge 合约以在每一层上交易 USDC,并在 Layer 2 上部署 USDC 以与 Layer 1 上的 USDC 无缝交易。

Layer 1

  • L1UsdcBridge.sol:一个直接管理 USDC 资产从 Layer 1 移动到 Layer 2 的合约。它监听 Layer 2 的 ERC20DepositInitiated 和 ERC20WithdrawalFinalized 事件。
  • L1UsdcBridgeProxy.sol:作为 L1UsdcBridge.sol 的代理,它支持合约的升级和维护。

Layer 2

  • L2UsdcBridge.sol:一个直接管理 USDC 资产从 Layer 2 移动到 Layer 1 的合约。它监听 Layer 1 的 WithdrawalInitiated 和 DepositFinalized 事件。
  • L2UsdcBridgeProxy.sol:作为 L2UsdcBridge.sol 的代理,它支持合约的升级和维护。

Layer 2 - USDC 合约

  • FiatTokenV2_2.sol:它包含 USDC 的基本 token 信息,并根据 ERC-20 标准提供诸如发行、发送和余额跟踪之类的核心功能。
  • FiatTokenProxy.sol:这是一个支持 FiatTokenV2_2.sol 升级和维护的代理。
  • SignatureChecker.sol:此库负责 FiatTokenV2_2.sol 的签名验证。它通过增强 USDC 的安全性并防止非法交易来确保交易的真实性。
  • MasterMinter.sol:它继承自 MintController,通过设置 mint 权限来控制代币发行并对其进行监管。

上述合约的预部署在网络上线之前提供了必要的功能,从而实现了可靠而高效的 USDC 交易和资产管理。

开发流程

预部署合约涉及几个步骤。

首先,我们将描述在 Layer 2 预部署 L2UsdcBridge、L2UsdcBridgeProxy 和 USDC 合约的过程,以及随之而来的代码更改。

  • addresses.go

将合约预部署到 Layer 2 涉及预定义和初始化每个合约的地址。在 Layer 2 预部署的所有合约的地址均以 "0x42" 开头,以方便识别和管理。

此外,合约的处理方式取决于它们是否使用代理模式。不使用代理模式的合约设置 ProxyDisabled: true。涉及 SignatureChecker 和 MasterMinter,并且 ProxyDisabled: true 设置将应用于这些合约。

addresses.go – Medium

const{
L2UsdcBridge="0x4200000000000000000000000000000000000775"
SignatureChecker="0x4200000000000000000000000000000000000776"
MasterMinter="0x4200000000000000000000000000000000000777"
FiatTokenV2_2="0x4200000000000000000000000000000000000778"
}
funcinit() {
Predeploys["L2UsdcBridge"] =&Predeploy{Address: L2UsdcBridgeAddr}
Predeploys["SignatureChecker"] =&Predeploy{Address: SignatureCheckerAddr, ProxyDisabled: true}
Predeploys["MasterMinter"] =&Predeploy{Address: MasterMinterAddr, ProxyDisabled: true}
Predeploys["FiatTokenV2_2"] =&Predeploy{Address: FiatTokenV2_2Addr}
}
// addresses.go 代码示例
const {
    L2UsdcBridge="0x4200000000000000000000000000000000000775"
    SignatureChecker="0x4200000000000000000000000000000000000776"
    MasterMinter="0x4200000000000000000000000000000000000777"
    FiatTokenV2_2="0x4200000000000000000000000000000000000778"
}

func init() {
    Predeploys["L2UsdcBridge"] = &Predeploy{Address: L2UsdcBridgeAddr}
    Predeploys["SignatureChecker"] = &Predeploy{Address: SignatureCheckerAddr, ProxyDisabled: true}
    Predeploys["MasterMinter"] = &Predeploy{Address: MasterMinterAddr, ProxyDisabled: true}
    Predeploys["FiatTokenV2_2"] = &Predeploy{Address: FiatTokenV2_2Addr}
}
  • config.go

此过程在定义 Layer 2 网络的配置设置方面起着重要作用。如果合约具有构造函数,则通过 NewL2ImmutableConfig 定义它,并通过 NewL2StorageConfig 设置需要存储初始化的地址和值。

构造函数特定于 MasterMinter,并且存储设置定义了除了 SignatureChecker 以外的合约所需的地址和初始值。

具体来说,MasterMinter 的构造函数是 _minterManager,它设置了预部署的 FiatTokenV2_2 的地址。

存储定义了初始地址值,例如 l1Usdcl2Usdc 等,以供 L2UsdcBridge 与 Layer 1 上的 UsdcBridge 交互,而 MasterMinter 定义了用于提供权限的地址值,例如 ownercontrollers 等。最后,FiatTokenV2_2 初始化 USDC 的基本信息,例如 namesymboldecimalscurrency 等。

这些设置为每个合约的初始状态及其与 Layer 1 的交互奠定了基础。

config.go – Medium

funcNewL2ImmutableConfig(config*DeployConfig, block*types.Block) (immutables.ImmutableConfig, error) {
immutable:=make(immutables.ImmutableConfig)
immutable["L2UsdcBridge"] = immutables.ImmutableValues{}
immutable["SignatureChecker"] = immutables.ImmutableValues{}
immutable["MasterMinter"] = immutables.ImmutableValues{
"_minterManager": predeploys.FiatTokenV2_2Addr,
}
immutable["FiatTokenV2_2"] = immutables.ImmutableValues{}
returnimmutable, nil
}
...
funcNewL2StorageConfig(config*DeployConfig, block*types.Block) (state.StorageConfig, error) {
storage:=make(state.StorageConfig)
storage["L2UsdcBridge"] = state.StorageValues{
"messenger": predeploys.L2CrossDomainMessengerAddr,
"otherBridge": config.L1UsdcBridgeProxy,
"l1Usdc": config.L1UsdcAddr,
"l2Usdc": predeploys.FiatTokenV2_2Addr,
"l2UsdcMasterMinter": predeploys.MasterMinterAddr,
}
storage["MasterMinter"] = state.StorageValues{
"_owner": config.MasterMinterOwner,
"controllers": map[any]any{
predeploys.L2UsdcBridgeAddr: predeploys.L2UsdcBridgeAddr,
},
"minterManager": predeploys.FiatTokenV2_2Addr,
}
storage["FiatTokenV2_2"] = state.StorageValues{
"_owner": config.FiatTokenOwner,
"pauser": config.NewPauser,
"blacklister": config.NewBlacklister,
"name": config.UsdcTokenName,
"symbol": "USDC.e",
"decimals": 6,
"currency": "USD",
"masterMinter": predeploys.MasterMinterAddr,
"initialized": true,
"_initializedVersion": 3,
}
returnstorage, nil
}
// config.go 代码示例
func NewL2ImmutableConfig(config *DeployConfig, block *types.Block) (immutables.ImmutableConfig, error) {
    immutable := make(immutables.ImmutableConfig)

    immutable["L2UsdcBridge"] = immutables.ImmutableValues{}
    immutable["SignatureChecker"] = immutables.ImmutableValues{}
    immutable["MasterMinter"] = immutables.ImmutableValues{
        "_minterManager": predeploys.FiatTokenV2_2Addr,
    }
    immutable["FiatTokenV2_2"] = immutables.ImmutableValues{}

    return immutable, nil
}

func NewL2StorageConfig(config *DeployConfig, block *types.Block) (state.StorageConfig, error) {
    storage := make(state.StorageConfig)

    storage["L2UsdcBridge"] = state.StorageValues{
        "messenger":          predeploys.L2CrossDomainMessengerAddr,
        "otherBridge":        config.L1UsdcBridgeProxy,
        "l1Usdc":             config.L1UsdcAddr,
        "l2Usdc":             predeploys.FiatTokenV2_2Addr,
        "l2UsdcMasterMinter": predeploys.MasterMinterAddr,
    }
    storage["MasterMinter"] = state.StorageValues{
        "_owner":       config.MasterMinterOwner,
        "controllers": map[any]any{
            predeploys.L2UsdcBridgeAddr: predeploys.L2UsdcBridgeAddr,
        },
        "minterManager": predeploys.FiatTokenV2_2Addr,
    }
    storage["FiatTokenV2_2"] = state.StorageValues{
        "_owner":            config.FiatTokenOwner,
        "pauser":           config.NewPauser,
        "blacklister":          config.NewBlacklister,
        "name":           config.UsdcTokenName,
        "symbol":         "USDC.e",
        "decimals":       6,
        "currency":         "USD",
        "masterMinter":   predeploys.MasterMinterAddr,
        "initialized":        true,
        "_initializedVersion": 3,
    }
    return storage, nil
}

  • immutables.go

immutables.go 负责处理将在 Layer 2 网络上预部署的合约中的不可变变量。

PredeploysImmutableConfig 结构定义了在 Layer 2 使用的预部署合约初始化所需的数据结构。

对于 MasterMinter,它被定义为具有 MinterManager 字段的结构,该字段表示 MasterMinter 合约初始化所需的地址。其他合约被定义为空结构。

immutables.go – Medium

typePredeploysImmutableConfigstruct {
L2UsdcBridgestruct{}
SignatureCheckerstruct{}
MasterMinterstruct {
MinterManager common.Address
}
FiatTokenV2_2struct{}
}
// immutables.go 代码示例
type PredeploysImmutableConfig struct {
    L2UsdcBridge     struct{}
    SignatureChecker struct{}
    MasterMinter     struct {
        MinterManager common.Address
    }
    FiatTokenV2_2    struct{}
}

l2ImmutableDeployer 函数负责部署预部署合约中包含不可变变量的合约。

deployment.Args[0] 表示构造函数,它检查 _minterManager(MasterMinter 的构造函数)的地址是否为 common.Address 类型。

如果类型正确,它将调用 bindings.DeployMasterMinter 函数来部署 MasterMinter 合约,使用 _minterManager 地址作为初始化参数。

immutables.go – Medium

// l2ImmutableDeployer will deploy L2 predeploys that contain immutables to the simulated backend.
// It only needs to care about the predeploys that have immutables so that the deployed bytecode
// has the dynamic value set at the correct location in the bytecode.
funcl2ImmutableDeployer(backend*backends.SimulatedBackend, opts*bind.TransactOpts, deployment deployer.Constructor) (*types.Transaction, error) {
case"MasterMinter":
_minterManager, ok:=deployment.Args[0].(common.Address)
if!ok {
returnnil, fmt.Errorf("invalid type for _minterManager")
}
_, tx, _, err=bindings.DeployMasterMinter(opts, backend, _minterManager)
}
// immutables.go 代码示例
// l2ImmutableDeployer 将把包含 immutables 的 L2 预部署合约部署到模拟后端。
// 它只需要关心那些具有 immutables 的预部署合约,以便部署的字节码
// 在字节码中的正确位置设置了动态值。
func l2ImmutableDeployer(backend *backends.SimulatedBackend, opts *bind.TransactOpts, deployment deployer.Constructor) (*types.Transaction, error) {
    case "MasterMinter":
        _minterManager, ok := deployment.Args[0].(common.Address)
        if !ok {
            return nil, fmt.Errorf("invalid type for _minterManager")
        }
        _, tx, _, err = bindings.DeployMasterMinter(opts, backend, _minterManager)
    }
}
  • setters.go

setters.go 负责特定代理合约的管理设置。

L2UsdcBridgeProxy 和 FiatTokenProxy 使用不同的代理实现,因此 AdminSlotImplementationSlot 的地址不同。L2UsdcBridgeProxy 使用与 Thanos 网络使用的代理相同的插槽地址,而 FiatTokenProxy 使用与 Zepplin 库关联的插槽地址。

setters.go – Medium

var (
// ImplementationSlot represents the EIP 1967 implementation storage slot
ImplementationSlot=common.HexToHash("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
// AdminSlot represents the EIP 1967 admin storage slot
AdminSlot=common.HexToHash("0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103")
// implementationSlot represents the org.zeppelinos.proxy.implementation storage slot
ImplementationSlotForZepplin=common.HexToHash("0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3")
// implementationSlot represents org.zeppelinos.proxy.admin storage slot
AdminSlotForZepplin=common.HexToHash("0x10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b")
)
// setters.go 代码示例
var (
    // ImplementationSlot represents the EIP 1967 implementation storage slot
    ImplementationSlot        = common.HexToHash("0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
    // AdminSlot represents the EIP 1967 admin storage slot
    AdminSlot               = common.HexToHash("0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103")
    // implementationSlot represents the org.zeppelinos.proxy.implementation storage slot
    ImplementationSlotForZepplin = common.HexToHash("0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3")
    // implementationSlot represents org.zeppelinos.proxy.admin storage slot
    AdminSlotForZepplin        = common.HexToHash("0x10d6a54a4754c8869d6886b5f5d7fbfa5b4522237ea5c60d11bc4e7a1ff9390b")
)

setProxies 函数在 setters.go 中将代理合约的字节码部署到 addresses.go 中定义的预部署地址。这将设置代理模式以将调用委托给实际的实现合约。

具体来说,predeploys.L2UsdcBridgeAddr 地址使用 SetCode 函数定义了 L2UsdcBridgeProxy 的字节码,并使用 SetState 函数存储了 AdminSlot 地址,以授予管理合约的权限。类似地,对于 predeploys.FiatTokenV2_2Addr 地址,我们设置了 FiatTokenProxy 的字节码,并为其提供了 AdminSlotForZepplin 地址以设置其代理权限。

这就是每个代理合约的字节码如何分发、进行初始设置和授权管理的方式。

setters.go – Medium

funcsetProxies(db vm.StateDB, proxyAdminAddr common.Address, namespace*big.Int, countuint64) error {
depBytecode, err:=bindings.GetDeployedBytecode("Proxy")
iferr!=nil {
returnerr
}
iflen(depBytecode) ==0 {
returnerrors.New("Proxy has empty bytecode")
}
l2UsdcBridgeProxyBytecode, err:=bindings.GetDeployedBytecode("L2UsdcBridgeProxy")
iferr!=nil {
returnerr
}
iflen(l2UsdcBridgeProxyBytecode) ==0 {
returnerrors.New("the contract L2UsdcBridgeProxy has empty bytecode")
}
fiatTokenProxyBytecode, err:=bindings.GetDeployedBytecode("FiatTokenProxy")
iferr!=nil {
returnerr
}
iflen(fiatTokenProxyBytecode) ==0 {
returnerrors.New("the contract FiatTokenProxy has empty bytecode")
}
fori:=uint64(0); i<=count; i++ {
bigAddr:=new(big.Int).Or(namespace, new(big.Int).SetUint64(i))
addr:=common.BigToAddress(bigAddr)
if!db.Exist(addr) {
db.CreateAccount(addr)
}
switchaddr {
casepredeploys.L2UsdcBridgeAddr:
db.SetCode(addr, l2UsdcBridgeProxyBytecode)
db.SetState(addr, AdminSlot, eth.AddressAsLeftPaddedHash(proxyAdminAddr))
casepredeploys.FiatTokenV2_2Addr:
db.SetCode(addr, fiatTokenProxyBytecode)
db.SetState(addr, AdminSlotForZepplin, eth.AddressAsLeftPaddedHash(proxyAdminAddr))
default:
db.SetCode(addr, depBytecode)
db.SetState(addr, AdminSlot, eth.AddressAsLeftPaddedHash(proxyAdminAddr))
}
log.Trace("Set proxy", "address", addr, "admin", proxyAdminAddr)
}
returnnil
}
// setters.go 代码示例
func setProxies(db vm.StateDB, proxyAdminAddr common.Address, namespace *big.Int, count uint64) error {
    depBytecode, err := bindings.GetDeployedBytecode("Proxy")
    if err != nil {
        return err
    }
    if len(depBytecode) == 0 {
        return errors.New("Proxy has empty bytecode")
    }

    l2UsdcBridgeProxyBytecode, err := bindings.GetDeployedBytecode("L2UsdcBridgeProxy")
    if err != nil {
        return err
    }
    if len(l2UsdcBridgeProxyBytecode) == 0 {
        return errors.New("the contract L2UsdcBridgeProxy has empty bytecode")
    }

    fiatTokenProxyBytecode, err := bindings.GetDeployedBytecode("FiatTokenProxy")
    if err != nil {
        return err
    }
    if len(fiatTokenProxyBytecode) == 0 {
        return errors.New("the contract FiatTokenProxy has empty bytecode")
    }

    for i := uint64(0); i &lt;= count; i++ {
        bigAddr := new(big.Int).Or(namespace, new(big.Int).SetUint64(i))
        addr := common.BigToAddress(bigAddr)

        if !db.Exist(addr) {
            db.CreateAccount(addr)
        }

        switch addr {
        case predeploys.L2UsdcBridgeAddr:
            db.SetCode(addr, l2UsdcBridgeProxyBytecode)
            db.SetState(addr, AdminSlot, eth.AddressAsLeftPaddedHash(proxyAdminAddr))
        case predeploys.FiatTokenV2_2Addr:
            db.SetCode(addr, fiatTokenProxyBytecode)
            db.SetState(addr, AdminSlotForZepplin, eth.AddressAsLeftPaddedHash(proxyAdminAddr))
        default:
            db.SetCode(addr, depBytecode)
            db.SetState(addr, AdminSlot, eth.AddressAsLeftPaddedHash(proxyAdminAddr))
        }

        log.Trace("Set proxy", "address", addr, "admin", proxyAdminAddr)
    }

    return nil
}
  • layer_two.go

layer_two.go 中,我们定义了构建 Layer 2 创世区块的过程涉及的函数。

如你在下面的代码中看到的,layer_two.go 调用 NewL2StorageConfigNewL2ImmutableConfig 函数来配置存储和配置 immutable 的设置。它还以规定的格式定义了每个代理合约和实现合约的地址。实现合约以 “0xc0D3C0d3C0d3C0d3C0d3C0d3C0d3C0d3C0d3C0d3C0d30000” 这样的地址格式部署。

L2UsdcBridge: 0xC0D3c0D3c0D3C0D3c0D3c0d3C0D3c0d3c0D30775
L2UsdcBridgeProxy: 0x4200000000000000000000000000000000000775

FiatTokenV2_2: 0xc0D3c0d3C0D3C0d3C0D3C0D3C0D3c0D3C0D30778
FiatTokenProxy: 0x4200000000000000000000000000000000000778

SignatureChecker 是 FiatTokenV2_2 中的一个库。库的特点是在分发时,它们的字节码中包含了它们的分发地址。但是,无法在预部署过程中修改库的字节码,因此我们需要一种将 SignatureChecker 的预部署地址硬编码到字节码中的方法。为此,我们使用 case 语句将 SignatureChecker 的预发布地址插入到字节码中。

layer_two.go – Medium

// BuildL2Genesis will build the L2 genesis block.
funcBuildL2Genesis(config*DeployConfig, l1StartBlock*types.Block) (*core.Genesis, error) {
genspec, err:=NewL2Genesis(config, l1StartBlock)
iferr!=nil {
returnnil, err
}
db:=state.NewMemoryStateDB(genspec)
ifconfig.FundDevAccounts {
log.Info("Funding developer accounts in L2 genesis")
FundDevAccounts(db)
}
ifconfig.SetPrecompileBalances {
log.Info("Setting precompile balances in L2 genesis")
SetPrecompileBalances(db)
}
storage, err:=NewL2StorageConfig(config, l1StartBlock)
iferr!=nil {
returnnil, err
}
immutableConfig, err:=NewL2ImmutableConfig(config, l1StartBlock)
iferr!=nil {
returnnil, err
}
// Set up the proxies
err=setProxies(db, predeploys.ProxyAdminAddr, BigL2PredeployNamespace, 2048)
iferr!=nil {
returnnil, err
}
// Set up the implementations that contain immutables
deployResults, err:=immutables.Deploy(immutableConfig)
iferr!=nil {
returnnil, err
}
forname, predeploy:=rangepredeploys.Predeploys {
ifpredeploy.Enabled!=nil&&!predeploy.Enabled(config) {
log.Warn("Skipping disabled predeploy.", "name", name, "address", predeploy.Address)
continue
}
codeAddr:=predeploy.Address
switchname {
case"SignatureChecker":
bytecode, err:=bindings.GetDeployedBytecode(name)
iferr!=nil {
returnnil, err
}
a:=bytecode[:1]
b:=bytecode[21:]
c:= hexutil.Bytes{}
d, _:=hex.DecodeString("4200000000000000000000000000000000000776")
c=append(c, a...)
c=append(c, d...)
c=append(c, b...)
bytecode=c
deployResults[name] =bytecode
case"FiatTokenV2_2":
codeAddr, err=AddressToCodeNamespace(predeploy.Address)
iferr!=nil {
returnnil, fmt.Errorf("error converting to code namespace: %w", err)
}
db.CreateAccount(codeAddr)
db.SetState(predeploy.Address, ImplementationSlotForZepplin, eth.AddressAsLeftPaddedHash(codeAddr))
log.Info("Set proxy for FiatTokenV2_2", "name", name, "address", predeploy.Address, "implementation", codeAddr)
default:
if!predeploy.ProxyDisabled {
codeAddr, err=AddressToCodeNamespace(predeploy.Address)
iferr!=nil {
returnnil, fmt.Errorf("error converting to code namespace: %w", err)
}
db.CreateAccount(codeAddr)
db.SetState(predeploy.Address, ImplementationSlot, eth.AddressAsLeftPaddedHash(codeAddr))
log.Info("Set proxy", "name", name, "address", predeploy.Address, "implementation", codeAddr)
}

在Layer1,部署 USDC 桥接完成了该过程。在这些层之间预部署 USDC 桥接的好处是,涉及 USDC 的交易可以高效处理,并且可以更无缝地跟踪 USDC 存款和取款事件。

以上描述了在Layer1和Layer2上预部署 USDC 桥接和 USDC 相关合约的过程,从而可以通过 USDC 桥接在 Thanos 网络上实现 USDC 资产交易。

Thanos 开发故事的下一部分是关于 UniswapV3 的预部署开发,我们将讨论在 Thanos 网络上预部署 UniswapV3 的好处以及开发过程。

  • 原文链接: medium.com/tokamak-netwo...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Xeoyoung
Xeoyoung
江湖只有他的大名,没有他的介绍。