Skip to main content

SocialVault Contract

The SocialVault contract implements social recovery with timelock, multi-signature guardians, and duress PIN protection.

Contract Overview

// 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

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

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)

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)

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

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

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

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

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

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

ParameterRangeDescription
dailyLimit10-10,000 USDCInstant withdrawal limit
timelockDuration1 hour - 7 daysDelay for large withdrawals
guardians1-5 addressesTrusted recovery contacts
requiredSignatures1 - guardian countMulti-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

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...