SocialVault Contract
The SocialVault contract implements social recovery with timelock, multi-signature guardians, and duress PIN protection.Contract Overview
Copy
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SocialVault is ReentrancyGuard {
struct Vault {
address owner;
uint256 balance;
uint256 dailyLimit;
uint256 spentToday;
uint256 lastSpendDate;
uint256 timelockDuration;
address[] guardians;
uint256 requiredSignatures;
bool isLocked;
bytes32 duressCodeHash;
}
struct PendingWithdrawal {
uint256 amount;
address recipient;
uint256 executeAfter;
uint256 approvals;
bool executed;
bool cancelled;
mapping(address => bool) hasApproved;
}
mapping(address => Vault) public vaults;
mapping(address => mapping(uint256 => PendingWithdrawal)) public pendingWithdrawals;
mapping(address => uint256) public withdrawalCounter;
IERC20 public immutable usdc;
}
Key Functions
Create Vault
Copy
function createVault(
uint256 dailyLimit,
uint256 timelockDuration,
address[] calldata guardians,
uint256 requiredSignatures,
bytes32 duressCodeHash
) external {
require(vaults[msg.sender].owner == address(0), "Vault exists");
require(guardians.length >= requiredSignatures, "Not enough guardians");
require(timelockDuration >= 1 hours, "Timelock too short");
vaults[msg.sender] = Vault({
owner: msg.sender,
balance: 0,
dailyLimit: dailyLimit,
spentToday: 0,
lastSpendDate: 0,
timelockDuration: timelockDuration,
guardians: guardians,
requiredSignatures: requiredSignatures,
isLocked: false,
duressCodeHash: duressCodeHash
});
emit VaultCreated(msg.sender, guardians.length, timelockDuration);
}
Deposit
Copy
function deposit(uint256 amount) external nonReentrant {
Vault storage vault = vaults[msg.sender];
require(vault.owner != address(0), "No vault");
usdc.transferFrom(msg.sender, address(this), amount);
vault.balance += amount;
emit Deposited(msg.sender, amount);
}
Instant Withdrawal (Within Daily Limit)
Copy
function withdrawInstant(uint256 amount, address recipient) external nonReentrant {
Vault storage vault = vaults[msg.sender];
require(!vault.isLocked, "Vault locked");
require(vault.balance >= amount, "Insufficient balance");
// Reset daily spend if new day
if (block.timestamp / 1 days > vault.lastSpendDate / 1 days) {
vault.spentToday = 0;
vault.lastSpendDate = block.timestamp;
}
require(vault.spentToday + amount <= vault.dailyLimit, "Daily limit exceeded");
vault.spentToday += amount;
vault.balance -= amount;
usdc.transfer(recipient, amount);
emit InstantWithdrawal(msg.sender, recipient, amount);
}
Request Large Withdrawal (Timelock)
Copy
function requestWithdrawal(uint256 amount, address recipient) external returns (uint256) {
Vault storage vault = vaults[msg.sender];
require(!vault.isLocked, "Vault locked");
require(vault.balance >= amount, "Insufficient balance");
uint256 withdrawalId = ++withdrawalCounter[msg.sender];
PendingWithdrawal storage pending = pendingWithdrawals[msg.sender][withdrawalId];
pending.amount = amount;
pending.recipient = recipient;
pending.executeAfter = block.timestamp + vault.timelockDuration;
pending.approvals = 0;
pending.executed = false;
pending.cancelled = false;
// Notify guardians
emit WithdrawalRequested(msg.sender, withdrawalId, amount, recipient, pending.executeAfter);
return withdrawalId;
}
Guardian Approval
Copy
function approveWithdrawal(address vaultOwner, uint256 withdrawalId) external {
Vault storage vault = vaults[vaultOwner];
require(isGuardian(vault, msg.sender), "Not a guardian");
PendingWithdrawal storage pending = pendingWithdrawals[vaultOwner][withdrawalId];
require(!pending.executed && !pending.cancelled, "Invalid state");
require(!pending.hasApproved[msg.sender], "Already approved");
pending.hasApproved[msg.sender] = true;
pending.approvals++;
emit WithdrawalApproved(vaultOwner, withdrawalId, msg.sender, pending.approvals);
}
Execute Withdrawal
Copy
function executeWithdrawal(uint256 withdrawalId) external nonReentrant {
Vault storage vault = vaults[msg.sender];
PendingWithdrawal storage pending = pendingWithdrawals[msg.sender][withdrawalId];
require(!pending.executed && !pending.cancelled, "Invalid state");
require(block.timestamp >= pending.executeAfter, "Timelock active");
require(pending.approvals >= vault.requiredSignatures, "Insufficient approvals");
require(vault.balance >= pending.amount, "Insufficient balance");
pending.executed = true;
vault.balance -= pending.amount;
usdc.transfer(pending.recipient, pending.amount);
emit WithdrawalExecuted(msg.sender, withdrawalId, pending.amount);
}
Duress PIN - Emergency Lock
Copy
function emergencyLock(bytes32 duressCode) external {
Vault storage vault = vaults[msg.sender];
require(keccak256(abi.encodePacked(duressCode)) == vault.duressCodeHash, "Invalid code");
vault.isLocked = true;
// Cancel all pending withdrawals
for (uint256 i = 1; i <= withdrawalCounter[msg.sender]; i++) {
if (!pendingWithdrawals[msg.sender][i].executed) {
pendingWithdrawals[msg.sender][i].cancelled = true;
}
}
// Alert guardians silently (off-chain notification)
emit VaultLocked(msg.sender, block.timestamp);
}
Guardian Recovery
Copy
function initiateRecovery(address vaultOwner) external {
Vault storage vault = vaults[vaultOwner];
require(isGuardian(vault, msg.sender), "Not a guardian");
require(vault.isLocked, "Vault not locked");
// Requires majority of guardians to unlock
// Implementation uses time-delayed multi-sig
emit RecoveryInitiated(vaultOwner, msg.sender);
}
function completeRecovery(
address vaultOwner,
address newOwner,
bytes[] calldata signatures
) external {
Vault storage vault = vaults[vaultOwner];
require(vault.isLocked, "Vault not locked");
require(verifyGuardianSignatures(vault, newOwner, signatures), "Invalid signatures");
vault.owner = newOwner;
vault.isLocked = false;
emit RecoveryCompleted(vaultOwner, newOwner);
}
Events
Copy
event VaultCreated(address indexed owner, uint256 guardianCount, uint256 timelockDuration);
event Deposited(address indexed owner, uint256 amount);
event InstantWithdrawal(address indexed owner, address recipient, uint256 amount);
event WithdrawalRequested(address indexed owner, uint256 indexed id, uint256 amount, address recipient, uint256 executeAfter);
event WithdrawalApproved(address indexed owner, uint256 indexed id, address guardian, uint256 totalApprovals);
event WithdrawalExecuted(address indexed owner, uint256 indexed id, uint256 amount);
event VaultLocked(address indexed owner, uint256 timestamp);
event RecoveryInitiated(address indexed owner, address indexed initiator);
event RecoveryCompleted(address indexed oldOwner, address indexed newOwner);
Configuration
| Parameter | Range | Description |
|---|---|---|
dailyLimit | 10-10,000 USDC | Instant withdrawal limit |
timelockDuration | 1 hour - 7 days | Delay for large withdrawals |
guardians | 1-5 addresses | Trusted recovery contacts |
requiredSignatures | 1 - guardian count | Multi-sig threshold |
Security Features
Timelock
Large withdrawals require waiting period, giving time to cancel suspicious activity
Multi-Sig
Guardians must approve withdrawals exceeding daily limit
Duress PIN
Secret code that locks vault and silently alerts guardians
Daily Limits
Instant withdrawals capped to limit theft impact
Integration Example
Copy
import { ethers } from 'ethers';
// Create vault with 3 guardians, 2 required signatures
await socialVault.createVault(
ethers.parseUnits("100", 6), // $100 daily limit
86400, // 24h timelock
[guardian1, guardian2, guardian3],
2, // 2-of-3 multi-sig
ethers.keccak256(ethers.toUtf8Bytes("my-duress-pin"))
);
// Instant withdrawal within limit
await socialVault.withdrawInstant(
ethers.parseUnits("50", 6),
recipientAddress
);
// Large withdrawal with timelock
const withdrawalId = await socialVault.requestWithdrawal(
ethers.parseUnits("500", 6),
recipientAddress
);
// Wait for guardian approvals and timelock...