// File: /contracts/OwnerBalance.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import './Governed.sol';
import './OwnerBalanceContributor.sol';

/**
 * @title Tracks owner's share of the funds in various Macabris contracts
 */
contract OwnerBalance is Governed {

    address public owner;

    // All three contracts, that contribute to the owner's balance
    OwnerBalanceContributor public release;
    OwnerBalanceContributor public bank;
    OwnerBalanceContributor public market;

    /**
     * @param governanceAddress Address of the Governance contract
     *
     * Requirements:
     * - Governance contract must be deployed at the given address
     */
    constructor(address governanceAddress) Governed(governanceAddress) {}

    /**
     * @dev Sets the release contract address
     * @param releaseAddress Address of the Release contract
     *
     * Requirements:
     * - the caller must have the bootstrap permission
     */
    function setReleaseAddress(address releaseAddress) external canBootstrap(msg.sender) {
        release = OwnerBalanceContributor(releaseAddress);
    }

    /**
     * @dev Sets Bank contract address
     * @param bankAddress Address of the Bank contract
     *
     * Requirements:
     * - the caller must have the bootstrap permission
     */
    function setBankAddress(address bankAddress) external canBootstrap(msg.sender) {
        bank = OwnerBalanceContributor(bankAddress);
    }

    /**
     * @dev Sets the market contract address
     * @param marketAddress Address of the Market contract
     *
     * Requirements:
     * - the caller must have the bootstrap permission
     */
    function setMarketAddress(address marketAddress) external canBootstrap(msg.sender) {
        market = OwnerBalanceContributor(marketAddress);
    }

    /**
     * @dev Sets owner address where the funds will be sent during withdrawal
     * @param _owner Owner's address
     *
     * Requirements:
     * - sender must have canSetOwnerAddress permission
     * - address must not be 0
     */
    function setOwner(address _owner) external canSetOwnerAddress(msg.sender) {
        require(_owner != address(0), "Empty owner address is not allowed!");
        owner = _owner;
    }

    /**
     * @dev Returns total available balance in all contributing contracts
     * @return Balance in wei
     */
    function getBalance() external view returns (uint) {
        uint balance;

        balance += release.ownerBalanceDeposits();
        balance += bank.ownerBalanceDeposits();
        balance += market.ownerBalanceDeposits();

        return balance;
    }

    /**
     * @dev Withdraws available balance to the owner address
     *
     * Requirements:
     * - owner address must be set
     * - sender must have canTriggerOwnerWithdraw permission
     */
    function withdraw() external canTriggerOwnerWithdraw(msg.sender) {
        require(owner != address(0), "Owner address is not set");

        release.withdrawOwnerBalanceDeposits(owner);
        bank.withdrawOwnerBalanceDeposits(owner);
        market.withdrawOwnerBalanceDeposits(owner);
    }
}

// File: /contracts/OwnerBalanceContributor.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import './OwnerBalance.sol';

/**
 * @title Allows allocating portion of the contract's funds to the owner balance
 */
abstract contract OwnerBalanceContributor {

    // OwnerBalance contract address
    address public immutable ownerBalanceAddress;

    uint public ownerBalanceDeposits;

    /**
     * @param _ownerBalanceAddress Address of the OwnerBalance contract
     */
    constructor (address _ownerBalanceAddress) {
        ownerBalanceAddress = _ownerBalanceAddress;
    }

    /**
     * @dev Assigns given amount of contract funds to the owner's balance
     * @param amount Amount in wei
     */
    function _transferToOwnerBalance(uint amount) internal {
        ownerBalanceDeposits += amount;
    }

    /**
     * @dev Allows OwnerBalance contract to withdraw deposits
     * @param ownerAddress Owner address to send funds to
     *
     * Requirements:
     * - caller must be the OwnerBalance contract
     */
    function withdrawOwnerBalanceDeposits(address ownerAddress) external {
        require(msg.sender == ownerBalanceAddress, 'Caller must be the OwnerBalance contract');
        uint currentBalance = ownerBalanceDeposits;
        ownerBalanceDeposits = 0;
        payable(ownerAddress).transfer(currentBalance);
    }
}

// File: /contracts/Governed.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import './Governance.sol';

/**
 * @title Provides permission check modifiers for child contracts
 */
abstract contract Governed {

    // Governance contract
    Governance public immutable governance;

    /**
     * @param governanceAddress Address of the Governance contract
     *
     * Requirements:
     * - Governance contract must be deployed at the given address
     */
    constructor (address governanceAddress) {
        governance = Governance(governanceAddress);
    }

    /**
     * @dev Throws if given address that doesn't have ManagesDeaths permission
     * @param subject Address to check permissions for, usually msg.sender
     */
    modifier canManageDeaths(address subject) {
        require(
            governance.hasPermission(subject, Governance.Actions.ManageDeaths),
            "Governance: subject is not allowed to manage deaths"
        );
        _;
    }

    /**
     * @dev Throws if given address that doesn't have Configure permission
     * @param subject Address to check permissions for, usually msg.sender
     */
    modifier canConfigure(address subject) {
        require(
            governance.hasPermission(subject, Governance.Actions.Configure),
            "Governance: subject is not allowed to configure contracts"
        );
        _;
    }

    /**
     * @dev Throws if given address that doesn't have Bootstrap permission
     * @param subject Address to check permissions for, usually msg.sender
     */
    modifier canBootstrap(address subject) {
        require(
            governance.hasPermission(subject, Governance.Actions.Bootstrap),
            "Governance: subject is not allowed to bootstrap"
        );
        _;
    }

    /**
     * @dev Throws if given address that doesn't have SetOwnerAddress permission
     * @param subject Address to check permissions for, usually msg.sender
     */
    modifier canSetOwnerAddress(address subject) {
        require(
            governance.hasPermission(subject, Governance.Actions.SetOwnerAddress),
            "Governance: subject is not allowed to set owner address"
        );
        _;
    }

    /**
     * @dev Throws if given address that doesn't have TriggerOwnerWithdraw permission
     * @param subject Address to check permissions for, usually msg.sender
     */
    modifier canTriggerOwnerWithdraw(address subject) {
        require(
            governance.hasPermission(subject, Governance.Actions.TriggerOwnerWithdraw),
            "Governance: subject is not allowed to trigger owner withdraw"
        );
        _;
    }

    /**
     * @dev Throws if given address that doesn't have StopPayouyts permission
     * @param subject Address to check permissions for, usually msg.sender
     */
    modifier canStopPayouts(address subject) {
        require(
            governance.hasPermission(subject, Governance.Actions.StopPayouts),
            "Governance: subject is not allowed to stop payouts"
        );
        _;
    }
}

// File: /contracts/Governance.sol



// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

/**
 * @title Manages address permissions to act on Macabris contracts
 */
contract Governance {

    enum Actions { Vote, Configure, SetOwnerAddress, TriggerOwnerWithdraw, ManageDeaths, StopPayouts, Bootstrap }

    // Stores permissions of an address
    struct Permissions {
        bool canVote;
        bool canConfigure;
        bool canSetOwnerAddress;
        bool canTriggerOwnerWithdraw;
        bool canManageDeaths;
        bool canStopPayouts;

        // Special permission that can't be voted in and only the deploying address receives
        bool canBootstrap;
    }

    // A call for vote to change address permissions
    struct CallForVote {

        // Address that will be assigned the permissions if the vote passes
        address subject;

        // Permissions to be assigned if the vote passes
        Permissions permissions;

        // Total number of votes for and against the permission change
        uint128 yeas;
        uint128 nays;
    }

    // A vote in a call for vote
    struct Vote {
        uint64 callForVoteIndex;
        bool yeaOrNay;
    }

    // Permissions of addresses
    mapping(address => Permissions) private permissions;

    // List of calls for a vote: callForVoteIndex => CallForVote, callForVoteIndex starts from 1
    mapping(uint => CallForVote) private callsForVote;

    // Last registered call for vote of every address: address => callForVoteIndex
    mapping(address => uint64) private lastRegisteredCallForVote;

    // Votes of every address: address => Vote
    mapping(address => Vote) private votes;

    uint64 public resolvedCallsForVote;
    uint64 public totalCallsForVote;
    uint64 public totalVoters;

    /**
     * @dev Emitted when a new call for vote is registered
     * @param callForVoteIndex Index of the call for vote (1-based)
     * @param subject Subject address to change permissions to if vote passes
     * @param canVote Allow subject address to vote
     * @param canConfigure Allow subject address to configure prices, fees and base URI
     * @param canSetOwnerAddress Allows subject to change owner withdraw address
     * @param canTriggerOwnerWithdraw Allow subject address to trigger withdraw from owner's balance
     * @param canManageDeaths Allow subject to set tokens as dead or alive
     * @param canStopPayouts Allow subject to stop the bank payout schedule early
     */
    event CallForVoteRegistered(
        uint64 indexed callForVoteIndex,
        address indexed caller,
        address indexed subject,
        bool canVote,
        bool canConfigure,
        bool canSetOwnerAddress,
        bool canTriggerOwnerWithdraw,
        bool canManageDeaths,
        bool canStopPayouts
    );

    /**
     * @dev Emitted when a call for vote is resolved
     * @param callForVoteIndex Index of the call for vote (1-based)
     * @param yeas Total yeas for the call after the vote
     * @param nays Total nays for the call after the vote
     */
    event CallForVoteResolved(
        uint64 indexed callForVoteIndex,
        uint128 yeas,
        uint128 nays
    );

    /**
     * @dev Emitted when a vote is casted
     * @param callForVoteIndex Index of the call for vote (1-based)
     * @param voter Voter address
     * @param yeaOrNay Vote, true if yea, false if nay
     * @param totalVoters Total addresses with vote permission at the time of event
     * @param yeas Total yeas for the call after the vote
     * @param nays Total nays for the call after the vote
     */
    event VoteCasted(
        uint64 indexed callForVoteIndex,
        address indexed voter,
        bool yeaOrNay,
        uint64 totalVoters,
        uint128 yeas,
        uint128 nays
    );

    /**
     * @dev Inits the contract and gives the deployer address all permissions
     */
    constructor() {
        _setPermissions(msg.sender, Permissions({
            canVote: true,
            canConfigure: true,
            canSetOwnerAddress: true,
            canTriggerOwnerWithdraw: true,
            canManageDeaths: true,
            canStopPayouts: true,
            canBootstrap: true
        }));
    }

    /**
     * @dev Checks if the given address has permission to perform given action
     * @param subject Address to check
     * @param action Action to check permissions against
     * @return True if given address has permission to perform given action
     */
    function hasPermission(address subject, Actions action) public view returns (bool) {
        if (action == Actions.ManageDeaths) {
            return permissions[subject].canManageDeaths;
        }

        if (action == Actions.Vote) {
            return permissions[subject].canVote;
        }

        if (action == Actions.SetOwnerAddress) {
            return permissions[subject].canSetOwnerAddress;
        }

        if (action == Actions.TriggerOwnerWithdraw) {
            return permissions[subject].canTriggerOwnerWithdraw;
        }

        if (action == Actions.Configure) {
            return permissions[subject].canConfigure;
        }

        if (action == Actions.StopPayouts) {
            return permissions[subject].canStopPayouts;
        }

        if (action == Actions.Bootstrap) {
            return permissions[subject].canBootstrap;
        }

        return false;
    }

    /**
     * Sets permissions for a given address
     * @param subject Subject address to set permissions to
     * @param _permissions Permissions
     */
    function _setPermissions(address subject, Permissions memory _permissions) private {

        // Tracks count of total voting addresses to be able to calculate majority
        if (permissions[subject].canVote != _permissions.canVote) {
            if (_permissions.canVote) {
                totalVoters += 1;
            } else {
                totalVoters -= 1;

                // Cleaning up voting-related state for the address
                delete votes[subject];
                delete lastRegisteredCallForVote[subject];
            }
        }

        permissions[subject] = _permissions;
    }

    /**
     * @dev Registers a new call for vote to change address permissions
     * @param subject Subject address to change permissions to if vote passes
     * @param canVote Allow subject address to vote
     * @param canConfigure Allow subject address to configure prices, fees and base URI
     * @param canSetOwnerAddress Allows subject to change owner withdraw address
     * @param canTriggerOwnerWithdraw Allow subject address to trigger withdraw from owner's balance
     * @param canManageDeaths Allow subject to set tokens as dead or alive
     * @param canStopPayouts Allow subject to stop the bank payout schedule early
     *
     * Requirements:
     * - the caller must have the vote permission
     * - the caller shouldn't have any unresolved calls for vote
     */
    function callForVote(
        address subject,
        bool canVote,
        bool canConfigure,
        bool canSetOwnerAddress,
        bool canTriggerOwnerWithdraw,
        bool canManageDeaths,
        bool canStopPayouts
    ) external {
        require(
            hasPermission(msg.sender, Actions.Vote),
            "Only addresses with vote permission can register a call for vote"
        );

        // If the sender has previously created a call for vote that hasn't been resolved yet,
        // a second call for vote can't be registered. Prevents a denial of service attack, where
        // a minority of voters could flood the call for vote queue.
        require(
            lastRegisteredCallForVote[msg.sender] <= resolvedCallsForVote,
            "Only one active call for vote per address is allowed"
        );

        totalCallsForVote++;

        lastRegisteredCallForVote[msg.sender] = totalCallsForVote;

        callsForVote[totalCallsForVote] = CallForVote({
            subject: subject,
            permissions: Permissions({
                canVote: canVote,
                canConfigure: canConfigure,
                canSetOwnerAddress: canSetOwnerAddress,
                canTriggerOwnerWithdraw: canTriggerOwnerWithdraw,
                canManageDeaths: canManageDeaths,
                canStopPayouts: canStopPayouts,
                canBootstrap: false
            }),
            yeas: 0,
            nays: 0
        });

        emit CallForVoteRegistered(
            totalCallsForVote,
            msg.sender,
            subject,
            canVote,
            canConfigure,
            canSetOwnerAddress,
            canTriggerOwnerWithdraw,
            canManageDeaths,
            canStopPayouts
        );
    }

    /**
     * @dev Registers a vote
     * @param callForVoteIndex Call for vote index
     * @param yeaOrNay True to vote yea, false to vote nay
     *
     * Requirements:
     * - unresolved call for vote must exist
     * - call for vote index must match the current active call for vote
     * - the caller must have the vote permission
     */
    function vote(uint64 callForVoteIndex, bool yeaOrNay) external {
        require(hasUnresolvedCallForVote(), "No unresolved call for vote exists");
        require(
            callForVoteIndex == _getCurrenCallForVoteIndex(),
            "Call for vote does not exist or is not active"
        );
        require(
            hasPermission(msg.sender, Actions.Vote),
            "Sender address does not have vote permission"
        );

        uint128 yeas = callsForVote[callForVoteIndex].yeas;
        uint128 nays = callsForVote[callForVoteIndex].nays;

        // If the voter has already voted in this call for vote, undo the last vote
        if (votes[msg.sender].callForVoteIndex == callForVoteIndex) {
            if (votes[msg.sender].yeaOrNay) {
                yeas -= 1;
            } else {
                nays -= 1;
            }
        }

        if (yeaOrNay) {
            yeas += 1;
        } else {
            nays += 1;
        }

        emit VoteCasted(callForVoteIndex, msg.sender, yeaOrNay, totalVoters, yeas, nays);

        if (yeas == (totalVoters / 2 + 1) || nays == (totalVoters - totalVoters / 2)) {

            if (yeas > nays) {
                _setPermissions(
                    callsForVote[callForVoteIndex].subject,
                    callsForVote[callForVoteIndex].permissions
                );
            }

            resolvedCallsForVote += 1;

            // Cleaning up what we can
            delete callsForVote[callForVoteIndex];
            delete votes[msg.sender];

            emit CallForVoteResolved(callForVoteIndex, yeas, nays);

            return;
        }

        votes[msg.sender] = Vote({
            callForVoteIndex: callForVoteIndex,
            yeaOrNay: yeaOrNay
        });

        callsForVote[callForVoteIndex].yeas = yeas;
        callsForVote[callForVoteIndex].nays = nays;
    }

    /**
     * @dev Returns information about the current unresolved call for vote
     * @return callForVoteIndex Call for vote index (1-based)
     * @return yeas Total yea votes
     * @return nays Total nay votes
     *
     * Requirements:
     * - Unresolved call for vote must exist
     */
    function getCurrentCallForVote() public view returns (
        uint64 callForVoteIndex,
        uint128 yeas,
        uint128 nays
    ) {
        require(hasUnresolvedCallForVote(), "No unresolved call for vote exists");
        uint64 index = _getCurrenCallForVoteIndex();
        return (index, callsForVote[index].yeas, callsForVote[index].nays);
    }

    /**
     * @dev Checks if there is an unresolved call for vote
     * @return True if an unresolved call for vote exists
     */
    function hasUnresolvedCallForVote() public view returns (bool) {
        return totalCallsForVote > resolvedCallsForVote;
    }

    /**
     * @dev Returns current call for vote index
     * @return Call for vote index (1-based)
     *
     * Doesn't check if an unresolved call for vote exists, hasUnresolvedCallForVote should be used
     * before using the index that this method returns.
     */
    function _getCurrenCallForVoteIndex() private view returns (uint64) {
        return resolvedCallsForVote + 1;
    }
}