pragma solidity >=0.8.0;


interface IShaman {
    function memberAction(address member, uint96 loot, uint96 shares) external payable returns (uint96 lootOut, uint96 sharesOut);
}



contract Baal {
    bool public lootPaused; 
    bool public sharesPaused; 
    bool singleSummoner; 
    
    uint8  constant public decimals = 18; 
    uint16 constant MAX_GUILD_TOKEN_COUNT = 400; 
    
    uint96 public totalLoot;   
    uint96 public totalSupply; 
    
    uint32 public gracePeriod; 
    uint32 public minVotingPeriod; 
    uint32 public maxVotingPeriod; 
    uint public proposalCount; 
    uint status; 
    
    string public name; 
    string public symbol; 
    
    bytes32 constant DOMAIN_TYPEHASH = keccak256('EIP712Domain(string name,uint chainId,address verifyingContract)'); 
    bytes32 constant DELEGATION_TYPEHASH = keccak256('Delegation(address delegatee,uint nonce,uint expiry)'); 
    bytes32 constant PERMIT_TYPEHASH = keccak256('Permit(address owner,address spender,uint value,uint nonce,uint deadline)'); 
    bytes32 constant VOTE_TYPEHASH = keccak256('Vote(uint proposalId,bool support)'); 
    
    address[] guildTokens; 
    
    mapping(address => mapping(address => uint))    public allowance; 
    mapping(address => uint)                        public balanceOf; 
    mapping(address => mapping(uint => Checkpoint)) public checkpoints; 
    mapping(address => uint)                        public numCheckpoints; 
    mapping(address => address)                     public delegates; 
    mapping(address => uint)                        public nonces; 
    
    mapping(address => Member) public members; 
    mapping(uint => Proposal)  public proposals; 
    mapping(uint => bool)      public proposalsPassed; 
    mapping(address => bool)   public shamans; 
    
    event SummonComplete(bool lootPaused, bool sharesPaused, uint gracePeriod, uint minVotingPeriod, uint maxVotingPeriod, string name, string symbol, address[] guildTokens, address[] shamans, address[] summoners, uint96[] loot, uint96[] shares); 
    event SubmitProposal(uint8 indexed flag, uint indexed proposal, uint indexed votingPeriod, address[] to, uint96[] value, bytes[] data, string details); 
    event SponsorProposal(address indexed member, uint indexed proposal, uint indexed votingStarts); 
    event SubmitVote(address indexed member, uint balance, uint indexed proposal, bool indexed approved); 
    event ProcessProposal(uint indexed proposal); 
    event Ragequit(address indexed member, address to, uint96 indexed lootToBurn, uint96 indexed sharesToBurn); 
    event Approval(address indexed owner, address indexed spender, uint amount); 
    event Transfer(address indexed from, address indexed to, uint amount); 
    event TransferLoot(address indexed from, address indexed to, uint96 amount); 
    event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); 
    event DelegateVotesChanged(address indexed delegate, uint previousBalance, uint newBalance); 
    
    modifier nonReentrant() { 
        require(status == 1,'reentrant'); 
        status = 2; 
        _;
        status = 1;
    }
    
    struct Checkpoint { 
        uint32 fromTimeStamp; 
        uint96 votes; 
    }
 
    struct Member { 
        uint96 loot; 
        uint highestIndexYesVote; 
    }
    
    struct Proposal { 
        uint32 votingPeriod; 
        uint32 votingStarts; 
        uint32 votingEnds; 
        uint96 yesVotes; 
        uint96 noVotes; 
        bool[4] flags; 
        address[] to; 
        uint96[] value; 
        bytes[] data; 
        string details; 
    }

    
    
    
    
    
    
    
    
    
    
    
    
    constructor(
        bool             _sharesPaused,
        uint32           _gracePeriod,
        uint32           _minVotingPeriod, 
        uint32           _maxVotingPeriod,
        string    memory _name, 
        string    memory _symbol,
        address[] memory _guildTokens,
        address[] memory _shamans, 
        address[] memory _summoners, 
        uint96[]  memory _loot, 
        uint96[]  memory _shares
    ) {
        require(_minVotingPeriod != 0,'0_min'); 
        require(_minVotingPeriod <= _maxVotingPeriod,'min>max'); 
        require(_guildTokens.length != 0,'0_tokens'); 
        require(_summoners.length != 0,'0_summoners'); 
        require(_summoners.length == _loot.length && _loot.length == _shares.length,'!member array parity'); 
        
        unchecked {
            for (uint i; i < _shamans.length; i++) shamans[_shamans[i]] = true; 
            for (uint i; i < _guildTokens.length; i++) guildTokens.push(_guildTokens[i]); 
            for (uint i; i < _summoners.length; i++) {
                _mintLoot(_summoners[i], _loot[i]); 
                _mintShares(_summoners[i], _shares[i]);  
                _delegate(_summoners[i], _summoners[i]); 
                if (_summoners.length == 1) singleSummoner = true; 
            }
        }
        
        gracePeriod = _gracePeriod; 
        minVotingPeriod = _minVotingPeriod; 
        maxVotingPeriod = _maxVotingPeriod; 
        if (_sharesPaused) lootPaused = true; 
        sharesPaused = _sharesPaused; 
        name = _name; 
        symbol = _symbol; 
        status = 1; 
        
        emit SummonComplete(lootPaused, _sharesPaused, _gracePeriod, _minVotingPeriod, _maxVotingPeriod, _name, _symbol, _guildTokens, _shamans, _summoners, _loot, _shares); 
    }

    
    
    
    
    
    
    function memberAction(
        address shaman, 
        uint96 loot, 
        uint96 shares, 
        bool mint
    ) external nonReentrant payable returns (uint96 lootOut, uint96 sharesOut) {
        require(shamans[shaman],'!shaman'); 
        
        (lootOut, sharesOut) = IShaman(shaman).memberAction{value: msg.value}(msg.sender, loot, shares); 
        
        if (mint) { 
            if (lootOut != 0) _mintLoot(msg.sender, lootOut); 
            if (sharesOut != 0) _mintShares(msg.sender, sharesOut); 
        } else { 
            if (lootOut != 0) _burnLoot(msg.sender, lootOut); 
            if (sharesOut != 0) _burnShares(msg.sender, sharesOut); 
        }
    }
    
    
    
    
    
    
    
    
    
    
    function submitProposal(
        uint8 flag, 
        uint32 votingPeriod, 
        address[] calldata to, 
        uint96[] calldata value, 
        bytes[] calldata data, 
        string calldata details
    ) external nonReentrant returns (uint proposal) {
        require(minVotingPeriod <= votingPeriod && votingPeriod <= maxVotingPeriod,'!votingPeriod'); 
        require(to.length <= 10,'array max'); 
        require(flag <= 3,'!flag'); 
        
        bool[4] memory flags; 
        flags[flag] = true;  
        
        if (flag == 2) {
            if (value.length == 1) {
                require(value[0] <= maxVotingPeriod,'over max');
            } else if (value.length == 2) {
                require(value[1] >= minVotingPeriod,'under min');
            }
        } else {
            require(to.length == value.length && value.length == data.length,'!array parity'); 
        }
        
        bool selfSponsor; 
        if (balanceOf[msg.sender] != 0) selfSponsor = true; 

        unchecked {
            proposalCount++; 
            proposals[proposalCount] = Proposal( 
                votingPeriod,
                selfSponsor ? uint32(block.timestamp) : 0, 
                selfSponsor ? uint32(block.timestamp) + votingPeriod : 0, 
                0, 0, flags, to, value, data, details
            );
        }
        
        emit SubmitProposal(flag, proposal, votingPeriod, to, value, data, details); 
    }
    
    
    
    function sponsorProposal(uint proposal) external nonReentrant {
        Proposal storage prop = proposals[proposal]; 
        
        require(balanceOf[msg.sender] != 0,'!member'); 
        require(prop.votingPeriod != 0,'!exist'); 
        require(prop.votingStarts == 0,'sponsored'); 
        
        prop.votingStarts = uint32(block.timestamp);
        
        unchecked {
            prop.votingEnds = uint32(block.timestamp) + prop.votingPeriod;
        }

        emit SponsorProposal(msg.sender, proposal, block.timestamp);
    }

    
    
    
    function submitVote(uint proposal, bool approved) external nonReentrant {
        Proposal storage prop = proposals[proposal]; 
        
        uint96 balance = getPriorVotes(msg.sender, prop.votingStarts); 
        
        require(prop.votingEnds >= block.timestamp,'ended'); 
        
        unchecked {
            if (approved) { 
                prop.yesVotes += balance; 
                members[msg.sender].highestIndexYesVote = proposal;
            } else { 
                prop.noVotes += balance;
            }
        }

        emit SubmitVote(msg.sender, balance, proposal, approved); 
    }
    
    
    
    
    
    
    
    function submitVoteWithSig(
        uint proposal, 
        bool approved, 
        uint8 v, 
        bytes32 r, 
        bytes32 s
    ) external nonReentrant {
        Proposal storage prop = proposals[proposal]; 
        
        bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), block.chainid, address(this))); 
        bytes32 structHash = keccak256(abi.encode(VOTE_TYPEHASH, proposal, approved)); 
        bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); 
        address signatory = ecrecover(digest, v, r, s); 
        
        require(signatory != address(0),'!signatory'); 
        
        uint96 balance = getPriorVotes(signatory, prop.votingStarts); 
        
        require(prop.votingEnds >= block.timestamp,'ended'); 
        
        unchecked {
            if (approved) { 
                prop.yesVotes += balance; members[signatory].highestIndexYesVote = proposal;
            } else { 
                prop.noVotes += balance;
            }
        }
        
        emit SubmitVote(signatory, balance, proposal, approved); 
    }
        
    
    
    
    
    
    function processProposal(uint proposal) external nonReentrant {
        Proposal storage prop = proposals[proposal]; 
        
        _processingReady(proposal, prop); 
        
        if (prop.yesVotes > prop.noVotes) 
            proposalsPassed[proposal] = true; 
            if (prop.flags[0]) processActionProposal(prop); 
            else if (prop.flags[1]) processMemberProposal(prop); 
            else if (prop.flags[2]) processPeriodProposal(prop); 
            else processWhitelistProposal(prop); 
            
        delete proposals[proposal]; 
        
        emit ProcessProposal(proposal); 
    }
    
    
    function processActionProposal(Proposal memory prop) private {
        unchecked {
            for (uint i; i < prop.to.length; i++) 
                prop.to[i].call{value:prop.value[i]} 
                (prop.data[i]); 
        }
    }
    
    
    function processMemberProposal(Proposal memory prop) private {
        unchecked {
            for (uint i; i < prop.to.length; i++) {
                if (prop.data[i].length == 0) {
                    _mintShares(prop.to[i], prop.value[i]); 
                } else {
                    uint96 removedBalance = uint96(balanceOf[prop.to[i]]); 
                    _burnShares(prop.to[i], removedBalance); 
                    _mintLoot(prop.to[i], removedBalance); 
                }
            }
        }
    }
    
    
    function processPeriodProposal(Proposal memory prop) private {
        uint length = prop.value.length;
        
        if (length == 1) {
            if (prop.value[0] != 0) minVotingPeriod = uint32(prop.value[0]);  
        } else if (length == 2) {
            if (prop.value[1] != 0) maxVotingPeriod = uint32(prop.value[1]); 
        } else if (length == 3) {
            if (prop.value[2] != 0) gracePeriod = uint32(prop.value[2]); 
        } else if (length == 4) {
            prop.value[3] == 0 ? lootPaused = false : lootPaused = true; 
        } else if (length == 5) {
            prop.value[4] == 0 ? sharesPaused = false : sharesPaused = true; 
        }
    }  
        
    
    function processWhitelistProposal(Proposal memory prop) private {
        unchecked {
            for (uint i; i < prop.to.length; i++) {
                if (prop.value[i] == 0 && prop.data[i].length == 0) { 
                    shamans[prop.to[i]] = true; 
                } else if (prop.value[i] == 0 && prop.data[i].length != 0) { 
                    shamans[prop.to[i]] = false; 
                } else if (prop.value[i] != 0 && prop.data[i].length == 0) { 
                    if (guildTokens.length != MAX_GUILD_TOKEN_COUNT) guildTokens.push(prop.to[i]); 
                }
            }
        }
    }
    
    
    
    
    function approve(address to, uint amount) external returns (bool success) {
        allowance[msg.sender][to] = amount; 
        
        emit Approval(msg.sender, to, amount); 
        
        success = true; 
    }
    
    
    
    function delegate(address delegatee) external {
        _delegate(msg.sender, delegatee);
    }
    
    
    
    
    
    
    
    
    function delegateBySig(
        address delegatee, 
        uint nonce, 
        uint deadline, 
        uint8 v, 
        bytes32 r, 
        bytes32 s
    ) external {
        bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), block.chainid, address(this))); 
        bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, deadline)); 
        bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); 
        address signatory = ecrecover(digest, v, r, s); 
            
        require(signatory != address(0),'!signatory'); 
        unchecked {
            require(nonce == nonces[signatory]++,'!nonce'); 
        }
        require(block.timestamp <= deadline,'expired'); 
            
        _delegate(signatory, delegatee); 
    }

    
    
    
    
    
    
    
    
    function permit(
        address owner, 
        address spender, 
        uint96 amount, 
        uint deadline, 
        uint8 v, 
        bytes32 r, 
        bytes32 s
    ) external {
        bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), block.chainid, address(this))); 
        
        unchecked {
            bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, amount, nonces[owner]++, deadline)); 
            bytes32 digest = keccak256(abi.encodePacked('\x19\x01', domainSeparator, structHash)); 
            address signatory = ecrecover(digest, v, r, s); 
            require(signatory != address(0),'!signatory'); 
            require(signatory == owner,'!authorized'); 
        }
        
        require(block.timestamp <= deadline,'expired'); 
        
        allowance[owner][spender] = amount; 
        
        emit Approval(owner, spender, amount); 
    }
    
    
    
    
    
    function transfer(address to, uint96 amount) external returns (bool success) {
        require(!sharesPaused,'!transferable');
        
        balanceOf[msg.sender] -= amount;
        
        unchecked {
            balanceOf[to] += amount;
        }
        
        _moveDelegates(delegates[msg.sender], delegates[to], amount);
        
        emit Transfer(msg.sender, to, amount);
        
        success = true;
    }
        
    
    
    
    
    
    function transferFrom(address from, address to, uint96 amount) external returns (bool success) {
        require(!sharesPaused,'!transferable');
        
        if (allowance[from][msg.sender] != type(uint).max) {
            allowance[from][msg.sender] -= amount;
        }
        
        balanceOf[from] -= amount;
        
        unchecked {
            balanceOf[to] += amount;
        }
        
        _moveDelegates(delegates[from], delegates[to], amount);
        
        emit Transfer(from, to, amount);
        
        success = true;
    }
    
    
    
    
    function transferLoot(address to, uint96 amount) external {
        require(!lootPaused,'!transferable');
        
        members[msg.sender].loot -= amount;
        
        unchecked {
            members[to].loot += amount;
        }
        
        emit TransferLoot(msg.sender, to, amount);
    }

    
    
    
    
    function ragequit(address to, uint96 lootToBurn, uint96 sharesToBurn) external nonReentrant {
        require(proposals[members[msg.sender].highestIndexYesVote].votingEnds == 0,'processed'); 
        
        for (uint i; i < guildTokens.length; i++) {
            (,bytes memory balanceData) = guildTokens[i].staticcall(abi.encodeWithSelector(0x70a08231, address(this))); 
            uint balance = abi.decode(balanceData, (uint)); 
            
            uint amountToRagequit = ((lootToBurn + sharesToBurn) * balance) / (totalSupply + totalLoot); 
            
            if (amountToRagequit != 0) { 
                _safeTransfer(guildTokens[i], to, amountToRagequit); 
            }
        }
        
        if (lootToBurn != 0) {  
            _burnLoot(msg.sender, lootToBurn); 
        }
        
        if (sharesToBurn != 0) {  
            _burnShares(msg.sender, sharesToBurn);  
        }
        
        emit Ragequit(msg.sender, to, lootToBurn, sharesToBurn); 
    }

    
    
    
    
    function getCurrentVotes(address account) external view returns (uint96 votes) {
        uint nCheckpoints = numCheckpoints[account];
        unchecked { votes = nCheckpoints != 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; }
    }
    
    
    
    
    
    function getPriorVotes(address account, uint timeStamp) public view returns (uint96 votes) {
        require(timeStamp < block.timestamp,'!determined');
        
        uint nCheckpoints = numCheckpoints[account];
        if (nCheckpoints == 0) return 0;
        
        unchecked {
            if (checkpoints[account][nCheckpoints - 1].fromTimeStamp <= timeStamp)
                return checkpoints[account][nCheckpoints - 1].votes; 
            if (checkpoints[account][0].fromTimeStamp > timeStamp) return 0;
            uint lower = 0; 
            uint upper = nCheckpoints - 1;
            while (upper > lower) {
                uint center = upper - (upper - lower) / 2;
                Checkpoint memory cp = checkpoints[account][center];
                if (cp.fromTimeStamp == timeStamp) return cp.votes; 
                else if (cp.fromTimeStamp < timeStamp) lower = center; 
                else upper = center - 1;
            }
            votes = checkpoints[account][lower].votes;
        }
    }
    
    
    
    function getGuildTokens() external view returns (address[] memory tokens) {
        tokens = guildTokens;
    }

    
    
    
    function getProposalFlags(uint proposal) external view returns (bool[4] memory flags) {
        flags = proposals[proposal].flags;
    }

    
    
    
    function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
        results = new bytes[](data.length);
        unchecked {
            for (uint256 i = 0; i < data.length; i++) {
                (bool success, bytes memory result) = address(this).delegatecall(data[i]);
                if (!success) {
                    if (result.length < 68) revert();
                    assembly { result := add(result, 0x04) }
                    revert(abi.decode(result, (string)));
                }
                results[i] = result;
            }
        }
    }

    
    function onERC721Received(address, address, uint, bytes calldata) external pure returns (bytes4 sig) {
        sig = 0x150b7a02; 
    }
    
    
    function onERC1155Received(address, address, uint, uint, bytes calldata) external pure returns (bytes4 sig) {
        sig = 0xf23a6e61; 
    }
    
    
    function onERC1155BatchReceived(address, address, uint[] calldata, uint[] calldata, bytes calldata) external pure returns (bytes4 sig) {
        sig = 0xbc197c81; 
    }
    
    
    receive() external payable {}

    
    function _delegate(address delegator, address delegatee) private {
        address currentDelegate = delegates[delegator];
        
        delegates[delegator] = delegatee;
        
        _moveDelegates(currentDelegate, delegatee, uint96(balanceOf[delegator]));
        
        emit DelegateChanged(delegator, currentDelegate, delegatee);
    }
    
    
    function _moveDelegates(address srcRep, address dstRep, uint96 amount) private {
        unchecked {
            if (srcRep != dstRep && amount != 0) {
                if (srcRep != address(0)) {
                    uint srcRepNum = numCheckpoints[srcRep];
                    uint96 srcRepOld = srcRepNum != 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0;
                    uint96 srcRepNew = srcRepOld - amount;
                    _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
                }
            
                if (dstRep != address(0)) {
                    uint dstRepNum = numCheckpoints[dstRep];
                    uint96 dstRepOld = dstRepNum != 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0;
                    uint96 dstRepNew = dstRepOld + amount;
                    _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
                }
            }
        }
    }
    
    
    function _writeCheckpoint(address delegatee, uint nCheckpoints, uint96 oldVotes, uint96 newVotes) private {
        uint32 timeStamp = uint32(block.timestamp);
        
        unchecked {
            if (nCheckpoints != 0 && checkpoints[delegatee][nCheckpoints - 1].fromTimeStamp == timeStamp) {
                checkpoints[delegatee][nCheckpoints - 1].votes = newVotes;
            } else {
                checkpoints[delegatee][nCheckpoints] = Checkpoint(timeStamp, newVotes);
                numCheckpoints[delegatee] = nCheckpoints + 1;
            }
        }
        
        emit DelegateVotesChanged(delegatee, oldVotes, newVotes);
    }
    
    
    function _burnLoot(address from, uint96 loot) private {
        members[from].loot -= loot; 
        
        unchecked {
            totalLoot -= loot; 
        }
        
        emit TransferLoot(from, address(0), loot); 
    }
    
    
    function _burnShares(address from, uint96 shares) private {
        balanceOf[from] -= shares; 
        
        unchecked {
            totalSupply -= shares; 
        }
        
        _moveDelegates(delegates[from], address(0), shares); 
        
        emit Transfer(from, address(0), shares); 
    }
    
    
    function _mintLoot(address to, uint96 loot) private {
        unchecked {
            if (totalSupply + loot <= type(uint96).max / 2) {
                members[to].loot += loot; 
        
                totalLoot += loot; 
            
                emit TransferLoot(address(0), to, loot); 
            }
        }
    }
    
    
    function _mintShares(address to, uint96 shares) private {
        unchecked {
            if (totalSupply + shares <= type(uint96).max / 2) {
                balanceOf[to] += shares; 
        
                totalSupply += shares; 
            
                _moveDelegates(address(0), delegates[to], shares); 
        
                emit Transfer(address(0), to, shares); 
            }
        }
    }
 
    
    function _processingReady(uint proposal, Proposal memory prop) private view returns (bool ready) {
        unchecked {
            require(proposal <= proposalCount,'!exist'); 
            require(proposals[proposal - 1].votingEnds == 0,'prev!processed'); 
            require(proposals[proposal].votingEnds != 0,'processed'); 
            if (singleSummoner) return true; 
            if (prop.yesVotes > totalSupply / 2) return true; 
            require(prop.votingEnds + gracePeriod <= block.timestamp,'!ended'); 
            ready = true; 
        }
    }
    
    
    function _safeTransfer(address token, address to, uint amount) private {
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount)); 
        require(success && (data.length == 0 || abi.decode(data, (bool))),'transfer failed'); 
    }
}