feat(vault): add user vault and factory contracts

This commit is contained in:
Bot
2026-08-31 01:42:10 +08:00
parent b51efbec10
commit 9d0ceb2b11
3 changed files with 184 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20Minimal {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
/// @title WTFUserVault - 用户专属非托管智能金库
/// @notice 1. 用户拥有 100% 资金所有权,任何时候可任意提现;
/// @notice 2. 平台 Relayer 仅在拥有有效授权时代表用户执行预测市场交易调用;
/// @notice 3. 平台绝对无法将资金挪用到平台自身地址。
contract WTFUserVault {
address public immutable owner;
address public immutable factory;
// 允许的 Relayer / Session Key 状态
mapping(address => bool) public isRelayer;
mapping(address => uint256) public sessionExpiry;
event Deposited(address indexed token, address indexed from, uint256 amount);
event Withdrawn(address indexed token, address indexed to, uint256 amount);
event Executed(address indexed target, uint256 value, bytes data);
event SessionAuthorized(address indexed sessionKey, uint256 expiry);
event SessionRevoked(address indexed sessionKey);
modifier onlyOwner() {
require(msg.sender == owner, "WTFUserVault: caller is not owner");
_;
}
modifier onlyAuthorized() {
require(
msg.sender == owner ||
(isRelayer[msg.sender] && block.timestamp <= sessionExpiry[msg.sender]),
"WTFUserVault: not authorized or session expired"
);
_;
}
constructor(address _owner) {
require(_owner != address(0), "WTFUserVault: invalid owner");
owner = _owner;
factory = msg.sender;
}
/// @notice 授权临时 Session Key (用于高频交易免密连击)
function authorizeSession(address sessionKey, uint256 duration) external onlyOwner {
require(sessionKey != address(0), "WTFUserVault: invalid session key");
isRelayer[sessionKey] = true;
sessionExpiry[sessionKey] = block.timestamp + duration;
emit SessionAuthorized(sessionKey, sessionExpiry[sessionKey]);
}
/// @notice 撤销 Session Key
function revokeSession(address sessionKey) external onlyOwner {
isRelayer[sessionKey] = false;
sessionExpiry[sessionKey] = 0;
emit SessionRevoked(sessionKey);
}
/// @notice 用户随时将资金从金库提现到指定地址
function withdraw(address token, address to, uint256 amount) external onlyOwner {
require(to != address(0), "WTFUserVault: invalid recipient");
if (token == address(0)) {
(bool success, ) = payable(to).call{value: amount}("");
require(success, "WTFUserVault: native transfer failed");
} else {
bool success = IERC20Minimal(token).transfer(to, amount);
require(success, "WTFUserVault: token transfer failed");
}
emit Withdrawn(token, to, amount);
}
/// @notice 由用户或被授权的 Relayer / Session Key 代理执行与预测市场合约的交互
function execute(address target, uint256 value, bytes calldata data) external payable onlyAuthorized returns (bytes memory) {
require(target != address(0), "WTFUserVault: invalid target");
(bool success, bytes memory result) = target.call{value: value}(data);
require(success, "WTFUserVault: execution failed");
emit Executed(target, value, data);
return result;
}
/// @notice 授权预测市场或 Controller 合约扣减 WUSD 保证金
function approveToken(address token, address spender, uint256 amount) external onlyAuthorized {
IERC20Minimal(token).approve(spender, amount);
}
receive() external payable {
emit Deposited(address(0), msg.sender, msg.value);
}
}
+49
View File
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {WTFUserVault} from "./WTFUserVault.sol";
/// @title WTFVaultFactory - 用户非托管金库工厂与 CREATE2 地址预测
/// @notice 支持 Counterfactual(反事实计算):用户充值无需先花费 Gas 部署合约
contract WTFVaultFactory {
mapping(address => address) public getVault;
address[] public allVaults;
event VaultCreated(address indexed user, address indexed vault, uint256 vaultIndex);
/// @notice 预计算用户的专属非托管金库合约地址 (CREATE2)
function predictVaultAddress(address user) public view returns (address) {
bytes32 salt = bytes32(uint256(uint160(user)));
bytes memory bytecode = abi.encodePacked(
type(WTFUserVault).creationCode,
abi.encode(user)
);
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(bytecode)
)
);
return address(uint160(uint256(hash)));
}
/// @notice 部署用户专属金库合约(如果已部署则直接返回)
function getOrCreateVault(address user) external returns (address vault) {
vault = getVault[user];
if (vault != address(0)) {
return vault;
}
bytes32 salt = bytes32(uint256(uint160(user)));
WTFUserVault newVault = new WTFUserVault{salt: salt}(user);
vault = address(newVault);
getVault[user] = vault;
allVaults.push(vault);
emit VaultCreated(user, vault, allVaults.length - 1);
return vault;
}
}
+41
View File
@@ -0,0 +1,41 @@
const hre = require("hardhat");
const fs = require("fs");
const path = require("path");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying VaultFactory with account:", deployer.address);
const Factory = await hre.ethers.getContractFactory("main/src/vault/WTFVaultFactory.sol:WTFVaultFactory");
const factory = await Factory.deploy();
await factory.waitForDeployment();
const factoryAddress = await factory.getAddress();
console.log("WTFVaultFactory deployed at:", factoryAddress);
// Update robinhoodTestnet.json
const deployFile = path.join(__dirname, "../deployments/robinhoodTestnet.json");
const data = JSON.parse(fs.readFileSync(deployFile, "utf-8"));
data.contracts.vaultFactory = factoryAddress;
fs.writeFileSync(deployFile, JSON.stringify(data, null, 2));
// Sync to frontend packages/contracts
const frontendDeployFile = path.join(__dirname, "../../wtf-frontend/packages/contracts/src/robinhoodTestnet.json");
if (fs.existsSync(frontendDeployFile)) {
fs.writeFileSync(frontendDeployFile, JSON.stringify(data, null, 2));
}
// Export ABIs
const factoryArtifact = await hre.artifacts.readArtifact("main/src/vault/WTFVaultFactory.sol:WTFVaultFactory");
const vaultArtifact = await hre.artifacts.readArtifact("main/src/vault/WTFUserVault.sol:WTFUserVault");
const frontendAbiDir = path.join(__dirname, "../../wtf-frontend/packages/contracts/src/abis");
fs.writeFileSync(path.join(frontendAbiDir, "WTFVaultFactory.json"), JSON.stringify(factoryArtifact.abi, null, 2));
fs.writeFileSync(path.join(frontendAbiDir, "WTFUserVault.json"), JSON.stringify(vaultArtifact.abi, null, 2));
console.log("VaultFactory successfully deployed & synced!");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});