From 9d0ceb2b11724cc6c4a92c788dac7a1867d3c1df Mon Sep 17 00:00:00 2001 From: Bot Date: Mon, 31 Aug 2026 01:42:10 +0800 Subject: [PATCH] feat(vault): add user vault and factory contracts --- main/src/vault/WTFUserVault.sol | 94 ++++++++++++++++++++++++++++++ main/src/vault/WTFVaultFactory.sol | 49 ++++++++++++++++ scripts/deploy_vault.js | 41 +++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 main/src/vault/WTFUserVault.sol create mode 100644 main/src/vault/WTFVaultFactory.sol create mode 100644 scripts/deploy_vault.js diff --git a/main/src/vault/WTFUserVault.sol b/main/src/vault/WTFUserVault.sol new file mode 100644 index 0000000..67c864f --- /dev/null +++ b/main/src/vault/WTFUserVault.sol @@ -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); + } +} diff --git a/main/src/vault/WTFVaultFactory.sol b/main/src/vault/WTFVaultFactory.sol new file mode 100644 index 0000000..39130fc --- /dev/null +++ b/main/src/vault/WTFVaultFactory.sol @@ -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; + } +} diff --git a/scripts/deploy_vault.js b/scripts/deploy_vault.js new file mode 100644 index 0000000..44727fc --- /dev/null +++ b/scripts/deploy_vault.js @@ -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; +});