feat(vault): add user vault and factory contracts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
Reference in New Issue
Block a user