// Contract: Governance
// Target function: function transferAnyERC20Token(address tokenAddress, address recipient, uint amount) external onlyOwner
burse | yes
        TWO // burn | no
    }
    
    mapping (PoolGroupName => StakingPool[4]) public hardcodedStakingPools;
    
    constructor() public {
        contractStartTime = now;
        
        hardcodedStakingPools[PoolGroupName.WETH][0] = StakingPool(0xa7d6F5fa9b0be0e98b3b40E6aC884e53F2F9460e);
        hardcodedStakingPools[PoolGroupName.WETH][1] = StakingPool(0x0b0A544AE6131801522E3aC1FBAc6D311094c94c);
        hardcodedStakingPools[PoolGroupName.WETH][2] = StakingPool(0x16cAaD63BDFC3Ec4A2850336B28efE17e802b896);
        hardcodedStakingPools[PoolGroupName.WETH][3] = StakingPool(0x512FF8739d39e55d75d80046921E7dE20c3e9BFf);
        
        hardcodedStakingPools[PoolGroupName.WBTC][0] = StakingPool(0xeF71DE5Cb40f7985FEb92AA49D8e3E84063Af3BB);
        hardcodedStakingPools[PoolGroupName.WBTC][1] = StakingPool(0x8B0e324EEdE360CaB670a6AD12940736d74f701e);
        hardcodedStakingPools[PoolGroupName.WBTC][2] = StakingPool(0x78e2dA2eda6dF49BaE46E3B51528BAF5c106e654);
        hardcodedStakingPools[PoolGroupName.WBTC][3] = StakingPool(0x350F3fE979bfad4766298713c83b387C2D2D7a7a);
        
        hardcodedStakingPools[PoolGroupName.USDT][0] = StakingPool(0x4a76Fc15D3fbf3855127eC5DA8AAf02DE7ca06b3);
        hardcodedStakingPools[PoolGroupName.USDT][1] = StakingPool(0xF4abc60a08B546fA879508F4261eb4400B55099D);
        hardcodedStakingPools[PoolGroupName.USDT][2] = StakingPool(0x13F421Aa823f7D90730812a33F8Cac8656E47dfa);
        hardcodedStakingPools[PoolGroupName.USDT][3] = StakingPool(0x86690BbE7a9683A8bAd4812C2e816fd17bC9715C);
        
        hardcodedStakingPools[PoolGroupName.USDC][0] = StakingPool(0x2b5D7a865A3888836d15d69dCCBad682663DCDbb);
        hardcodedStakingPools[PoolGroupName.USDC][1] = StakingPool(0xa52250f98293c17C894d58cf4f78c925dC8955d0);
        hardcodedStakingPools[PoolGroupName.USDC][2] = StakingPool(0x924BECC8F4059987E4bc4B741B7C354FF52c25e4);
        hardcodedStakingPools[PoolGroupName.USDC][3] = StakingPool(0xbE528593781988974D83C2655CBA4c45FC75c033);
    }
    
    
    
    // proposal id => action
    mapping (uint => Action) public actions;
    
    // proposal id => option one votes
    mapping (uint => uint) public optionOneVotes;
    
    // proposal id => option two votes
    mapping (uint => uint) public optionTwoVotes;
    
    // proposal id => staking pool
    mapping (uint => StakingPool[]) public stakingPools;
    
    // proposal id => newGovernance
    mapping (uint => address) public newGovernances;
    
    // proposal id => unix time for proposal start
    mapping (uint => uint) public proposalStartTime;
    
    // proposal id => bool
    mapping (uint => bool) public isProposalExecuted;
    
    mapping (uint => uint) public newQuorums;
    mapping (uint => uint) public newMinBalances;
    mapping (uint => string) public proposalTexts;
    
    // address user => total deposited DYP
    mapping (address => uint) public totalDepositedTokens;
    
    // address user => uint proposal id => uint vote amounts
    mapping (address => mapping (uint => uint)) public votesForProposalByAddress;
    
    // address user => uint proposal id => Option voted for option
    mapping (address => mapping (uint => Option)) public votedForOption;
    
    // address user => start time for the latest proposal the user voted on
    mapping (address => uint) public lastVotedProposalStartTime;
    
    // uint last proposal id
    // proposal ids start at 1
    uint public lastIndex = 0;
    
    // view function to get proposal details
    function getProposal(uint proposalId) external view returns (
        uint _proposalId, 
        Action _proposalAction,
        uint _optionOneVotes,
        uint _optionTwoVotes,
        StakingPool[] memory _stakingPool,
        address _newGovernance,
        uint _proposalStartTime,
        bool _isProposalExecuted,
        uint _newQuorum,
        string memory _proposalText,
        uint _newMinBalance
        ) {
        _proposalId = proposalId;
        _proposalAction = actions[proposalId];
        _optionOneVotes = optionOneVotes[proposalId];
        _optionTwoVotes = optionTwoVotes[proposalId];
        _stakingPool = stakingPools[proposalId];
        _newGovernance = newGovernances[proposalId];
        _proposalStartTime = proposalStartTime[proposalId];
        _isProposalExecuted = isProposalExecuted[proposalId];
        _newQuorum = newQuorums[proposalId];
        _proposalText = proposalTexts[proposalId];
        _newMinBalance = newMinBalances[proposalId];
    }
    
    function changeQuorum(uint newQuorum) external onlyOwner {
        require(now < contractStartTime.add(ADMIN_FEATURES_EXPIRE_AFTER), "Change quorum feature expired!");
        QUORUM = newQuorum;
    }
    
    function changeMinBalanceToInitProposal(uint newMinBalanceToInitProposal) external onlyOwner {
        require(now < contractStartTime.add(ADMIN_FEATURES_EXPIRE_AFTER), "This admin feature has expired!");
        MIN_BALANCE_TO_INIT_PROPOSAL = newMinBalanceToInitProposal;
    }
    
    // Any DYP holder with a minimum required DYP balance may initiate a proposal
    // with the TEXT_PROPOSAL action for a given staking pool
    function proposeText(string memory text) external noContractsAllowed {
        require(Token(TRUSTED_TOKEN_ADDRESS).balanceOf(msg.sender) >= MIN_BALANCE_TO_INIT_PROPOSAL, "Insufficient Governance Token Balance");
        lastIndex = lastIndex.add(1);
        proposalStartTime[lastIndex] = now;
        actions[lastIndex] = Action.TEXT_PROPOSAL;
        proposalTexts[lastIndex] = text;
    }
    
    // Any DYP holder with a minimum required DYP balance may initiate a proposal
    // with the DISBURSE_OR_BURN action for a given staking pool
    function proposeDisburseOrBurn(PoolGroupName poolGroupName) external noContractsAllowed {
        require(poolGroupName == PoolGroupName.WETH ||
                poolGroupName == PoolGroupName.WBTC ||
                poolGroupName == PoolGroupName.USDT ||
                poolGroupName == PoolGroupName.USDC, "Invalid Pool Group Name!");
        require(Token(TRUSTED_TOKEN_ADDRESS).balanceOf(msg.sender) >= MIN_BALANCE_TO_INIT_PROPOSAL, "Insufficient Governance Token Balance");
        lastIndex = lastIndex.add(1);
        
        stakingPools[lastIndex] = hardcodedStakingPools[poolGroupName];
        
        proposalStartTime[lastIndex] = now;
        actions[lastIndex] = Action.DISBURSE_OR_BURN;
    }
    
    // Admin may initiate a proposal
    // with the UPGRADE_GOVERNANCE action for a given staking pool
    function proposeUpgradeGovernance(PoolGroupName poolGroupName, address newGovernance) external noContractsAllowed onlyOwner {
        require(poolGroupName == PoolGroupName.WETH ||
                poolGroupName == PoolGroupName.WBTC ||
                poolGroupName == PoolGroupName.USDT ||
                poolGroupName == PoolGroupName.USDC, "Invalid Pool Group Name!");
                
        require(Token(TRUSTED_TOKEN_ADDRESS).balanceOf(msg.sender) >= MIN_BALANCE_TO_INIT_PROPOSAL, "Insufficient Governance Token Balance");
        lastIndex = lastIndex.add(1);
        
        stakingPools[lastIndex] = hardcodedStakingPools[poolGroupName];
        
        newGovernances[lastIndex] = newGovernance;
        proposalStartTime[lastIndex] = now;
        actions[lastIndex] = Action.UPGRADE_GOVERNANCE;
    }
    
    // Admin may initiate a proposal
    // with the CHANGE_QUORUM action for the Governance
    function proposeNewQuorum(uint newQuorum) external noContractsAllowed onlyOwner {
        require(Token(TRUSTED_TOKEN_ADDRESS).balanceOf(msg.sender) >= MIN_BALANCE_TO_INIT_PROPOSAL, "Insufficient Governance Token Balance");
        lastIndex = lastIndex.add(1);
        newQuorums[lastIndex] = newQuorum;
        proposalStartTime[lastIndex] = now;
        actions[lastIndex] = Action.CHANGE_QUORUM;
    }
    
    // Admin may initiate a proposal
    // with the CHANGE_MIN_BALANCE_TO_INIT_PROPOSAL action for the Governance
    function proposeNewMinBalanceToInitProposal(uint newMinBalance) external noContractsAllowed onlyOwner {
        require(Token(TRUSTED_TOKEN_ADDRESS).balanceOf(msg.sender) >= MIN_BALANCE_TO_INIT_PROPOSAL, "Insufficient Governance Token Balance");
        lastIndex = lastIndex.add(1);
        newMinBalances[lastIndex] = newMinBalance;
        proposalStartTime[lastIndex] = now;
        actions[lastIndex] = Action.CHANGE_MIN_BALANCE_TO_INIT_PROPOSAL;
    }
    
    // Any DYP holder may add votes for a particular open proposal, 
    // with options YES / NO | DISBURSE / BURN | ONE / TWO
    // with `amount` DYP, each DYP unit corresponds to one vote unit
    
    // If user has already voted for a proposal with an option,
    // user may not add votes with another option, 
    // they will need to add votes for the same option
    function addVotes(uint proposalId, Option option, uint amount) external noContractsAllowed {
        require(amount > 0, "Cannot add 0 votes!");
        require(isProposalOpen(proposalId), "Proposal is closed!");
        
        require(Token(TRUSTED_TOKEN_ADDRESS).transferFrom(msg.sender, address(this), amount), "transferFrom failed!");
        
        // if user is voting for this proposal first time
        if (votesForProposalByAddress[msg.sender][proposalId] == 0) {
            votedForOption[msg.sender][proposalId] = option;
        } else {
            if (votedForOption[msg.sender][proposalId] != option) {
                revert("Cannot vote for both options!");
            }
        }
        
        if (option == Option.ONE) {
            optionOneVotes[proposalId] = optionOneVotes[proposalId].add(amount);
        } else {
            optionTwoVotes[proposalId] = optionTwoVotes[proposalId].add(amount);
        }
        totalDepositedTokens[msg.sender] = totalDepositedTokens[msg.sender].add(amount);
        votesForProposalByAddress[msg.sender][proposalId] = votesForProposalByAddress[msg.sender][proposalId].add(amount);
        
        if (lastVotedProposalStartTime[msg.sender] < proposalStartTime[proposalId]) {
            lastVotedProposalStartTime[msg.sender] = proposalStartTime[proposalId];
        }
    }
    
    // Any voter may remove their votes (DYP) from any proposal they voted for 
    // only when the proposal is open - removing votes refund DYP to user and deduct their votes
    function removeVotes(uint proposalId, uint amount) external noContractsAllowed {
        require(amount > 0, "Cannot remove 0 votes!");
        require(isProposalOpen(proposalId), "Proposal is closed!");
        
        require(amount <= votesForProposalByAddress[msg.sender][proposalId], "Cannot remove more tokens than deposited!");
        
        votesForProposalByAddress[msg.sender][proposalId] = votesForProposalByAddress[msg.sender][proposalId].sub(amount);
        totalDepositedTokens[msg.sender] = totalDepositedTokens[msg.sender].sub(amount);
        
        if (votedForOption[msg.sender][proposalId] == Option.ONE) {
            optionOneVotes[proposalId] = optionOneVotes[proposalId].sub(amount);
        } else {
            optionTwoVotes[proposalId] = optionTwoVotes[proposalId].sub(amount);
        }
        
        require(Token(TRUSTED_TOKEN_ADDRESS).transfer(msg.sender, amount), "transfer failed");
    }

    // After the latest proposal the user voted for, is closed for voting,
    // The user may remove all DYP they added to this contract
    function withdrawAllTokens() external noContractsAllowed {
        require(now > lastVotedProposalStartTime[msg.sender].add(VOTE_DURATION), "Tokens are still in voting!");
        require(Token(TRUSTED_TOKEN_ADDRESS).transfer(msg.sender, totalDepositedTokens[msg.sender]), "transfer failed!");
        totalDepositedTokens[msg.sender] = 0;
    }
    
    // After votes for a proposal are closed, the proposal may be executed by anyone
    // If QUORUM is not reached, transaction must revert
    // If winning option has more votes than losing option, winning action is executed
    // Else losing action is executed
    // Each proposal may be executed only once
    function executeProposal(uint proposalId) external noContractsAllowed {
        require (actions[proposalId] != Action.TEXT_PROPOSAL, "Cannot programmatically execute text proposals");
        require (optionOneVotes[proposalId] != optionTwoVotes[proposalId], "This is a TIE! Cannot execute!");
        require (isProposalExecutible(proposalId), "Proposal Expired!");
        
        isProposalExecuted[proposalId] = true;
    
        Option winningOption;
        uint winningOptionVotes;
        
        if (optionOneVotes[proposalId] > optionTwoVotes[proposalId]) {
            winningOption = Option.ONE;
            winningOptionVotes = optionOneVotes[proposalId];
        } else {
            winningOption = Option.TWO;
            winningOptionVotes = optionTwoVotes[proposalId];
        }
        
        // no action will be taken if winningOptionVotes are less than QUORUM
        if (winningOptionVotes < QUORUM) {
            revert("QUORUM not reached!");
        }
        
        if (actions[proposalId] == Action.DISBURSE_OR_BURN) {
            if (winningOption == Option.ONE) {
                for (uint8 i = 0; i < 4; i++) {
                    StakingPool pool = stakingPools[proposalId][i];
                    try pool.disburseRewardTokens() {
                        emit PoolCallSucceeded(pool);
                    } catch Error(string memory reason) {
                        emit PoolCallReverted(pool, reason);
                    } catch (bytes memory lowLevelData) {
                        emit PoolCallReverted(pool, lowLevelData);
                    }
                }
            } else {
                for (uint8 i = 0; i < 4; i++) {
                    StakingPool pool = stakingPools[proposalId][i];
                    try pool.burnRewardTokens() {
                        emit PoolCallSucceeded(pool);
                    } catch Error(string memory reason) {
                        emit PoolCallReverted(pool, reason);
                    } catch (bytes memory lowLevelData) {
                        emit PoolCallReverted(pool, lowLevelData);
                    }
                }
            }
        } else if (actions[proposalId] == Action.UPGRADE_GOVERNANCE) {
            if (winningOption == Option.ONE) {
                for (uint8 i = 0; i < 4; i++) {
                    StakingPool pool = stakingPools[proposalId][i];
                    try pool.transferOwnership(newGovernances[proposalId]) {
                        emit PoolCallSucceeded(pool);
                    } catch Error(string memory reason) {
                        emit PoolCallReverted(pool, reason);
                    } catch (bytes memory lowLevelData) {
                        emit PoolCallReverted(pool, lowLevelData);
                    }
                }
            }
        } else if (actions[proposalId] == Action.CHANGE_QUORUM) {
            if (winningOption == Option.ONE) {
                QUORUM = newQuorums[proposalId];
            }
        } else if (actions[proposalId] == Action.CHANGE_MIN_BALANCE_TO_INIT_PROPOSAL) {
            if (winningOption == Option.ONE) {
                MIN_BALANCE_TO_INIT_PROPOSAL = newMinBalances[proposalId];
            }
        }
    }
    
    // view function to know whether voting for a particular proposal is open
    function isProposalOpen(uint proposalId) public view returns (bool) {
        if (now < proposalStartTime[proposalId].add(VOTE_DURATION)) {
            return true;
        }
        return false;
    }
    
    // View function to know whether voting for a proposal is closed AND 
    // The proposal is within the RESULT_EXECUTION_ALLOWANCE_PERIOD AND
    // Has not been executed yet
    function isProposalExecutible(uint proposalId) public view returns (bool) {
        if ((!isProposalOpen(proposalId)) && 
            (now < proposalStartTime[proposalId].add(VOTE_DURATION).add(RESULT_EXECUTION_ALLOWANCE_PERIOD)) &&
            !isProposalExecuted[proposalId] &&
            optionOneVotes[proposalId] != optionTwoVotes[proposalId]) {
                return true;
            }
        return false;
    }