基于模板方法模式的去中心化交易所(DEX)协议设计与实现
1. 题目要求
本项目要求开发一个类似 Raydium 的去中心化交易所(DEX)协议。在该协议中,每个流动性池(Pool)的创建和首次交易都必须遵循一套固定流程。该流程不能被子类随意打乱,必须按照预设阶段依次执行。
| 阶段 | 方法名 | 职责 |
|---|---|---|
| 阶段1 | pre_initialize() |
校验创建者权限、验证代币铸造地址合法性、计算初始价格滑点容忍度 |
| 阶段2 | initialize() |
创建池子的账户空间、初始化代币储备量、设置交易费率 |
| 阶段3 | initialize2() |
启动首次流动性注入(mint LP token)、向交易所注册该池子 |
| 阶段4 | pre_input() |
检查交易是否在允许的时间窗口内、防抢跑(anti-frontrunning)验证 |
| 阶段5 | process_input() |
执行核心交易逻辑(Swap / AddLiquidity / RemoveLiquidity) |
从业务角度看,DEX 协议中的池子创建、流动性注入和交易处理都属于高风险操作。如果执行顺序混乱,可能会导致池子未初始化就开始交易、储备量错误、LP Token 铸造异常,甚至出现交易安全问题。因此,本项目选择使用模板方法模式来约束整个执行流程。
2. 设计目标
本项目的核心目标包括以下几个方面:
-
固定执行顺序
将 DEX 指令执行流程固定为五个阶段,保证所有操作都按照统一顺序运行。
-
隔离通用流程与具体业务
抽象父类负责规定流程,具体子类负责实现不同业务逻辑,例如创建池子、兑换交易等。
-
提高代码复用性
通用初始化逻辑和通用交易逻辑可以放在父类中,子类在需要时直接复用。
-
提高扩展性
后续如果需要加入
AddLiquidityInstruction、RemoveLiquidityInstruction等新指令,只需要继承父类并实现对应阶段方法即可。 -
模拟链上 DEX 指令执行过程
通过
Context模拟区块链上下文,通过不同 Instruction 类模拟链上不同交易指令。
3. 相关知识点
3.1 模板方法模式
模板方法模式是一种行为型设计模式。它的核心思想是:
在父类中定义一个算法的整体骨架,将某些具体步骤延迟到子类中实现。
在本项目中,PoolInstruction::execute() 就是模板方法。它负责规定五个阶段的执行顺序:
text
pre_initialize()
↓
initialize()
↓
initialize2()
↓
pre_input()
↓
process_input()
具体的创建池子逻辑、兑换逻辑、流动性处理逻辑并不直接写死在 execute() 中,而是由子类分别实现。这样既保证了执行流程统一,又保留了业务扩展能力。
3.2 虚函数
虚函数是 C++ 中实现运行时多态的重要机制。父类中使用 virtual 声明函数后,子类可以通过 override 重写该函数。
在本项目中,pre_initialize()、initialize2()、pre_input() 等方法都是虚函数。父类只规定这些阶段必须存在,而具体行为由子类决定。
例如:
CreatePoolInstruction::pre_initialize()用于检查管理员权限;SwapInstruction::pre_input()用于检查储备量和滑点;SwapInstruction::process_input()用于执行 AMM 兑换。
这体现了"父类定义流程,子类实现细节"的设计思想。
3.3 纯虚函数
纯虚函数使用 = 0 表示,它没有默认实现,子类必须实现该函数,否则子类仍然是抽象类,不能实例化。
在本项目中,部分阶段被设计为纯虚函数,是因为这些阶段与具体业务强相关,父类无法给出统一实现。例如:
cpp
virtual bool pre_initialize(Context& ctx) = 0;
virtual bool initialize2(Context& ctx) = 0;
virtual bool pre_input(Context& ctx) = 0;
这样可以强制每个具体指令类对关键阶段作出明确处理。
3.4 final
final 用于限制子类继续重写某个虚函数。
本项目中,execute() 被声明为:
cpp
virtual bool execute(Context& ctx) final;
这表示子类不能重写 execute()。这样可以防止子类改变五个阶段的执行顺序,从而保证模板方法模式中的"算法骨架"不会被破坏。
3.5 钩子方法
钩子方法是一种给子类提供额外扩展点的方式。父类提供默认实现,子类可以根据自身需要选择是否重写。
本项目中,should_skip_pre_initialize()、should_skip_initialize() 等方法就是钩子方法。它们默认返回 false,表示默认不跳过阶段;具体子类可以重写它们,以表达某些阶段在该指令中不需要执行具体业务。
需要注意的是,钩子方法并没有改变 execute() 中的判断顺序。也就是说,整体流程仍然按照五个阶段依次检查,只是某些具体阶段可以根据子类业务选择跳过实际处理。
4. 系统总体设计
本项目主要包含三个核心类:
| 类名 | 类型 | 作用 |
|---|---|---|
PoolInstruction |
抽象父类 | 定义 DEX 指令执行流程,是模板方法模式的核心 |
CreatePoolInstruction |
具体子类 | 实现创建流动性池时的具体逻辑 |
SwapInstruction |
具体子类 | 实现代币兑换交易的具体逻辑 |
此外,项目使用 Context 结构体模拟区块链执行上下文。它保存调用者、公钥信息以及池子中的代币储备量。
4.1 Context 上下文
Context 用于模拟链上执行环境。在真实的 Solana 或 Raydium 协议中,交易执行时会涉及账户、公钥、代币 mint、slot、交易签名、手续费账户、程序派生地址等复杂信息。为了突出设计模式,本项目对这些内容进行了简化。
当前 Context 主要包含:
caller:模拟当前交易调用者;reserve_a:代币 A 的储备量;reserve_b:代币 B 的储备量。
这些字段足以用于演示创建池子、初始化储备量和执行 AMM 兑换的基本过程。
4.2 PoolInstruction 抽象父类
PoolInstruction 是整个系统的核心。它承担两个作用:
- 定义统一执行入口
execute(); - 规定五个阶段方法。
其中,execute() 是模板方法。它按照固定顺序依次调用五个阶段方法,并在每个阶段失败时立即返回 false。
这种设计的优点是:
- 调用方只需要调用
execute(); - 子类不需要关心整体流程;
- 父类统一控制执行顺序;
- 子类只负责各阶段的具体业务。
4.3 CreatePoolInstruction 创建池子指令
CreatePoolInstruction 表示创建流动性池的指令。
它主要负责:
- 检查调用者是否为管理员;
- 检查代币信息是否符合要求;
- 使用父类默认初始化逻辑设置初始储备量;
- 铸造 LP Token;
- 将池子注册到交易所;
- 完成创建池子阶段所需的时间窗口检查。
在当前示例中,initialize() 和 process_input() 使用父类默认实现,说明某些通用逻辑可以被多个指令复用。
4.4 SwapInstruction 兑换指令
SwapInstruction 表示代币兑换交易。
它主要负责:
- 检查池子储备是否充足;
- 模拟滑点检查;
- 执行 AMM 交换;
- 更新池子中代币 A 和代币 B 的储备量。
在当前实现中,兑换指令通过钩子方法跳过创建池子相关阶段,仅保留交易前检查和交易处理阶段。这体现了钩子方法在模板方法模式中的扩展作用。
5. 执行流程分析
整个指令执行流程由 PoolInstruction::execute() 控制。无论具体指令是创建池子还是执行兑换,外部调用者都通过同一个入口执行:
cpp
instruction.execute(ctx);
执行过程如下:
text
开始执行指令
↓
阶段1:pre_initialize()
↓
阶段2:initialize()
↓
阶段3:initialize2()
↓
阶段4:pre_input()
↓
阶段5:process_input()
↓
执行成功 / 执行失败
如果任意阶段返回 false,说明该阶段执行失败,整个流程立即终止。这样可以避免在前置条件不满足的情况下继续执行后续操作。
例如,在创建池子时,如果 pre_initialize() 中发现调用者不是管理员,则直接返回 false,不会继续执行初始化储备量、铸造 LP Token 或注册池子等后续操作。
这种设计符合区块链交易执行中的安全思想:
先校验,再执行;前置条件不满足时立即终止。
实现
PoolInstruction
MyComponent.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
// ------------------------------------------------------------
// 模拟区块链上下文(简化版,仅包含必要字段)
// ------------------------------------------------------------
struct Context {
std::string caller; // 调用者公钥(模拟)
int64_t reserve_a = 0; // 池子中代币A的储备量
int64_t reserve_b = 0; // 池子中代币B的储备量
// 实际项目中还会有 slot、compute_unit 等
};
// ------------------------------------------------------------
// 抽象基类:定义模板方法
// ------------------------------------------------------------
class PoolInstruction {
public:
// ~PoolInstruction():析构函数,当一个对象被销毁时(例如离开作用域或被 delete),该函数会自动执行,用于清理资源(如释放内存、关闭文件等)
// 若未显式定义,编译器会自动生成一个默认的析构函数
// virtual 虚函数:允许子类重写该函数,确保在删除子类对象时能正确调用子类的析构函数,避免资源泄漏
// = default:指示编译器使用默认的析构行为,无需手动实现
virtual ~PoolInstruction() = default;
virtual bool execute(Context& ctx) final; // 模板方法(虚函数 + final,防止子类覆盖)
// ---- 纯虚函数(必须由子类实现) ----
virtual bool pre_initialize(Context& ctx) = 0; // 阶段1
virtual bool initialize(Context& ctx); //虚函数,有模板
virtual bool initialize2(Context& ctx) = 0; // 阶段3
virtual bool pre_input(Context& ctx) = 0; // 阶段4
virtual bool process_input(Context& ctx); // 虚函数,有模板
// 子类可重写以决定是否跳过某个阶段
// virtual ------ "留给子类去改写"
// const { return false; } ------ "默认答案是:不跳过"
virtual bool should_skip_pre_initialize() const { return false; }
virtual bool should_skip_initialize() const { return false; }
virtual bool should_skip_initialize2() const { return false; }
virtual bool should_skip_pre_input() const { return false; }
virtual bool should_skip_process_input() const { return false; }
};
MyComponent.cpp
cpp
#include "MyComponent.hpp"
// ★ 模板方法
bool PoolInstruction::execute(Context& ctx) {
std::cout << "=== 开始执行指令 ===" << std::endl;
if (!should_skip_pre_initialize()) {
std::cout << "[阶段1] pre_initialize()" << std::endl;
if (!pre_initialize(ctx)) return false;
} else {
std::cout << "[阶段1] 跳过" << std::endl;
}
if (!should_skip_initialize()) {
std::cout << "[阶段2] initialize()" << std::endl;
if (!initialize(ctx)) return false;
} else {
std::cout << "[阶段2] 跳过" << std::endl;
}
if (!should_skip_initialize2()) {
std::cout << "[阶段3] initialize2()" << std::endl;
if (!initialize2(ctx)) return false;
} else {
std::cout << "[阶段3] 跳过" << std::endl;
}
if (!should_skip_pre_input()) {
std::cout << "[阶段4] pre_input()" << std::endl;
if (!pre_input(ctx)) return false;
} else {
std::cout << "[阶段4] 跳过" << std::endl;
}
if (!should_skip_process_input()) {
std::cout << "[阶段5] process_input()" << std::endl;
if (!process_input(ctx)) return false;
} else {
std::cout << "[阶段5] 跳过" << std::endl;
}
std::cout << "✅ 执行成功" << std::endl;
return true;
}
// ---- 默认实现(子类可以覆盖) ----
bool PoolInstruction::initialize(Context& ctx) {
std::cout << " [默认] 初始化池子账户和储备量" << std::endl;
// 模拟:设置初始储备
ctx.reserve_a = 1000;
ctx.reserve_b = 1000;
return true;
}
bool PoolInstruction::process_input(Context& ctx) {
std::cout << " [默认] 执行通用交易逻辑" << std::endl;
// 模拟:简单的转移操作
return true;
}
CreatePoolInstruction
CreatePoolInstruction.hpp
cpp
#pragma once
#include <iostream>
#include "MyComponent.hpp"
class CreatePoolInstruction : public PoolInstruction {
public:
CreatePoolInstruction(const std::string& admin,
const std::string& tokenA,
const std::string& tokenB,
int64_t price)
: admin_(admin), tokenA_(tokenA), tokenB_(tokenB), price_(price) {}
bool pre_initialize(Context& ctx) override;
bool initialize2(Context& ctx) override;
bool pre_input(Context& ctx) override;
// initialize() 和 process_input() 方法使用父类的默认实现
private:
std::string admin_, tokenA_, tokenB_;
int64_t price_;
};
CreatePoolInstruction.cpp
cpp
#include "CreatePoolInstruction.hpp"
bool CreatePoolInstruction::pre_initialize(Context& ctx) {
std::cout << " [创建池] 检查管理员权限与代币有效性" << std::endl;
if (ctx.caller != admin_){
std::cerr << " 调用者无权创建资金池" << std::endl;
return false;
}
return true;
}
bool CreatePoolInstruction::initialize2(Context& ctx) {
std::cout << " [创建池] 铸造 LP Token 并完成注册" << std::endl;
return true;
}
bool CreatePoolInstruction::pre_input(Context& ctx){
std::cout << " [创建池] 检查时间窗口" << std::endl;
return true;
}
SwapInstruction
SwapInstruction.hpp
cpp
#pragma once
#include "MyComponent.hpp"
// 具体子类2:兑换
class SwapInstruction : public PoolInstruction {
public:
SwapInstruction(int64_t amountIn, int64_t minOut, int64_t slippage)
: amount_in_(amountIn), min_out_(minOut), slippage_(slippage) {}
// 必须实现的三个纯虚函数(因相应阶段被跳过,仅提供空实现)
bool pre_initialize(Context&) override ;
bool initialize2(Context&) override ;
bool pre_input(Context& ctx) override ;
bool process_input(Context& ctx) override ;
// ---- 钩子方法:跳过不需要的阶段 ----
bool should_skip_pre_initialize() const override { return true; }
bool should_skip_initialize() const override { return true; }
bool should_skip_initialize2() const override { return true; }
// pre_input 和 process_input 不跳过
private:
int64_t amount_in_, min_out_, slippage_;
};
SwapInstruction.cpp
cpp
#include "SwapInstruction.hpp"
bool SwapInstruction::pre_initialize(Context&) {
// 此阶段被跳过,因此留空
return true;
}
bool SwapInstruction::initialize2(Context&) {
return true; // 此阶段被跳过
}
bool SwapInstruction::pre_input(Context& ctx) {
std::cout << " [兑换] 检查储备与滑点" << std::endl;
if (ctx.reserve_a == 0 || ctx.reserve_b == 0) return false;
// 模拟滑点检查通过
return true;
}
bool SwapInstruction::process_input(Context& ctx) {
std::cout << " [兑换] 执行AMM交换" << std::endl;
ctx.reserve_a += amount_in_;
ctx.reserve_b -= 100; // 简化
return true;
}
编译运行
1 命令行
可以在项目目录下使用以下命令进行编译:
bash
g++ -std=c++11 main.cpp MyComponent.cpp CreatePoolInstruction.cpp SwapInstruction.cpp -o program
./program
2 配置编译器 JSON
如果使用 VS Code,可以通过 tasks.json 配置自动编译任务,并在 launch.json 中指定调试程序路径。
基本思路如下:
- 在
tasks.json中加入所有需要编译的.cpp文件; - 输出文件命名为
program; - 在
launch.json中将program设置为${workspaceFolder}/program; - 使用
preLaunchTask在调试前自动执行编译任务。
将 tasks.json 修改为:
bash
"args": [
"-fdiagnostics-color=always",
"-g",
"main.cpp",
"MyComponent.cpp",
"CreatePoolInstruction.cpp",
"SwapInstruction.cpp",
"-o",
"${workspaceFolder}/program"
],
将 launch.json 修改为:
bash
"configurations": [
{
"name": "(gdb) 启动",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/program", // 固定指向项目根目录下的 program
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "build-all" // 👈 调试前自动运行 tasks.json 中的编译任务
}
]
3 使用 CMake
如果使用 CMake,可以创建 CMakeLists.txt,将所有 .cpp 文件加入 add_executable() 中。CMake 方式更适合后续项目扩展,因为当源文件数量增加时,项目结构会更加清晰。
::创建 CMakeLists.txt 文件::
bash
cmake_minimum_required(VERSION 3.10)
# 项目名称(可自定义))
project(DexProject)
# 指定使用 C++11 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON))
# 【关键】生成调试信息(等价于 -g)并关闭优化(便于调试))
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0")
# 设置 CMake 构建类型为 Debug(使 -g 调试标志生效))
set(CMAKE_BUILD_TYPE Debug)
# 指定所有源文件(只需列出 .cpp 文件,.hpp 会自动找)
add_executable(program
main.cpp
MyComponent.cpp
CreatePoolInstruction.cpp
SwapInstruction.cpp
)
修改 task.json 文件::
bash
{
"version": "2.0.0",
"tasks": [
{
"label": "cmake-build",
"type": "shell",
"command": "bash",
"args": [
"-c",
"mkdir -p build && cd build && cmake .. && make -j4"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$gcc"], "detail": "使用 CMake 配置并编译整个项目。""
}
]
}
修改 launch.json 文件::
bash
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) 启动",
"type": "cppdbg",
"request": "launch", "program": "${workspaceFolder}/build/program", // 指向 build 文件夹件夹
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
] "preLaunchTask": "cmake-build" // 调试前自动执行 CMake 编译 编译
}
]
}