// File: @openzeppelin/contracts/security/ReentrancyGuard.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}

// File: @openzeppelin/contracts/utils/math/SafeMath.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/SafeMath.sol)

pragma solidity ^0.8.0;

// CAUTION
// This version of SafeMath should only be used with Solidity 0.8 or later,
// because it relies on the compiler's built in overflow checks.

/**
 * @dev Wrappers over Solidity's arithmetic operations.
 *
 * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler
 * now has built in overflow checking.
 */
library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            uint256 c = a + b;
            if (c < a) return (false, 0);
            return (true, c);
        }
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b > a) return (false, 0);
            return (true, a - b);
        }
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, with an overflow flag.
     *
     * _Available since v3.4._
     */
    function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
            // benefit is lost if 'b' is also tested.
            // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
            if (a == 0) return (true, 0);
            uint256 c = a * b;
            if (c / a != b) return (false, 0);
            return (true, c);
        }
    }

    /**
     * @dev Returns the division of two unsigned integers, with a division by zero flag.
     *
     * _Available since v3.4._
     */
    function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a / b);
        }
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag.
     *
     * _Available since v3.4._
     */
    function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a % b);
        }
    }

    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return a - b;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        return a * b;
    }

    /**
     * @dev Returns the integer division of two unsigned integers, reverting on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator.
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return a / b;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * reverting when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return a % b;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {trySub}.
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        unchecked {
            require(b <= a, errorMessage);
            return a - b;
        }
    }

    /**
     * @dev Returns the integer division of two unsigned integers, reverting with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a / b;
        }
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * reverting with custom message when dividing by zero.
     *
     * CAUTION: This function is deprecated because it requires allocating memory for the error
     * message unnecessarily. For custom revert reasons use {tryMod}.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a % b;
        }
    }
}

// File: contracts/DTrustFactory.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./DTrustV1.sol";

/// @title DTRUSTFactory
/// @notice Factory contract for creating new DTrust contracts
/// @dev This contract deploys new instances of DTrust and tracks them
contract DTRUSTFactory {
    /// Public variable with the address of the bank wallet
    address public bankWallet;

    /// Fee percentage for the bank; scaled by 100 (e.g., 10 represents 0.10%, 100 represents 1%, 1000 represents 10%)
    uint256 public bankFee = 10;

    /// Dynamic array of all deployed DTrust contracts
    address[] public allDTrusts;

    /// Mapping from user addresses to their respective DTrust contracts
    mapping(address => address[]) public dTrustsByUser;

    /// Mapping to check if a user has been added to a DTrust contract to prevent duplicates
    mapping(address => mapping(address => bool)) public isUserAddedToDTrust;

    /// Event emitted when a new DTrust contract is created
    event DTrustCreated(address indexed settlor, address trustAddress);

    /// Event emitted when a user is added to a DTrust contract
    event UserAddedToDTrust(address indexed user, address trustAddress);

    /// @notice Constructs the DTRUSTFactory and sets the bank wallet address
    /// @param _bankWallet The address of the bank wallet where fees are collected
    constructor(address _bankWallet) {
        bankWallet = _bankWallet;
    }

    /// @notice Creates a new DTRUST contract and registers the involved parties
    /// @param _name Name of the dtrust
    /// @param _settlor Address of the settlor creating the dtrust
    /// @param _trustees Array of addresses designated as trustees
    /// @param _beneficiaries Array of addresses designated as beneficiaries
    /// @param _canRevokeAddresses Array of addresses with permission to revoke the dtrust
    function createDTRUST(
        string calldata _name,
        address _settlor,
        address[] calldata _trustees,
        address[] calldata _beneficiaries,
        address[] calldata _canRevokeAddresses
    ) external {
        DTRUST newDTRUST = new DTRUST(
            _name,
            _settlor,
            address(this),
            _trustees,
            _beneficiaries,
            _canRevokeAddresses
        );
        allDTrusts.push(address(newDTRUST));
        addUniqueUser(_settlor, newDTRUST);
        addUniqueUsers(_trustees, newDTRUST);
        addUniqueUsers(_beneficiaries, newDTRUST);
        emit DTrustCreated(_settlor, address(newDTRUST));
    }

    /// @dev Adds an array of unique users to the internal tracking for a given DTRUST
    /// @param _users Array of user addresses to be added
    /// @param newDTRUST Address of the DTRUST contract to which users are added
    function addUniqueUsers(address[] calldata _users, DTRUST newDTRUST) internal {
        for (uint i = 0; i < _users.length; i++) {
            addUniqueUser(_users[i], newDTRUST);
        }
    }

    /// @dev Adds a single unique user to the internal tracking for a given DTRUST
    /// @param _user Address of the user to be added
    /// @param newDTRUST Address of the DTRUST contract to which the user is added
    function addUniqueUser(address _user, DTRUST newDTRUST) internal {
        if (!isUserAddedToDTrust[_user][address(newDTRUST)]) {
            dTrustsByUser[_user].push(address(newDTRUST));
            isUserAddedToDTrust[_user][address(newDTRUST)] = true;
            emit UserAddedToDTrust(_user, address(newDTRUST));
        }
    }

    /// @notice Retrieves the DTRUST contracts associated with a given user
    /// @param _user The address of the user
    /// @return An array of addresses of DTRUST contracts linked to the user
    function getDTrustsByUser(address _user) external view returns (address[] memory) {
        return dTrustsByUser[_user];
    }

    /// @notice Collects the annual fee for a specified DTRUST contract
    /// @param dtrustAddress The payable address of the DTRUST contract from which to collect the fee
    function collectAnnualFeeForTrust(address payable dtrustAddress) external {
        DTRUST(dtrustAddress).takeAnnualFee(bankWallet, bankFee);
    }

    /// @notice Returns the total number of DTRUST contracts created by the factory
    /// @return The count of all DTRUST contracts
    function getAllDTrustsCount() external view returns (uint256) {
        return allDTrusts.length;
    }
}

// File: contracts/DTrustV1.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/// @title ERC-20 interface
interface IERC20 {
    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);
    function allowance(address owner, address spender) external view returns (uint256);
}

/// @title DTRUST contract for managing dtrusts
/// @notice This contract allows for the creation and management of dtrusts
/// @dev Extends ReentrancyGuard from OpenZeppelin to prevent reentrancy attacks
contract DTRUST is ReentrancyGuard {
    using SafeMath for uint256;

    // State variables
    address settlor;
    address factoryAddress;
    address[] trustees;
    address[] beneficiaries;
    address[] tokens;
    string name;
    bool public isRevoked = false;
    
    // Mappings
    mapping(address => bool) trusteesLookup;
    mapping(address => bool) beneficiariesLookup;
    mapping(address => bool) public revokeAddressLookup;
    mapping(address => bool) tokenLookup;

    // Balances
    uint256 etherBalance = 0;
    uint256 public startFeeTime;
    uint256 public dateCreated;

    // Events
    event Paid(address indexed token, address indexed beneficiary, uint256 amount);
    event Revoked();
    event RemoveRevokableAddress(address indexed revokableAddress);
    event ReceivedEther(address indexed sender, uint256 amount);
    event DepositedEther(address indexed sender, uint256 amount);
    event DepositedToken(address indexed token, address indexed sender, uint256 amount);

    // Modifiers
    modifier isTrustee() {
        require(trusteesLookup[msg.sender], "Only a trustee can perform this action");
        _;
    }

    modifier isActive() {
        require(!isRevoked, "The contract has been revoked");
        _;
    }

    /// @notice Constructor to create a DTRUST
    /// @param _name Name of the dtrust
    /// @param _settlor Address of the settlor creating the dtrust
    /// @param _factoryAddress Address of the factory contract creating this dtrust
    /// @param _trustees Array of addresses of the trustees
    /// @param _beneficiaries Array of addresses of the beneficiaries
    /// @param _canRevokeAddresses Array of addresses that can revoke the dtrust
    constructor(
        string memory _name,
        address _settlor,
        address _factoryAddress,
        address[] memory _trustees,
        address[] memory _beneficiaries,
        address[] memory _canRevokeAddresses
    ) {
        name = _name;
        settlor = _settlor;
        factoryAddress = _factoryAddress;
        addTrustees(_trustees);
        addBeneficiaries(_beneficiaries);
        addRevokableAddresses(_canRevokeAddresses);
        dateCreated = block.timestamp;
        startFeeTime = block.timestamp;
    }

    /// @notice Fallback function to receive Ether
    receive() external payable {
        etherBalance += msg.value;
        emit DepositedEther(msg.sender, msg.value);
    }

    /// @notice Internal function to add beneficiaries to the dtrust
    /// @param _beneficiaries Array of beneficiary addresses
    function addBeneficiaries(address[] memory _beneficiaries) internal {
        for (uint i = 0; i < _beneficiaries.length; i++) {
            beneficiariesLookup[_beneficiaries[i]] = true;
            beneficiaries.push(_beneficiaries[i]);
        }
    }

    /// @notice Internal function to add addresses with revocation rights
    /// @param _canRevokeAddresses Array of addresses with permission to revoke the dtrust
    function addRevokableAddresses(address[] memory _canRevokeAddresses) internal {
        for(uint i = 0; i < _canRevokeAddresses.length; i++) {
            require(trusteesLookup[_canRevokeAddresses[i]] || _canRevokeAddresses[i] == settlor, "Address must be a trustee or the settlor");
            revokeAddressLookup[_canRevokeAddresses[i]] = true;
        }
    }

    /// @notice Internal function to add trustees to the dtrust
    /// @param _trustees Array of trustee addresses
    function addTrustees(address[] memory _trustees) internal {
        for (uint i = 0; i < _trustees.length; i++) {
            trusteesLookup[_trustees[i]] = true;
            trustees.push(_trustees[i]);
        }
    }

    /// @notice Allows a trustee to deposit Ether into the dtrust
    /// @dev Emits a DepositedEther event upon success
    function depositEth() external payable isActive { 
        require(msg.value > 0, "Deposit amount should be greater than 0");
        etherBalance += msg.value;
        emit DepositedEther(msg.sender, msg.value);
    }

    /// @notice Allows a trustee to deposit ERC-20 tokens into the dtrust
    /// @param token Address of the ERC-20 token
    /// @param amount Amount of the ERC-20 tokens to deposit
    /// @dev Emits a DepositedToken event upon success
    function depositToken(address token, uint256 amount) external isActive { 
        uint256 allowedAmount = IERC20(token).allowance(msg.sender, address(this));
        require(amount > 0, "Enter an amount greater than 0");
        require(allowedAmount >= amount, "Contract not approved to move this amount of tokens");
        
        if (!tokenLookup[token]) {
            tokenLookup[token] = true;
            tokens.push(token);
        }

        require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Token transfer failed");
        emit DepositedToken(token, msg.sender, amount);
    }

    /// @notice Allows trustees to make a payout from the dtrust to a beneficiary
    /// @param _token Address of the ERC-20 token
    /// @param _amount Amount of the ERC-20 tokens to payout
    /// @param _beneficiary Address of the beneficiary to receive the payout
    /// @dev Emits a Paid event upon success
    function payout(
        address _token,
        uint256 _amount,
        address _beneficiary
    ) external isTrustee isActive nonReentrant {   
        require(beneficiariesLookup[_beneficiary], "Beneficiary provided is not a beneficiary of this dtrust");
        require(IERC20(_token).balanceOf(address(this)) >= _amount, "Not enough balance of the token");
        require(IERC20(_token).transfer(_beneficiary, _amount), "Token transfer failed");
        emit Paid(_token, _beneficiary, _amount);
    }

        /// @notice Revokes the dtrust and performs payouts of remaining balances
    /// @dev Emits a Revoked event upon successful revocation
    function revokeContract() external isActive nonReentrant {
        require(revokeAddressLookup[msg.sender], "You do not have permission to revoke");
        payoutAll(tokens);
        isRevoked = true;
        emit Revoked();
    }

    /// @notice Allows trustees to payout Ether from the dtrust to a beneficiary
    /// @param _ethAmount Amount of Ether to payout
    /// @param _beneficiary Address of the beneficiary to receive the Ether
    /// @dev Emits a Paid event upon success
    function payoutEth(uint256 _ethAmount, address _beneficiary) public isTrustee isActive nonReentrant {
        require(beneficiariesLookup[_beneficiary], "Beneficiary provided is not a beneficiary of this dtrust");
        require(_ethAmount > 0, "Enter Eth amount > 0");
        require(address(this).balance >= _ethAmount, "Not enough Ether to payout");
        address payable user = payable(_beneficiary);
        user.transfer(_ethAmount);
        etherBalance -= _ethAmount;
        emit Paid(address(this), _beneficiary, _ethAmount);
    }

    /// @notice Allows trustees to payout remaining balances after the dtrust has been revoked
    /// @param _tokens Array of ERC-20 token addresses to be paid out
    /// @dev Emits Paid events for each token paid out
    function payoutRemaining(address[] memory _tokens) external isTrustee nonReentrant {
        require(isRevoked, "The dtrust must be revoked before the remaining balance can be paid out");
        payoutAll(_tokens);
    }

    /// @notice Internal function to payout all balances of Ether and tokens to the settlor
    /// @param _tokens Array of ERC-20 token addresses to be paid out
    /// @dev Emits Paid events for each token paid out
    function payoutAll(address[] memory _tokens) internal {
        if(address(this).balance > 0){
            address payable user = payable(settlor);
            user.transfer(address(this).balance);
            etherBalance -= address(this).balance;
            emit Paid(address(this), settlor, address(this).balance);
        }

        for (uint256 i = 0; i < _tokens.length; i++) {
            address token = _tokens[i];
            uint256 amount = IERC20(token).balanceOf(address(this));
            
            if(amount > 0){
                require(IERC20(token).transfer(settlor, amount), "Token transfer failed");
                emit Paid(token, settlor, amount);
            }
        }
    }

    /// @notice Allows an address with revocation rights to remove themselves from the list of revokable addresses
    /// @dev Emits a RemoveRevokableAddress event upon success
    function removeRevokableAddress() external isActive {
        require(revokeAddressLookup[msg.sender], "Address is not revokable");
        revokeAddressLookup[msg.sender] = false;
        emit RemoveRevokableAddress(msg.sender);
    }

    /// @notice Collects an annual fee for the dtrust, payable to the bank wallet
    /// @param _bankWallet Address of the bank wallet where fees are collected
    /// @param _feePercentage Annual fee percentage
    /// @dev Emits a Paid event for fee collection
    function takeAnnualFee(address _bankWallet, uint256 _feePercentage) external isActive nonReentrant {
        require(block.timestamp >= startFeeTime, "Not yet time to collect fee");
        require(msg.sender == factoryAddress, "You must be the control wallet");
        
        uint256 feeFraction = _feePercentage.mul(1e14);

        if(address(this).balance > 0 ){
            uint256 ethFee = address(this).balance.mul(feeFraction).div(1e18);
            payable(_bankWallet).transfer(ethFee);
            etherBalance -= ethFee;
            emit Paid(address(0), _bankWallet, ethFee); // address(0) denotes Ether
        }
        
        for (uint256 i = 0; i < tokens.length; i++) {
            address token = tokens[i];
            uint256 tokenBalance = IERC20(token).balanceOf(address(this));
            if(tokenBalance > 0 ){
                uint256 tokenFee = tokenBalance.mul(feeFraction).div(1e18);
                require(IERC20(token).transfer(_bankWallet, tokenFee), "Token transfer failed");
                emit Paid(token, _bankWallet, tokenFee);
            }
        }

        startFeeTime += 365 days;
    }

    /// @notice Retrieves information about the dtrust
    /// @return name of the dtrust
    /// @return settlor of the dtrust
    /// @return list of trustees
    /// @return list of beneficiaries
    /// @return creation date of the dtrust
    /// @return start time for the next fee collection
    /// @return revocation status of the dtrust
    /// @dev This function is view-only and does not modify state
    function getTrustInfo() isActive public view returns (
        string memory,
        address, 
        address[] memory, 
        address[] memory,
        uint256,
        uint256,
        bool
    ) {
        return (name, settlor, trustees, beneficiaries, dateCreated, startFeeTime, isRevoked);
    }
}