Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions contracts/InheritX.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @title InheritX
* @dev Contract for managing inheritance plans with emergency exit functionality
*/
contract InheritX is Ownable {
using SafeERC20 for IERC20;

struct Plan {
address owner;
IERC20 token;
uint256 principal;
uint256 accumulatedYield;
uint256 livenessExpiration;
bool isActive;
uint256 createdAt;
}

mapping(uint256 => Plan) public plans;
uint256 public planCounter;
uint256 public constant DEFAULT_LIVENESS_PERIOD = 30 days;

event PlanCreated(
uint256 indexed planId,
address indexed owner,
address token,
uint256 principal,
uint256 livenessExpiration
);

event YieldAccumulated(uint256 indexed planId, uint256 amount);

event PlanWithdrawn(
uint256 indexed planId,
address indexed owner,
uint256 principal,
uint256 yield,
uint256 totalWithdrawn
);

event PlanClosed(uint256 indexed planId);

/**
* @dev Constructor to initialize the contract with deployer as owner
*/
constructor() Ownable(msg.sender) {}

/**
* @dev Create a new inheritance plan
* @param token The ERC20 token address to lock
* @param principal The amount of tokens to lock as principal
* @param livenessPeriod The time period before liveness expires (in seconds)
*/
function createPlan(
address token,
uint256 principal,
uint256 livenessPeriod
) external returns (uint256) {
require(token != address(0), "Invalid token address");
require(principal > 0, "Principal must be greater than 0");
require(livenessPeriod > 0, "Liveness period must be greater than 0");

uint256 planId = ++planCounter;
uint256 livenessExpiration = block.timestamp + livenessPeriod;

// Transfer tokens from caller to contract
IERC20(token).safeTransferFrom(msg.sender, address(this), principal);

plans[planId] = Plan({
owner: msg.sender,
token: IERC20(token),
principal: principal,
accumulatedYield: 0,
livenessExpiration: livenessExpiration,
isActive: true,
createdAt: block.timestamp
});

emit PlanCreated(planId, msg.sender, token, principal, livenessExpiration);

return planId;
}

/**
* @dev Accumulate yield to a plan (can be called by anyone)
* @param planId The plan identifier
* @param amount The amount of yield to add
*/
function accumulateYield(uint256 planId, uint256 amount) external {
Plan storage plan = plans[planId];
require(plan.isActive, "Plan is not active");
require(amount > 0, "Yield amount must be greater than 0");

// Transfer yield tokens to contract
plan.token.safeTransferFrom(msg.sender, address(this), amount);

plan.accumulatedYield += amount;

emit YieldAccumulated(planId, amount);
}

/**
* @dev Emergency exit: withdraw plan and return all locked principal plus accumulated yield
* @param planId The plan identifier
*
* Requirements:
* - Caller must be the plan owner (owner signature)
* - Plan must be active
* - Current time must be before liveness expiration
*
* Effects:
* - Transfers principal + accumulated yield back to owner
* - Deactivates the plan
*/
function withdrawPlan(uint256 planId) external {
Plan storage plan = plans[planId];

// Owner signature verification (msg.sender must be plan owner)
require(msg.sender == plan.owner, "Only plan owner can withdraw");

// Plan must be active
require(plan.isActive, "Plan is not active");

// Liveness check: can only withdraw before expiration
require(block.timestamp < plan.livenessExpiration, "Liveness has expired");

uint256 totalAmount = plan.principal + plan.accumulatedYield;
require(totalAmount > 0, "No funds to withdraw");

// Deactivate plan before transfer to prevent reentrancy
plan.isActive = false;

// Transfer all tokens back to owner
plan.token.safeTransfer(msg.sender, totalAmount);

emit PlanWithdrawn(
planId,
msg.sender,
plan.principal,
plan.accumulatedYield,
totalAmount
);

emit PlanClosed(planId);

// Clear plan data (optional, for gas optimization)
delete plans[planId];
}

/**
* @dev Get plan details
* @param planId The plan identifier
*/
function getPlan(uint256 planId) external view returns (
address owner,
address token,
uint256 principal,
uint256 accumulatedYield,
uint256 livenessExpiration,
bool isActive,
uint256 createdAt
) {
Plan memory plan = plans[planId];
return (
plan.owner,
address(plan.token),
plan.principal,
plan.accumulatedYield,
plan.livenessExpiration,
plan.isActive,
plan.createdAt
);
}

/**
* @dev Check if a plan can be withdrawn (before liveness expiration)
* @param planId The plan identifier
*/
function canWithdraw(uint256 planId) external view returns (bool) {
Plan memory plan = plans[planId];
return plan.isActive && block.timestamp < plan.livenessExpiration;
}

/**
* @dev Get total value of a plan (principal + yield)
* @param planId The plan identifier
*/
function getPlanTotalValue(uint256 planId) external view returns (uint256) {
Plan memory plan = plans[planId];
return plan.principal + plan.accumulatedYield;
}
}
113 changes: 112 additions & 1 deletion contracts/inheritance-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub struct InheritancePlan {
pub waterfall_enabled: bool,
pub grace_period: u64,
pub earn_yield: bool,
pub liveness_expiration: u64, // Timestamp when emergency exit expires
}

#[contracterror]
Expand Down Expand Up @@ -125,6 +126,7 @@ pub enum InheritanceError {
WillAlreadyLinked = 48,
WillAlreadyFinalized = 49,
WillVersionNotFound = 50,
LivenessExpired = 51,
}

#[contracttype]
Expand Down Expand Up @@ -1855,6 +1857,8 @@ impl InheritanceContract {
}

// Create the inheritance plan with net amount (user input minus 2% fee)
let created_at = env.ledger().timestamp();
let liveness_expiration = created_at + 2_592_000; // 30 days in seconds
let plan = InheritancePlan {
plan_name,
description,
Expand All @@ -1864,13 +1868,14 @@ impl InheritanceContract {
beneficiaries,
total_allocation_bp,
owner: owner.clone(),
created_at: env.ledger().timestamp(),
created_at,
is_active: true,
is_lendable,
total_loaned: 0,
waterfall_enabled: false,
grace_period: 0,
earn_yield: false,
liveness_expiration,
};

// Store the plan
Expand Down Expand Up @@ -2152,6 +2157,112 @@ impl InheritanceContract {
Ok(())
}

/// Emergency exit: withdraw entire plan and return all locked principal plus accumulated yield
///
/// # Arguments
/// * `env` - The contract environment
/// * `caller` - The address calling the function (must be plan owner)
/// * `token` - The token address to transfer
/// * `plan_id` - The plan identifier
///
/// # Requirements
/// - Caller must be the plan owner (owner signature)
/// - Plan must be active
/// - Current time must be before liveness expiration
///
/// # Effects
/// - Transfers total_amount + any yield back to owner
/// - Deactivates the plan
pub fn withdraw_plan(
env: Env,
caller: Address,
token: Address,
plan_id: u64,
) -> Result<(), InheritanceError> {
caller.require_auth();
Self::check_not_paused(&env);
Self::enter_guard(&env);

let mut plan = Self::get_plan(&env, plan_id).ok_or(InheritanceError::PlanNotFound)?;

// Owner signature verification
if plan.owner != caller {
return Err(InheritanceError::Unauthorized);
}

// Plan must be active
if !plan.is_active {
return Err(InheritanceError::PlanNotActive);
}

// Liveness check: can only withdraw before expiration
let now = env.ledger().timestamp();
if now >= plan.liveness_expiration {
return Err(InheritanceError::LivenessExpired);
}

// Freeze/legal hold check
if env
.storage()
.persistent()
.has(&DataKey::FreezePlan(plan_id))
{
return Err(InheritanceError::PlanNotActive);
}
if env.storage().persistent().has(&DataKey::LegalHold(plan_id)) {
return Err(InheritanceError::PlanNotActive);
}

// Calculate total amount to withdraw (principal + yield)
let total_amount = plan.total_amount;

// Deactivate plan before transfer to prevent reentrancy
plan.is_active = false;
Self::store_plan(&env, plan_id, &plan);

// Transfer all tokens back to owner
let contract_id = env.current_contract_address();
let required = total_amount as i128;
let args: Vec<Val> = vec![
&env,
contract_id.clone().into_val(&env),
caller.clone().into_val(&env),
required.into_val(&env),
];
let res =
env.try_invoke_contract::<(), InvokeError>(&token, &symbol_short!("transfer"), args);
if res.is_err() {
// Revert plan activation if transfer fails
plan.is_active = true;
Self::store_plan(&env, plan_id, &plan);
return Err(InheritanceError::FeeTransferFailed);
}

// Add to deactivated plans list
let mut deactivated: Vec<u64> = env
.storage()
.persistent()
.get(&DataKey::DeactivatedPlans)
.unwrap_or(Vec::new(&env));
deactivated.push_back(plan_id);
env.storage()
.persistent()
.set(&DataKey::DeactivatedPlans, &deactivated);

env.events().publish(
(symbol_short!("PLAN"), symbol_short!("WITHDRAWN")),
PlanDeactivatedEvent {
plan_id,
owner: caller.clone(),
total_amount,
deactivated_at: now,
},
);
log!(&env, "Plan {} withdrawn by owner, total amount: {}", plan_id, total_amount);
Self::exit_guard(&env);
Ok(())
}

pub fn set_beneficiary_priority(
env: Env,
owner: Address,
Expand Down
Loading
Loading