// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "./Will.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
/**
* @title Source (Estate Planning Factory)
* @notice A factory contract for creating individual 'Will' contracts with co-founder governance.
*/
contract Source {
struct CoFounder { address primary; address secondary; }
mapping(address => address) public userWills;
uint256 public constant basePlatformFee = 0;
uint256 public diaryPlatformFee;
CoFounder public coFounderOne;
CoFounder public coFounderTwo;
address public executorAddress;
event WillCreated(address indexed user, address indexed willAddress, bool hasDiary);
event WillCleared(address indexed user, address indexed willAddress);
event FeesWithdrawn(address indexed caller, uint256 amountForCoFounderOne, uint256 amountForCoFounderTwo);
event CoFounderAddressesUpdated(uint8 indexed coFounderId, address newPrimary, address newSecondary);
event ExecutorAddressUpdated(address indexed newExecutor);
modifier onlyCoFounder() {
require(
msg.sender == coFounderOne.primary || msg.sender == coFounderOne.secondary ||
msg.sender == coFounderTwo.primary || msg.sender == coFounderTwo.secondary,
"Source: Caller is not a co-founder"
);
_;
}
modifier onlyCoFounderOne() {
require(msg.sender == coFounderOne.primary || msg.sender == coFounderOne.secondary, "Source: Caller is not Co-Founder One");
_;
}
modifier onlyCoFounderTwo() {
require(msg.sender == coFounderTwo.primary || msg.sender == coFounderTwo.secondary, "Source: Caller is not Co-Founder Two");
_;
}
modifier canSetDiaryFee() {
require(
msg.sender == coFounderOne.primary || msg.sender == coFounderOne.secondary ||
msg.sender == coFounderTwo.primary || msg.sender == coFounderTwo.secondary ||
msg.sender == executorAddress,
"Source: Caller cannot set diary fee"
);
_;
}
constructor(
address _coFounderOnePrimary, address _coFounderOneSecondary,
address _coFounderTwoPrimary, address _coFounderTwoSecondary,
address _initialExecutor
) {
require(
_coFounderOnePrimary != address(0) && _coFounderOneSecondary != address(0) &&
_coFounderTwoPrimary != address(0) && _coFounderTwoSecondary != address(0),
"Source: Co-founder addresses cannot be zero"
);
coFounderOne = CoFounder({ primary: _coFounderOnePrimary, secondary: _coFounderOneSecondary });
coFounderTwo = CoFounder({ primary: _coFounderTwoPrimary, secondary: _coFounderTwoSecondary });
require(_initialExecutor != address(0), "Source: Executor cannot be zero");
executorAddress = _initialExecutor;
diaryPlatformFee = 0.3 ether;
}
receive() external payable {}
function setDiaryPlatformFee(uint256 _newFee) external canSetDiaryFee { diaryPlatformFee = _newFee; }
function setExecutorAddress(address _newExecutor) external onlyCoFounder {
require(_newExecutor != address(0), "Source: New executor cannot be zero");
executorAddress = _newExecutor;
emit ExecutorAddressUpdated(_newExecutor);
}
function withdrawFees() external onlyCoFounder {
uint256 balance = address(this).balance;
require(balance > 0, "No fees to withdraw.");
uint256 shareForOne = (balance * 90) / 100;
uint256 shareForTwo = balance - shareForOne;
(bool s1, ) = coFounderOne.primary.call{value: shareForOne}(""); require(s1, "ETH transfer to Co-Founder One failed.");
(bool s2, ) = coFounderTwo.primary.call{value: shareForTwo}(""); require(s2, "ETH transfer to Co-Founder Two failed.");
emit FeesWithdrawn(msg.sender, shareForOne, shareForTwo);
}
function updateCoFounderOneAddresses(address _newPrimary, address _newSecondary) external onlyCoFounderOne {
require(_newPrimary != address(0) && _newSecondary != address(0), "Source: New addresses cannot be zero");
coFounderOne.primary = _newPrimary; coFounderOne.secondary = _newSecondary;
emit CoFounderAddressesUpdated(1, _newPrimary, _newSecondary);
}
function updateCoFounderTwoAddresses(address _newPrimary, address _newSecondary) external onlyCoFounderTwo {
require(_newPrimary != address(0) && _newSecondary != address(0), "Source: New addresses cannot be zero");
coFounderTwo.primary = _newPrimary; coFounderTwo.secondary = _newSecondary;
emit CoFounderAddressesUpdated(2, _newPrimary, _newSecondary);
}
function createWill(
uint256 interval,
address[] memory heirs,
uint256[] memory distribution,
Will.Erc20Distribution[] calldata erc20s,
Will.NftDistribution[] calldata nfts
) external payable {
require(msg.value >= basePlatformFee, "Msg.value must cover required fee.");
require(userWills[msg.sender] == address(0), "User already has an existing will.");
uint256 willValue = msg.value - basePlatformFee;
Will newWill = new Will{value: willValue}(
msg.sender, address(this), false, executorAddress
);
userWills[msg.sender] = address(newWill);
for (uint i = 0; i < erc20s.length; i++) {
if (erc20s[i].amount > 0) {
IERC20(erc20s[i].tokenContract).transferFrom(msg.sender, address(newWill), erc20s[i].amount);
}
}
for (uint i = 0; i < nfts.length; i++) {
IERC721(nfts[i].tokenContract).transferFrom(msg.sender, address(newWill), nfts[i].tokenId);
}
newWill.initialize(interval, heirs, distribution, erc20s, nfts);
emit WillCreated(msg.sender, address(newWill), false);
}
function createWillDiary() external payable {
require(msg.value == diaryPlatformFee, "Msg.value must exactly cover the diary fee.");
require(userWills[msg.sender] == address(0), "User already has an existing will.");
Will newWill = new Will(
msg.sender, address(this), true, executorAddress
);
userWills[msg.sender] = address(newWill);
emit WillCreated(msg.sender, address(newWill), true);
}
function clearWillRecord(address _user) external {
address willAddress = userWills[_user];
require(willAddress != address(0), "No will found for this user.");
require(msg.sender == willAddress || msg.sender == _user, "Caller should be the user or their registered will.");
delete userWills[_user];
emit WillCleared(_user, willAddress);
}
}
// contracts/Source.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/ISource.sol";
// --- PASTED FROM OPENZEPPELIN - START ---
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() { _status = _NOT_ENTERED; }
modifier nonReentrant() {
_preReentryCheck();
_status = _ENTERED;
_;
_status = _NOT_ENTERED;
}
function _preReentryCheck() private view { if (_status == _ENTERED) { revert ReentrancyGuardReentrantCall(); } }
}
error ReentrancyGuardReentrantCall();
// --- PASTED FROM OPENZEPPELIN - END ---
struct Erc20Detail { address tokenContract; uint256 balance; }
struct Erc721Detail { address tokenContract; uint256 tokenId; address heir; }
struct WillDetails {
address owner;
uint256 interval;
uint256 lastUpdate;
bool executed;
bool hasDiary;
uint256 ethBalance;
address[] heirs;
uint256[] distributionPercentages;
Erc20Detail[] erc20Details;
Erc721Detail[] erc721Details;
}
contract Will is Ownable, ReentrancyGuard {
struct Erc20Asset { IERC20 token; }
struct Erc721Asset { IERC721 token; uint256 tokenId; address heir; }
struct Erc20Distribution { address tokenContract; uint256 amount; }
struct NftDistribution { address tokenContract; uint256 tokenId; address heir; }
enum WillState { Empty, Active, Executed }
WillState public currentState;
uint256 private constant EXECUTOR_WINDOW = 1 days;
uint256 public constant EXECUTION_FEE_BPS = 50;
uint256 public lastUpdate;
uint256 public interval; // No longer immutable
address public immutable sourceContract;
bool public immutable hasDiary;
address public immutable executorAddress;
address[] public heirs;
uint256[] public distributionPercentages;
Erc20Asset[] public erc20Assets;
Erc721Asset[] public erc721Assets;
event Ping(uint256 newLastUpdate);
event Executed(address executor, uint256 ethFee, address feeRecipient);
event Cancelled(); // No longer has feePaid
event WillConfigured(address indexed owner);
event WillEmptied(address indexed owner);
modifier requiresState(WillState _state) {
require(currentState == _state, "Will: Invalid state for this action");
_;
}
constructor(
address initialOwner,
address _sourceContract,
bool _hasDiary,
address _executorAddress
) payable Ownable(initialOwner) {
sourceContract = _sourceContract;
hasDiary = _hasDiary;
executorAddress = _executorAddress;
currentState = WillState.Empty;
}
function initialize(
uint256 _interval,
address[] memory _heirs,
uint256[] memory _distro,
Erc20Distribution[] calldata _erc20s,
NftDistribution[] calldata _nfts
) external requiresState(WillState.Empty) {
require(msg.sender == sourceContract, "Will: Not authorized by factory");
interval = _interval;
_configure(_heirs, _distro, _erc20s, _nfts);
currentState = WillState.Active;
lastUpdate = block.timestamp;
emit WillConfigured(owner());
}
function fundAndConfigure(
uint256 _interval,
address[] memory _heirs,
uint256[] memory _distro,
Erc20Distribution[] calldata _erc20s,
NftDistribution[] calldata _nfts
) external payable onlyOwner requiresState(WillState.Empty) {
interval = _interval;
for (uint i = 0; i < _erc20s.length; i++) {
if (_erc20s[i].amount > 0) {
IERC20(_erc20s[i].tokenContract).transferFrom(msg.sender, address(this), _erc20s[i].amount);
}
}
for (uint i = 0; i < _nfts.length; i++) {
IERC721(_nfts[i].tokenContract).transferFrom(msg.sender, address(this), _nfts[i].tokenId);
}
_configure(_heirs, _distro, _erc20s, _nfts);
currentState = WillState.Active;
lastUpdate = block.timestamp;
emit WillConfigured(owner());
}
function emptyWillForEdit() external onlyOwner requiresState(WillState.Active) nonReentrant {
_returnAllAssets();
delete heirs;
delete distributionPercentages;
delete erc20Assets;
delete erc721Assets;
interval = 0; // Reset interval
currentState = WillState.Empty;
emit WillEmptied(owner());
}
function cancelAndWithdraw() external onlyOwner requiresState(WillState.Active) nonReentrant {
_returnAllAssets();
ISource(sourceContract).clearWillRecord(owner());
currentState = WillState.Executed;
emit Cancelled();
}
function ping() external onlyOwner requiresState(WillState.Active) {
lastUpdate = block.timestamp;
emit Ping(lastUpdate);
}
function execute() external requiresState(WillState.Active) nonReentrant {
uint256 gracePeriodEnd = lastUpdate + interval;
require(block.timestamp >= gracePeriodEnd, "Grace period has not ended.");
uint256 executorPeriodEnd = gracePeriodEnd + EXECUTOR_WINDOW;
if (block.timestamp < executorPeriodEnd) { require(msg.sender == executorAddress, "Only designated executor can call now"); }
currentState = WillState.Executed;
address feeRecipient = (block.timestamp < executorPeriodEnd) ? sourceContract : msg.sender;
uint256 totalEthFee = 0;
uint256 totalEth = address(this).balance;
if (totalEth > 0) {
uint256 ethFee = (totalEth * EXECUTION_FEE_BPS) / 10000;
totalEthFee = ethFee;
uint256 distributableEth = totalEth - ethFee;
if (ethFee > 0) { (bool s, ) = payable(feeRecipient).call{value: ethFee}(""); require(s, "ETH fee transfer failed"); }
for (uint i = 0; i < heirs.length; i++) {
uint256 share = (distributableEth * distributionPercentages[i]) / 100;
if (share > 0) { (bool s, ) = payable(heirs[i]).call{value: share}(""); require(s, "ETH transfer to heir failed"); }
}
}
for (uint i = 0; i < erc20Assets.length; i++) {
IERC20 token = erc20Assets[i].token;
uint256 totalTokens = token.balanceOf(address(this));
if (totalTokens > 0) {
uint256 tokenFee = (totalTokens * EXECUTION_FEE_BPS) / 10000;
uint256 distributableTokens = totalTokens - tokenFee;
if (tokenFee > 0) { token.transfer(feeRecipient, tokenFee); }
for (uint j = 0; j < heirs.length; j++) {
uint256 share = (distributableTokens * distributionPercentages[j]) / 100;
if (share > 0) { token.transfer(heirs[j], share); }
}
}
}
for (uint i = 0; i < erc721Assets.length; i++) {
Erc721Asset memory asset = erc721Assets[i];
asset.token.safeTransferFrom(address(this), asset.heir, asset.tokenId);
}
emit Executed(msg.sender, totalEthFee, feeRecipient);
}
function _configure(
address[] memory _heirs,
uint256[] memory _distro,
Erc20Distribution[] calldata _erc20s,
NftDistribution[] calldata _nfts
) internal {
require(_heirs.length == _distro.length, "Heirs/distributions mismatch");
if (_heirs.length > 0) {
uint256 totalPercentage = 0;
for (uint256 i = 0; i < _distro.length; i++) { totalPercentage += _distro[i]; }
require(totalPercentage == 100, "Distribution must sum to 100");
}
heirs = _heirs;
distributionPercentages = _distro;
for (uint i = 0; i < _erc20s.length; i++) {
erc20Assets.push(Erc20Asset(IERC20(_erc20s[i].tokenContract)));
}
for (uint i = 0; i < _nfts.length; i++) {
erc721Assets.push(Erc721Asset(IERC721(_nfts[i].tokenContract), _nfts[i].tokenId, _nfts[i].heir));
}
}
function _returnAllAssets() internal {
address payable _owner = payable(owner());
for (uint i = 0; i < erc20Assets.length; i++) {
IERC20 token = erc20Assets[i].token;
uint256 balance = token.balanceOf(address(this));
if (balance > 0) { token.transfer(_owner, balance); }
}
for (uint i = 0; i < erc721Assets.length; i++) {
Erc721Asset memory asset = erc721Assets[i];
asset.token.safeTransferFrom(address(this), _owner, asset.tokenId);
}
uint256 remainingEth = address(this).balance;
if (remainingEth > 0) {
(bool success, ) = _owner.call{value: remainingEth}("");
require(success, "ETH transfer failed.");
}
}
function getWillDetails() external view returns (WillDetails memory) {
Erc20Detail[] memory _erc20Details = new Erc20Detail[](erc20Assets.length);
for (uint i = 0; i < erc20Assets.length; i++) {
IERC20 token = erc20Assets[i].token;
_erc20Details[i] = Erc20Detail(address(token), token.balanceOf(address(this)));
}
Erc721Detail[] memory _erc721Details = new Erc721Detail[](erc721Assets.length);
for (uint i = 0; i < erc721Assets.length; i++) {
Erc721Asset memory asset = erc721Assets[i];
_erc721Details[i] = Erc721Detail(address(asset.token), asset.tokenId, asset.heir);
}
return WillDetails(
owner(), interval, lastUpdate, (currentState == WillState.Executed),
hasDiary, address(this).balance, heirs, distributionPercentages,
_erc20Details, _erc721Details
);
}
}
// contracts/Will.sol